implement dnssec-awareness throughout code, and dane for incoming/outgoing mail delivery

the vendored dns resolver code is a copy of the go stdlib dns resolver, with
awareness of the "authentic data" (i.e. dnssec secure) added, as well as support
for enhanced dns errors, and looking up tlsa records (for dane). ideally it
would be upstreamed, but the chances seem slim.

dnssec-awareness is added to all packages, e.g. spf, dkim, dmarc, iprev. their
dnssec status is added to the Received message headers for incoming email.

but the main reason to add dnssec was for implementing dane. with dane, the
verification of tls certificates can be done through certificates/public keys
published in dns (in the tlsa records). this only makes sense (is trustworthy)
if those dns records can be verified to be authentic.

mox now applies dane to delivering messages over smtp. mox already implemented
mta-sts for webpki/pkix-verification of certificates against the (large) pool
of CA's, and still enforces those policies when present. but it now also checks
for dane records, and will verify those if present. if dane and mta-sts are
both absent, the regular opportunistic tls with starttls is still done. and the
fallback to plaintext is also still done.

mox also makes it easy to setup dane for incoming deliveries, so other servers
can deliver with dane tls certificate verification. the quickstart now
generates private keys that are used when requesting certificates with acme.
the private keys are pre-generated because they must be static and known during
setup, because their public keys must be published in tlsa records in dns.
autocert would generate private keys on its own, so had to be forked to add the
option to provide the private key when requesting a new certificate. hopefully
upstream will accept the change and we can drop the fork.

with this change, using the quickstart to setup a new mox instance, the checks
at internet.nl result in a 100% score, provided the domain is dnssec-signed and
the network doesn't have any issues.
This commit is contained in:
Mechiel Lukkien 2023-10-10 12:09:35 +02:00
parent c4324fdaa1
commit daa908e9f4
No known key found for this signature in database
177 changed files with 12907 additions and 3131 deletions

View file

@ -19,16 +19,15 @@ See Quickstart below to get started.
- Internationalized email, with unicode in email address usernames
("localparts"), and in domain names (IDNA).
- Automatic TLS with ACME, for use with Let's Encrypt and other CA's.
- TLSRPT, parsing reports about TLS usage and issues.
- MTA-STS, for ensuring TLS is used whenever it is required. Both serving of
policies, and tracking and applying policies of remote servers.
- DANE and MTA-STS for inbound and outbound delivery over SMTP with STARTTLS,
with incoming TLSRPT reporting.
- Web admin interface that helps you set up your domains and accounts
(instructions to create DNS records, configure
SPF/DKIM/DMARC/TLSRPT/MTA-STS), for status information, managing
accounts/domains, and modifying the configuration file.
- Autodiscovery (with SRV records, Microsoft-style, Thunderbird-style, and Apple
device management profiles) for easy account setup (though client support is
limited).
- Account autodiscovery (with SRV records, Microsoft-style, Thunderbird-style,
and Apple device management profiles) for easy account setup (though client
support is limited).
- Webserver with serving static files and forwarding requests (reverse
proxy), so port 443 can also be used to serve websites.
- Prometheus metrics and structured logging for operational insight.
@ -111,7 +110,6 @@ https://nlnet.nl/project/Mox/.
## Roadmap
- DANE and DNSSEC
- Authentication other than HTTP-basic for webmail/webadmin/webaccount
- Per-domain webmail and IMAP/SMTP host name (and TLS cert) and client settings
- Require TLS SMTP extension (RFC 8689)

View file

@ -31,7 +31,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"github.com/mjl-/autocert"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
@ -64,10 +65,16 @@ type Manager struct {
// Load returns an initialized autotls manager for "name" (used for the ACME key
// file and requested certs and their keys). All files are stored within acmeDir.
//
// contactEmail must be a valid email address to which notifications about ACME can
// be sent. directoryURL is the ACME starting point. When shutdown is closed, no
// new TLS connections can be created.
func Load(name, acmeDir, contactEmail, directoryURL string, shutdown <-chan struct{}) (*Manager, error) {
// be sent. directoryURL is the ACME starting point.
//
// getPrivateKey is called to get the private key for the host and key type. It
// can be used to deliver a specific (e.g. always the same) private key for a
// host, or a newly generated key.
//
// When shutdown is closed, no new TLS connections can be created.
func Load(name, acmeDir, contactEmail, directoryURL string, getPrivateKey func(host string, keyType autocert.KeyType) (crypto.Signer, error), shutdown <-chan struct{}) (*Manager, error) {
if directoryURL == "" {
return nil, fmt.Errorf("empty ACME directory URL")
}
@ -136,6 +143,7 @@ func Load(name, acmeDir, contactEmail, directoryURL string, shutdown <-chan stru
Key: key,
UserAgent: "mox/" + moxvar.Version,
},
GetPrivateKey: getPrivateKey,
// HostPolicy set below.
}
@ -146,7 +154,7 @@ func Load(name, acmeDir, contactEmail, directoryURL string, shutdown <-chan stru
// At startup, during config initialization, we already adjust the tls config to
// inject the listener hostname if there isn't one in the TLS client hello. This is
// common for SMTP STARTTLS connections, which often do not care about the
// validation of the certificate.
// verification of the certificate.
if hello.ServerName == "" {
log.Debug("tls request without sni servername, rejecting", mlog.Field("localaddr", hello.Conn.LocalAddr()), mlog.Field("supportedprotos", hello.SupportedProtos))
return nil, fmt.Errorf("sni server name required")
@ -225,7 +233,7 @@ func (m *Manager) SetAllowedHostnames(resolver dns.Resolver, hostnames map[dns.D
xlog.Debug("checking ips of hosts configured for acme tls cert validation")
for _, h := range added {
ips, err := resolver.LookupIP(ctx, "ip", h.ASCII+".")
ips, _, err := resolver.LookupIP(ctx, "ip", h.ASCII+".")
if err != nil {
xlog.Errorx("warning: acme tls cert validation for host may fail due to dns lookup error", err, mlog.Field("host", h))
continue

View file

@ -2,12 +2,14 @@ package autotls
import (
"context"
"crypto"
"errors"
"fmt"
"os"
"reflect"
"testing"
"golang.org/x/crypto/acme/autocert"
"github.com/mjl-/autocert"
"github.com/mjl-/mox/dns"
)
@ -17,7 +19,11 @@ func TestAutotls(t *testing.T) {
os.MkdirAll("../testdata/autotls", 0770)
shutdown := make(chan struct{})
m, err := Load("test", "../testdata/autotls", "mox@localhost", "https://localhost/", shutdown)
getPrivateKey := func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
return nil, fmt.Errorf("not used")
}
m, err := Load("test", "../testdata/autotls", "mox@localhost", "https://localhost/", getPrivateKey, shutdown)
if err != nil {
t.Fatalf("load manager: %v", err)
}
@ -74,7 +80,7 @@ func TestAutotls(t *testing.T) {
key0 := m.Manager.Client.Key
m, err = Load("test", "../testdata/autotls", "mox@localhost", "https://localhost/", shutdown)
m, err = Load("test", "../testdata/autotls", "mox@localhost", "https://localhost/", getPrivateKey, shutdown)
if err != nil {
t.Fatalf("load manager again: %v", err)
}
@ -87,7 +93,7 @@ func TestAutotls(t *testing.T) {
t.Fatalf("hostpolicy, got err %v, expected no error", err)
}
m2, err := Load("test2", "../testdata/autotls", "mox@localhost", "https://localhost/", shutdown)
m2, err := Load("test2", "../testdata/autotls", "mox@localhost", "https://localhost/", nil, shutdown)
if err != nil {
t.Fatalf("load another manager: %v", err)
}

View file

@ -419,12 +419,15 @@ type KeyCert struct {
}
type TLS struct {
ACME string `sconf:"optional" sconf-doc:"Name of provider from top-level configuration to use for ACME, e.g. letsencrypt."`
KeyCerts []KeyCert `sconf:"optional" sconf-doc:"Key and certificate files are opened by the privileged root process and passed to the unprivileged mox process, so no special permissions are required."`
MinVersion string `sconf:"optional" sconf-doc:"Minimum TLS version. Default: TLSv1.2."`
ACME string `sconf:"optional" sconf-doc:"Name of provider from top-level configuration to use for ACME, e.g. letsencrypt."`
KeyCerts []KeyCert `sconf:"optional" sconf-doc:"Keys and certificates to use for this listener. The files are opened by the privileged root process and passed to the unprivileged mox process, so no special permissions are required on the files. If the private key will not be replaced when refreshing certificates, also consider adding the private key to HostPrivateKeyFiles and configuring DANE TLSA DNS records."`
MinVersion string `sconf:"optional" sconf-doc:"Minimum TLS version. Default: TLSv1.2."`
HostPrivateKeyFiles []string `sconf:"optional" sconf-doc:"Private keys used for ACME certificates. Specified explicitly so DANE TLSA DNS records can be generated, even before the certificates are requested. DANE is a mechanism to authenticate remote TLS certificates based on a public key or certificate specified in DNS, protected with DNSSEC. DANE is opportunistic and attempted when delivering SMTP with STARTTLS. The private key files must be in PEM format. PKCS8 is recommended, but PKCS1 and EC private keys are recognized as well. Only RSA 2048 bit and ECDSA P-256 keys are currently used. The first of each is used when requesting new certificates through ACME."`
Config *tls.Config `sconf:"-" json:"-"` // TLS config for non-ACME-verification connections, i.e. SMTP and IMAP, and not port 443.
ACMEConfig *tls.Config `sconf:"-" json:"-"` // TLS config that handles ACME verification, for serving on port 443.
Config *tls.Config `sconf:"-" json:"-"` // TLS config for non-ACME-verification connections, i.e. SMTP and IMAP, and not port 443.
ACMEConfig *tls.Config `sconf:"-" json:"-"` // TLS config that handles ACME verification, for serving on port 443.
HostPrivateRSA2048Keys []crypto.Signer `sconf:"-" json:"-"` // Private keys for new TLS certificates for listener host name, for new certificates with ACME, and for DANE records.
HostPrivateECDSAP256Keys []crypto.Signer `sconf:"-" json:"-"`
}
type WebHandler struct {

View file

@ -136,9 +136,11 @@ describe-static" and "mox config describe-domains":
# (optional)
ACME:
# Key and certificate files are opened by the privileged root process and passed
# to the unprivileged mox process, so no special permissions are required.
# (optional)
# Keys and certificates to use for this listener. The files are opened by the
# privileged root process and passed to the unprivileged mox process, so no
# special permissions are required on the files. If the private key will not be
# replaced when refreshing certificates, also consider adding the private key to
# HostPrivateKeyFiles and configuring DANE TLSA DNS records. (optional)
KeyCerts:
-
@ -152,6 +154,17 @@ describe-static" and "mox config describe-domains":
# Minimum TLS version. Default: TLSv1.2. (optional)
MinVersion:
# Private keys used for ACME certificates. Specified explicitly so DANE TLSA DNS
# records can be generated, even before the certificates are requested. DANE is a
# mechanism to authenticate remote TLS certificates based on a public key or
# certificate specified in DNS, protected with DNSSEC. DANE is opportunistic and
# attempted when delivering SMTP with STARTTLS. The private key files must be in
# PEM format. PKCS8 is recommended, but PKCS1 and EC private keys are recognized
# as well. Only RSA 2048 bit and ECDSA P-256 keys are currently used. The first of
# each is used when requesting new certificates through ACME. (optional)
HostPrivateKeyFiles:
-
# Maximum size in bytes for incoming and outgoing messages. Default is 100MB.
# (optional)
SMTPMaxMessageSize: 0

519
dane/dane.go Normal file
View file

@ -0,0 +1,519 @@
// Package dane verifies TLS certificates through DNSSEC-verified TLSA records.
//
// On the internet, TLS certificates are commonly verified by checking if they are
// signed by one of many commonly trusted Certificate Authorities (CAs). This is
// PKIX or WebPKI. With DANE, TLS certificates are verified through
// DNSSEC-protected DNS records of type TLSA. These TLSA records specify the rules
// for verification ("usage") and whether a full certificate ("selector" cert) is
// checked or only its "subject public key info" ("selector" spki). The (hash of)
// the certificate or "spki" is included in the TLSA record ("matchtype").
//
// DANE SMTP connections have two allowed "usages" (verification rules):
// - DANE-EE, which only checks if the certificate or spki match, without the
// WebPKI verification of expiration, name or signed-by-trusted-party verification.
// - DANE-TA, which does verification similar to PKIX/WebPKI, but verifies against
// a certificate authority ("trust anchor", or "TA") specified in the TLSA record
// instead of the CA pool.
//
// DANE has two more "usages", that may be used with protocols other than SMTP:
// - PKIX-EE, which matches the certificate or spki, and also verifies the
// certificate against the CA pool.
// - PKIX-TA, which verifies the certificate or spki against a "trust anchor"
// specified in the TLSA record, that also has to be trusted by the CA pool.
//
// TLSA records are looked up for a specific port number, protocol (tcp/udp) and
// host name. Each port can have different TLSA records. TLSA records must be
// signed and verified with DNSSEC before they can be trusted and used.
//
// TLSA records are looked up under "TLSA candidate base domains". The domain
// where the TLSA records are found is the "TLSA base domain". If the host to
// connect to is a CNAME that can be followed with DNSSEC protection, it is the
// first TLSA candidate base domain. If no protected records are found, the
// original host name is the second TLSA candidate base domain.
//
// For TLS connections, the TLSA base domain is used with SNI during the
// handshake.
//
// For TLS certificate verification that requires PKIX/WebPKI/trusted-anchor
// verification (all except DANE-EE), the potential second TLSA candidate base
// domain name is also valid. With SMTP, additionally for hosts found in MX records
// for a "next-hop domain", the "original next-hop domain" (domain of an email
// address to deliver to) is also a valid name, as is the "CNAME-expanded original
// next-hop domain", bringing the potential total allowed names to four (if CNAMEs
// are followed for the MX hosts).
package dane
// todo: why is https://datatracker.ietf.org/doc/html/draft-barnes-dane-uks-00 not in use? sounds reasonable.
// todo: add a DialSRV function that accepts a domain name, looks up srv records, dials the service, verifies dane certificate and returns the connection. for ../rfc/7673
import (
"bytes"
"context"
"crypto/sha256"
"crypto/sha512"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/mjl-/adns"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
)
var (
metricVerify = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mox_dane_verify_total",
Help: "Total number of DANE verification attempts, including mox_dane_verify_errors_total.",
},
)
metricVerifyErrors = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mox_dane_verify_errors_total",
Help: "Total number of DANE verification failures, causing connections to fail.",
},
)
)
var (
// ErrNoRecords means no TLSA records were found and host has not opted into DANE.
ErrNoRecords = errors.New("dane: no tlsa records")
// ErrInsecure indicates insecure DNS responses were encountered while looking up
// the host, CNAME records, or TLSA records.
ErrInsecure = errors.New("dane: dns lookups insecure")
// ErrNoMatch means some TLSA records were found, but none can be verified against
// the remote TLS certificate.
ErrNoMatch = errors.New("dane: no match between certificate and tlsa records")
)
// VerifyError is an error encountered while verifying a DANE TLSA record. For
// example, an error encountered with x509 certificate trusted-anchor verification.
// A TLSA record that does not match a TLS certificate is not a VerifyError.
type VerifyError struct {
Err error // Underlying error, possibly from crypto/x509.
Record adns.TLSA // Cause of error.
}
// Error returns a string explaining this is a dane verify error along with the
// underlying error.
func (e VerifyError) Error() string {
return fmt.Sprintf("dane verify error: %s", e.Err)
}
// Unwrap returns the underlying error.
func (e VerifyError) Unwrap() error {
return e.Err
}
// Dial looks up a DNSSEC-protected DANE TLSA record for the domain name and
// port/service in address, checks for allowed usages, makes a network connection
// and verifies the remote certificate against the TLSA records. If
// verification succeeds, the verified record is returned.
//
// Different protocols require different usages. For example, SMTP with STARTTLS
// for delivery only allows usages DANE-TA and DANE-EE. If allowedUsages is
// non-nil, only the specified usages are taken into account when verifying, and
// any others ignored.
//
// Errors that can be returned, possibly in wrapped form:
// - ErrNoRecords, also in case the DNS response indicates "not found".
// - adns.DNSError, potentially wrapping adns.ExtendedError of which some can
// indicate DNSSEC errors.
// - ErrInsecure
// - VerifyError, potentially wrapping errors from crypto/x509.
func Dial(ctx context.Context, resolver dns.Resolver, network, address string, allowedUsages []adns.TLSAUsage) (net.Conn, adns.TLSA, error) {
log := mlog.New("dane").WithContext(ctx)
// Split host and port.
host, portstr, err := net.SplitHostPort(address)
if err != nil {
return nil, adns.TLSA{}, fmt.Errorf("parsing address: %w", err)
}
port, err := resolver.LookupPort(ctx, network, portstr)
if err != nil {
return nil, adns.TLSA{}, fmt.Errorf("parsing port: %w", err)
}
hostDom, err := dns.ParseDomain(strings.TrimSuffix(host, "."))
if err != nil {
return nil, adns.TLSA{}, fmt.Errorf("parsing host: %w", err)
}
// ../rfc/7671:1015
// First follow CNAMEs for host. If the path to the final name is secure, we must
// lookup TLSA there first, then fallback to the original name. If the final name
// is secure that's also the SNI server name we must use, with the original name as
// allowed host during certificate name checks (for all TLSA usages other than
// DANE-EE).
cnameDom := hostDom
cnameAuthentic := true
for i := 0; ; i += 1 {
if i == 10 {
return nil, adns.TLSA{}, fmt.Errorf("too many cname lookups")
}
cname, cnameResult, err := resolver.LookupCNAME(ctx, cnameDom.ASCII+".")
cnameAuthentic = cnameAuthentic && cnameResult.Authentic
if !cnameResult.Authentic && i == 0 {
return nil, adns.TLSA{}, fmt.Errorf("%w: cname lookup insecure", ErrInsecure)
} else if dns.IsNotFound(err) {
break
} else if err != nil {
return nil, adns.TLSA{}, fmt.Errorf("resolving cname %s: %w", cnameDom, err)
} else if d, err := dns.ParseDomain(strings.TrimSuffix(cname, ".")); err != nil {
return nil, adns.TLSA{}, fmt.Errorf("parsing cname: %w", err)
} else {
cnameDom = d
}
}
// We lookup the IP.
ipnetwork := "ip"
if strings.HasSuffix(network, "4") {
ipnetwork += "4"
} else if strings.HasSuffix(network, "6") {
ipnetwork += "6"
}
ips, _, err := resolver.LookupIP(ctx, ipnetwork, cnameDom.ASCII+".")
// note: For SMTP with opportunistic DANE we would stop here with an insecure
// response. But as long as long as we have a verified original tlsa base name, we
// can continue with regular DANE.
if err != nil {
return nil, adns.TLSA{}, fmt.Errorf("resolving ips: %w", err)
} else if len(ips) == 0 {
return nil, adns.TLSA{}, &adns.DNSError{Err: "no ips for host", Name: cnameDom.ASCII, IsNotFound: true}
}
// Lookup TLSA records. If resolving CNAME was secure, we try that first. Otherwise
// we try at the secure original domain.
baseDom := hostDom
if cnameAuthentic {
baseDom = cnameDom
}
var records []adns.TLSA
var result adns.Result
for {
var err error
records, result, err = resolver.LookupTLSA(ctx, port, network, baseDom.ASCII+".")
// If no (secure) records can be found at the final cname, and there is an original
// name, try at original name.
// ../rfc/7671:1015
if baseDom != hostDom && (dns.IsNotFound(err) || !result.Authentic) {
baseDom = hostDom
continue
}
if !result.Authentic {
return nil, adns.TLSA{}, ErrInsecure
} else if dns.IsNotFound(err) {
return nil, adns.TLSA{}, ErrNoRecords
} else if err != nil {
return nil, adns.TLSA{}, fmt.Errorf("lookup dane tlsa records: %w", err)
}
break
}
// Keep only the allowed usages.
if allowedUsages != nil {
o := 0
for _, r := range records {
for _, usage := range allowedUsages {
if r.Usage == usage {
records[o] = r
o++
break
}
}
}
records = records[:o]
if len(records) == 0 {
// No point in dialing when we know we won't be able to verify the remote TLS
// certificate.
return nil, adns.TLSA{}, fmt.Errorf("no usable tlsa records remaining: %w", ErrNoMatch)
}
}
// We use the base domain for SNI, allowing the original domain as well.
// ../rfc/7671:1021
var moreAllowedHosts []dns.Domain
if baseDom != hostDom {
moreAllowedHosts = []dns.Domain{hostDom}
}
// Dial the remote host.
timeout := 30 * time.Second
if deadline, ok := ctx.Deadline(); ok && len(ips) > 0 {
timeout = time.Until(deadline) / time.Duration(len(ips))
}
dialer := &net.Dialer{Timeout: timeout}
var conn net.Conn
var dialErrs []error
for _, ip := range ips {
addr := net.JoinHostPort(ip.String(), portstr)
c, err := dialer.DialContext(ctx, network, addr)
if err != nil {
dialErrs = append(dialErrs, err)
continue
}
conn = c
break
}
if conn == nil {
return nil, adns.TLSA{}, errors.Join(dialErrs...)
}
var verifiedRecord adns.TLSA
config := TLSClientConfig(log, records, baseDom, moreAllowedHosts, &verifiedRecord)
tlsConn := tls.Client(conn, &config)
if err := tlsConn.HandshakeContext(ctx); err != nil {
conn.Close()
return nil, adns.TLSA{}, err
}
return tlsConn, verifiedRecord, nil
}
// TLSClientConfig returns a tls.Config to be used for dialing/handshaking a
// TLS connection with DANE verification.
//
// Callers should only pass records that are allowed for the use of DANE. DANE
// with SMTP only allows DANE-EE and DANE-TA usages, not the PKIX-usages.
//
// The config has InsecureSkipVerify set to true, with a custom VerifyConnection
// function for verifying DANE. Its VerifyConnection can return ErrNoMatch and
// additionally one or more (wrapped) errors of type VerifyError.
//
// The TLS config uses allowedHost for SNI.
//
// If verifiedRecord is not nil, it is set to the record that was successfully
// verified, if any.
func TLSClientConfig(log *mlog.Log, records []adns.TLSA, allowedHost dns.Domain, moreAllowedHosts []dns.Domain, verifiedRecord *adns.TLSA) tls.Config {
return tls.Config{
ServerName: allowedHost.ASCII,
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
verified, record, err := Verify(log, records, cs, allowedHost, moreAllowedHosts)
log.Debugx("dane verification", err, mlog.Field("verified", verified), mlog.Field("record", record))
if verified {
if verifiedRecord != nil {
*verifiedRecord = record
}
return nil
} else if err == nil {
return ErrNoMatch
}
return fmt.Errorf("%w, and error(s) encountered during verification: %w", ErrNoMatch, err)
},
MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
}
}
// Verify checks if the TLS connection state can be verified against DANE TLSA
// records.
//
// allowedHost along with the optional moreAllowedHosts are the host names that are
// allowed during certificate verification (as used by PKIX-TA, PKIX-EE, DANE-TA,
// but not DANE-EE). A typical connection would allow just one name, but some uses
// of DANE allow multiple, like SMTP which allow up to four valid names for a TLS
// certificate based on MX/CNAME/TLSA/DNSSEC lookup results.
//
// When one of the records matches, Verify returns true, along with the matching
// record and a nil error.
// If there is no match, then in the typical case false, a zero record value and a
// nil error is returned.
// If an error is encountered while verifying a record, e.g. for x509
// trusted-anchor verification, an error may be returned, typically one or more
// (wrapped) errors of type VerifyError.
func Verify(log *mlog.Log, records []adns.TLSA, cs tls.ConnectionState, allowedHost dns.Domain, moreAllowedHosts []dns.Domain) (verified bool, matching adns.TLSA, rerr error) {
metricVerify.Inc()
if len(records) == 0 {
metricVerifyErrors.Inc()
return false, adns.TLSA{}, fmt.Errorf("verify requires at least one tlsa record")
}
var errs []error
for _, r := range records {
ok, err := verifySingle(log, r, cs, allowedHost, moreAllowedHosts)
if err != nil {
errs = append(errs, VerifyError{err, r})
} else if ok {
return true, r, nil
}
}
metricVerifyErrors.Inc()
return false, adns.TLSA{}, errors.Join(errs...)
}
// verifySingle verifies the TLS connection against a single DANE TLSA record.
//
// If the remote TLS certificate matches with the TLSA record, true is
// returned. Errors may be encountered while verifying, e.g. when checking one
// of the allowed hosts against a TLSA record. A typical non-matching/verified
// TLSA record returns a nil error. But in some cases, e.g. when encountering
// errors while verifying certificates against a trust-anchor, an error can be
// returned with one or more underlying x509 verification errors. A nil-nil error
// is only returned when verified is false.
func verifySingle(log *mlog.Log, tlsa adns.TLSA, cs tls.ConnectionState, allowedHost dns.Domain, moreAllowedHosts []dns.Domain) (verified bool, rerr error) {
if len(cs.PeerCertificates) == 0 {
return false, fmt.Errorf("no server certificate")
}
match := func(cert *x509.Certificate) bool {
var buf []byte
switch tlsa.Selector {
case adns.TLSASelectorCert:
buf = cert.Raw
case adns.TLSASelectorSPKI:
buf = cert.RawSubjectPublicKeyInfo
default:
return false
}
switch tlsa.MatchType {
case adns.TLSAMatchTypeFull:
case adns.TLSAMatchTypeSHA256:
d := sha256.Sum256(buf)
buf = d[:]
case adns.TLSAMatchTypeSHA512:
d := sha512.Sum512(buf)
buf = d[:]
default:
return false
}
return bytes.Equal(buf, tlsa.CertAssoc)
}
pkixVerify := func(host dns.Domain) ([][]*x509.Certificate, error) {
// Default Verify checks for expiration. We pass the host name to check. And we
// configure the intermediates. The roots are filled in by the x509 package.
opts := x509.VerifyOptions{
DNSName: host.ASCII,
Intermediates: x509.NewCertPool(),
Roots: mox.Conf.Static.TLS.CertPool,
}
for _, cert := range cs.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
chains, err := cs.PeerCertificates[0].Verify(opts)
return chains, err
}
switch tlsa.Usage {
case adns.TLSAUsagePKIXTA:
// We cannot get at the system trusted ca certificates to look for the trusted
// anchor. So we just ask Go to verify, then see if any of the chains include the
// ca certificate.
var errs []error
for _, host := range append([]dns.Domain{allowedHost}, moreAllowedHosts...) {
chains, err := pkixVerify(host)
log.Debugx("pkix-ta verify", err)
if err != nil {
errs = append(errs, err)
continue
}
// The chains by x509's Verify should include the longest possible match, so it is
// sure to include the trusted anchor. ../rfc/7671:835
for _, chain := range chains {
// If pkix verified, check if any of the certificates match.
for i := len(chain) - 1; i >= 0; i-- {
if match(chain[i]) {
return true, nil
}
}
}
}
return false, errors.Join(errs...)
case adns.TLSAUsagePKIXEE:
// Check for a certificate match.
if !match(cs.PeerCertificates[0]) {
return false, nil
}
// And do regular pkix checks, ../rfc/7671:799
var errs []error
for _, host := range append([]dns.Domain{allowedHost}, moreAllowedHosts...) {
_, err := pkixVerify(host)
log.Debugx("pkix-ee verify", err)
if err == nil {
return true, nil
}
errs = append(errs, err)
}
return false, errors.Join(errs...)
case adns.TLSAUsageDANETA:
// We set roots, so the system defaults don't get used. Verify checks the host name
// (set below) and checks for expiration.
opts := x509.VerifyOptions{
Roots: x509.NewCertPool(),
}
// If the full certificate was included, we must add it to the valid roots, the TLS
// server may not send it. ../rfc/7671:692
var found bool
if tlsa.Selector == adns.TLSASelectorCert && tlsa.MatchType == adns.TLSAMatchTypeFull {
cert, err := x509.ParseCertificate(tlsa.CertAssoc)
if err != nil {
log.Debugx("parsing full exact certificate from tlsa record to use as root for usage dane-trusted-anchor", err)
// Continue anyway, perhaps the servers sends it again in a way that the tls package can parse? (unlikely)
} else {
opts.Roots.AddCert(cert)
found = true
}
}
for _, cert := range cs.PeerCertificates {
if match(cert) {
opts.Roots.AddCert(cert)
found = true
break
}
}
if !found {
// Trusted anchor was not found in TLS certificates so we won't be able to
// verify.
return false, nil
}
// Trusted anchor was found, still need to verify.
var errs []error
for _, host := range append([]dns.Domain{allowedHost}, moreAllowedHosts...) {
opts.DNSName = host.ASCII
_, err := cs.PeerCertificates[0].Verify(opts)
if err == nil {
return true, nil
}
errs = append(errs, err)
}
return false, errors.Join(errs...)
case adns.TLSAUsageDANEEE:
// ../rfc/7250 is about raw public keys instead of x.509 certificates in tls
// handshakes. Go's crypto/tls does not implement the extension (see
// crypto/tls/common.go, the extensions values don't appear in the
// rfc, but have values 19 and 20 according to
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#tls-extensiontype-values-1
// ../rfc/7671:1148 mentions the raw public keys are allowed. It's still
// questionable that this is commonly implemented. For now the world can probably
// live with an ignored certificate wrapped around the subject public key info.
// We don't verify host name in certificate, ../rfc/7671:489
// And we don't check for expiration. ../rfc/7671:527
// The whole point of this type is to have simple secure infrastructure that
// doesn't automatically expire (at the most inconvenient times).
return match(cs.PeerCertificates[0]), nil
default:
// Unknown, perhaps defined in the future. Not an error.
log.Debug("unrecognized tlsa usage, skipping", mlog.Field("tlsausage", tlsa.Usage))
return false, nil
}
}

479
dane/dane_test.go Normal file
View file

@ -0,0 +1,479 @@
package dane
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
cryptorand "crypto/rand"
"crypto/sha256"
"crypto/sha512"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"fmt"
"math/big"
"net"
"reflect"
"strconv"
"sync/atomic"
"time"
"testing"
"github.com/mjl-/adns"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
)
func tcheckf(t *testing.T, err error, format string, args ...any) {
t.Helper()
if err != nil {
t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
}
}
// Test dialing and DANE TLS verification.
func TestDial(t *testing.T) {
mlog.SetConfig(map[string]mlog.Level{"": mlog.LevelDebug})
// Create fake CA/trusted-anchor certificate.
taTempl := x509.Certificate{
SerialNumber: big.NewInt(1), // Required field.
Subject: pkix.Name{CommonName: "fake ca"},
Issuer: pkix.Name{CommonName: "fake ca"},
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().Add(1 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}
taPriv, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
tcheckf(t, err, "generating trusted-anchor ca private key")
taCertBuf, err := x509.CreateCertificate(cryptorand.Reader, &taTempl, &taTempl, taPriv.Public(), taPriv)
tcheckf(t, err, "create trusted-anchor ca certificate")
taCert, err := x509.ParseCertificate(taCertBuf)
tcheckf(t, err, "parsing generated trusted-anchor ca certificate")
tacertsha256 := sha256.Sum256(taCert.Raw)
taCertSHA256 := tacertsha256[:]
// Generate leaf private key & 2 certs, one expired and one valid, both signed by
// trusted-anchor cert.
leafPriv, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
tcheckf(t, err, "generating leaf private key")
makeLeaf := func(expired bool) (tls.Certificate, []byte, []byte) {
now := time.Now()
if expired {
now = now.Add(-2 * time.Hour)
}
leafTempl := x509.Certificate{
SerialNumber: big.NewInt(1), // Required field.
Issuer: taTempl.Subject,
NotBefore: now.Add(-1 * time.Hour),
NotAfter: now.Add(1 * time.Hour),
DNSNames: []string{"localhost"},
}
leafCertBuf, err := x509.CreateCertificate(cryptorand.Reader, &leafTempl, taCert, leafPriv.Public(), taPriv)
tcheckf(t, err, "create trusted-anchor ca certificate")
leafCert, err := x509.ParseCertificate(leafCertBuf)
tcheckf(t, err, "parsing generated trusted-anchor ca certificate")
leafSPKISHA256 := sha256.Sum256(leafCert.RawSubjectPublicKeyInfo)
leafSPKISHA512 := sha512.Sum512(leafCert.RawSubjectPublicKeyInfo)
tlsLeafCert := tls.Certificate{
Certificate: [][]byte{leafCertBuf, taCertBuf},
PrivateKey: leafPriv, // .(crypto.PrivateKey),
Leaf: leafCert,
}
return tlsLeafCert, leafSPKISHA256[:], leafSPKISHA512[:]
}
tlsLeafCert, leafSPKISHA256, leafSPKISHA512 := makeLeaf(false)
tlsLeafCertExpired, _, _ := makeLeaf(true)
// Set up loopback tls server.
listenConn, err := net.Listen("tcp", "127.0.0.1:0")
tcheckf(t, err, "listen for test server")
addr := listenConn.Addr().String()
_, portstr, err := net.SplitHostPort(addr)
tcheckf(t, err, "get localhost port")
uport, err := strconv.ParseUint(portstr, 10, 16)
tcheckf(t, err, "parse localhost port")
port := int(uport)
defer listenConn.Close()
// Config for server, replaced during tests.
var tlsConfig atomic.Pointer[tls.Config]
tlsConfig.Store(&tls.Config{
Certificates: []tls.Certificate{tlsLeafCert},
})
// Loop handling incoming TLS connections.
go func() {
for {
conn, err := listenConn.Accept()
if err != nil {
return
}
tlsConn := tls.Server(conn, tlsConfig.Load())
tlsConn.Handshake()
tlsConn.Close()
}
}()
dialHost := "localhost"
var allowedUsages []adns.TLSAUsage
// Helper function for dialing with DANE.
test := func(resolver dns.Resolver, expRecord adns.TLSA, expErr any) {
t.Helper()
conn, record, err := Dial(context.Background(), resolver, "tcp", net.JoinHostPort(dialHost, portstr), allowedUsages)
if err == nil {
conn.Close()
}
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr.(error)) && !errors.As(err, expErr) {
t.Fatalf("got err %v (%#v), expected %#v", err, err, expErr)
}
if !reflect.DeepEqual(record, expRecord) {
t.Fatalf("got verified record %v, expected %v", record, expRecord)
}
}
tlsaName := fmt.Sprintf("_%d._tcp.localhost.", port)
// Make all kinds of records, some invalid or non-matching.
var zeroRecord adns.TLSA
recordDANEEESPKISHA256 := adns.TLSA{
Usage: adns.TLSAUsageDANEEE,
Selector: adns.TLSASelectorSPKI,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: leafSPKISHA256,
}
recordDANEEESPKISHA512 := adns.TLSA{
Usage: adns.TLSAUsageDANEEE,
Selector: adns.TLSASelectorSPKI,
MatchType: adns.TLSAMatchTypeSHA512,
CertAssoc: leafSPKISHA512,
}
recordDANEEESPKIFull := adns.TLSA{
Usage: adns.TLSAUsageDANEEE,
Selector: adns.TLSASelectorSPKI,
MatchType: adns.TLSAMatchTypeFull,
CertAssoc: tlsLeafCert.Leaf.RawSubjectPublicKeyInfo,
}
mismatchRecordDANEEESPKISHA256 := adns.TLSA{
Usage: adns.TLSAUsageDANEEE,
Selector: adns.TLSASelectorSPKI,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: make([]byte, sha256.Size), // Zero, no match.
}
malformedRecordDANEEESPKISHA256 := adns.TLSA{
Usage: adns.TLSAUsageDANEEE,
Selector: adns.TLSASelectorSPKI,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: leafSPKISHA256[:16], // Too short.
}
unknownparamRecordDANEEESPKISHA256 := adns.TLSA{
Usage: adns.TLSAUsage(10), // Unrecognized value.
Selector: adns.TLSASelectorSPKI,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: leafSPKISHA256,
}
recordDANETACertSHA256 := adns.TLSA{
Usage: adns.TLSAUsageDANETA,
Selector: adns.TLSASelectorCert,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: taCertSHA256,
}
recordDANETACertFull := adns.TLSA{
Usage: adns.TLSAUsageDANETA,
Selector: adns.TLSASelectorCert,
MatchType: adns.TLSAMatchTypeFull,
CertAssoc: taCert.Raw,
}
malformedRecordDANETACertFull := adns.TLSA{
Usage: adns.TLSAUsageDANETA,
Selector: adns.TLSASelectorCert,
MatchType: adns.TLSAMatchTypeFull,
CertAssoc: taCert.Raw[1:], // Cannot parse certificate.
}
mismatchRecordDANETACertSHA256 := adns.TLSA{
Usage: adns.TLSAUsageDANETA,
Selector: adns.TLSASelectorCert,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: make([]byte, sha256.Size), // Zero, no match.
}
recordPKIXEESPKISHA256 := adns.TLSA{
Usage: adns.TLSAUsagePKIXEE,
Selector: adns.TLSASelectorSPKI,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: leafSPKISHA256,
}
recordPKIXTACertSHA256 := adns.TLSA{
Usage: adns.TLSAUsagePKIXTA,
Selector: adns.TLSASelectorCert,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: taCertSHA256,
}
resolver := dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordDANEEESPKISHA256}},
AllAuthentic: true,
}
// DANE-EE SPKI SHA2-256 record.
test(resolver, recordDANEEESPKISHA256, nil)
// Check that record isn't used if not allowed.
allowedUsages = []adns.TLSAUsage{adns.TLSAUsagePKIXTA}
test(resolver, zeroRecord, ErrNoMatch)
allowedUsages = nil // Restore.
// Mixed allowed/not allowed usages are fine.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {mismatchRecordDANETACertSHA256, recordDANEEESPKISHA256}},
AllAuthentic: true,
}
allowedUsages = []adns.TLSAUsage{adns.TLSAUsageDANEEE}
test(resolver, recordDANEEESPKISHA256, nil)
allowedUsages = nil // Restore.
// DANE-TA CERT SHA2-256 record.
resolver.TLSA = map[string][]adns.TLSA{
tlsaName: {recordDANETACertSHA256},
}
test(resolver, recordDANETACertSHA256, nil)
// No TLSA record.
resolver.TLSA = nil
test(resolver, zeroRecord, ErrNoRecords)
// Insecure TLSA record.
resolver.TLSA = map[string][]adns.TLSA{
tlsaName: {recordDANEEESPKISHA256},
}
resolver.Inauthentic = []string{"tlsa " + tlsaName}
test(resolver, zeroRecord, ErrInsecure)
// Insecure CNAME.
resolver.Inauthentic = []string{"cname localhost."}
test(resolver, zeroRecord, ErrInsecure)
// Insecure TLSA
resolver.Inauthentic = []string{"tlsa " + tlsaName}
test(resolver, zeroRecord, ErrInsecure)
// Insecure CNAME should not look at TLSA records under that name, only under original.
// Initial name/cname is secure. And it has secure TLSA records. But the lookup for
// example1 is not secure, though the final example2 records are.
resolver = dns.MockResolver{
A: map[string][]string{"example2.": {"127.0.0.1"}},
CNAME: map[string]string{"localhost.": "example1.", "example1.": "example2."},
TLSA: map[string][]adns.TLSA{
fmt.Sprintf("_%d._tcp.example2.", port): {mismatchRecordDANETACertSHA256}, // Should be ignored.
tlsaName: {recordDANEEESPKISHA256}, // Should match.
},
AllAuthentic: true,
Inauthentic: []string{"cname example1."},
}
test(resolver, recordDANEEESPKISHA256, nil)
// Matching records after following cname.
resolver = dns.MockResolver{
A: map[string][]string{"example.": {"127.0.0.1"}},
CNAME: map[string]string{"localhost.": "example."},
TLSA: map[string][]adns.TLSA{fmt.Sprintf("_%d._tcp.example.", port): {recordDANETACertSHA256}},
AllAuthentic: true,
}
test(resolver, recordDANETACertSHA256, nil)
// Fallback to original name for TLSA records if cname-expanded name doesn't have records.
resolver = dns.MockResolver{
A: map[string][]string{"example.": {"127.0.0.1"}},
CNAME: map[string]string{"localhost.": "example."},
TLSA: map[string][]adns.TLSA{tlsaName: {recordDANETACertSHA256}},
AllAuthentic: true,
}
test(resolver, recordDANETACertSHA256, nil)
// Invalid DANE-EE record.
resolver = dns.MockResolver{
A: map[string][]string{
"localhost.": {"127.0.0.1"},
},
TLSA: map[string][]adns.TLSA{
tlsaName: {mismatchRecordDANEEESPKISHA256},
},
AllAuthentic: true,
}
test(resolver, zeroRecord, ErrNoMatch)
// DANE-EE SPKI SHA2-512 record.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordDANEEESPKISHA512}},
AllAuthentic: true,
}
test(resolver, recordDANEEESPKISHA512, nil)
// DANE-EE SPKI Full record.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordDANEEESPKIFull}},
AllAuthentic: true,
}
test(resolver, recordDANEEESPKIFull, nil)
// DANE-TA with full certificate.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordDANETACertFull}},
AllAuthentic: true,
}
test(resolver, recordDANETACertFull, nil)
// DANE-TA for cert not in TLS handshake.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {mismatchRecordDANETACertSHA256}},
AllAuthentic: true,
}
test(resolver, zeroRecord, ErrNoMatch)
// DANE-TA with leaf cert for other name.
resolver = dns.MockResolver{
A: map[string][]string{"example.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{fmt.Sprintf("_%d._tcp.example.", port): {recordDANETACertSHA256}},
AllAuthentic: true,
}
origDialHost := dialHost
dialHost = "example."
test(resolver, zeroRecord, ErrNoMatch)
dialHost = origDialHost
// DANE-TA with expired cert.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordDANETACertSHA256}},
AllAuthentic: true,
}
tlsConfig.Store(&tls.Config{
Certificates: []tls.Certificate{tlsLeafCertExpired},
})
test(resolver, zeroRecord, ErrNoMatch)
test(resolver, zeroRecord, &VerifyError{})
test(resolver, zeroRecord, &x509.CertificateInvalidError{})
// Restore.
tlsConfig.Store(&tls.Config{
Certificates: []tls.Certificate{tlsLeafCert},
})
// Malformed TLSA record is unusable, resulting in failure if none left.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {malformedRecordDANEEESPKISHA256}},
AllAuthentic: true,
}
test(resolver, zeroRecord, ErrNoMatch)
// Malformed TLSA record is unusable and skipped, other verified record causes Dial to succeed.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {malformedRecordDANEEESPKISHA256, recordDANEEESPKISHA256}},
AllAuthentic: true,
}
test(resolver, recordDANEEESPKISHA256, nil)
// Record with unknown parameters (usage in this case) is unusable, resulting in failure if none left.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {unknownparamRecordDANEEESPKISHA256}},
AllAuthentic: true,
}
test(resolver, zeroRecord, ErrNoMatch)
// Unknown parameter does not prevent other valid record to verify.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {unknownparamRecordDANEEESPKISHA256, recordDANEEESPKISHA256}},
AllAuthentic: true,
}
test(resolver, recordDANEEESPKISHA256, nil)
// Malformed full TA certificate.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {malformedRecordDANETACertFull}},
AllAuthentic: true,
}
test(resolver, zeroRecord, ErrNoMatch)
// Full TA certificate without getting it from TLS server.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordDANETACertFull}},
AllAuthentic: true,
}
tlsLeafOnlyCert := tlsLeafCert
tlsLeafOnlyCert.Certificate = tlsLeafOnlyCert.Certificate[:1]
tlsConfig.Store(&tls.Config{
Certificates: []tls.Certificate{tlsLeafOnlyCert},
})
test(resolver, recordDANETACertFull, nil)
// Restore.
tlsConfig.Store(&tls.Config{
Certificates: []tls.Certificate{tlsLeafCert},
})
// PKIXEE, will fail due to not being CA-signed.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordPKIXEESPKISHA256}},
AllAuthentic: true,
}
test(resolver, zeroRecord, &x509.UnknownAuthorityError{})
// PKIXTA, will fail due to not being CA-signed.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordPKIXTACertSHA256}},
AllAuthentic: true,
}
test(resolver, zeroRecord, &x509.UnknownAuthorityError{})
// Now we add the TA to the "system" trusted roots and try again.
pool, err := x509.SystemCertPool()
tcheckf(t, err, "get system certificate pool")
mox.Conf.Static.TLS.CertPool = pool
pool.AddCert(taCert)
// PKIXEE, will now succeed.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordPKIXEESPKISHA256}},
AllAuthentic: true,
}
test(resolver, recordPKIXEESPKISHA256, nil)
// PKIXTA, will fail due to not being CA-signed.
resolver = dns.MockResolver{
A: map[string][]string{"localhost.": {"127.0.0.1"}},
TLSA: map[string][]adns.TLSA{tlsaName: {recordPKIXTACertSHA256}},
AllAuthentic: true,
}
test(resolver, recordPKIXTACertSHA256, nil)
}

View file

@ -38,7 +38,7 @@ import (
var xlog = mlog.New("dkim")
var (
metricDKIMSign = promauto.NewCounterVec(
metricSign = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "mox_dkim_sign_total",
Help: "DKIM messages signings, label key is the type of key, rsa or ed25519.",
@ -47,7 +47,7 @@ var (
"key",
},
)
metricDKIMVerify = promauto.NewHistogramVec(
metricVerify = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "mox_dkim_verify_duration_seconds",
Help: "DKIM verify, including lookup, duration and result.",
@ -113,10 +113,11 @@ var (
// To decide what to do with a message, both the signature parameters and the DNS
// TXT record have to be consulted.
type Result struct {
Status Status
Sig *Sig // Parsed form of DKIM-Signature header. Can be nil for invalid DKIM-Signature header.
Record *Record // Parsed form of DKIM DNS record for selector and domain in Sig. Optional.
Err error // If Status is not StatusPass, this error holds the details and can be checked using errors.Is.
Status Status
Sig *Sig // Parsed form of DKIM-Signature header. Can be nil for invalid DKIM-Signature header.
Record *Record // Parsed form of DKIM DNS record for selector and domain in Sig. Optional.
RecordAuthentic bool // Whether DKIM DNS record was DNSSEC-protected. Only valid if Sig is non-nil.
Err error // If Status is not StatusPass, this error holds the details and can be checked using errors.Is.
}
// todo: use some io.Writer to hash the body and the header.
@ -157,10 +158,10 @@ func Sign(ctx context.Context, localpart smtp.Localpart, domain dns.Domain, c co
switch sel.Key.(type) {
case *rsa.PrivateKey:
sig.AlgorithmSign = "rsa"
metricDKIMSign.WithLabelValues("rsa").Inc()
metricSign.WithLabelValues("rsa").Inc()
case ed25519.PrivateKey:
sig.AlgorithmSign = "ed25519"
metricDKIMSign.WithLabelValues("ed25519").Inc()
metricSign.WithLabelValues("ed25519").Inc()
default:
return "", fmt.Errorf("internal error, unknown pivate key %T", sel.Key)
}
@ -267,7 +268,9 @@ func Sign(ctx context.Context, localpart smtp.Localpart, domain dns.Domain, c co
//
// A requested record is <selector>._domainkey.<domain>. Exactly one valid DKIM
// record should be present.
func Lookup(ctx context.Context, resolver dns.Resolver, selector, domain dns.Domain) (rstatus Status, rrecord *Record, rtxt string, rerr error) {
//
// authentic indicates if DNS results were DNSSEC-verified.
func Lookup(ctx context.Context, resolver dns.Resolver, selector, domain dns.Domain) (rstatus Status, rrecord *Record, rtxt string, authentic bool, rerr error) {
log := xlog.WithContext(ctx)
start := timeNow()
defer func() {
@ -275,14 +278,14 @@ func Lookup(ctx context.Context, resolver dns.Resolver, selector, domain dns.Dom
}()
name := selector.ASCII + "._domainkey." + domain.ASCII + "."
records, err := dns.WithPackage(resolver, "dkim").LookupTXT(ctx, name)
records, lookupResult, err := dns.WithPackage(resolver, "dkim").LookupTXT(ctx, name)
if dns.IsNotFound(err) {
// ../rfc/6376:2608
// We must return StatusPermerror. We may want to return StatusTemperror because in
// practice someone will start using a new key before DNS changes have propagated.
return StatusPermerror, nil, "", fmt.Errorf("%w: dns name %q", ErrNoRecord, name)
return StatusPermerror, nil, "", lookupResult.Authentic, fmt.Errorf("%w: dns name %q", ErrNoRecord, name)
} else if err != nil {
return StatusTemperror, nil, "", fmt.Errorf("%w: dns name %q: %s", ErrDNS, name, err)
return StatusTemperror, nil, "", lookupResult.Authentic, fmt.Errorf("%w: dns name %q: %s", ErrDNS, name, err)
}
// ../rfc/6376:2612
@ -298,7 +301,7 @@ func Lookup(ctx context.Context, resolver dns.Resolver, selector, domain dns.Dom
var isdkim bool
r, isdkim, err = ParseRecord(s)
if err != nil && isdkim {
return StatusPermerror, nil, txt, fmt.Errorf("%w: %s", ErrSyntax, err)
return StatusPermerror, nil, txt, lookupResult.Authentic, fmt.Errorf("%w: %s", ErrSyntax, err)
} else if err != nil {
// Hopefully the remote MTA admin discovers the configuration error and fix it for
// an upcoming delivery attempt, in case we rejected with temporary status.
@ -310,7 +313,7 @@ func Lookup(ctx context.Context, resolver dns.Resolver, selector, domain dns.Dom
// ../rfc/6376:1609
// ../rfc/6376:2584
if record != nil {
return StatusTemperror, nil, "", fmt.Errorf("%w: dns name %q", ErrMultipleRecords, name)
return StatusTemperror, nil, "", lookupResult.Authentic, fmt.Errorf("%w: dns name %q", ErrMultipleRecords, name)
}
record = r
txt = s
@ -318,9 +321,9 @@ func Lookup(ctx context.Context, resolver dns.Resolver, selector, domain dns.Dom
}
if record == nil {
return status, nil, "", err
return status, nil, "", lookupResult.Authentic, err
}
return StatusNeutral, record, txt, nil
return StatusNeutral, record, txt, lookupResult.Authentic, nil
}
// Verify parses the DKIM-Signature headers in a message and verifies each of them.
@ -346,7 +349,7 @@ func Verify(ctx context.Context, resolver dns.Resolver, smtputf8 bool, policy fu
alg = r.Sig.Algorithm()
}
status := string(r.Status)
metricDKIMVerify.WithLabelValues(alg, status).Observe(duration)
metricVerify.WithLabelValues(alg, status).Observe(duration)
}
if len(results) == 0 {
@ -373,26 +376,26 @@ func Verify(ctx context.Context, resolver dns.Resolver, smtputf8 bool, policy fu
if err != nil {
// ../rfc/6376:2503
err := fmt.Errorf("parsing DKIM-Signature header: %w", err)
results = append(results, Result{StatusPermerror, nil, nil, err})
results = append(results, Result{StatusPermerror, nil, nil, false, err})
continue
}
h, canonHeaderSimple, canonDataSimple, err := checkSignatureParams(ctx, sig)
if err != nil {
results = append(results, Result{StatusPermerror, nil, nil, err})
results = append(results, Result{StatusPermerror, nil, nil, false, err})
continue
}
// ../rfc/6376:2560
if err := policy(sig); err != nil {
err := fmt.Errorf("%w: %s", ErrPolicy, err)
results = append(results, Result{StatusPolicy, nil, nil, err})
results = append(results, Result{StatusPolicy, nil, nil, false, err})
continue
}
br := bufio.NewReader(&moxio.AtReader{R: r, Offset: int64(bodyOffset)})
status, txt, err := verifySignature(ctx, resolver, sig, h, canonHeaderSimple, canonDataSimple, hdrs, verifySig, br, ignoreTestMode)
results = append(results, Result{status, sig, txt, err})
status, txt, authentic, err := verifySignature(ctx, resolver, sig, h, canonHeaderSimple, canonDataSimple, hdrs, verifySig, br, ignoreTestMode)
results = append(results, Result{status, sig, txt, authentic, err})
}
return results, nil
}
@ -477,15 +480,15 @@ func checkSignatureParams(ctx context.Context, sig *Sig) (hash crypto.Hash, cano
}
// lookup the public key in the DNS and verify the signature.
func verifySignature(ctx context.Context, resolver dns.Resolver, sig *Sig, hash crypto.Hash, canonHeaderSimple, canonDataSimple bool, hdrs []header, verifySig []byte, body *bufio.Reader, ignoreTestMode bool) (Status, *Record, error) {
func verifySignature(ctx context.Context, resolver dns.Resolver, sig *Sig, hash crypto.Hash, canonHeaderSimple, canonDataSimple bool, hdrs []header, verifySig []byte, body *bufio.Reader, ignoreTestMode bool) (Status, *Record, bool, error) {
// ../rfc/6376:2604
status, record, _, err := Lookup(ctx, resolver, sig.Selector, sig.Domain)
status, record, _, authentic, err := Lookup(ctx, resolver, sig.Selector, sig.Domain)
if err != nil {
// todo: for temporary errors, we could pass on information so caller returns a 4.7.5 ecode, ../rfc/6376:2777
return status, nil, err
return status, nil, authentic, err
}
status, err = verifySignatureRecord(record, sig, hash, canonHeaderSimple, canonDataSimple, hdrs, verifySig, body, ignoreTestMode)
return status, record, err
return status, record, authentic, err
}
// verify a DKIM signature given the record from dns and signature from the email message.

View file

@ -81,6 +81,8 @@ type Result struct {
Domain dns.Domain
// Parsed DMARC record.
Record *Record
// Whether DMARC DNS response was DNSSEC-signed, regardless of whether SPF/DKIM records were DNSSEC-signed.
RecordAuthentic bool
// Details about possible error condition, e.g. when parsing the DMARC record failed.
Err error
}
@ -93,7 +95,9 @@ type Result struct {
// domain is determined using the public suffix list. E.g. for
// "sub.example.com", the organizational domain is "example.com". The returned
// domain is the domain with the DMARC record.
func Lookup(ctx context.Context, resolver dns.Resolver, from dns.Domain) (status Status, domain dns.Domain, record *Record, txt string, rerr error) {
//
// rauthentic indicates if the DNS results were DNSSEC-verified.
func Lookup(ctx context.Context, resolver dns.Resolver, from dns.Domain) (status Status, domain dns.Domain, record *Record, txt string, rauthentic bool, rerr error) {
log := xlog.WithContext(ctx)
start := time.Now()
defer func() {
@ -102,27 +106,29 @@ func Lookup(ctx context.Context, resolver dns.Resolver, from dns.Domain) (status
// ../rfc/7489:859 ../rfc/7489:1370
domain = from
status, record, txt, err := lookupRecord(ctx, resolver, domain)
status, record, txt, authentic, err := lookupRecord(ctx, resolver, domain)
if status != StatusNone {
return status, domain, record, txt, err
return status, domain, record, txt, authentic, err
}
if record == nil {
// ../rfc/7489:761 ../rfc/7489:1377
domain = publicsuffix.Lookup(ctx, from)
if domain == from {
return StatusNone, domain, nil, txt, err
return StatusNone, domain, nil, txt, authentic, err
}
status, record, txt, err = lookupRecord(ctx, resolver, domain)
var xauth bool
status, record, txt, xauth, err = lookupRecord(ctx, resolver, domain)
authentic = authentic && xauth
}
return status, domain, record, txt, err
return status, domain, record, txt, authentic, err
}
func lookupRecord(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (Status, *Record, string, error) {
func lookupRecord(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (Status, *Record, string, bool, error) {
name := "_dmarc." + domain.ASCII + "."
txts, err := dns.WithPackage(resolver, "dmarc").LookupTXT(ctx, name)
txts, result, err := dns.WithPackage(resolver, "dmarc").LookupTXT(ctx, name)
if err != nil && !dns.IsNotFound(err) {
return StatusTemperror, nil, "", fmt.Errorf("%w: %s", ErrDNS, err)
return StatusTemperror, nil, "", result.Authentic, fmt.Errorf("%w: %s", ErrDNS, err)
}
var record *Record
var text string
@ -133,24 +139,24 @@ func lookupRecord(ctx context.Context, resolver dns.Resolver, domain dns.Domain)
// ../rfc/7489:1374
continue
} else if err != nil {
return StatusPermerror, nil, text, fmt.Errorf("%w: %s", ErrSyntax, err)
return StatusPermerror, nil, text, result.Authentic, fmt.Errorf("%w: %s", ErrSyntax, err)
}
if record != nil {
// ../ ../rfc/7489:1388
return StatusNone, nil, "", ErrMultipleRecords
return StatusNone, nil, "", result.Authentic, ErrMultipleRecords
}
text = txt
record = r
rerr = nil
}
return StatusNone, record, text, rerr
return StatusNone, record, text, result.Authentic, rerr
}
func lookupReportsRecord(ctx context.Context, resolver dns.Resolver, dmarcDomain, extDestDomain dns.Domain) (Status, *Record, string, error) {
func lookupReportsRecord(ctx context.Context, resolver dns.Resolver, dmarcDomain, extDestDomain dns.Domain) (Status, *Record, string, bool, error) {
name := dmarcDomain.ASCII + "._report._dmarc." + extDestDomain.ASCII + "."
txts, err := dns.WithPackage(resolver, "dmarc").LookupTXT(ctx, name)
txts, result, err := dns.WithPackage(resolver, "dmarc").LookupTXT(ctx, name)
if err != nil && !dns.IsNotFound(err) {
return StatusTemperror, nil, "", fmt.Errorf("%w: %s", ErrDNS, err)
return StatusTemperror, nil, "", result.Authentic, fmt.Errorf("%w: %s", ErrDNS, err)
}
var record *Record
var text string
@ -168,17 +174,17 @@ func lookupReportsRecord(ctx context.Context, resolver dns.Resolver, dmarcDomain
// ../rfc/7489:1374
continue
} else if err != nil {
return StatusPermerror, nil, text, fmt.Errorf("%w: %s", ErrSyntax, err)
return StatusPermerror, nil, text, result.Authentic, fmt.Errorf("%w: %s", ErrSyntax, err)
}
if record != nil {
// ../ ../rfc/7489:1388
return StatusNone, nil, "", ErrMultipleRecords
return StatusNone, nil, "", result.Authentic, ErrMultipleRecords
}
text = txt
record = r
rerr = nil
}
return StatusNone, record, text, rerr
return StatusNone, record, text, result.Authentic, rerr
}
// LookupExternalReportsAccepted returns whether the extDestDomain has opted in
@ -191,16 +197,18 @@ func lookupReportsRecord(ctx context.Context, resolver dns.Resolver, dmarcDomain
//
// The normally invalid "v=DMARC1" record is accepted since it is used as
// example in RFC 7489.
func LookupExternalReportsAccepted(ctx context.Context, resolver dns.Resolver, dmarcDomain dns.Domain, extDestDomain dns.Domain) (accepts bool, status Status, record *Record, txt string, rerr error) {
//
// authentic indicates if the DNS results were DNSSEC-verified.
func LookupExternalReportsAccepted(ctx context.Context, resolver dns.Resolver, dmarcDomain dns.Domain, extDestDomain dns.Domain) (accepts bool, status Status, record *Record, txt string, authentic bool, rerr error) {
log := xlog.WithContext(ctx)
start := time.Now()
defer func() {
log.Debugx("dmarc externalreports result", rerr, mlog.Field("accepts", accepts), mlog.Field("dmarcdomain", dmarcDomain), mlog.Field("extdestdomain", extDestDomain), mlog.Field("record", record), mlog.Field("duration", time.Since(start)))
}()
status, record, txt, rerr = lookupReportsRecord(ctx, resolver, dmarcDomain, extDestDomain)
status, record, txt, authentic, rerr = lookupReportsRecord(ctx, resolver, dmarcDomain, extDestDomain)
accepts = rerr == nil
return accepts, status, record, txt, rerr
return accepts, status, record, txt, authentic, rerr
}
// Verify evaluates the DMARC policy for the domain in the From-header of a
@ -231,12 +239,13 @@ func Verify(ctx context.Context, resolver dns.Resolver, from dns.Domain, dkimRes
log.Debugx("dmarc verify result", result.Err, mlog.Field("fromdomain", from), mlog.Field("dkimresults", dkimResults), mlog.Field("spfresult", spfResult), mlog.Field("status", result.Status), mlog.Field("reject", result.Reject), mlog.Field("use", useResult), mlog.Field("duration", time.Since(start)))
}()
status, recordDomain, record, _, err := Lookup(ctx, resolver, from)
status, recordDomain, record, _, authentic, err := Lookup(ctx, resolver, from)
if record == nil {
return false, Result{false, status, recordDomain, record, err}
return false, Result{false, status, recordDomain, record, authentic, err}
}
result.Domain = recordDomain
result.Record = record
result.RecordAuthentic = authentic
// Record can request sampling of messages to apply policy.
// See ../rfc/7489:1432

View file

@ -29,7 +29,7 @@ func TestLookup(t *testing.T) {
test := func(d string, expStatus Status, expDomain string, expRecord *Record, expErr error) {
t.Helper()
status, dom, record, _, err := Lookup(context.Background(), resolver, dns.Domain{ASCII: d})
status, dom, record, _, _, err := Lookup(context.Background(), resolver, dns.Domain{ASCII: d})
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
t.Fatalf("got err %#v, expected %#v", err, expErr)
}
@ -68,7 +68,7 @@ func TestLookupExternalReportsAccepted(t *testing.T) {
test := func(dom, extdom string, expStatus Status, expAccepts bool, expErr error) {
t.Helper()
accepts, status, _, _, err := LookupExternalReportsAccepted(context.Background(), resolver, dns.Domain{ASCII: dom}, dns.Domain{ASCII: extdom})
accepts, status, _, _, _, err := LookupExternalReportsAccepted(context.Background(), resolver, dns.Domain{ASCII: dom}, dns.Domain{ASCII: extdom})
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
t.Fatalf("got err %#v, expected %#v", err, expErr)
}
@ -137,7 +137,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusNone,
nil,
true, Result{true, StatusFail, dns.Domain{ASCII: "reject.example"}, &reject, nil},
true, Result{true, StatusFail, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
)
// Accept with spf pass.
@ -145,7 +145,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusPass,
&dns.Domain{ASCII: "sub.reject.example"},
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, nil},
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
)
// Accept with dkim pass.
@ -161,7 +161,7 @@ func TestVerify(t *testing.T) {
},
spf.StatusFail,
&dns.Domain{ASCII: "reject.example"},
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, nil},
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
)
// Reject due to spf and dkim "strict".
@ -181,7 +181,7 @@ func TestVerify(t *testing.T) {
},
spf.StatusPass,
&dns.Domain{ASCII: "sub.strict.example"},
true, Result{true, StatusFail, dns.Domain{ASCII: "strict.example"}, &strict, nil},
true, Result{true, StatusFail, dns.Domain{ASCII: "strict.example"}, &strict, false, nil},
)
// No dmarc policy, nothing to say.
@ -189,7 +189,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusNone,
nil,
false, Result{false, StatusNone, dns.Domain{ASCII: "absent.example"}, nil, ErrNoRecord},
false, Result{false, StatusNone, dns.Domain{ASCII: "absent.example"}, nil, false, ErrNoRecord},
)
// No dmarc policy, spf pass does nothing.
@ -197,7 +197,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusPass,
&dns.Domain{ASCII: "absent.example"},
false, Result{false, StatusNone, dns.Domain{ASCII: "absent.example"}, nil, ErrNoRecord},
false, Result{false, StatusNone, dns.Domain{ASCII: "absent.example"}, nil, false, ErrNoRecord},
)
none := DefaultRecord
@ -207,7 +207,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusPass,
&dns.Domain{ASCII: "none.example"},
true, Result{false, StatusPass, dns.Domain{ASCII: "none.example"}, &none, nil},
true, Result{false, StatusPass, dns.Domain{ASCII: "none.example"}, &none, false, nil},
)
// No actual reject due to pct=0.
@ -218,7 +218,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusNone,
nil,
false, Result{true, StatusFail, dns.Domain{ASCII: "test.example"}, &testr, nil},
false, Result{true, StatusFail, dns.Domain{ASCII: "test.example"}, &testr, false, nil},
)
// No reject if subdomain has "none" policy.
@ -229,7 +229,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusFail,
&dns.Domain{ASCII: "sub.subnone.example"},
true, Result{false, StatusFail, dns.Domain{ASCII: "subnone.example"}, &sub, nil},
true, Result{false, StatusFail, dns.Domain{ASCII: "subnone.example"}, &sub, false, nil},
)
// No reject if spf temperror and no other pass.
@ -237,7 +237,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusTemperror,
&dns.Domain{ASCII: "mail.reject.example"},
true, Result{false, StatusTemperror, dns.Domain{ASCII: "reject.example"}, &reject, nil},
true, Result{false, StatusTemperror, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
)
// No reject if dkim temperror and no other pass.
@ -253,7 +253,7 @@ func TestVerify(t *testing.T) {
},
spf.StatusNone,
nil,
true, Result{false, StatusTemperror, dns.Domain{ASCII: "reject.example"}, &reject, nil},
true, Result{false, StatusTemperror, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
)
// No reject if spf temperror but still dkim pass.
@ -269,7 +269,7 @@ func TestVerify(t *testing.T) {
},
spf.StatusTemperror,
&dns.Domain{ASCII: "mail.reject.example"},
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, nil},
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
)
// No reject if dkim temperror but still spf pass.
@ -285,7 +285,7 @@ func TestVerify(t *testing.T) {
},
spf.StatusPass,
&dns.Domain{ASCII: "mail.reject.example"},
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, nil},
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
)
// Bad DMARC record results in permerror without reject.
@ -293,7 +293,7 @@ func TestVerify(t *testing.T) {
[]dkim.Result{},
spf.StatusNone,
nil,
false, Result{false, StatusPermerror, dns.Domain{ASCII: "malformed.example"}, nil, ErrSyntax},
false, Result{false, StatusPermerror, dns.Domain{ASCII: "malformed.example"}, nil, false, ErrSyntax},
)
// DKIM domain that is higher-level than organizational can not result in a pass. ../rfc/7489:525
@ -309,6 +309,6 @@ func TestVerify(t *testing.T) {
},
spf.StatusNone,
nil,
true, Result{true, StatusFail, dns.Domain{ASCII: "example.com"}, &reject, nil},
true, Result{true, StatusFail, dns.Domain{ASCII: "example.com"}, &reject, false, nil},
)
}

View file

@ -5,10 +5,11 @@ package dns
import (
"errors"
"fmt"
"net"
"strings"
"golang.org/x/net/idna"
"github.com/mjl-/adns"
)
var errTrailingDot = errors.New("dns name has trailing dot")
@ -100,16 +101,16 @@ func ParseDomain(s string) (Domain, error) {
return Domain{ascii, unicode}, nil
}
// IsNotFound returns whether an error is a net.DNSError with IsNotFound set.
// IsNotFound returns whether an error is an adns.DNSError with IsNotFound set.
// IsNotFound means the requested type does not exist for the given domain (a
// nodata or nxdomain response). It doesn't not necessarily mean no other types
// for that name exist.
// nodata or nxdomain response). It doesn't not necessarily mean no other types for
// that name exist.
//
// A DNS server can respond to a lookup with an error "nxdomain" to indicate a
// name does not exist (at all), or with a success status with an empty list.
// The Go resolver returns an IsNotFound error for both cases, there is no need
// to explicitly check for zero entries.
func IsNotFound(err error) bool {
var dnsErr *net.DNSError
var dnsErr *adns.DNSError
return err != nil && errors.As(err, &dnsErr) && dnsErr.IsNotFound
}

View file

@ -4,139 +4,185 @@ import (
"context"
"fmt"
"net"
"golang.org/x/exp/slices"
"github.com/mjl-/adns"
)
// MockResolver is a Resolver used for testing.
// Set DNS records in the fields, which map FQDNs (with trailing dot) to values.
type MockResolver struct {
PTR map[string][]string
A map[string][]string
AAAA map[string][]string
TXT map[string][]string
MX map[string][]*net.MX
CNAME map[string]string
Fail map[Mockreq]struct{}
PTR map[string][]string
A map[string][]string
AAAA map[string][]string
TXT map[string][]string
MX map[string][]*net.MX
TLSA map[string][]adns.TLSA // Keys are e.g. _25._tcp.<host>.
CNAME map[string]string
Fail map[Mockreq]struct{}
AllAuthentic bool // Default value for authentic in responses. Overridden with Authentic and Inauthentic
Authentic []string // Records of the form "type name", e.g. "cname localhost."
Inauthentic []string
}
type Mockreq struct {
Type string // E.g. "cname", "txt", "mx", "ptr", etc.
Name string
Name string // Name of request. For TLSA, the full requested DNS name, e.g. _25._tcp.<host>.
}
var _ Resolver = MockResolver{}
func (r MockResolver) nxdomain(s string) *net.DNSError {
return &net.DNSError{
func (r MockResolver) result(ctx context.Context, mr Mockreq) (string, adns.Result, error) {
result := adns.Result{Authentic: r.AllAuthentic}
if err := ctx.Err(); err != nil {
return "", result, err
}
updateAuthentic := func(mock string) {
if slices.Contains(r.Authentic, mock) {
result.Authentic = true
}
if slices.Contains(r.Inauthentic, mock) {
result.Authentic = false
}
}
for {
if _, ok := r.Fail[mr]; ok {
updateAuthentic(mr.Type + " " + mr.Name)
return mr.Name, adns.Result{}, r.servfail(mr.Name)
}
cname, ok := r.CNAME[mr.Name]
if !ok {
updateAuthentic(mr.Type + " " + mr.Name)
break
}
updateAuthentic("cname " + mr.Name)
if mr.Type == "cname" {
return mr.Name, result, nil
}
mr.Name = cname
}
return mr.Name, result, nil
}
func (r MockResolver) nxdomain(s string) error {
return &adns.DNSError{
Err: "no record",
Name: s,
Server: "localhost",
Server: "mock",
IsNotFound: true,
}
}
func (r MockResolver) servfail(s string) *net.DNSError {
return &net.DNSError{
func (r MockResolver) servfail(s string) error {
return &adns.DNSError{
Err: "temp error",
Name: s,
Server: "localhost",
Server: "mock",
IsTemporary: true,
}
}
func (r MockResolver) LookupCNAME(ctx context.Context, name string) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}
if _, ok := r.Fail[Mockreq{"cname", name}]; ok {
return "", r.servfail(name)
}
if cname, ok := r.CNAME[name]; ok {
return cname, nil
}
return "", r.nxdomain(name)
}
func (r MockResolver) LookupAddr(ctx context.Context, ip string) ([]string, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if _, ok := r.Fail[Mockreq{"ptr", ip}]; ok {
return nil, r.servfail(ip)
}
l, ok := r.PTR[ip]
if !ok {
return nil, r.nxdomain(ip)
}
return l, nil
}
func (r MockResolver) LookupNS(ctx context.Context, name string) ([]*net.NS, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, r.servfail("ns not implemented")
}
func (r MockResolver) LookupPort(ctx context.Context, network, service string) (port int, err error) {
if err := ctx.Err(); err != nil {
return 0, err
}
return 0, r.servfail("port not implemented")
return net.LookupPort(network, service)
}
func (r MockResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
if err := ctx.Err(); err != nil {
return "", nil, err
}
return "", nil, r.servfail("srv not implemented")
}
func (r MockResolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if _, ok := r.Fail[Mockreq{"ipaddr", host}]; ok {
return nil, r.servfail(host)
}
addrs, err := r.LookupHost(ctx, host)
func (r MockResolver) LookupCNAME(ctx context.Context, name string) (string, adns.Result, error) {
mr := Mockreq{"cname", name}
name, result, err := r.result(ctx, mr)
if err != nil {
return nil, err
return name, result, err
}
cname, ok := r.CNAME[name]
if !ok {
return cname, result, r.nxdomain(name)
}
return cname, result, nil
}
func (r MockResolver) LookupAddr(ctx context.Context, ip string) ([]string, adns.Result, error) {
mr := Mockreq{"ptr", ip}
_, result, err := r.result(ctx, mr)
if err != nil {
return nil, result, err
}
l, ok := r.PTR[ip]
if !ok {
return nil, result, r.nxdomain(ip)
}
return l, result, nil
}
func (r MockResolver) LookupNS(ctx context.Context, name string) ([]*net.NS, adns.Result, error) {
mr := Mockreq{"ns", name}
_, result, err := r.result(ctx, mr)
if err != nil {
return nil, result, err
}
return nil, result, r.servfail("ns not implemented")
}
func (r MockResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, adns.Result, error) {
xname := fmt.Sprintf("_%s._%s.%s", service, proto, name)
mr := Mockreq{"srv", xname}
name, result, err := r.result(ctx, mr)
if err != nil {
return name, nil, result, err
}
return name, nil, result, r.servfail("srv not implemented")
}
func (r MockResolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, adns.Result, error) {
// todo: make closer to resolver, doing a & aaaa lookups, including their error/(in)secure status.
mr := Mockreq{"ipaddr", host}
_, result, err := r.result(ctx, mr)
if err != nil {
return nil, result, err
}
addrs, result1, err := r.LookupHost(ctx, host)
result.Authentic = result.Authentic && result1.Authentic
if err != nil {
return nil, result, err
}
ips := make([]net.IPAddr, len(addrs))
for i, a := range addrs {
ip := net.ParseIP(a)
if ip == nil {
return nil, fmt.Errorf("malformed ip %q", a)
return nil, result, fmt.Errorf("malformed ip %q", a)
}
ips[i] = net.IPAddr{IP: ip}
}
return ips, nil
return ips, result, nil
}
func (r MockResolver) LookupHost(ctx context.Context, host string) (addrs []string, err error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if _, ok := r.Fail[Mockreq{"host", host}]; ok {
return nil, r.servfail(host)
func (r MockResolver) LookupHost(ctx context.Context, host string) ([]string, adns.Result, error) {
// todo: make closer to resolver, doing a & aaaa lookups, including their error/(in)secure status.
mr := Mockreq{"host", host}
_, result, err := r.result(ctx, mr)
if err != nil {
return nil, result, err
}
var addrs []string
addrs = append(addrs, r.A[host]...)
addrs = append(addrs, r.AAAA[host]...)
if len(addrs) > 0 {
return addrs, nil
if len(addrs) == 0 {
return nil, result, r.nxdomain(host)
}
if cname, ok := r.CNAME[host]; ok {
return []string{cname}, nil
}
return nil, r.nxdomain(host)
return addrs, result, nil
}
func (r MockResolver) LookupIP(ctx context.Context, network, host string) ([]net.IP, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if _, ok := r.Fail[Mockreq{"ip", host}]; ok {
return nil, r.servfail(host)
func (r MockResolver) LookupIP(ctx context.Context, network, host string) ([]net.IP, adns.Result, error) {
mr := Mockreq{"ip", host}
_, result, err := r.result(ctx, mr)
if err != nil {
return nil, result, err
}
var ips []net.IP
switch network {
@ -152,35 +198,52 @@ func (r MockResolver) LookupIP(ctx context.Context, network, host string) ([]net
}
}
if len(ips) == 0 {
return nil, r.nxdomain(host)
return nil, result, r.nxdomain(host)
}
return ips, nil
return ips, result, nil
}
func (r MockResolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if _, ok := r.Fail[Mockreq{"mx", name}]; ok {
return nil, r.servfail(name)
func (r MockResolver) LookupMX(ctx context.Context, name string) ([]*net.MX, adns.Result, error) {
mr := Mockreq{"mx", name}
_, result, err := r.result(ctx, mr)
if err != nil {
return nil, result, err
}
l, ok := r.MX[name]
if !ok {
return nil, r.nxdomain(name)
return nil, result, r.nxdomain(name)
}
return l, nil
return l, result, nil
}
func (r MockResolver) LookupTXT(ctx context.Context, name string) ([]string, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if _, ok := r.Fail[Mockreq{"txt", name}]; ok {
return nil, r.servfail(name)
func (r MockResolver) LookupTXT(ctx context.Context, name string) ([]string, adns.Result, error) {
mr := Mockreq{"txt", name}
_, result, err := r.result(ctx, mr)
if err != nil {
return nil, result, err
}
l, ok := r.TXT[name]
if !ok {
return nil, r.nxdomain(name)
return nil, result, r.nxdomain(name)
}
return l, nil
return l, result, nil
}
func (r MockResolver) LookupTLSA(ctx context.Context, port int, protocol string, host string) ([]adns.TLSA, adns.Result, error) {
var name string
if port == 0 && protocol == "" {
name = host
} else {
name = fmt.Sprintf("_%d._%s.%s", port, protocol, host)
}
mr := Mockreq{"tlsa", name}
_, result, err := r.result(ctx, mr)
if err != nil {
return nil, result, err
}
l, ok := r.TLSA[name]
if !ok {
return nil, result, r.nxdomain(name)
}
return l, result, nil
}

View file

@ -13,6 +13,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/mjl-/adns"
"github.com/mjl-/mox/mlog"
)
@ -22,6 +24,10 @@ import (
var xlog = mlog.New("dns")
func init() {
net.DefaultResolver.StrictErrors = true
}
var (
metricLookup = promauto.NewHistogramVec(
prometheus.HistogramOpts{
@ -39,16 +45,17 @@ var (
// Resolver is the interface strict resolver implements.
type Resolver interface {
LookupAddr(ctx context.Context, addr string) ([]string, error) // Always returns absolute names, with trailing dot.
LookupCNAME(ctx context.Context, host string) (string, error) // NOTE: returns an error if no CNAME record is present.
LookupHost(ctx context.Context, host string) (addrs []string, err error)
LookupIP(ctx context.Context, network, host string) ([]net.IP, error)
LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error)
LookupMX(ctx context.Context, name string) ([]*net.MX, error)
LookupNS(ctx context.Context, name string) ([]*net.NS, error)
LookupPort(ctx context.Context, network, service string) (port int, err error)
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
LookupTXT(ctx context.Context, name string) ([]string, error)
LookupAddr(ctx context.Context, addr string) ([]string, adns.Result, error) // Always returns absolute names, with trailing dot.
LookupCNAME(ctx context.Context, host string) (string, adns.Result, error) // NOTE: returns an error if no CNAME record is present.
LookupHost(ctx context.Context, host string) ([]string, adns.Result, error)
LookupIP(ctx context.Context, network, host string) ([]net.IP, adns.Result, error)
LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, adns.Result, error)
LookupMX(ctx context.Context, name string) ([]*net.MX, adns.Result, error)
LookupNS(ctx context.Context, name string) ([]*net.NS, adns.Result, error)
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, adns.Result, error)
LookupTXT(ctx context.Context, name string) ([]string, adns.Result, error)
LookupTLSA(ctx context.Context, port int, protocol, host string) ([]adns.TLSA, adns.Result, error)
}
// WithPackage sets Pkg on resolver if it is a StrictResolve and does not have a package set yet.
@ -65,8 +72,8 @@ func WithPackage(resolver Resolver, name string) Resolver {
// StrictResolver is a net.Resolver that enforces that DNS names end with a dot,
// preventing "search"-relative lookups.
type StrictResolver struct {
Pkg string // Name of subsystem that is making DNS requests, for metrics.
Resolver *net.Resolver // Where the actual lookups are done. If nil, net.DefaultResolver is used for lookups.
Pkg string // Name of subsystem that is making DNS requests, for metrics.
Resolver *adns.Resolver // Where the actual lookups are done. If nil, adns.DefaultResolver is used for lookups.
}
var _ Resolver = StrictResolver{}
@ -75,7 +82,7 @@ var ErrRelativeDNSName = errors.New("dns: host to lookup must be absolute, endin
func metricLookupObserve(pkg, typ string, err error, start time.Time) {
var result string
var dnsErr *net.DNSError
var dnsErr *adns.DNSError
switch {
case err == nil:
result = "ok"
@ -101,7 +108,7 @@ func (r StrictResolver) WithPackage(name string) Resolver {
func (r StrictResolver) resolver() Resolver {
if r.Resolver == nil {
return net.DefaultResolver
return adns.DefaultResolver
}
return r.Resolver
}
@ -111,26 +118,52 @@ func resolveErrorHint(err *error) {
if e == nil {
return
}
dnserr, ok := e.(*net.DNSError)
dnserr, ok := e.(*adns.DNSError)
if !ok {
return
}
// If the dns server is not running, and it is one of the default/fallback IPs,
// hint at where to look.
if dnserr.IsTemporary && runtime.GOOS == "linux" && (dnserr.Server == "127.0.0.1:53" || dnserr.Server == "[::1]:53") && strings.HasSuffix(dnserr.Err, "connection refused") {
*err = fmt.Errorf("%w (hint: does /etc/resolv.conf point to a running nameserver? in case of systemd-resolved, see systemd-resolved.service(8))", *err)
*err = fmt.Errorf("%w (hint: does /etc/resolv.conf point to a running nameserver? in case of systemd-resolved, see systemd-resolved.service(8); better yet, install a proper dnssec-verifying recursive resolver like unbound)", *err)
}
}
func (r StrictResolver) LookupAddr(ctx context.Context, addr string) (resp []string, err error) {
func (r StrictResolver) LookupPort(ctx context.Context, network, service string) (resp int, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "addr", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "addr"), mlog.Field("addr", addr), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
metricLookupObserve(r.Pkg, "port", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "port"),
mlog.Field("network", network),
mlog.Field("service", service),
mlog.Field("resp", resp),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
resp, err = r.resolver().LookupAddr(ctx, addr)
resp, err = r.resolver().LookupPort(ctx, network, service)
return
}
func (r StrictResolver) LookupAddr(ctx context.Context, addr string) (resp []string, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "addr", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "addr"),
mlog.Field("addr", addr),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
resp, result, err = r.resolver().LookupAddr(ctx, addr)
// For addresses from /etc/hosts without dot, we add the missing trailing dot.
for i, s := range resp {
if !strings.HasSuffix(s, ".") {
@ -142,20 +175,27 @@ func (r StrictResolver) LookupAddr(ctx context.Context, addr string) (resp []str
// LookupCNAME looks up a CNAME. Unlike "net" LookupCNAME, it returns a "not found"
// error if there is no CNAME record.
func (r StrictResolver) LookupCNAME(ctx context.Context, host string) (resp string, err error) {
func (r StrictResolver) LookupCNAME(ctx context.Context, host string) (resp string, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "cname", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "cname"), mlog.Field("host", host), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "cname"),
mlog.Field("host", host),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(host, ".") {
return "", ErrRelativeDNSName
return "", result, ErrRelativeDNSName
}
resp, err = r.resolver().LookupCNAME(ctx, host)
resp, result, err = r.resolver().LookupCNAME(ctx, host)
if err == nil && resp == host {
return "", &net.DNSError{
return "", result, &adns.DNSError{
Err: "no cname record",
Name: host,
Server: "",
@ -164,119 +204,185 @@ func (r StrictResolver) LookupCNAME(ctx context.Context, host string) (resp stri
}
return
}
func (r StrictResolver) LookupHost(ctx context.Context, host string) (resp []string, err error) {
func (r StrictResolver) LookupHost(ctx context.Context, host string) (resp []string, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "host", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "host"), mlog.Field("host", host), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "host"),
mlog.Field("host", host),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(host, ".") {
return nil, ErrRelativeDNSName
return nil, result, ErrRelativeDNSName
}
resp, err = r.resolver().LookupHost(ctx, host)
resp, result, err = r.resolver().LookupHost(ctx, host)
return
}
func (r StrictResolver) LookupIP(ctx context.Context, network, host string) (resp []net.IP, err error) {
func (r StrictResolver) LookupIP(ctx context.Context, network, host string) (resp []net.IP, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "ip", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "ip"), mlog.Field("network", network), mlog.Field("host", host), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "ip"),
mlog.Field("network", network),
mlog.Field("host", host),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(host, ".") {
return nil, ErrRelativeDNSName
return nil, result, ErrRelativeDNSName
}
resp, err = r.resolver().LookupIP(ctx, network, host)
resp, result, err = r.resolver().LookupIP(ctx, network, host)
return
}
func (r StrictResolver) LookupIPAddr(ctx context.Context, host string) (resp []net.IPAddr, err error) {
func (r StrictResolver) LookupIPAddr(ctx context.Context, host string) (resp []net.IPAddr, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "ipaddr", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "ipaddr"), mlog.Field("host", host), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "ipaddr"),
mlog.Field("host", host),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(host, ".") {
return nil, ErrRelativeDNSName
return nil, result, ErrRelativeDNSName
}
resp, err = r.resolver().LookupIPAddr(ctx, host)
resp, result, err = r.resolver().LookupIPAddr(ctx, host)
return
}
func (r StrictResolver) LookupMX(ctx context.Context, name string) (resp []*net.MX, err error) {
func (r StrictResolver) LookupMX(ctx context.Context, name string) (resp []*net.MX, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "mx", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "mx"), mlog.Field("name", name), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "mx"),
mlog.Field("name", name),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(name, ".") {
return nil, ErrRelativeDNSName
return nil, result, ErrRelativeDNSName
}
resp, err = r.resolver().LookupMX(ctx, name)
resp, result, err = r.resolver().LookupMX(ctx, name)
return
}
func (r StrictResolver) LookupNS(ctx context.Context, name string) (resp []*net.NS, err error) {
func (r StrictResolver) LookupNS(ctx context.Context, name string) (resp []*net.NS, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "ns", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "ns"), mlog.Field("name", name), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "ns"),
mlog.Field("name", name),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(name, ".") {
return nil, ErrRelativeDNSName
return nil, result, ErrRelativeDNSName
}
resp, err = r.resolver().LookupNS(ctx, name)
resp, result, err = r.resolver().LookupNS(ctx, name)
return
}
func (r StrictResolver) LookupPort(ctx context.Context, network, service string) (resp int, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "port", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "port"), mlog.Field("network", network), mlog.Field("service", service), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
}()
defer resolveErrorHint(&err)
resp, err = r.resolver().LookupPort(ctx, network, service)
return
}
func (r StrictResolver) LookupSRV(ctx context.Context, service, proto, name string) (resp0 string, resp1 []*net.SRV, err error) {
func (r StrictResolver) LookupSRV(ctx context.Context, service, proto, name string) (resp0 string, resp1 []*net.SRV, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "srv", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "srv"), mlog.Field("service", service), mlog.Field("proto", proto), mlog.Field("name", name), mlog.Field("resp0", resp0), mlog.Field("resp1", resp1), mlog.Field("duration", time.Since(start)))
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "srv"),
mlog.Field("service", service),
mlog.Field("proto", proto),
mlog.Field("name", name),
mlog.Field("resp0", resp0),
mlog.Field("resp1", resp1),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(name, ".") {
return "", nil, ErrRelativeDNSName
return "", nil, result, ErrRelativeDNSName
}
resp0, resp1, err = r.resolver().LookupSRV(ctx, service, proto, name)
resp0, resp1, result, err = r.resolver().LookupSRV(ctx, service, proto, name)
return
}
func (r StrictResolver) LookupTXT(ctx context.Context, name string) (resp []string, err error) {
func (r StrictResolver) LookupTXT(ctx context.Context, name string) (resp []string, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "txt", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err, mlog.Field("pkg", r.Pkg), mlog.Field("type", "txt"), mlog.Field("name", name), mlog.Field("resp", resp), mlog.Field("duration", time.Since(start)))
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "txt"),
mlog.Field("name", name),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(name, ".") {
return nil, ErrRelativeDNSName
return nil, result, ErrRelativeDNSName
}
resp, err = r.resolver().LookupTXT(ctx, name)
resp, result, err = r.resolver().LookupTXT(ctx, name)
return
}
func (r StrictResolver) LookupTLSA(ctx context.Context, port int, protocol, host string) (resp []adns.TLSA, result adns.Result, err error) {
start := time.Now()
defer func() {
metricLookupObserve(r.Pkg, "tlsa", err, start)
xlog.WithContext(ctx).Debugx("dns lookup result", err,
mlog.Field("pkg", r.Pkg),
mlog.Field("type", "tlsa"),
mlog.Field("port", port),
mlog.Field("protocol", protocol),
mlog.Field("host", host),
mlog.Field("resp", resp),
mlog.Field("authentic", result.Authentic),
mlog.Field("duration", time.Since(start)),
)
}()
defer resolveErrorHint(&err)
if !strings.HasSuffix(host, ".") {
return nil, result, ErrRelativeDNSName
}
resp, result, err = r.resolver().LookupTLSA(ctx, port, protocol, host)
return
}

View file

@ -82,14 +82,14 @@ func Lookup(ctx context.Context, resolver dns.Resolver, zone dns.Domain, ip net.
addr := b.String()
// ../rfc/5782:175
_, err := dns.WithPackage(resolver, "dnsbl").LookupIP(ctx, "ip4", addr)
_, _, err := dns.WithPackage(resolver, "dnsbl").LookupIP(ctx, "ip4", addr)
if dns.IsNotFound(err) {
return StatusPass, "", nil
} else if err != nil {
return StatusTemperr, "", fmt.Errorf("%w: %s", ErrDNS, err)
}
txts, err := dns.WithPackage(resolver, "dnsbl").LookupTXT(ctx, addr)
txts, _, err := dns.WithPackage(resolver, "dnsbl").LookupTXT(ctx, addr)
if dns.IsNotFound(err) {
return StatusFail, "", nil
} else if err != nil {

116
doc.go
View file

@ -1,15 +1,7 @@
/*
Command mox is a modern full-featured open source secure mail server for
Command mox is a modern, secure, full-featured, open source mail server for
low-maintenance self-hosted email.
- Quick and easy to set up with quickstart and automatic TLS with ACME and
Let's Encrypt.
- IMAP4 with extensions for accessing email.
- SMTP with SPF, DKIM, DMARC, DNSBL, MTA-STS, TLSRPT for exchanging email.
- Reputation-based and content-based spam filtering.
- Internationalized email.
- Admin web interface.
# Commands
mox [-config config/mox.conf] [-pedantic] ...
@ -44,10 +36,15 @@ low-maintenance self-hosted email.
mox config domain rm domain
mox config describe-sendmail >/etc/moxsubmit.conf
mox config printservice >mox.service
mox config ensureacmehostprivatekeys
mox example [name]
mox checkupdate
mox cid cid
mox clientconfig domain
mox dane dial host:port
mox dane dialmx domain [destination-host]
mox dane makerecord usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]
mox dns lookup [ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name
mox dkim gened25519 >$selector._domainkey.$domain.ed25519key.pkcs8.pem
mox dkim genrsa >$selector._domainkey.$domain.rsakey.pkcs8.pem
mox dkim lookup selector domain
@ -541,6 +538,31 @@ date version.
usage: mox config printservice >mox.service
# mox config ensureacmehostprivatekeys
Ensure host private keys exist for TLS listeners with ACME.
In mox.conf, each listener can have TLS configured. Long-lived private key files
can be specified, which will be used when requesting ACME certificates.
Configuring these private keys makes it feasible to publish DANE TLSA records
for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
certificate verification without depending on a list of Certificate Authorities
(CAs). Previous versions of mox did not pre-generate private keys for use with
ACME certificates, but would generate private keys on-demand. By explicitly
configuring private keys, they will not change automatedly with new
certificates, and the DNS TLSA records stay valid.
This command looks for listeners in mox.conf with TLS with ACME configured. For
each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
to config/hostkeys/. If a certificate exists in the ACME "cache", its private
key is copied. Otherwise a new private key is generated. Snippets for manually
updating/editing mox.conf are printed.
After running this command, and updating mox.conf, run "mox config dnsrecords"
for a domain and create the TLSA DNS records it suggests to enable DANE.
usage: mox config ensureacmehostprivatekeys
# mox example
List available examples, or print a specific example.
@ -553,7 +575,7 @@ Check if a newer version of mox is available.
A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
available. If so, a changelog is fetched from https://updates.xmox.nl, and the
individual entries validated with a builtin public key. The changelog is
individual entries verified with a builtin public key. The changelog is
printed.
usage: mox checkupdate
@ -582,6 +604,80 @@ configured over otherwise secured connections, like a VPN.
usage: mox clientconfig domain
# mox dane dial
Dial the address using TLS with certificate verification using DANE.
Data is copied between connection and stdin/stdout until either side closes the
connection.
usage: mox dane dial host:port
-usages string
allowed usages for dane, comma-separated list (default "pkix-ta,pkix-ee,dane-ta,dane-ee")
# mox dane dialmx
Connect to MX server for domain using STARTTLS verified with DANE.
If no destination host is specified, regular delivery logic is used to find the
hosts to attempt delivery too. This involves following CNAMEs for the domain,
looking up MX records, and possibly falling back to the domain name itself as
host.
If a destination host is specified, that is the only candidate host considered
for dialing.
With a list of destinations gathered, each is dialed until a successful SMTP
session verified with DANE has been initialized, including EHLO and STARTTLS
commands.
Once connected, data is copied between connection and stdin/stdout, until
either side closes the connection.
This command follows the same logic as delivery attempts made from the queue,
sharing most of its code.
usage: mox dane dialmx domain [destination-host]
-ehlohostname string
hostname to send in smtp ehlo command (default "localhost")
# mox dane makerecord
Print TLSA record for given certificate/key and parameters.
Valid values:
- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
- selector: cert (0), spki (1)
- matchtype: full (0), sha2-256 (1), sha2-512 (2)
Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
from the certificate. An example DNS zone file entry:
_25._tcp.example.com. IN TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
The first usable information from the pem file is used to compose the TLSA
record. In case of selector "cert", a certificate is required. Otherwise the
"subject public key info" (spki) of the first certificate or public or private
key (pkcs#8, pkcs#1 or ec private key) is used.
usage: mox dane makerecord usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]
# mox dns lookup
Lookup DNS name of given type.
Lookup always prints whether the response was DNSSEC-protected.
Examples:
mox dns lookup ptr 1.1.1.1
mox dns lookup mx xmox.nl
mox dns lookup txt _dmarc.xmox.nl.
mox dns lookup tlsa _25._tcp.xmox.nl
usage: mox dns lookup [ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name
# mox dkim gened25519
Generate a new ed25519 key for use with DKIM.

View file

@ -3,17 +3,9 @@
(
cat <<EOF
/*
Command mox is a modern full-featured open source secure mail server for
Command mox is a modern, secure, full-featured, open source mail server for
low-maintenance self-hosted email.
- Quick and easy to set up with quickstart and automatic TLS with ACME and
Let's Encrypt.
- IMAP4 with extensions for accessing email.
- SMTP with SPF, DKIM, DMARC, DNSBL, MTA-STS, TLSRPT for exchanging email.
- Reputation-based and content-based spam filtering.
- Internationalized email.
- Admin web interface.
# Commands
EOF

12
go.mod
View file

@ -1,8 +1,10 @@
module github.com/mjl-/mox
go 1.18
go 1.20
require (
github.com/mjl-/adns v0.0.0-20231009145311-e3834995f16c
github.com/mjl-/autocert v0.0.0-20231009155929-d0d48f2f0290
github.com/mjl-/bstore v0.0.2
github.com/mjl-/sconf v0.0.5
github.com/mjl-/sherpa v0.6.6
@ -11,9 +13,9 @@ require (
github.com/mjl-/sherpats v0.0.4
github.com/prometheus/client_golang v1.14.0
go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.13.0
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
golang.org/x/net v0.15.0
golang.org/x/net v0.16.0
golang.org/x/text v0.13.0
rsc.io/qr v0.2.0
)
@ -28,7 +30,7 @@ require (
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/tools v0.12.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)

20
go.sum
View file

@ -145,6 +145,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mjl-/adns v0.0.0-20231009145311-e3834995f16c h1:ZOr9KnCxfAwJWSeZn8Qs6cSF7TrmBa8hVIpLcEvx/Ec=
github.com/mjl-/adns v0.0.0-20231009145311-e3834995f16c/go.mod h1:JWhGACVviyVUEra9Zv1M8JMkDVXArVt+AIXjTXtuwb4=
github.com/mjl-/autocert v0.0.0-20231009155929-d0d48f2f0290 h1:0hCRSu8+XCZ2cSRW+ZtP/7L5wMYjOKFSQthoyj+4cN8=
github.com/mjl-/autocert v0.0.0-20231009155929-d0d48f2f0290/go.mod h1:taMFU86abMxKLPV4Bynhv8enbYmS67b8LG80qZv2Qus=
github.com/mjl-/bstore v0.0.2 h1:4fdpIOY/+Dv1dBHyzdqa4PD90p8Mz86FeyRpI4qcehw=
github.com/mjl-/bstore v0.0.2/go.mod h1:/cD25FNBaDfvL/plFRxI3Ba3E+wcB0XVOS8nJDqndg0=
github.com/mjl-/sconf v0.0.5 h1:4CMUTENpSnaeP2g6RKtrs8udTxnJgjX2MCCovxGId6s=
@ -226,8 +230,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -294,8 +298,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -351,8 +355,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -407,8 +411,8 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -1,5 +1,7 @@
//go:build integration
// todo: set up a test for dane, mta-sts, etc.
package main
import (
@ -127,7 +129,7 @@ This is the message.
`, mailfrom, rcptto)
msg = strings.ReplaceAll(msg, "\n", "\r\n")
auth := []sasl.Client{sasl.NewClientPlain(mailfrom, password)}
c, err := smtpclient.New(mox.Context, xlog, conn, smtpclient.TLSSkip, ourHostname, dns.Domain{ASCII: desthost}, auth)
c, err := smtpclient.New(mox.Context, xlog, conn, smtpclient.TLSSkip, ourHostname, dns.Domain{ASCII: desthost}, auth, nil, nil, nil)
tcheck(t, err, "smtp hello")
err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false)
tcheck(t, err, "deliver with smtp")

View file

@ -56,7 +56,7 @@ const (
// "names".
//
// If a temporary error occurred, rerr is set.
func Lookup(ctx context.Context, resolver dns.Resolver, ip net.IP) (rstatus Status, name string, names []string, rerr error) {
func Lookup(ctx context.Context, resolver dns.Resolver, ip net.IP) (rstatus Status, name string, names []string, authentic bool, rerr error) {
log := xlog.WithContext(ctx)
start := time.Now()
defer func() {
@ -64,19 +64,21 @@ func Lookup(ctx context.Context, resolver dns.Resolver, ip net.IP) (rstatus Stat
log.Debugx("iprev lookup result", rerr, mlog.Field("ip", ip), mlog.Field("status", rstatus), mlog.Field("duration", time.Since(start)))
}()
revNames, revErr := dns.WithPackage(resolver, "iprev").LookupAddr(ctx, ip.String())
revNames, result, revErr := dns.WithPackage(resolver, "iprev").LookupAddr(ctx, ip.String())
if dns.IsNotFound(revErr) {
return StatusPermerror, "", nil, ErrNoRecord
return StatusPermerror, "", nil, result.Authentic, ErrNoRecord
} else if revErr != nil {
return StatusTemperror, "", nil, fmt.Errorf("%w: %s", ErrDNS, revErr)
return StatusTemperror, "", nil, result.Authentic, fmt.Errorf("%w: %s", ErrDNS, revErr)
}
var lastErr error
authentic = result.Authentic
for _, rname := range revNames {
ips, err := dns.WithPackage(resolver, "iprev").LookupIP(ctx, "ip", rname)
ips, result, err := dns.WithPackage(resolver, "iprev").LookupIP(ctx, "ip", rname)
authentic = authentic && result.Authentic
for _, fwdIP := range ips {
if ip.Equal(fwdIP) {
return StatusPass, rname, revNames, nil
return StatusPass, rname, revNames, authentic, nil
}
}
if err != nil && !dns.IsNotFound(err) {
@ -84,7 +86,7 @@ func Lookup(ctx context.Context, resolver dns.Resolver, ip net.IP) (rstatus Stat
}
}
if lastErr != nil {
return StatusTemperror, "", revNames, fmt.Errorf("%w: %s", ErrDNS, lastErr)
return StatusTemperror, "", revNames, authentic, fmt.Errorf("%w: %s", ErrDNS, lastErr)
}
return StatusFail, "", revNames, nil
return StatusFail, "", revNames, authentic, nil
}

View file

@ -39,30 +39,36 @@ func TestIPRev(t *testing.T) {
{Type: "ip", Name: "temperror.example."}: {},
{Type: "ip", Name: "temperror2.example."}: {},
},
Authentic: []string{
"ptr 10.0.0.1",
"ptr 10.0.0.5", // Only IP to name authentic, not name to IP.
"ip basic.example.",
"ip d.example.", // Only name to IP authentic, not IP to name.
},
}
test := func(ip string, expStatus Status, expName string, expNames string, expErr error) {
test := func(ip string, expStatus Status, expName string, expNames string, expAuth bool, expErr error) {
t.Helper()
status, name, names, err := Lookup(context.Background(), resolver, net.ParseIP(ip))
status, name, names, auth, err := Lookup(context.Background(), resolver, net.ParseIP(ip))
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
t.Fatalf("got err %v, expected err %v", err, expErr)
} else if err != nil {
return
} else if status != expStatus || name != expName || strings.Join(names, ",") != expNames {
t.Fatalf("got status %q, name %q, expNames %v, expected %q %q %v", status, name, names, expStatus, expName, expNames)
} else if status != expStatus || name != expName || strings.Join(names, ",") != expNames || auth != expAuth {
t.Fatalf("got status %q, name %q, names %v, auth %v, expected %q %q %v %v", status, name, names, auth, expStatus, expName, expNames, expAuth)
}
}
test("10.0.0.1", StatusPass, "basic.example.", "basic.example.", nil)
test("10.0.0.2", StatusPermerror, "", "", ErrNoRecord)
test("10.0.0.3", StatusTemperror, "", "", ErrDNS)
test("10.0.0.4", StatusPass, "b.example.", "absent.example.,b.example.", nil)
test("10.0.0.5", StatusPass, "c.example.", "other.example.,c.example.", nil)
test("10.0.0.6", StatusPass, "d.example.", "temperror.example.,d.example.", nil)
test("10.0.0.7", StatusTemperror, "", "temperror.example.,temperror2.example.", ErrDNS)
test("10.0.0.8", StatusFail, "", "other.example.", nil)
test("2001:db8::1", StatusPass, "basic6.example.", "basic6.example.", nil)
test("2001:db8::2", StatusPermerror, "", "", ErrNoRecord)
test("2001:db8::3", StatusTemperror, "", "", ErrDNS)
test("10.0.0.1", StatusPass, "basic.example.", "basic.example.", true, nil)
test("10.0.0.2", StatusPermerror, "", "", false, ErrNoRecord)
test("10.0.0.3", StatusTemperror, "", "", false, ErrDNS)
test("10.0.0.4", StatusPass, "b.example.", "absent.example.,b.example.", false, nil)
test("10.0.0.5", StatusPass, "c.example.", "other.example.,c.example.", false, nil)
test("10.0.0.6", StatusPass, "d.example.", "temperror.example.,d.example.", false, nil)
test("10.0.0.7", StatusTemperror, "", "temperror.example.,temperror2.example.", false, ErrDNS)
test("10.0.0.8", StatusFail, "", "other.example.", false, nil)
test("2001:db8::1", StatusPass, "basic6.example.", "basic6.example.", false, nil)
test("2001:db8::2", StatusPermerror, "", "", false, ErrNoRecord)
test("2001:db8::3", StatusTemperror, "", "", false, ErrDNS)
}

800
main.go
View file

@ -3,15 +3,23 @@ package main
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"log"
"net"
"net/url"
@ -24,11 +32,15 @@ import (
"golang.org/x/crypto/bcrypt"
"github.com/mjl-/adns"
"github.com/mjl-/autocert"
"github.com/mjl-/bstore"
"github.com/mjl-/sconf"
"github.com/mjl-/sherpa"
"github.com/mjl-/mox/config"
"github.com/mjl-/mox/dane"
"github.com/mjl-/mox/dkim"
"github.com/mjl-/mox/dmarc"
"github.com/mjl-/mox/dmarcdb"
@ -43,6 +55,7 @@ import (
"github.com/mjl-/mox/mtasts"
"github.com/mjl-/mox/publicsuffix"
"github.com/mjl-/mox/smtp"
"github.com/mjl-/mox/smtpclient"
"github.com/mjl-/mox/spf"
"github.com/mjl-/mox/store"
"github.com/mjl-/mox/tlsrpt"
@ -109,12 +122,17 @@ var commands = []struct {
{"config domain rm", cmdConfigDomainRemove},
{"config describe-sendmail", cmdConfigDescribeSendmail},
{"config printservice", cmdConfigPrintservice},
{"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
{"example", cmdExample},
{"checkupdate", cmdCheckupdate},
{"cid", cmdCid},
{"clientconfig", cmdClientConfig},
{"deliver", cmdDeliver},
{"dane dial", cmdDANEDial},
{"dane dialmx", cmdDANEDialmx},
{"dane makerecord", cmdDANEMakeRecord},
{"dns lookup", cmdDNSLookup},
{"dkim gened25519", cmdDKIMGened25519},
{"dkim genrsa", cmdDKIMGenrsa},
{"dkim lookup", cmdDKIMLookup},
@ -772,7 +790,14 @@ configured.
if !ok {
log.Fatalf("unknown domain")
}
records, err := mox.DomainRecords(domConf, d)
resolver := dns.StrictResolver{Pkg: "main"}
_, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
if !dns.IsNotFound(err) {
xcheckf(err, "looking up record for dnssec-status")
}
records, err := mox.DomainRecords(domConf, d, result.Authentic)
xcheckf(err, "records")
fmt.Print(strings.Join(records, "\n") + "\n")
}
@ -819,19 +844,238 @@ func cmdConfigDNSCheck(c *cmd) {
}
result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
printResult("DNSSEC", result.DNSSEC.Result)
printResult("IPRev", result.IPRev.Result)
printResult("MX", result.MX.Result)
printResult("TLS", result.TLS.Result)
printResult("DANE", result.DANE.Result)
printResult("SPF", result.SPF.Result)
printResult("DKIM", result.DKIM.Result)
printResult("DMARC", result.DMARC.Result)
printResult("TLSRPT", result.TLSRPT.Result)
printResult("MTASTS", result.MTASTS.Result)
printResult("SRVConf", result.SRVConf.Result)
printResult("SRV", result.SRVConf.Result)
printResult("Autoconf", result.Autoconf.Result)
printResult("Autodiscover", result.Autodiscover.Result)
}
func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
c.params = ""
c.help = `Ensure host private keys exist for TLS listeners with ACME.
In mox.conf, each listener can have TLS configured. Long-lived private key files
can be specified, which will be used when requesting ACME certificates.
Configuring these private keys makes it feasible to publish DANE TLSA records
for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
certificate verification without depending on a list of Certificate Authorities
(CAs). Previous versions of mox did not pre-generate private keys for use with
ACME certificates, but would generate private keys on-demand. By explicitly
configuring private keys, they will not change automatedly with new
certificates, and the DNS TLSA records stay valid.
This command looks for listeners in mox.conf with TLS with ACME configured. For
each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
to config/hostkeys/. If a certificate exists in the ACME "cache", its private
key is copied. Otherwise a new private key is generated. Snippets for manually
updating/editing mox.conf are printed.
After running this command, and updating mox.conf, run "mox config dnsrecords"
for a domain and create the TLSA DNS records it suggests to enable DANE.
`
args := c.Parse()
if len(args) != 0 {
c.Usage()
}
// Load a private key from p, in various forms. We only look at the first PEM
// block. Files with only a private key, or with multiple blocks but private key
// first like autocert does, can be loaded.
loadPrivateKey := func(f *os.File) (any, error) {
buf, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("reading private key file: %v", err)
}
block, _ := pem.Decode(buf)
if block == nil {
return nil, fmt.Errorf("no pem block found in pem file")
}
var privKey any
switch block.Type {
case "EC PRIVATE KEY":
privKey, err = x509.ParseECPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
case "PRIVATE KEY":
privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
}
if err != nil {
return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
}
return privKey, nil
}
// Either load a private key from file, or if it doesn't exist generate a new
// private key.
xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
f, err := os.Open(p)
if err != nil && errors.Is(err, fs.ErrNotExist) {
switch kt {
case autocert.KeyRSA2048:
privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
xcheckf(err, "generating new 2048-bit rsa private key")
return privKey
case autocert.KeyECDSAP256:
privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
xcheckf(err, "generating new ecdsa p-256 private key")
return privKey
}
log.Fatalf("unexpected keytype %v", kt)
return nil
}
xcheckf(err, "%s: open acme key and certificate file", p)
// Load private key from file. autocert stores a PEM file that starts with a
// private key, followed by certificate(s). So we can just read it and should find
// the private key we are looking for.
privKey, err := loadPrivateKey(f)
if xerr := f.Close(); xerr != nil {
log.Printf("closing private key file: %v", xerr)
}
xcheckf(err, "parsing private key from acme key and certificate file")
switch k := privKey.(type) {
case *rsa.PrivateKey:
if k.N.BitLen() == 2048 {
return privKey
}
log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
xcheckf(err, "generating new 2048-bit rsa private key")
return privKey
case *ecdsa.PrivateKey:
if k.Curve == elliptic.P256() {
return privKey
}
log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
xcheckf(err, "generating new ecdsa p-256 private key")
return privKey
default:
log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
return nil
}
}
// Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
writeHostPrivateKey := func(privKey any, p string) error {
os.MkdirAll(filepath.Dir(p), 0700)
f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("create: %v", err)
}
defer func() {
if f != nil {
if err := f.Close(); err != nil {
log.Printf("closing new hostkey file %s after error: %v", p, err)
}
if err := os.Remove(p); err != nil {
log.Printf("removing new hostkey file %s after error: %v", p, err)
}
}
}()
buf, err := x509.MarshalPKCS8PrivateKey(privKey)
if err != nil {
return fmt.Errorf("marshal private host key: %v", err)
}
block := pem.Block{
Type: "PRIVATE KEY",
Bytes: buf,
}
if err := pem.Encode(f, &block); err != nil {
return fmt.Errorf("write as pem: %v", err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("close: %v", err)
}
f = nil
return nil
}
mustLoadConfig()
timestamp := time.Now().Format("20060102T150405")
didCreate := false
for listenerName, l := range mox.Conf.Static.Listeners {
if l.TLS == nil || l.TLS.ACME == "" {
continue
}
haveKeyTypes := map[autocert.KeyType]bool{}
for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
p := mox.ConfigDirPath(privKeyFile)
f, err := os.Open(p)
xcheckf(err, "open host private key")
privKey, err := loadPrivateKey(f)
if err := f.Close(); err != nil {
log.Printf("closing host private key file: %v", err)
}
xcheckf(err, "loading host private key")
switch k := privKey.(type) {
case *rsa.PrivateKey:
if k.N.BitLen() == 2048 {
haveKeyTypes[autocert.KeyRSA2048] = true
}
case *ecdsa.PrivateKey:
if k.Curve == elliptic.P256() {
haveKeyTypes[autocert.KeyECDSAP256] = true
}
}
}
created := []string{}
for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
if haveKeyTypes[kt] {
continue
}
// Lookup key in ACME cache.
host := l.HostnameDomain
if host.ASCII == "" {
host = mox.Conf.Static.HostnameDomain
}
filename := host.ASCII
kind := "ecdsap256"
if kt == autocert.KeyRSA2048 {
filename += "+rsa"
kind = "rsa2048"
}
p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
privKey := xtryLoadPrivateKey(kt, p)
relPath := fmt.Sprintf("hostkeys/%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind)
destPath := mox.ConfigDirPath(relPath)
err := writeHostPrivateKey(privKey, destPath)
xcheckf(err, "writing host private key file to %s: %v", destPath, err)
created = append(created, relPath)
fmt.Printf("Wrote host private key: %s\n", destPath)
}
didCreate = didCreate || len(created) > 0
if len(created) > 0 {
tls := config.TLS{
HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
}
fmt.Printf("\nEnsure Listener %q in %s has the following in its TLS section, below \"ACME: %s\" (don't forget to indent with tabs):\n\n", listenerName, mox.ConfigStaticPath, l.TLS.ACME)
err := sconf.Write(os.Stdout, tls)
xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
fmt.Println()
}
}
if didCreate {
fmt.Printf(`
After updating mox.conf and restarting, run "mox config dnsrecords" for a
domain and create the TLSA DNS records it suggests to enable DANE.
`)
}
}
var examples = []struct {
Name string
Get func() string
@ -1326,6 +1570,517 @@ with DKIM, by mox.
xcheckf(err, "writing rsa private key")
}
func cmdDANEDial(c *cmd) {
c.params = "host:port"
var usages string
c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
c.help = `Dial the address using TLS with certificate verification using DANE.
Data is copied between connection and stdin/stdout until either side closes the
connection.
`
args := c.Parse()
if len(args) != 1 {
c.Usage()
}
allowedUsages := []adns.TLSAUsage{}
if usages != "" {
for _, s := range strings.Split(usages, ",") {
var usage adns.TLSAUsage
switch strings.ToLower(s) {
case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
usage = adns.TLSAUsagePKIXTA
case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
usage = adns.TLSAUsagePKIXEE
case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
usage = adns.TLSAUsageDANETA
case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
usage = adns.TLSAUsageDANEEE
default:
log.Fatalf("unknown dane usage %q", s)
}
allowedUsages = append(allowedUsages, usage)
}
}
resolver := dns.StrictResolver{Pkg: "danedial"}
conn, record, err := dane.Dial(context.Background(), resolver, "tcp", args[0], allowedUsages)
xcheckf(err, "dial")
log.Printf("(connected, verified with %s)", record)
go func() {
_, err := io.Copy(os.Stdout, conn)
xcheckf(err, "copy from connection to stdout")
conn.Close()
}()
_, err = io.Copy(conn, os.Stdin)
xcheckf(err, "copy from stdin to connection")
}
func cmdDANEDialmx(c *cmd) {
c.params = "domain [destination-host]"
var ehloHostname string
c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
If no destination host is specified, regular delivery logic is used to find the
hosts to attempt delivery too. This involves following CNAMEs for the domain,
looking up MX records, and possibly falling back to the domain name itself as
host.
If a destination host is specified, that is the only candidate host considered
for dialing.
With a list of destinations gathered, each is dialed until a successful SMTP
session verified with DANE has been initialized, including EHLO and STARTTLS
commands.
Once connected, data is copied between connection and stdin/stdout, until
either side closes the connection.
This command follows the same logic as delivery attempts made from the queue,
sharing most of its code.
`
args := c.Parse()
if len(args) != 1 && len(args) != 2 {
c.Usage()
}
ehloDomain, err := dns.ParseDomain(ehloHostname)
xcheckf(err, "parsing ehlo hostname")
origNextHop, err := dns.ParseDomain(args[0])
xcheckf(err, "parse domain")
clog := mlog.New("danedialmx")
ctxbg := context.Background()
resolver := dns.StrictResolver{}
var haveMX bool
var origNextHopAuthentic, expandedNextHopAuthentic bool
var expandedNextHop dns.Domain
var hosts []dns.IPDomain
if len(args) == 1 {
var permanent bool
haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, clog, resolver, dns.IPDomain{Domain: origNextHop})
status := "temporary"
if permanent {
status = "permanent"
}
if err != nil {
log.Fatalf("gathering destinations: %v (%s)", err, status)
}
if expandedNextHop != origNextHop {
log.Printf("followed cnames to %s", expandedNextHop)
}
if haveMX {
log.Printf("found mx record, trying mx hosts")
} else {
log.Printf("no mx record found, will try to connect to domain directly")
}
if !origNextHopAuthentic {
log.Fatalf("error: initial domain not dnssec-secure")
}
if !expandedNextHopAuthentic {
log.Fatalf("error: expanded domain not dnssec-secure")
}
l := []string{}
for _, h := range hosts {
l = append(l, h.String())
}
log.Printf("destinations: %s", strings.Join(l, ", "))
} else {
d, err := dns.ParseDomain(args[1])
if err != nil {
log.Fatalf("parsing destination host: %v", err)
}
log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
origNextHopAuthentic = true
expandedNextHopAuthentic = true
expandedNextHop = d
hosts = []dns.IPDomain{{Domain: d}}
}
dialedIPs := map[string][]net.IP{}
for _, host := range hosts {
// It should not be possible for hosts to have IP addresses: They are not
// allowed by dns.ParseDomain, and MX records cannot contain them.
if host.IsIP() {
log.Fatalf("unexpected IP address for destination host")
}
log.Printf("attempting to connect to %s", host)
authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, clog, resolver, host, dialedIPs)
if err != nil {
log.Printf("resolving ips for %s: %v, skipping", host, err)
continue
}
if !authentic {
log.Printf("no dnssec for ips of %s, skipping", host)
continue
}
if !expandedAuthentic {
log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
continue
}
if expandedHost != host.Domain {
log.Printf("host %s cname-expanded to %s", host, expandedHost)
}
log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, clog, resolver, host.Domain, expandedAuthentic, expandedHost)
if err != nil {
log.Printf("looking up tlsa records: %s, skipping", err)
continue
}
tlsMode := smtpclient.TLSStrictStartTLS
if len(daneRecords) == 0 {
if !daneRequired {
log.Printf("host %s has no tlsa records, skipping", expandedHost)
continue
}
log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
tlsMode = smtpclient.TLSUnverifiedStartTLS
} else {
var l []string
for _, r := range daneRecords {
l = append(l, r.String())
}
log.Printf("tlsa records: %s", strings.Join(l, "; "))
}
tlsRemoteHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
var l []string
for _, name := range tlsRemoteHostnames {
l = append(l, name.String())
}
log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
dialer := &net.Dialer{Timeout: 5 * time.Second}
conn, _, err := smtpclient.Dial(ctxbg, clog, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs)
if err != nil {
log.Printf("dial %s: %v, skipping", expandedHost, err)
continue
}
log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
var verifiedRecord adns.TLSA
sc, err := smtpclient.New(ctxbg, clog, conn, tlsMode, ehloDomain, tlsRemoteHostnames[0], nil, daneRecords, tlsRemoteHostnames[1:], &verifiedRecord)
if err != nil {
log.Printf("setting up smtp session: %v, skipping", err)
conn.Close()
continue
}
smtpConn, err := sc.Conn()
if err != nil {
log.Fatalf("error: taking over smtp connection: %s", err)
}
log.Printf("tls verified with tlsa record: %s", verifiedRecord)
log.Printf("smtp session initialized and connected to stdin/stdout")
go func() {
_, err := io.Copy(os.Stdout, smtpConn)
xcheckf(err, "copy from connection to stdout")
smtpConn.Close()
}()
_, err = io.Copy(smtpConn, os.Stdin)
xcheckf(err, "copy from stdin to connection")
}
log.Fatalf("no remaining destinations")
}
func cmdDANEMakeRecord(c *cmd) {
c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
c.help = `Print TLSA record for given certificate/key and parameters.
Valid values:
- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
- selector: cert (0), spki (1)
- matchtype: full (0), sha2-256 (1), sha2-512 (2)
Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
from the certificate. An example DNS zone file entry:
_25._tcp.example.com. IN TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
The first usable information from the pem file is used to compose the TLSA
record. In case of selector "cert", a certificate is required. Otherwise the
"subject public key info" (spki) of the first certificate or public or private
key (pkcs#8, pkcs#1 or ec private key) is used.
`
args := c.Parse()
if len(args) != 4 {
c.Usage()
}
var usage adns.TLSAUsage
switch strings.ToLower(args[0]) {
case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
usage = adns.TLSAUsagePKIXTA
case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
usage = adns.TLSAUsagePKIXEE
case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
usage = adns.TLSAUsageDANETA
case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
usage = adns.TLSAUsageDANEEE
default:
if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
log.Fatalf("bad usage %q", args[0])
} else {
// Does not influence certificate association data, so we can accept other numbers.
log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
usage = adns.TLSAUsage(v)
}
}
var selector adns.TLSASelector
switch strings.ToLower(args[1]) {
case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
selector = adns.TLSASelectorCert
case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
selector = adns.TLSASelectorSPKI
default:
log.Fatalf("bad selector %q", args[1])
}
var matchType adns.TLSAMatchType
switch strings.ToLower(args[2]) {
case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
matchType = adns.TLSAMatchTypeFull
case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
matchType = adns.TLSAMatchTypeSHA256
case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
matchType = adns.TLSAMatchTypeSHA512
default:
log.Fatalf("bad matchtype %q", args[2])
}
buf, err := os.ReadFile(args[3])
xcheckf(err, "reading certificate")
for {
var block *pem.Block
block, buf = pem.Decode(buf)
if block == nil {
extra := ""
if len(buf) > 0 {
extra = " (with leftover data from pem file)"
}
if selector == adns.TLSASelectorCert {
log.Fatalf("no certificate found in pem file%s", extra)
} else {
log.Fatalf("no certificate or public or private key found in pem file%s", extra)
}
}
var cert *x509.Certificate
var data []byte
if block.Type == "CERTIFICATE" {
cert, err = x509.ParseCertificate(block.Bytes)
xcheckf(err, "parse certificate")
switch selector {
case adns.TLSASelectorCert:
data = cert.Raw
case adns.TLSASelectorSPKI:
data = cert.RawSubjectPublicKeyInfo
}
} else if selector == adns.TLSASelectorCert {
// We need a certificate, just a public/private key won't do.
log.Printf("skipping pem type %q, certificate is required", block.Type)
continue
} else {
var privKey, pubKey any
var err error
switch block.Type {
case "PUBLIC KEY":
_, err := x509.ParsePKIXPublicKey(block.Bytes)
xcheckf(err, "parse pkix subject public key info (spki)")
data = block.Bytes
case "EC PRIVATE KEY":
privKey, err = x509.ParseECPrivateKey(block.Bytes)
xcheckf(err, "parse ec private key")
case "RSA PRIVATE KEY":
privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
xcheckf(err, "parse pkcs#1 rsa private key")
case "RSA PUBLIC KEY":
pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
xcheckf(err, "parse pkcs#1 rsa public key")
case "PRIVATE KEY":
// PKCS#8 private key
privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
xcheckf(err, "parse pkcs#8 private key")
default:
log.Printf("skipping unrecognized pem type %q", block.Type)
continue
}
if data == nil {
if pubKey == nil && privKey != nil {
if signer, ok := privKey.(crypto.Signer); !ok {
log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
} else {
pubKey = signer.Public()
}
}
if pubKey == nil {
// Should not happen.
log.Fatalf("internal error: did not find private or public key")
}
data, err = x509.MarshalPKIXPublicKey(pubKey)
xcheckf(err, "marshal pkix subject public key info (spki)")
}
}
switch matchType {
case adns.TLSAMatchTypeFull:
case adns.TLSAMatchTypeSHA256:
p := sha256.Sum256(data)
data = p[:]
case adns.TLSAMatchTypeSHA512:
p := sha512.Sum512(data)
data = p[:]
}
fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
break
}
}
func cmdDNSLookup(c *cmd) {
c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
c.help = `Lookup DNS name of given type.
Lookup always prints whether the response was DNSSEC-protected.
Examples:
mox dns lookup ptr 1.1.1.1
mox dns lookup mx xmox.nl
mox dns lookup txt _dmarc.xmox.nl.
mox dns lookup tlsa _25._tcp.xmox.nl
`
args := c.Parse()
if len(args) != 2 {
c.Usage()
}
resolver := dns.StrictResolver{Pkg: "dns"}
// like xparseDomain, but treat unparseable domain as an ASCII name so names with
// underscores are still looked up, e,g <selector>._domainkey.<host>.
xdomain := func(s string) dns.Domain {
d, err := dns.ParseDomain(s)
if err != nil {
return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
}
return d
}
cmd, name := args[0], args[1]
switch cmd {
case "ptr":
ip := xparseIP(name, "ip")
ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
if err != nil {
log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
}
fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
for _, ptr := range ptrs {
fmt.Printf("- %s\n", ptr)
}
case "mx":
name := xdomain(name)
mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
if err != nil {
log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
// We can still have valid records...
}
fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
for _, mx := range mxl {
fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
}
case "cname":
name := xdomain(name)
target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
if err != nil {
log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
}
fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
case "ips", "a", "aaaa":
network := "ip"
if cmd == "a" {
network = "ip4"
} else if cmd == "aaaa" {
network = "ip6"
}
name := xdomain(name)
ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
if err != nil {
log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
}
fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
for _, ip := range ips {
fmt.Printf("- %s\n", ip)
}
case "ns":
name := xdomain(name)
nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
if err != nil {
log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
}
fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
for _, ns := range nsl {
fmt.Printf("- %s\n", ns)
}
case "txt":
host := xdomain(name)
l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
if err != nil {
log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
}
fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
for _, txt := range l {
fmt.Printf("- %s\n", txt)
}
case "srv":
host := xdomain(name)
_, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
if err != nil {
log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
}
fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
for _, srv := range l {
fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
}
case "tlsa":
host := xdomain(name)
l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
if err != nil {
log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
}
fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
for _, tlsa := range l {
fmt.Printf("- usage %q (%d), selector %q (%d), matchtype %q (%d), certificate association data %x\n", tlsa.Usage, tlsa.Usage, tlsa.Selector, tlsa.Selector, tlsa.MatchType, tlsa.MatchType, tlsa.CertAssoc)
}
default:
log.Fatalf("unknown record type %q", args[0])
}
}
func cmdDKIMGened25519(c *cmd) {
c.params = ">$selector._domainkey.$domain.ed25519key.pkcs8.pem"
c.help = `Generate a new ed25519 key for use with DKIM.
@ -1506,7 +2261,7 @@ func cmdDKIMLookup(c *cmd) {
selector := xparseDomain(args[0], "selector")
domain := xparseDomain(args[1], "domain")
status, record, txt, err := dkim.Lookup(context.Background(), dns.StrictResolver{}, selector, domain)
status, record, txt, authentic, err := dkim.Lookup(context.Background(), dns.StrictResolver{}, selector, domain)
if err != nil {
fmt.Printf("error: %s\n", err)
}
@ -1516,6 +2271,11 @@ func cmdDKIMLookup(c *cmd) {
if txt != "" {
fmt.Printf("TXT record: %s\n", txt)
}
if authentic {
fmt.Println("dnssec-signed: yes")
} else {
fmt.Println("dnssec-signed: no")
}
if record != nil {
fmt.Printf("Record:\n")
pairs := []any{
@ -1541,9 +2301,17 @@ func cmdDMARCLookup(c *cmd) {
}
fromdomain := xparseDomain(args[0], "domain")
_, domain, _, txt, err := dmarc.Lookup(context.Background(), dns.StrictResolver{}, fromdomain)
_, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), dns.StrictResolver{}, fromdomain)
xcheckf(err, "dmarc lookup domain %s", fromdomain)
fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
fmt.Printf("(%s)\n", dnssecStatus(authentic))
}
func dnssecStatus(v bool) string {
if v {
return "with dnssec"
}
return "without dnssec"
}
func cmdDMARCVerify(c *cmd) {
@ -1593,9 +2361,9 @@ can be found in message headers.
if heloDomain != nil {
spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
}
rspf, spfDomain, expl, err := spf.Verify(context.Background(), dns.StrictResolver{}, spfArgs)
rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), dns.StrictResolver{}, spfArgs)
if err != nil {
log.Printf("spf verify: %v (explanation: %q)", err, expl)
log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
} else {
received = &rspf
spfStatus = received.Result
@ -1605,7 +2373,7 @@ can be found in message headers.
} else {
spfIdentity = heloDomain
}
fmt.Printf("spf result: %s: %s\n", spfDomain, spfStatus)
fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
}
}
@ -1642,13 +2410,16 @@ address must opt-in to receiving DMARC reports by creating a DMARC record at
}
dom := xparseDomain(args[0], "domain")
_, domain, record, txt, err := dmarc.Lookup(context.Background(), dns.StrictResolver{}, dom)
_, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), dns.StrictResolver{}, dom)
xcheckf(err, "dmarc lookup domain %s", dom)
fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
fmt.Printf("(%s)\n", dnssecStatus(authentic))
check := func(kind, addr string) {
var authentic bool
printResult := func(format string, args ...any) {
fmt.Printf("%s %s: %s\n", kind, addr, fmt.Sprintf(format, args...))
fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
}
u, err := url.Parse(addr)
@ -1675,7 +2446,7 @@ address must opt-in to receiving DMARC reports by creating a DMARC record at
return
}
accepts, status, _, txt, err := dmarc.LookupExternalReportsAccepted(context.Background(), dns.StrictResolver{}, domain, destdom)
accepts, status, _, txt, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), dns.StrictResolver{}, domain, destdom)
var txtstr string
txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
if txt == "" {
@ -1853,14 +2624,14 @@ printed.
LocalIP: net.ParseIP("127.0.0.1"),
LocalHostname: dns.Domain{ASCII: "localhost"},
}
r, _, explanation, err := spf.Verify(context.Background(), dns.StrictResolver{}, spfargs)
r, _, explanation, authentic, err := spf.Verify(context.Background(), dns.StrictResolver{}, spfargs)
if err != nil {
fmt.Printf("error: %s\n", err)
}
if explanation != "" {
fmt.Printf("explanation: %s\n", explanation)
}
fmt.Printf("status: %s\n", r.Result)
fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
if r.Mechanism != "" {
fmt.Printf("mechanism: %s\n", r.Mechanism)
}
@ -1887,9 +2658,10 @@ func cmdSPFLookup(c *cmd) {
}
domain := xparseDomain(args[0], "domain")
_, txt, _, err := spf.Lookup(context.Background(), dns.StrictResolver{}, domain)
_, txt, _, authentic, err := spf.Lookup(context.Background(), dns.StrictResolver{}, domain)
xcheckf(err, "spf lookup for %s", domain)
fmt.Println(txt)
fmt.Printf("(%s)\n", dnssecStatus(authentic))
}
func cmdMTASTSLookup(c *cmd) {
@ -2027,7 +2799,7 @@ func cmdCheckupdate(c *cmd) {
A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
available. If so, a changelog is fetched from https://updates.xmox.nl, and the
individual entries validated with a builtin public key. The changelog is
individual entries verified with a builtin public key. The changelog is
printed.
`
if len(c.Parse()) != 0 {

View file

@ -41,7 +41,7 @@ type AuthProp struct {
// Whether value is address-like (localpart@domain, or domain). Or another value,
// which is subject to escaping.
IsAddrLike bool
Comment string // If not empty, header comment withtout "()", added after Value.
Comment string // If not empty, header comment without "()", added after Value.
}
// MakeAuthProp is a convenient way to make an AuthProp.

View file

@ -3,9 +3,11 @@ package mox
import (
"bytes"
"context"
"crypto"
"crypto/ed25519"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
@ -19,6 +21,8 @@ import (
"golang.org/x/exp/maps"
"github.com/mjl-/adns"
"github.com/mjl-/mox/config"
"github.com/mjl-/mox/dkim"
"github.com/mjl-/mox/dmarc"
@ -446,7 +450,7 @@ func WebserverConfigSet(ctx context.Context, domainRedirects map[string]string,
// DomainRecords returns text lines describing DNS records required for configuring
// a domain.
func DomainRecords(domConf config.Domain, domain dns.Domain) ([]string, error) {
func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool) ([]string, error) {
d := domain.ASCII
h := Conf.Static.HostnameDomain.ASCII
@ -457,6 +461,60 @@ func DomainRecords(domConf config.Domain, domain dns.Domain) ([]string, error) {
"",
}
if public, ok := Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
records = append(records,
"; DANE: These records indicate that a remote mail server trying to deliver email",
"; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based",
"; on the certificate public key (\"SPKI\", 1) that is SHA2-256-hashed (1) to the",
"; hexadecimal hash. DANE-EE verification means only the certificate or public",
"; key is verified, not whether the certificate is signed by a (centralized)",
"; certificate authority (CA), is expired, or matches the host name.",
";",
"; NOTE: Create the records below only once: They are for the machine, and apply",
"; to all hosted domains.",
)
if !hasDNSSEC {
records = append(records,
";",
"; WARNING: Domain does not appear to be DNSSEC-signed. To enable DANE, first",
"; enable DNSSEC on your domain, then add the TLSA records. Records below have been",
"; commented out.",
)
}
addTLSA := func(privKey crypto.Signer) error {
spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
if err != nil {
return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
}
sum := sha256.Sum256(spkiBuf)
tlsaRecord := adns.TLSA{
Usage: adns.TLSAUsageDANEEE,
Selector: adns.TLSASelectorSPKI,
MatchType: adns.TLSAMatchTypeSHA256,
CertAssoc: sum[:],
}
var s string
if hasDNSSEC {
s = fmt.Sprintf("_25._tcp.%-*s IN TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
} else {
s = fmt.Sprintf(";; _25._tcp.%-*s IN TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
}
records = append(records, s)
return nil
}
for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
if err := addTLSA(privKey); err != nil {
return nil, err
}
}
for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
if err := addTLSA(privKey); err != nil {
return nil, err
}
}
records = append(records, "")
}
if d != h {
records = append(records,
"; For the machine, only needs to be created once, for the first domain added.",
@ -537,7 +595,9 @@ func DomainRecords(domConf config.Domain, domain dns.Domain) ([]string, error) {
if sts := domConf.MTASTS; sts != nil {
records = append(records,
"; TLS must be used when delivering to us.",
"; Remote servers can use MTA-STS to verify our TLS certificate with the",
"; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
"; STARTTLSTLS.",
fmt.Sprintf(`mta-sts.%s. IN CNAME %s.`, d, h),
fmt.Sprintf(`_mta-sts.%s. IN TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
"",

View file

@ -3,7 +3,11 @@ package mox
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
@ -26,6 +30,8 @@ import (
"golang.org/x/text/unicode/norm"
"github.com/mjl-/autocert"
"github.com/mjl-/sconf"
"github.com/mjl-/mox/autotls"
@ -434,6 +440,8 @@ func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, c
errs = append(errs, fmt.Errorf(format, args...))
}
log := xlog.WithContext(ctx)
c := &conf.Static
// check that mailbox is in unicode NFC normalized form.
@ -496,13 +504,77 @@ func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, c
}
c.HostnameDomain = hostname
// Return private key for host name for use with an ACME. Used to return the same
// private key as pre-generated for use with DANE, with its public key in DNS.
// We only use this key for Listener's that have this ACME configured, and for
// which the effective listener host name (either specific to the listener, or the
// global name) is requested. Other host names can get a fresh private key, they
// don't appear in DANE records.
//
// - run 0: only use listener with explicitly matching host name in listener
// (default quickstart config does not set it).
// - run 1: only look at public listener (and host matching mox host name)
// - run 2: all listeners (and host matching mox host name)
findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
for listenerName, l := range Conf.Static.Listeners {
if l.TLS == nil || l.TLS.ACME != acmeName {
continue
}
if run == 0 && host != l.HostnameDomain.ASCII {
continue
}
if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
continue
}
switch keyType {
case autocert.KeyRSA2048:
if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
continue
}
return l.TLS.HostPrivateRSA2048Keys[0]
case autocert.KeyECDSAP256:
if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
continue
}
return l.TLS.HostPrivateECDSAP256Keys[0]
default:
return nil
}
}
return nil
}
// Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
if key == nil {
key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
}
if key == nil {
key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
}
if key != nil {
log.Debug("found existing private key for certificate for host", mlog.Field("acmename", acmeName), mlog.Field("host", host), mlog.Field("keytype", keyType))
return key, nil
}
log.Debug("generating new private key for certificate for host", mlog.Field("acmename", acmeName), mlog.Field("host", host), mlog.Field("keytype", keyType))
switch keyType {
case autocert.KeyRSA2048:
return rsa.GenerateKey(cryptorand.Reader, 2048)
case autocert.KeyECDSAP256:
return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
default:
return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
}
}
}
for name, acme := range c.ACME {
if checkOnly {
continue
}
acmeDir := dataDirPath(configFile, c.DataDir, "acme")
os.MkdirAll(acmeDir, 0770)
manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, Shutdown.Done())
manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, makeGetPrivateKey(name), Shutdown.Done())
if err != nil {
addErrorf("loading ACME identity for %q: %s", name, err)
}
@ -538,7 +610,7 @@ func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, c
l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
// SMTP STARTTLS connections are commonly made without SNI, because certificates
// often aren't validated.
// often aren't verified.
hostname := c.HostnameDomain
if l.Hostname != "" {
hostname = l.HostnameDomain
@ -561,6 +633,34 @@ func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, c
} else {
addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
}
for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
keyPath := configDirPath(configFile, privKeyFile)
privKey, err := loadPrivateKeyFile(keyPath)
if err != nil {
addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
continue
}
switch k := privKey.(type) {
case *rsa.PrivateKey:
if k.N.BitLen() != 2048 {
log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring", mlog.Field("listener", name), mlog.Field("file", keyPath), mlog.Field("bits", k.N.BitLen()))
continue
}
l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
case *ecdsa.PrivateKey:
if k.Curve != elliptic.P256() {
log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", mlog.Field("listener", name), mlog.Field("file", keyPath))
continue
}
l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
default:
log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring", mlog.Field("listener", name), mlog.Field("file", keyPath), mlog.Field("keytype", fmt.Sprintf("%T", privKey)))
continue
}
}
if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
log.Error("warning: uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
}
// TLS 1.2 was introduced in 2008. TLS <1.2 was deprecated by ../rfc/8996:31 and ../rfc/8997:66 in 2021.
var minVersion uint16 = tls.VersionTLS12
@ -1395,6 +1495,35 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config
return
}
func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
keyBuf, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("reading host private key: %v", err)
}
b, _ := pem.Decode(keyBuf)
if b == nil {
return nil, fmt.Errorf("parsing pem block for private key: %v", err)
}
var privKey any
switch b.Type {
case "PRIVATE KEY":
privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
case "RSA PRIVATE KEY":
privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
case "EC PRIVATE KEY":
privKey, err = x509.ParseECPrivateKey(b.Bytes)
default:
err = fmt.Errorf("unknown pem type %q", b.Type)
}
if err != nil {
return nil, fmt.Errorf("parsing private key: %v", err)
}
if k, ok := privKey.(crypto.Signer); ok {
return k, nil
}
return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
}
func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
certs := []tls.Certificate{}
for _, kp := range ctls.KeyCerts {

View file

@ -179,14 +179,14 @@ func LookupRecord(ctx context.Context, resolver dns.Resolver, domain dns.Domain)
var txts []string
for {
var err error
txts, err = dns.WithPackage(resolver, "mtasts").LookupTXT(ctx, name)
txts, _, err = dns.WithPackage(resolver, "mtasts").LookupTXT(ctx, name)
if dns.IsNotFound(err) {
// DNS has no specified limit on how many CNAMEs to follow. Chains of 10 CNAMEs
// have been seen on the internet.
if len(cnames) > 16 {
return nil, "", cnames, fmt.Errorf("too many cnames")
}
cname, err := dns.WithPackage(resolver, "mtasts").LookupCNAME(ctx, name)
cname, _, err := dns.WithPackage(resolver, "mtasts").LookupCNAME(ctx, name)
if dns.IsNotFound(err) {
return nil, "", cnames, ErrNoRecord
}
@ -266,7 +266,7 @@ func FetchPolicy(ctx context.Context, domain dns.Domain) (policy *Policy, policy
defer cancel()
// TLS requirements are what the Go standard library checks: trusted, non-expired,
// hostname validated against DNS-ID supporting wildcard. ../rfc/8461:524
// hostname verified against DNS-ID supporting wildcard. ../rfc/8461:524
url := "https://mta-sts." + domain.Name() + "/.well-known/mta-sts.txt"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {

View file

@ -18,6 +18,8 @@ import (
"testing"
"time"
"github.com/mjl-/adns"
"github.com/mjl-/mox/dns"
)
@ -223,7 +225,7 @@ func TestFetch(t *testing.T) {
HTTPClient.Transport = &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
if strings.HasPrefix(addr, "mta-sts.doesnotexist.example") {
return nil, &net.DNSError{IsNotFound: true}
return nil, &adns.DNSError{IsNotFound: true}
}
return l.Dial()
},

View file

@ -11,6 +11,10 @@ import (
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/mjl-/adns"
"github.com/mjl-/bstore"
"github.com/mjl-/mox/dns"
@ -23,6 +27,39 @@ import (
"github.com/mjl-/mox/store"
)
var (
metricDestinations = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mox_queue_destinations_total",
Help: "Total destination (e.g. MX) lookups for delivery attempts, including those in mox_smtpclient_destinations_authentic_total.",
},
)
metricDestinationsAuthentic = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mox_queue_destinations_authentic_total",
Help: "Destination (e.g. MX) lookups for delivery attempts authenticated with DNSSEC so they are candidates for DANE verification.",
},
)
metricDestinationDANERequired = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mox_queue_destination_dane_required_total",
Help: "Total number of connections to hosts with valid TLSA records making DANE required.",
},
)
metricDestinationDANESTARTTLSUnverified = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mox_queue_destination_dane_starttlsunverified_total",
Help: "Total number of connections with required DANE where all TLSA records were unusable.",
},
)
metricDestinationDANEGatherTLSAErrors = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mox_queue_destination_dane_gathertlsa_errors_total",
Help: "Total number of connections where looking up TLSA records resulted in an error.",
},
)
)
// todo: rename function, perhaps put some of the params in a delivery struct so we don't pass all the params all the time?
func fail(qlog *mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMTA dsn.NameIP, secodeOpt, errmsg string) {
if permanent || m.Attempts >= 8 {
@ -53,9 +90,27 @@ func fail(qlog *mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMT
}
}
// Delivery by directly dialing MX hosts for destination domain.
func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer contextDialer, ourHostname dns.Domain, transportName string, m Msg, backoff time.Duration) {
hosts, effectiveDomain, permanent, err := gatherHosts(resolver, m, cid, qlog)
// Delivery by directly dialing (MX) hosts for destination domain of message.
func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, ourHostname dns.Domain, transportName string, m Msg, backoff time.Duration) {
// High-level approach:
// - Resolve domain to deliver to (CNAME), and determine hosts to try to deliver to (MX)
// - Get MTA-STS policy for domain (optional). If present, only deliver to its
// allowlisted hosts and verify TLS against CA pool.
// - For each host, attempt delivery. If the attempt results in a permanent failure
// (as claimed by remote with a 5xx SMTP response, or perhaps decided by us), the
// attempt can be aborted. Other errors are often temporary and may result in later
// successful delivery. But hopefully the delivery just succeeds. For each host:
// - If there is an MTA-STS policy, we only connect to allow-listed hosts.
// - We try to lookup DANE records (optional) and verify them if present.
// Resolve domain and hosts to attempt delivery to.
// These next-hop names are often the name under which we find MX records. The
// expanded name is different from the original if the original was a CNAME,
// possibly a chain. If there are no MX records, it can be an IP or the host
// directly.
origNextHop := m.RecipientDomain.Domain
ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err := smtpclient.GatherDestinations(ctx, qlog, resolver, m.RecipientDomain)
if err != nil {
fail(qlog, m, backoff, permanent, dsn.NameIP{}, "", err.Error())
return
@ -63,65 +118,87 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer cont
// Check for MTA-STS policy and enforce it if needed. We have to check the
// effective domain (found after following CNAME record(s)): there will certainly
// not be an mtasts record for the original recipient domain, because that is not
// not be an MTA-STS record for the original recipient domain, because that is not
// allowed when a CNAME record is present.
var policyFresh bool
var policy *mtasts.Policy
tlsModeDefault := smtpclient.TLSOpportunistic
if !effectiveDomain.IsZero() {
if !expandedNextHop.IsZero() {
cidctx := context.WithValue(mox.Shutdown, mlog.CidKey, cid)
policy, policyFresh, err = mtastsdb.Get(cidctx, resolver, effectiveDomain)
policy, _, err = mtastsdb.Get(cidctx, resolver, expandedNextHop)
if err != nil {
// No need to refuse to deliver if we have some mtasts error.
qlog.Infox("mtasts failed, continuing with strict tls requirement", err, mlog.Field("domain", effectiveDomain))
qlog.Infox("mtasts failed, continuing with strict tls requirement", err, mlog.Field("domain", expandedNextHop))
tlsModeDefault = smtpclient.TLSStrictStartTLS
}
// note: policy can be nil, if a domain does not implement MTA-STS or its the first
// time we fetch the policy and if we encountered an error.
// note: policy can be nil, if a domain does not implement MTA-STS or it's the
// first time we fetch the policy and if we encountered an error.
}
// We try delivery to each record until we have success or a permanent failure. So
// for transient errors, we'll try the next MX record. For MX records pointing to a
// We try delivery to each host until we have success or a permanent failure. So
// for transient errors, we'll try the next host. For MX records pointing to a
// dual stack host, we turn a permanent failure due to policy on the first delivery
// attempt into a temporary failure and make sure to try the other address family
// the next attempt. This should reduce issues due to one of our IPs being on a
// block list. We won't try multiple IPs of the same address family. Surprisingly,
// RFC 5321 does not specify a clear algorithm, but common practicie is probably
// RFC 5321 does not specify a clear algorithm, but common practice is probably
// ../rfc/3974:268.
var remoteMTA dsn.NameIP
var secodeOpt, errmsg string
permanent = false
mtastsFailure := true
// todo: should make distinction between host permanently not accepting the message, and the message not being deliverable permanently. e.g. a mx host may have a size limit, or not accept 8bitmime, while another host in the list does accept the message. same for smtputf8, ../rfc/6531:555
for _, h := range hosts {
var badTLS, ok bool
// ../rfc/8461:913
if policy != nil && policy.Mode == mtasts.ModeEnforce && !policy.Matches(h.Domain) {
if policy != nil && !policy.Matches(h.Domain) {
var policyHosts []string
for _, mx := range policy.MX {
policyHosts = append(policyHosts, mx.LogString())
}
errmsg = fmt.Sprintf("mx host %s does not match enforced mta-sts policy with hosts %s", h.Domain, strings.Join(policyHosts, ","))
qlog.Error("mx host does not match enforce mta-sts policy, skipping", mlog.Field("host", h.Domain), mlog.Field("policyhosts", policyHosts))
continue
if policy.Mode == mtasts.ModeEnforce {
errmsg = fmt.Sprintf("mx host %s does not match enforced mta-sts policy with hosts %s", h.Domain, strings.Join(policyHosts, ","))
qlog.Error("mx host does not match mta-sts policy in mode enforce, skipping", mlog.Field("host", h.Domain), mlog.Field("policyhosts", policyHosts))
continue
}
qlog.Error("mx host does not match mta-sts policy, but it is not enforced, continuing", mlog.Field("host", h.Domain), mlog.Field("policyhosts", policyHosts))
}
qlog.Info("delivering to remote", mlog.Field("remote", h), mlog.Field("queuecid", cid))
cid := mox.Cid()
nqlog := qlog.WithCid(cid)
var remoteIP net.IP
tlsMode := tlsModeDefault
if policy != nil && policy.Mode == mtasts.ModeEnforce {
tlsMode = smtpclient.TLSStrictStartTLS
}
permanent, badTLS, secodeOpt, remoteIP, errmsg, ok = deliverHost(nqlog, resolver, dialer, cid, ourHostname, transportName, h, &m, tlsMode)
if !ok && badTLS && tlsMode == smtpclient.TLSOpportunistic {
// Try to deliver to host. We can get various errors back. Like permanent failure
// response codes, TCP, DNSSEC, TLS (opportunistic, i.e. optional with fallback to
// without), etc. It's a balancing act to handle these situations correctly. We
// don't want to bounce unnecessarily. But also not keep trying if there is no
// chance of success.
// Set if there TLSA records were found. Means TLS is required for this host,
// usually with verification of the certificate.
var daneRequired bool
enforceMTASTS := policy != nil && policy.Mode == mtasts.ModeEnforce
permanent, daneRequired, badTLS, secodeOpt, remoteIP, errmsg, ok = deliverHost(nqlog, resolver, dialer, cid, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, tlsMode)
// If we had a TLS-related failure when doing opportunistic (optional) TLS, and no
// DANE records were not found, we should try again without TLS. This could be an
// old server that only does ancient TLS versions, or has a misconfiguration. Note
// that opportunistic TLS does not do regular certificate verification, so that can't
// be the problem.
if !ok && badTLS && !enforceMTASTS && tlsMode == smtpclient.TLSOpportunistic && !daneRequired {
// In case of failure with opportunistic TLS, try again without TLS. ../rfc/7435:459
// todo future: revisit this decision. perhaps it should be a configuration option that defaults to not doing this?
// todo future: add a configuration option to not fall back?
nqlog.Info("connecting again for delivery attempt without tls")
permanent, badTLS, secodeOpt, remoteIP, errmsg, ok = deliverHost(nqlog, resolver, dialer, cid, ourHostname, transportName, h, &m, smtpclient.TLSSkip)
tlsMode = smtpclient.TLSSkip
permanent, _, _, secodeOpt, remoteIP, errmsg, ok = deliverHost(nqlog, resolver, dialer, cid, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, tlsMode)
}
if ok {
nqlog.Info("delivered from queue")
if err := queueDelete(context.Background(), m.ID); err != nil {
@ -130,141 +207,54 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer cont
return
}
remoteMTA = dsn.NameIP{Name: h.XString(false), IP: remoteIP}
if !badTLS {
mtastsFailure = false
}
if permanent {
break
}
}
if mtastsFailure && policyFresh {
permanent = true
}
// In theory, we could make a failure permanent if we didn't find any mx host
// matching the mta-sts policy AND the policy is fresh AND all DNS records leading
// to the MX targets (including CNAME) have a TTL that is beyond the latest
// possible delivery attempt. Until that time, configuration problems can be
// corrected through DNS or policy update. Not sure if worth it in practice, there
// is a good chance the MX records can still change, at least on initial delivery
// failures.
// todo: possibly detect that future deliveries will fail due to long ttl's of cached records that are preventing delivery.
fail(qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg)
}
var (
errCNAMELoop = errors.New("cname loop")
errCNAMELimit = errors.New("too many cname records")
errNoRecord = errors.New("no dns record")
errDNS = errors.New("dns lookup error")
errNoMail = errors.New("domain does not accept email as indicated with single dot for mx record")
)
// Gather hosts to try to deliver to. We start with the straight-forward MX record.
// If that does not exist, we'll look for CNAME of the entire domain (following
// chains if needed). If a CNAME does not exist, but the domain name has an A or
// AAAA record, we'll try delivery directly to that host.
// ../rfc/5321:3824
func gatherHosts(resolver dns.Resolver, m Msg, cid int64, qlog *mlog.Log) (hosts []dns.IPDomain, effectiveDomain dns.Domain, permanent bool, err error) {
if len(m.RecipientDomain.IP) > 0 {
return []dns.IPDomain{m.RecipientDomain}, effectiveDomain, false, nil
}
// We start out delivering to the recipient domain. We follow CNAMEs a few times.
rcptDomain := m.RecipientDomain.Domain
// Domain we are actually delivering to, after following CNAME record(s).
effectiveDomain = rcptDomain
domainsSeen := map[string]bool{}
for i := 0; ; i++ {
if domainsSeen[effectiveDomain.ASCII] {
return nil, effectiveDomain, true, fmt.Errorf("%w: recipient domain %s: already saw %s", errCNAMELoop, rcptDomain, effectiveDomain)
}
domainsSeen[effectiveDomain.ASCII] = true
// note: The Go resolver returns the requested name if the domain has no CNAME record but has a host record.
if i == 16 {
// We have a maximum number of CNAME records we follow. There is no hard limit for
// DNS, and you might think folks wouldn't configure CNAME chains at all, but for
// (non-mail) domains, CNAME chains of 10 records have been encountered according
// to the internet.
return nil, effectiveDomain, true, fmt.Errorf("%w: recipient domain %s, last resolved domain %s", errCNAMELimit, rcptDomain, effectiveDomain)
}
cidctx := context.WithValue(mox.Context, mlog.CidKey, cid)
ctx, cancel := context.WithTimeout(cidctx, 30*time.Second)
defer cancel()
// Note: LookupMX can return an error and still return records: Invalid records are
// filtered out and an error returned. We must process any records that are valid.
// Only if all are unusable will we return an error. ../rfc/5321:3851
mxl, err := resolver.LookupMX(ctx, effectiveDomain.ASCII+".")
cancel()
if err != nil && len(mxl) == 0 {
if !dns.IsNotFound(err) {
return nil, effectiveDomain, false, fmt.Errorf("%w: mx lookup for %s: %v", errDNS, effectiveDomain, err)
}
// No MX record. First attempt CNAME lookup. ../rfc/5321:3838 ../rfc/3974:197
ctx, cancel = context.WithTimeout(cidctx, 30*time.Second)
defer cancel()
cname, err := resolver.LookupCNAME(ctx, effectiveDomain.ASCII+".")
cancel()
if err != nil && !dns.IsNotFound(err) {
return nil, effectiveDomain, false, fmt.Errorf("%w: cname lookup for %s: %v", errDNS, effectiveDomain, err)
}
if err == nil && cname != effectiveDomain.ASCII+"." {
d, err := dns.ParseDomain(strings.TrimSuffix(cname, "."))
if err != nil {
return nil, effectiveDomain, true, fmt.Errorf("%w: parsing cname domain %s: %v", errDNS, effectiveDomain, err)
}
effectiveDomain = d
// Start again with new domain.
continue
}
// See if the host exists. If so, attempt delivery directly to host. ../rfc/5321:3842
ctx, cancel = context.WithTimeout(cidctx, 30*time.Second)
defer cancel()
_, err = resolver.LookupHost(ctx, effectiveDomain.ASCII+".")
cancel()
if dns.IsNotFound(err) {
return nil, effectiveDomain, true, fmt.Errorf("%w: recipient domain/host %s", errNoRecord, effectiveDomain)
} else if err != nil {
return nil, effectiveDomain, false, fmt.Errorf("%w: looking up host %s because of no mx record: %v", errDNS, effectiveDomain, err)
}
hosts = []dns.IPDomain{{Domain: effectiveDomain}}
} else if err != nil {
qlog.Infox("partial mx failure, attempting delivery to valid mx records", err)
}
// ../rfc/7505:122
if err == nil && len(mxl) == 1 && mxl[0].Host == "." {
return nil, effectiveDomain, true, errNoMail
}
// The Go resolver already sorts by preference, randomizing records of same
// preference. ../rfc/5321:3885
for _, mx := range mxl {
host, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
if err != nil {
// note: should not happen because Go resolver already filters these out.
return nil, effectiveDomain, true, fmt.Errorf("%w: invalid host name in mx record %q: %v", errDNS, mx.Host, err)
}
hosts = append(hosts, dns.IPDomain{Domain: host})
}
if len(hosts) > 0 {
err = nil
}
return hosts, effectiveDomain, false, err
}
}
// deliverHost attempts to deliver m to host.
// deliverHost updated m.DialedIPs, which must be saved in case of failure to deliver.
func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer contextDialer, cid int64, ourHostname dns.Domain, transportName string, host dns.IPDomain, m *Msg, tlsMode smtpclient.TLSMode) (permanent, badTLS bool, secodeOpt string, remoteIP net.IP, errmsg string, ok bool) {
// deliverHost attempts to deliver m to host. Depending on tlsMode, we'll do
// required TLS with WebPKI verification (with MTA-STS), opportunistic DANE TLS
// (opportunistic TLS) or non-verifying TLS (opportunistic TLS) deliverHost updates
// m.DialedIPs, which must be saved in case of failure to deliver.
//
// The haveMX and next-hop-authentic fields are used to determine if DANE is
// applicable. The next-hop fields themselves are used to determine valid names
// during DANE TLS certificate verification.
func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, cid int64, ourHostname dns.Domain, transportName string, host dns.IPDomain, enforceMTASTS, haveMX, origNextHopAuthentic bool, origNextHop dns.Domain, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, m *Msg, tlsMode smtpclient.TLSMode) (permanent, daneRequired, badTLS bool, secodeOpt string, remoteIP net.IP, errmsg string, ok bool) {
// About attempting delivery to multiple addresses of a host: ../rfc/5321:3898
start := time.Now()
var deliveryResult string
defer func() {
metricDelivery.WithLabelValues(fmt.Sprintf("%d", m.Attempts), transportName, string(tlsMode), deliveryResult).Observe(float64(time.Since(start)) / float64(time.Second))
log.Debug("queue deliverhost result", mlog.Field("host", host), mlog.Field("attempt", m.Attempts), mlog.Field("tlsmode", tlsMode), mlog.Field("permanent", permanent), mlog.Field("badtls", badTLS), mlog.Field("secodeopt", secodeOpt), mlog.Field("errmsg", errmsg), mlog.Field("ok", ok), mlog.Field("duration", time.Since(start)))
log.Debug("queue deliverhost result",
mlog.Field("host", host),
mlog.Field("attempt", m.Attempts),
mlog.Field("tlsmode", tlsMode),
mlog.Field("permanent", permanent),
mlog.Field("badtls", badTLS),
mlog.Field("secodeopt", secodeOpt),
mlog.Field("errmsg", errmsg),
mlog.Field("ok", ok),
mlog.Field("duration", time.Since(start)))
}()
// Open message to deliver.
f, err := os.Open(m.MessagePath())
if err != nil {
return false, false, "", nil, fmt.Sprintf("open message file: %s", err), false
return false, false, false, "", nil, fmt.Sprintf("open message file: %s", err), false
}
msgr := store.FileMsgReader(m.MsgPrefix, f)
defer func() {
@ -276,9 +266,83 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer contextDialer, cid
ctx, cancel := context.WithTimeout(cidctx, 30*time.Second)
defer cancel()
conn, ip, dualstack, err := dialHost(ctx, log, resolver, dialer, host, 25, m)
remoteIP = ip
// We must lookup the IPs for the host name before checking DANE TLSA records. And
// only check TLSA records for secure responses. This prevents problems with old
// name servers returning an error for TLSA requests or letting it timeout (not
// sending a response). ../rfc/7672:879
var daneRecords []adns.TLSA
var tlsRemoteHostnames []dns.Domain
if host.IsDomain() {
tlsRemoteHostnames = []dns.Domain{host.Domain}
}
if m.DialedIPs == nil {
m.DialedIPs = map[string][]net.IP{}
}
metricDestinations.Inc()
authentic, expandedAuthentic, expandedHost, ips, dualstack, err := smtpclient.GatherIPs(ctx, log, resolver, host, m.DialedIPs)
if err == nil && authentic && origNextHopAuthentic && (!haveMX || expandedNextHopAuthentic) && host.IsDomain() {
metricDestinationsAuthentic.Inc()
// Modes to skip and not verify aren't normally set when we get here. But in the
// future may perhaps be set on a message manually after delivery failures. We can
// handle them here.
switch tlsMode {
case smtpclient.TLSSkip:
// No TLS, so clearly no DANE.
case smtpclient.TLSUnverifiedStartTLS:
// Fallback mode for DANE without usable records, so skip DANE.
default:
// Look for TLSA records in either the expandedHost, or otherwise the original
// host. ../rfc/7672:912
var tlsaBaseDomain dns.Domain
daneRequired, daneRecords, tlsaBaseDomain, err = smtpclient.GatherTLSA(ctx, log, resolver, host.Domain, expandedNextHopAuthentic && expandedAuthentic, expandedHost)
if daneRequired {
metricDestinationDANERequired.Inc()
}
if err != nil {
metricDestinationDANEGatherTLSAErrors.Inc()
}
if err == nil && daneRequired {
tlsMode = smtpclient.TLSStrictStartTLS
if len(daneRecords) == 0 {
// If there are no usable DANE records, we still have to use TLS, but without
// verifying its certificate. At least when there is no MTA-STS. Why? Perhaps to
// prevent ossification? The SMTP TLSA specification has different behaviour than
// the generic TLSA. "Usable" means different things in different places.
// ../rfc/7672:718 ../rfc/6698:1845 ../rfc/6698:660
if !enforceMTASTS {
tlsMode = smtpclient.TLSUnverifiedStartTLS
log.Debug("no usable dane records, not verifying dane records, but doing required non-verifying opportunistic tls")
metricDestinationDANESTARTTLSUnverified.Inc()
}
daneRecords = nil
} else {
// Based on CNAMEs followed and DNSSEC-secure status, we must allow up to 4 host
// names.
tlsRemoteHostnames = smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
log.Debug("delivery with required starttls with dane verification", mlog.Field("allowedtlshostnames", tlsRemoteHostnames))
}
} else if !daneRequired {
log.Debugx("not doing opportunistic dane after gathering tlsa records", err)
err = nil
}
// else, err is propagated below.
}
} else {
log.Debugx("not attempting verification with dane", err, mlog.Field("authentic", authentic), mlog.Field("expandedauthentic", expandedAuthentic))
}
// Dial the remote host given the IPs if no error yet.
var conn net.Conn
if err == nil {
if m.DialedIPs == nil {
m.DialedIPs = map[string][]net.IP{}
}
conn, remoteIP, err = smtpclient.Dial(ctx, log, dialer, host, ips, 25, m.DialedIPs)
}
cancel()
// Set error for metrics.
var result string
switch {
case err == nil:
@ -293,7 +357,7 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer contextDialer, cid
metricConnection.WithLabelValues(result).Inc()
if err != nil {
log.Debugx("connecting to remote smtp", err, mlog.Field("host", host))
return false, false, "", ip, fmt.Sprintf("dialing smtp server: %v", err), false
return false, daneRequired, false, "", remoteIP, fmt.Sprintf("dialing smtp server: %v", err), false
}
var mailFrom string
@ -303,11 +367,21 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer contextDialer, cid
rcptTo := m.Recipient().XString(m.SMTPUTF8)
// todo future: get closer to timeouts specified in rfc? ../rfc/5321:3610
log = log.Fields(mlog.Field("remoteip", ip))
log = log.Fields(mlog.Field("remoteip", remoteIP))
ctx, cancel = context.WithTimeout(cidctx, 30*time.Minute)
defer cancel()
mox.Connections.Register(conn, "smtpclient", "queue")
sc, err := smtpclient.New(ctx, log, conn, tlsMode, ourHostname, host.Domain, nil)
// Initialize SMTP session, sending EHLO/HELO and STARTTLS with specified tls mode.
var firstHost dns.Domain
var moreHosts []dns.Domain
if len(tlsRemoteHostnames) > 0 {
// For use with DANE-TA.
firstHost = tlsRemoteHostnames[0]
moreHosts = tlsRemoteHostnames[1:]
}
var verifiedRecord adns.TLSA
sc, err := smtpclient.New(ctx, log, conn, tlsMode, ourHostname, firstHost, nil, daneRecords, moreHosts, &verifiedRecord)
defer func() {
if sc == nil {
conn.Close()
@ -317,6 +391,7 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer contextDialer, cid
mox.Connections.Unregister(conn)
}()
if err == nil {
// SMTP session is ready. Finally try to actually deliver.
has8bit := m.Has8bit
smtputf8 := m.SMTPUTF8
var msg io.Reader = msgr
@ -349,7 +424,7 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer contextDialer, cid
deliveryResult = "error"
}
if err == nil {
return false, false, "", ip, "", true
return false, daneRequired, false, "", remoteIP, "", true
} else if cerr, ok := err.(smtpclient.Error); ok {
// If we are being rejected due to policy reasons on the first
// attempt and remote has both IPv4 and IPv6, we'll give it
@ -359,8 +434,8 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer contextDialer, cid
if permanent && m.Attempts == 1 && dualstack && strings.HasPrefix(cerr.Secode, "7.") {
permanent = false
}
return permanent, errors.Is(cerr, smtpclient.ErrTLS), cerr.Secode, ip, cerr.Error(), false
return permanent, daneRequired, errors.Is(cerr, smtpclient.ErrTLS), cerr.Secode, remoteIP, cerr.Error(), false
} else {
return false, errors.Is(cerr, smtpclient.ErrTLS), "", ip, err.Error(), false
return false, daneRequired, errors.Is(cerr, smtpclient.ErrTLS), "", remoteIP, err.Error(), false
}
}

View file

@ -30,6 +30,7 @@ import (
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/moxio"
"github.com/mjl-/mox/smtp"
"github.com/mjl-/mox/smtpclient"
"github.com/mjl-/mox/store"
)
@ -60,24 +61,6 @@ var (
)
)
type contextDialer interface {
DialContext(ctx context.Context, network, addr string) (c net.Conn, err error)
}
// Used to dial remote SMTP servers.
// Overridden for tests.
var dial = func(ctx context.Context, dialer contextDialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
// If this is a net.Dialer, use its settings and add the timeout and localaddr.
// This is the typical case, but SOCKS5 support can use a different dialer.
if d, ok := dialer.(*net.Dialer); ok {
nd := *d
nd.Timeout = timeout
nd.LocalAddr = laddr
return nd.DialContext(ctx, "tcp", addr)
}
return dialer.DialContext(ctx, "tcp", addr)
}
var jitter = mox.NewRand()
var DBTypes = []any{Msg{}} // Types stored in DB.
@ -547,7 +530,7 @@ func deliver(resolver dns.Resolver, m Msg) {
qlog.Debug("delivering with transport", mlog.Field("transport", transportName))
}
var dialer contextDialer = &net.Dialer{}
var dialer smtpclient.Dialer = &net.Dialer{}
if transport.Submissions != nil {
deliverSubmit(cid, qlog, resolver, dialer, m, backoff, transportName, transport.Submissions, true, 465)
} else if transport.Submission != nil {
@ -561,7 +544,7 @@ func deliver(resolver dns.Resolver, m Msg) {
if err != nil {
fail(qlog, m, backoff, false, dsn.NameIP{}, "", fmt.Sprintf("socks dialer: %v", err))
return
} else if d, ok := socksdialer.(contextDialer); !ok {
} else if d, ok := socksdialer.(smtpclient.Dialer); !ok {
fail(qlog, m, backoff, false, dsn.NameIP{}, "", "socks dialer is not a contextdialer")
return
} else {
@ -611,98 +594,3 @@ func routeMatchDomain(l []string, d dns.Domain) bool {
}
return false
}
// dialHost dials host for delivering Msg, taking previous attempts into accounts.
// If the previous attempt used IPv4, this attempt will use IPv6 (in case one of the IPs is in a DNSBL).
// The second attempt for an address family we prefer the same IP as earlier, to increase our chances if remote is doing greylisting.
// dialHost updates m with the dialed IP and m should be saved in case of failure.
// If we have fully specified local smtp listen IPs, we set those for the outgoing
// connection. The admin probably configured these same IPs in SPF, but others
// possibly not.
func dialHost(ctx context.Context, log *mlog.Log, resolver dns.Resolver, dialer contextDialer, host dns.IPDomain, port int, m *Msg) (conn net.Conn, ip net.IP, dualstack bool, rerr error) {
var ips []net.IP
if len(host.IP) > 0 {
ips = []net.IP{host.IP}
} else {
// todo: The Go resolver automatically follows CNAMEs, which is not allowed for
// host names in MX records. ../rfc/5321:3861 ../rfc/2181:661
name := host.Domain.ASCII + "."
ipaddrs, err := resolver.LookupIPAddr(ctx, name)
if err != nil || len(ipaddrs) == 0 {
return nil, nil, false, fmt.Errorf("looking up %q: %v", name, err)
}
var have4, have6 bool
for _, ipaddr := range ipaddrs {
ips = append(ips, ipaddr.IP)
if ipaddr.IP.To4() == nil {
have6 = true
} else {
have4 = true
}
}
dualstack = have4 && have6
prevIPs := m.DialedIPs[host.String()]
if len(prevIPs) > 0 {
prevIP := prevIPs[len(prevIPs)-1]
prevIs4 := prevIP.To4() != nil
sameFamily := 0
for _, ip := range prevIPs {
is4 := ip.To4() != nil
if prevIs4 == is4 {
sameFamily++
}
}
preferPrev := sameFamily == 1
// We use stable sort so any preferred/randomized listing from DNS is kept intact.
sort.SliceStable(ips, func(i, j int) bool {
aIs4 := ips[i].To4() != nil
bIs4 := ips[j].To4() != nil
if aIs4 != bIs4 {
// Prefer "i" if it is not same address family.
return aIs4 != prevIs4
}
// Prefer "i" if it is the same as last and we should be preferring it.
return preferPrev && ips[i].Equal(prevIP)
})
log.Debug("ordered ips for dialing", mlog.Field("ips", ips))
}
}
var timeout time.Duration
deadline, ok := ctx.Deadline()
if !ok {
timeout = 30 * time.Second
} else {
timeout = time.Until(deadline) / time.Duration(len(ips))
}
var lastErr error
var lastIP net.IP
for _, ip := range ips {
addr := net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port))
log.Debug("dialing remote host for delivery", mlog.Field("addr", addr))
var laddr net.Addr
for _, lip := range mox.Conf.Static.SpecifiedSMTPListenIPs {
ipIs4 := ip.To4() != nil
lipIs4 := lip.To4() != nil
if ipIs4 == lipIs4 {
laddr = &net.TCPAddr{IP: lip}
break
}
}
conn, err := dial(ctx, dialer, timeout, addr, laddr)
if err == nil {
log.Debug("connected for smtp delivery", mlog.Field("host", host), mlog.Field("addr", addr), mlog.Field("laddr", laddr))
if m.DialedIPs == nil {
m.DialedIPs = map[string][]net.IP{}
}
name := host.String()
m.DialedIPs[name] = append(m.DialedIPs[name], ip)
return conn, ip, dualstack, nil
}
log.Debugx("connection attempt for smtp delivery", err, mlog.Field("host", host), mlog.Field("addr", addr), mlog.Field("laddr", laddr))
lastErr = err
lastIP = ip
}
return nil, lastIP, dualstack, lastErr
}

View file

@ -5,24 +5,25 @@ import (
"context"
"crypto/ed25519"
cryptorand "crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"math/big"
"net"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/mjl-/adns"
"github.com/mjl-/bstore"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/smtp"
"github.com/mjl-/mox/smtpclient"
"github.com/mjl-/mox/store"
)
@ -132,13 +133,18 @@ func TestQueue(t *testing.T) {
MX: map[string][]*net.MX{"mox.example.": {{Host: "mox.example", Pref: 10}}},
}
dialed := make(chan struct{}, 1)
dial = func(ctx context.Context, dialer contextDialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
smtpclient.DialHook = func(ctx context.Context, dialer smtpclient.Dialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
dialed <- struct{}{}
return nil, fmt.Errorf("failure from test")
}
defer func() {
smtpclient.DialHook = nil
}()
launchWork(resolver, map[string]struct{}{})
moxCert := fakeCert(t, "mox.example", false)
// Wait until we see the dial and the failed attempt.
timer := time.NewTimer(time.Second)
defer timer.Stop()
@ -192,23 +198,92 @@ func TestQueue(t *testing.T) {
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
fmt.Fprintf(server, "220 mox.example\r\n")
br := bufio.NewReader(server)
br.ReadString('\n') // Should be EHLO.
fmt.Fprintf(server, "250 ok\r\n")
br.ReadString('\n') // Should be MAIL FROM.
fmt.Fprintf(server, "250 ok\r\n")
br.ReadString('\n') // Should be RCPT TO.
fmt.Fprintf(server, "250 ok\r\n")
br.ReadString('\n') // Should be DATA.
fmt.Fprintf(server, "354 continue\r\n")
readline := func(cmd string) {
line, err := br.ReadString('\n')
if err == nil && !strings.HasPrefix(strings.ToLower(line), cmd) {
panic(fmt.Sprintf("unexpected line %q, expected %q", line, cmd))
}
}
writeline := func(s string) {
fmt.Fprintf(server, "%s\r\n", s)
}
readline("ehlo")
writeline("250 mox.example")
readline("mail")
writeline("250 ok")
readline("rcpt")
writeline("250 ok")
readline("data")
writeline("354 continue")
reader := smtp.NewDataReader(br)
io.Copy(io.Discard, reader)
fmt.Fprintf(server, "250 ok\r\n")
br.ReadString('\n') // Should be QUIT.
fmt.Fprintf(server, "221 ok\r\n")
writeline("250 ok")
readline("quit")
writeline("221 ok")
smtpdone <- struct{}{}
}
goodTLSConfig := tls.Config{Certificates: []tls.Certificate{moxCert}}
makeFakeSMTPSTARTTLSServer := func(tlsConfig *tls.Config, nstarttls int) func(server net.Conn) {
attempt := 0
return func(server net.Conn) {
attempt++
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
fmt.Fprintf(server, "220 mox.example\r\n")
br := bufio.NewReader(server)
readline := func(cmd string) {
line, err := br.ReadString('\n')
if err == nil && !strings.HasPrefix(strings.ToLower(line), cmd) {
panic(fmt.Sprintf("unexpected line %q, expected %q", line, cmd))
}
}
writeline := func(s string) {
fmt.Fprintf(server, "%s\r\n", s)
}
readline("ehlo")
writeline("250-mox.example")
writeline("250 starttls")
if nstarttls == 0 || attempt <= nstarttls {
readline("starttls")
writeline("220 ok")
tlsConn := tls.Server(server, tlsConfig)
err := tlsConn.Handshake()
if err != nil {
return
}
server = tlsConn
br = bufio.NewReader(server)
readline("ehlo")
writeline("250 mox.example")
}
readline("mail")
writeline("250 ok")
readline("rcpt")
writeline("250 ok")
readline("data")
writeline("354 continue")
reader := smtp.NewDataReader(br)
io.Copy(io.Discard, reader)
writeline("250 ok")
readline("quit")
writeline("221 ok")
smtpdone <- struct{}{}
}
}
fakeSMTPSTARTTLSServer := makeFakeSMTPSTARTTLSServer(&goodTLSConfig, 0)
makeBadFakeSMTPSTARTTLSServer := func() func(server net.Conn) {
return makeFakeSMTPSTARTTLSServer(&tls.Config{MaxVersion: tls.VersionTLS10, Certificates: []tls.Certificate{moxCert}}, 1)
}
fakeSubmitServer := func(server net.Conn) {
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
fmt.Fprintf(server, "220 mox.example\r\n")
@ -236,19 +311,37 @@ func TestQueue(t *testing.T) {
testDeliver := func(fakeServer func(conn net.Conn)) bool {
t.Helper()
// Setting up a pipe. We'll start a fake smtp server on the server-side. And return the
// client-side to the invocation dial, for the attempted delivery from the queue.
// The delivery should succeed.
server, client := net.Pipe()
defer server.Close()
defer client.Close()
var pipes []net.Conn
defer func() {
for _, conn := range pipes {
conn.Close()
}
}()
var wasNetDialer bool
dial = func(ctx context.Context, dialer contextDialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
smtpclient.DialHook = func(ctx context.Context, dialer smtpclient.Dialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
// Setting up a pipe. We'll start a fake smtp server on the server-side. And return the
// client-side to the invocation dial, for the attempted delivery from the queue.
server, client := net.Pipe()
for _, c := range pipes {
c.Close()
}
pipes = []net.Conn{server, client}
go fakeServer(server)
_, wasNetDialer = dialer.(*net.Dialer)
dialed <- struct{}{}
// For reconnects, we are already waiting for delivery below.
select {
case dialed <- struct{}{}:
default:
}
return client, nil
}
defer func() {
smtpclient.DialHook = nil
}()
waitDeliver := func() {
t.Helper()
@ -279,7 +372,6 @@ func TestQueue(t *testing.T) {
<-deliveryResult // Deliver sends here.
}
go fakeServer(server)
launchWork(resolver, map[string]struct{}{})
waitDeliver()
return wasNetDialer
@ -325,7 +417,7 @@ func TestQueue(t *testing.T) {
}
// Add a message to be delivered with socks.
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, prepareFile(t), nil, true)
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<socks@localhost>", nil, prepareFile(t), nil, true)
tcheck(t, err, "add message to queue for delivery")
transportSocks := "socks"
n, err = Kick(ctxbg, msgID, "", "", &transportSocks)
@ -338,6 +430,79 @@ func TestQueue(t *testing.T) {
t.Fatalf("expected non-net.Dialer as dialer") // SOCKS5 dialer is a private type, we cannot check for it.
}
// Add message to be delivered with opportunistic TLS verification.
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<opportunistictls@localhost>", nil, prepareFile(t), nil, true)
tcheck(t, err, "add message to queue for delivery")
n, err = Kick(ctxbg, msgID, "", "", nil)
tcheck(t, err, "kick queue")
if n != 1 {
t.Fatalf("kick changed %d messages, expected 1", n)
}
testDeliver(fakeSMTPSTARTTLSServer)
// Test fallback to plain text with TLS handshake fails.
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<badtls@localhost>", nil, prepareFile(t), nil, true)
tcheck(t, err, "add message to queue for delivery")
n, err = Kick(ctxbg, msgID, "", "", nil)
tcheck(t, err, "kick queue")
if n != 1 {
t.Fatalf("kick changed %d messages, expected 1", n)
}
testDeliver(makeBadFakeSMTPSTARTTLSServer())
// Add message to be delivered with DANE verification.
resolver.AllAuthentic = true
resolver.TLSA = map[string][]adns.TLSA{
"_25._tcp.mox.example.": {
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: moxCert.Leaf.RawSubjectPublicKeyInfo},
},
}
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<dane@localhost>", nil, prepareFile(t), nil, true)
tcheck(t, err, "add message to queue for delivery")
n, err = Kick(ctxbg, msgID, "", "", nil)
tcheck(t, err, "kick queue")
if n != 1 {
t.Fatalf("kick changed %d messages, expected 1", n)
}
testDeliver(fakeSMTPSTARTTLSServer)
// Check that message is delivered with all unusable DANE records.
resolver.TLSA = map[string][]adns.TLSA{
"_25._tcp.mox.example.": {
{},
},
}
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneunusable@localhost>", nil, prepareFile(t), nil, true)
tcheck(t, err, "add message to queue for delivery")
n, err = Kick(ctxbg, msgID, "", "", nil)
tcheck(t, err, "kick queue")
if n != 1 {
t.Fatalf("kick changed %d messages, expected 1", n)
}
testDeliver(fakeSMTPSTARTTLSServer)
// Check that message is delivered with insecure TLSA records. They should be
// ignored and regular STARTTLS tried.
resolver.Inauthentic = []string{"tlsa _25._tcp.mox.example."}
resolver.TLSA = map[string][]adns.TLSA{
"_25._tcp.mox.example.": {
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: make([]byte, sha256.Size)},
},
}
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneinsecure@localhost>", nil, prepareFile(t), nil, true)
tcheck(t, err, "add message to queue for delivery")
n, err = Kick(ctxbg, msgID, "", "", nil)
tcheck(t, err, "kick queue")
if n != 1 {
t.Fatalf("kick changed %d messages, expected 1", n)
}
testDeliver(makeBadFakeSMTPSTARTTLSServer())
resolver.Inauthentic = nil
// Restore pre-DANE behaviour.
resolver.AllAuthentic = false
resolver.TLSA = nil
// Add another message that we'll fail to deliver entirely.
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, prepareFile(t), nil, true)
tcheck(t, err, "add message to queue for delivery")
@ -349,10 +514,10 @@ func TestQueue(t *testing.T) {
}
msg = msgs[0]
prepServer := func(code string) (net.Conn, func()) {
prepServer := func(fn func(c net.Conn)) (net.Conn, func()) {
server, client := net.Pipe()
go func() {
fmt.Fprintf(server, "%s mox.example\r\n", code)
fn(server)
server.Close()
}()
return client, func() {
@ -361,15 +526,17 @@ func TestQueue(t *testing.T) {
}
}
conn2, cleanup2 := prepServer("220")
conn3, cleanup3 := prepServer("451")
conn2, cleanup2 := prepServer(func(conn net.Conn) { fmt.Fprintf(conn, "220 mox.example\r\n") })
conn3, cleanup3 := prepServer(func(conn net.Conn) { fmt.Fprintf(conn, "451 mox.example\r\n") })
conn4, cleanup4 := prepServer(fakeSMTPSTARTTLSServer)
defer func() {
cleanup2()
cleanup3()
cleanup4()
}()
seq := 0
dial = func(ctx context.Context, dialer contextDialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
smtpclient.DialHook = func(ctx context.Context, dialer smtpclient.Dialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
seq++
switch seq {
default:
@ -378,14 +545,31 @@ func TestQueue(t *testing.T) {
return conn2, nil
case 3:
return conn3, nil
case 4:
return conn4, nil
}
}
defer func() {
smtpclient.DialHook = nil
}()
comm := store.RegisterComm(acc)
defer comm.Unregister()
for i := 1; i < 8; i++ {
go func() { <-deliveryResult }() // Deliver sends here.
if i == 4 {
resolver.AllAuthentic = true
resolver.TLSA = map[string][]adns.TLSA{
"_25._tcp.mox.example.": {
// Non-matching zero CertAssoc, should cause failure.
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeSHA256, CertAssoc: make([]byte, sha256.Size)},
},
}
} else {
resolver.AllAuthentic = false
resolver.TLSA = nil
}
deliver(resolver, msg)
err = DB.Get(ctxbg, &msg)
tcheck(t, err, "get msg")
@ -436,10 +620,13 @@ func TestQueueStart(t *testing.T) {
MX: map[string][]*net.MX{"mox.example.": {{Host: "mox.example", Pref: 10}}},
}
dialed := make(chan struct{}, 1)
dial = func(ctx context.Context, dialer contextDialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
smtpclient.DialHook = func(ctx context.Context, dialer smtpclient.Dialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
dialed <- struct{}{}
return nil, fmt.Errorf("failure from test")
}
defer func() {
smtpclient.DialHook = nil
}()
_, cleanup := setup(t)
defer cleanup()
@ -491,139 +678,6 @@ func TestQueueStart(t *testing.T) {
time.Sleep(100 * time.Millisecond) // Racy... we won't get notified when work is done...
}
func TestGatherHosts(t *testing.T) {
mox.Context = ctxbg
// Test basic MX lookup case, but also following CNAME, detecting CNAME loops and
// having a CNAME limit, connecting directly to a host, and domain that does not
// exist or has temporary error.
resolver := dns.MockResolver{
MX: map[string][]*net.MX{
"basic.example.": {{Host: "mail.basic.example.", Pref: 10}},
"multimx.example.": {{Host: "mail1.multimx.example.", Pref: 10}, {Host: "mail2.multimx.example.", Pref: 10}},
"nullmx.example.": {{Host: ".", Pref: 10}},
"temperror-mx.example.": {{Host: "absent.example.", Pref: 10}},
},
A: map[string][]string{
"mail.basic.example": {"10.0.0.1"},
"justhost.example.": {"10.0.0.1"}, // No MX record for domain, only an A record.
"temperror-a.example.": {"10.0.0.1"},
},
AAAA: map[string][]string{
"justhost6.example.": {"2001:db8::1"}, // No MX record for domain, only an AAAA record.
},
CNAME: map[string]string{
"cname.example.": "basic.example.",
"cnameloop.example.": "cnameloop2.example.",
"cnameloop2.example.": "cnameloop.example.",
"danglingcname.example.": "absent.example.", // Points to missing name.
"temperror-cname.example.": "absent.example.",
},
Fail: map[dns.Mockreq]struct{}{
{Type: "mx", Name: "temperror-mx.example."}: {},
{Type: "host", Name: "temperror-a.example."}: {},
{Type: "cname", Name: "temperror-cname.example."}: {},
},
}
for i := 0; i <= 16; i++ {
s := fmt.Sprintf("cnamelimit%d.example.", i)
next := fmt.Sprintf("cnamelimit%d.example.", i+1)
resolver.CNAME[s] = next
}
test := func(ipd dns.IPDomain, expHosts []dns.IPDomain, expDomain dns.Domain, expPerm bool, expErr error) {
t.Helper()
m := Msg{RecipientDomain: ipd}
hosts, ed, perm, err := gatherHosts(resolver, m, 1, xlog)
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
// todo: could also check the individual errors? code currently does not have structured errors.
t.Fatalf("gather hosts: %v", err)
}
if err != nil {
return
}
if !reflect.DeepEqual(hosts, expHosts) || ed != expDomain || perm != expPerm {
t.Fatalf("got hosts %#v, effectiveDomain %#v, permanent %#v, expected %#v %#v %#v", hosts, ed, perm, expHosts, expDomain, expPerm)
}
}
domain := func(s string) dns.Domain {
d, err := dns.ParseDomain(s)
if err != nil {
t.Fatalf("parse domain: %v", err)
}
return d
}
ipdomain := func(s string) dns.IPDomain {
ip := net.ParseIP(s)
if ip != nil {
return dns.IPDomain{IP: ip}
}
d, err := dns.ParseDomain(s)
if err != nil {
t.Fatalf("parse domain %q: %v", s, err)
}
return dns.IPDomain{Domain: d}
}
ipdomains := func(s ...string) (l []dns.IPDomain) {
for _, e := range s {
l = append(l, ipdomain(e))
}
return
}
var zerodom dns.Domain
test(ipdomain("10.0.0.1"), ipdomains("10.0.0.1"), zerodom, false, nil)
test(ipdomain("basic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, nil) // Basic with simple MX.
test(ipdomain("multimx.example"), ipdomains("mail1.multimx.example", "mail2.multimx.example"), domain("multimx.example"), false, nil) // Basic with simple MX.
test(ipdomain("justhost.example"), ipdomains("justhost.example"), domain("justhost.example"), false, nil) // Only an A record.
test(ipdomain("justhost6.example"), ipdomains("justhost6.example"), domain("justhost6.example"), false, nil) // Only an AAAA record.
test(ipdomain("cname.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, nil) // Follow CNAME.
test(ipdomain("cnamelimit1.example"), nil, zerodom, true, errCNAMELimit)
test(ipdomain("cnameloop.example"), nil, zerodom, true, errCNAMELoop)
test(ipdomain("absent.example"), nil, zerodom, true, errNoRecord)
test(ipdomain("danglingcname.example"), nil, zerodom, true, errNoRecord)
test(ipdomain("nullmx.example"), nil, zerodom, true, errNoMail)
test(ipdomain("temperror-mx.example"), nil, zerodom, false, errDNS)
test(ipdomain("temperror-cname.example"), nil, zerodom, false, errDNS)
test(ipdomain("temperror-a.example"), nil, zerodom, false, errDNS)
}
func TestDialHost(t *testing.T) {
// We mostly want to test that dialing a second time switches to the other address family.
resolver := dns.MockResolver{
A: map[string][]string{
"dualstack.example.": {"10.0.0.1"},
},
AAAA: map[string][]string{
"dualstack.example.": {"2001:db8::1"},
},
}
dial = func(ctx context.Context, dialer contextDialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
return nil, nil // No error, nil connection isn't used.
}
ipdomain := func(s string) dns.IPDomain {
return dns.IPDomain{Domain: dns.Domain{ASCII: s}}
}
m := Msg{DialedIPs: map[string][]net.IP{}}
_, ip, dualstack, err := dialHost(ctxbg, xlog, resolver, nil, ipdomain("dualstack.example"), 25, &m)
if err != nil || ip.String() != "10.0.0.1" || !dualstack {
t.Fatalf("expected err nil, address 10.0.0.1, dualstack true, got %v %v %v", err, ip, dualstack)
}
_, ip, dualstack, err = dialHost(ctxbg, xlog, resolver, nil, ipdomain("dualstack.example"), 25, &m)
if err != nil || ip.String() != "2001:db8::1" || !dualstack {
t.Fatalf("expected err nil, address 2001:db8::1, dualstack true, got %v %v %v", err, ip, dualstack)
}
}
// Just a cert that appears valid.
func fakeCert(t *testing.T, name string, expired bool) tls.Certificate {
notAfter := time.Now()

View file

@ -24,7 +24,7 @@ import (
// deliver via another SMTP server, e.g. relaying to a smart host, possibly
// with authentication (submission).
func deliverSubmit(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer contextDialer, m Msg, backoff time.Duration, transportName string, transport *config.TransportSMTP, dialTLS bool, defaultPort int) {
func deliverSubmit(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, m Msg, backoff time.Duration, transportName string, transport *config.TransportSMTP, dialTLS bool, defaultPort int) {
// todo: configurable timeouts
port := transport.Port
@ -51,10 +51,25 @@ func deliverSubmit(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer cont
qlog.Debug("queue deliversubmit result", mlog.Field("host", transport.DNSHost), mlog.Field("port", port), mlog.Field("attempt", m.Attempts), mlog.Field("permanent", permanent), mlog.Field("secodeopt", secodeOpt), mlog.Field("errmsg", errmsg), mlog.Field("ok", success), mlog.Field("duration", time.Since(start)))
}()
// We don't have to attempt SMTP-DANE for submission, since it only applies to SMTP
// relaying on port 25. ../rfc/7672:1261
// todo: for submission, understand SRV records, and even DANE.
dialctx, dialcancel := context.WithTimeout(context.Background(), 30*time.Second)
defer dialcancel()
if m.DialedIPs == nil {
m.DialedIPs = map[string][]net.IP{}
}
_, _, _, ips, _, err := smtpclient.GatherIPs(dialctx, qlog, resolver, dns.IPDomain{Domain: transport.DNSHost}, m.DialedIPs)
var conn net.Conn
if err == nil {
if m.DialedIPs == nil {
m.DialedIPs = map[string][]net.IP{}
}
conn, _, err = smtpclient.Dial(dialctx, qlog, dialer, dns.IPDomain{Domain: transport.DNSHost}, ips, port, m.DialedIPs)
}
addr := net.JoinHostPort(transport.Host, fmt.Sprintf("%d", port))
conn, _, _, err := dialHost(dialctx, qlog, resolver, dialer, dns.IPDomain{Domain: transport.DNSHost}, port, &m)
var result string
switch {
case err == nil:
@ -103,7 +118,7 @@ func deliverSubmit(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer cont
}
clientctx, clientcancel := context.WithTimeout(context.Background(), 60*time.Second)
defer clientcancel()
client, err := smtpclient.New(clientctx, qlog, conn, tlsMode, mox.Conf.Static.HostnameDomain, transport.DNSHost, auth)
client, err := smtpclient.New(clientctx, qlog, conn, tlsMode, mox.Conf.Static.HostnameDomain, transport.DNSHost, auth, nil, nil, nil)
if err != nil {
smtperr, ok := err.(smtpclient.Error)
var remoteMTA dsn.NameIP

View file

@ -3,6 +3,13 @@ package main
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"log"
@ -146,6 +153,36 @@ logging in with IMAP.
resolveCtx, resolveCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer resolveCancel()
fmt.Printf("Checking if DNS resolvers are DNSSEC-verifying...")
_, resolverDNSSECResult, err := resolver.LookupNS(resolveCtx, ".")
if err != nil {
fmt.Println("")
fatalf("checking dnssec support in resolver: %v", err)
} else if !resolverDNSSECResult.Authentic {
fmt.Printf(`
WARNING: It looks like the DNS resolvers configured on your system do not
verify DNSSEC, or aren't trusted (by having loopback IPs or through "options
trust-ad" in /etc/resolv.conf). Without DNSSEC, outbound delivery with SMTP
used unprotected MX records, and SMTP STARTTLS connections cannot verify the TLS
certificate with DANE (based on a public key in DNS), and will fallback to
either MTA-STS for verification, or use "opportunistic TLS" with no certificate
verification.
Recommended action: Install unbound, a DNSSEC-verifying recursive DNS resolver,
and enable support for "extended dns errors" (EDE):
cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
server:
ede: yes
val-log-level: 2
EOF
`)
} else {
fmt.Println(" OK")
}
// We are going to find the (public) IPs to listen on and possibly the host name.
// Start with reasonable defaults. We'll replace them specific IPs, if we can find them.
@ -244,7 +281,7 @@ logging in with IMAP.
for _, ip := range publicIPs {
revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer revcancel()
l, err := resolver.LookupAddr(revctx, ip)
l, _, err := resolver.LookupAddr(revctx, ip)
if err != nil {
warnf("WARNING: looking up reverse name(s) for %s: %v", ip, err)
}
@ -306,7 +343,7 @@ again with the -hostname flag.
fmt.Printf("Looking up IPs for hostname %s...", dnshostname)
ipctx, ipcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer ipcancel()
ips, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
ips, domainDNSSECResult, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
ipcancel()
var xips []net.IPAddr
var hostIPs []string
@ -403,6 +440,23 @@ This likely means one of two things:
`, dnshostname, err)
} else if !domainDNSSECResult.Authentic {
if !dnswarned {
fmt.Printf("\n")
}
dnswarned = true
fmt.Printf(`
NOTE: It looks like the DNS records of your domain (zone) are not DNSSEC-signed.
Mail servers that send email to your domain, or receive email from your domain,
cannot verify that the MX/SPF/DKIM/DMARC/MTA-STS records they receive are
authentic. DANE, for authenticated delivery without relying on a pool of
certificate authorities, requires DNSSEC, so will not be configured at this
time.
Recommended action: Continue now, but consider enabling DNSSEC for your domain
later at your DNS operator, and adding DANE records for protecting incoming
messages over SMTP.
`)
}
if !dnswarned {
@ -421,7 +475,7 @@ This likely means one of two things:
go func() {
revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer revcancel()
addrs, err := resolver.LookupAddr(revctx, s)
addrs, _, err := resolver.LookupAddr(revctx, s)
results <- result{s, addrs, err}
}()
}
@ -581,9 +635,57 @@ many authentication failures).
{CertFile: autoconfigbase + "-chain.crt.pem", KeyFile: autoconfigbase + ".key.pem"},
},
}
fmt.Println(
`Placeholder paths to TLS certificates to be provided by the existing webserver
have been placed in config/mox.conf and need to be edited.
No private keys for the public listener have been generated for use with DANE.
To configure DANE (which requires DNSSEC), set config field HostPrivateKeyFiles
in the "public" Listener to both RSA 2048-bit and ECDSA P-256 private key files
and check the admin page for the needed DNS records.`)
} else {
// todo: we may want to generate a second set of keys, make the user already add it to the DNS, but keep the private key offline. would require config option to specify a public key only, so the dane records can be generated.
hostRSAPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
if err != nil {
fatalf("generating rsa private key for host: %s", err)
}
hostECDSAPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
if err != nil {
fatalf("generating ecsa private key for host: %s", err)
}
now := time.Now()
timestamp := now.Format("20060102T150405")
hostRSAPrivateKeyFile := fmt.Sprintf("hostkeys/%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "rsa2048")
hostECDSAPrivateKeyFile := fmt.Sprintf("hostkeys/%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "ecdsap256")
xwritehostkeyfile := func(path string, key crypto.Signer) {
buf, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
fatalf("marshaling host private key to pkcs8 for %s: %s", path, err)
}
var b bytes.Buffer
block := pem.Block{
Type: "PRIVATE KEY",
Bytes: buf,
}
err = pem.Encode(&b, &block)
if err != nil {
fatalf("pem-encoding host private key file for %s: %s", path, err)
}
xwritefile(path, b.Bytes(), 0600)
}
xwritehostkeyfile(filepath.Join("config", hostRSAPrivateKeyFile), hostRSAPrivateKey)
xwritehostkeyfile(filepath.Join("config", hostECDSAPrivateKeyFile), hostECDSAPrivateKey)
public.TLS = &config.TLS{
ACME: "letsencrypt",
HostPrivateKeyFiles: []string{
hostRSAPrivateKeyFile,
hostECDSAPrivateKeyFile,
},
HostPrivateRSA2048Keys: []crypto.Signer{hostRSAPrivateKey},
HostPrivateECDSAP256Keys: []crypto.Signer{hostECDSAPrivateKey},
}
public.AutoconfigHTTPS.Enabled = true
public.MTASTSHTTPS.Enabled = true
@ -780,7 +882,7 @@ configured correctly.
// priming dns caches with negative/absent records, causing our "quick setup" to
// appear to fail or take longer than "quick".
records, err := mox.DomainRecords(confDomain, domain)
records, err := mox.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic)
if err != nil {
fatalf("making required DNS records")
}
@ -837,9 +939,6 @@ To access these from your browser, run
"ssh -L 8080:localhost:80 you@yourmachine" locally and open
http://localhost:8080/[...].
For secure email exchange you should have a strictly validating DNSSEC
resolver. An easy and the recommended way is to install unbound.
If you run into problem, have questions/feedback or found a bug, please let us
know. Mox needs your help!

View file

@ -122,10 +122,14 @@ https://www.iana.org/assignments/message-headers/message-headers.xhtml
8904 DNS Whitelist (DNSWL) Email Authentication Method Extension
# DANE
6394 Use Cases and Requirements for DNS-Based Authentication of Named Entities (DANE)
6698 The DNS-Based Authentication of Named Entities (DANE) Transport Layer Security (TLS) Protocol: TLSA
7218 Adding Acronyms to Simplify Conversations about DNS-Based Authentication of Named Entities (DANE)
7671 The DNS-Based Authentication of Named Entities (DANE) Protocol: Updates and Operational Guidance
7672 SMTP Security via Opportunistic DNS-Based Authentication of Named Entities (DANE) Transport Layer Security (TLS)
7673 Using DNS-Based Authentication of Named Entities (DANE) TLSA Records with SRV Records
7929 DNS-Based Authentication of Named Entities (DANE) Bindings for OpenPGP
8162 Using Secure DNS to Associate Certificates with Domain Names for S/MIME
# TLS-RPT
8460 SMTP TLS Reporting
@ -283,6 +287,7 @@ See implementation guide, https://jmap.io/server.html
# TLS
6125 Representation and Verification of Domain-Based Application Service Identity within Internet Public Key Infrastructure Using X.509 (PKIX) Certificates in the Context of Transport Layer Security (TLS)
7250 Using Raw Public Keys in Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)
7525 Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)
8314 Cleartext Considered Obsolete: Use of Transport Layer Security (TLS) for Email Submission and Access
8996 Deprecating TLS 1.0 and TLS 1.1
@ -321,21 +326,31 @@ See implementation guide, https://jmap.io/server.html
1536 Common DNS Implementation Errors and Suggested Fixes
2181 Clarifications to the DNS Specification
2308 Negative Caching of DNS Queries (DNS NCACHE)
2672 (obsoleted by RFC 6672) Non-Terminal DNS Name Redirection
3226 DNSSEC and IPv6 A6 aware server/resolver message size requirements
3363 Representing Internet Protocol version 6 (IPv6) Addresses in the Domain Name System (DNS)
3596 DNS Extensions to Support IP Version 6
3597 Handling of Unknown DNS Resource Record (RR) Types
3833 Threat Analysis of the Domain Name System (DNS)
4343 Domain Name System (DNS) Case Insensitivity Clarification
4592 The Role of Wildcards in the Domain Name System
5001 DNS Name Server Identifier (NSID) Option
5452 Measures for Making DNS More Resilient against Forged Answers
6604 xNAME RCODE and Status Bits Clarification
6672 DNAME Redirection in the DNS
6891 Extension Mechanisms for DNS (EDNS(0))
6895 Domain Name System (DNS) IANA Considerations
7686 The ".onion" Special-Use Domain Name
7766 DNS Transport over TCP - Implementation Requirements
7828 The edns-tcp-keepalive EDNS0 Option
7873 Domain Name System (DNS) Cookies
8020 NXDOMAIN: There Really Is Nothing Underneath
8482 Providing Minimal-Sized Responses to DNS Queries That Have QTYPE=ANY
8490 DNS Stateful Operations
8499 DNS Terminology
8767 Serving Stale Data to Improve DNS Resiliency
8914 Extended DNS Errors
9018 Interoperable Domain Name System (DNS) Server Cookies
9210 DNS Transport over TCP - Operational Requirements
# DNSSEC
@ -352,6 +367,7 @@ See implementation guide, https://jmap.io/server.html
6014 Cryptographic Algorithm Identifier Allocation for DNSSEC
6781 DNSSEC Operational Practices, Version 2
6840 Clarifications and Implementation Notes for DNS Security (DNSSEC)
7901 CHAIN Query Requests in DNS
8198 Aggressive Use of DNSSEC-Validated Cache
8624 Algorithm Implementation Requirements and Usage Guidance for DNSSEC
8749 Moving DNSSEC Lookaside Validation (DLV) to Historic Status

View file

@ -279,7 +279,8 @@ binary should be setgid that group:
xsavecheckf(err, "parsing remote hostname")
}
client, err := smtpclient.New(ctx, mlog.New("sendmail"), conn, tlsMode, ourHostname, remoteHostname, auth)
// todo: implement SRV and DANE, allowing for a simpler config file (just the email address & password)
client, err := smtpclient.New(ctx, mlog.New("sendmail"), conn, tlsMode, ourHostname, remoteHostname, auth, nil, nil, nil)
xsavecheckf(err, "open smtp session")
err = client.Deliver(ctx, submitconf.From, recipient, int64(len(msg)), strings.NewReader(msg), true, false)

View file

@ -17,6 +17,9 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/mjl-/adns"
"github.com/mjl-/mox/dane"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/metrics"
"github.com/mjl-/mox/mlog"
@ -49,7 +52,7 @@ var (
ErrSMTPUTF8Unsupported = errors.New("remote smtp server does not implement smtputf8 extension, required by message")
ErrStatus = errors.New("remote smtp server sent unexpected response status code") // Relatively common, e.g. when a 250 OK was expected and server sent 451 temporary error.
ErrProtocol = errors.New("smtp protocol error") // After a malformed SMTP response or inconsistent multi-line response.
ErrTLS = errors.New("tls error") // E.g. handshake failure, or hostname validation was required and failed.
ErrTLS = errors.New("tls error") // E.g. handshake failure, or hostname verification was required and failed.
ErrBotched = errors.New("smtp connection is botched") // Set on a client, and returned for new operations, after an i/o error or malformed SMTP response.
ErrClosed = errors.New("client is closed")
)
@ -58,13 +61,20 @@ var (
type TLSMode string
const (
// TLS with STARTTLS for MX SMTP servers, with validated certificate is required: matching name, not expired, trusted by CA.
// Required TLS with STARTTLS for SMTP servers, with either verified DANE TLSA
// record, or a WebPKI-verified certificate (with matching name, not expired, etc).
TLSStrictStartTLS TLSMode = "strictstarttls"
// TLS immediately ("implicit TLS"), with validated certificate is required: matching name, not expired, trusted by CA.
// Required TLS with STARTTLS for SMTP servers, without verifiying the certificate.
// This mode is needed to fallback after only unusable DANE records were found
// (e.g. with unknown parameters in the TLSA records).
TLSUnverifiedStartTLS TLSMode = "unverifiedstarttls"
// TLS immediately ("implicit TLS"), with either verified DANE TLSA records or a
// verified certificate: matching name, not expired, trusted by CA.
TLSStrictImmediate TLSMode = "strictimmediate"
// Use TLS if remote claims to support it, but do not validate the certificate
// Use TLS if remote claims to support it, but do not verify the certificate
// (not trusted by CA, different host name or expired certificate is accepted).
TLSOpportunistic TLSMode = "opportunistic"
@ -80,8 +90,12 @@ type Client struct {
// can be wrapped in a tls.Client. We close origConn instead of conn because
// closing the TLS connection would send a TLS close notification, which may block
// for 5s if the server isn't reading it (because it is also sending it).
origConn net.Conn
conn net.Conn
origConn net.Conn
conn net.Conn
remoteHostname dns.Domain // TLS with SNI and name verification.
daneRecords []adns.TLSA // For authenticating (START)TLS connection.
moreRemoteHostnames []dns.Domain // Additional allowed names in TLS certificate.
verifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any.
r *bufio.Reader
w *bufio.Writer
@ -164,21 +178,27 @@ func (e Error) Error() string {
// records with preferences, other DNS records, MTA-STS, retries and special
// cases into account.
//
// tlsMode indicates if TLS is required, optional or should not be used. A
// certificate is only validated (trusted, match remoteHostname and not expired)
// for the strict tls modes. By default, SMTP does not verify TLS for
// interopability reasons, but MTA-STS or DANE can require it. If opportunistic TLS
// is used, and a TLS error is encountered, the caller may want to try again (on a
// new connection) without TLS.
// tlsMode indicates if TLS is required, optional or should not be used. Only for
// strict TLS modes is the certificate verified: Either with DANE, or through
// the trusted CA pool with matching remoteHostname and not expired. For DANE,
// additional host names in moreRemoteHostnames are allowed during TLS certificate
// verification. By default, SMTP does not verify TLS for interopability reasons,
// but MTA-STS or DANE can require it. If opportunistic TLS is used, and a TLS
// error is encountered, the caller may want to try again (on a new connection)
// without TLS.
//
// If auth is non-empty, authentication will be done with the first algorithm
// supported by the server. If none of the algorithms are supported, an error is
// returned.
func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, ourHostname, remoteHostname dns.Domain, auth []sasl.Client) (*Client, error) {
func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, ehloHostname, remoteHostname dns.Domain, auth []sasl.Client, daneRecords []adns.TLSA, moreRemoteHostnames []dns.Domain, verifiedRecord *adns.TLSA) (*Client, error) {
c := &Client{
origConn: conn,
lastlog: time.Now(),
cmds: []string{"(none)"},
origConn: conn,
remoteHostname: remoteHostname,
daneRecords: daneRecords,
moreRemoteHostnames: moreRemoteHostnames,
verifiedRecord: verifiedRecord,
lastlog: time.Now(),
cmds: []string{"(none)"},
}
c.log = log.Fields(mlog.Field("smtpclient", "")).MoreFields(func() []mlog.Pair {
now := time.Now()
@ -190,12 +210,9 @@ func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, our
})
if tlsMode == TLSStrictImmediate {
tlsconfig := tls.Config{
ServerName: remoteHostname.ASCII,
RootCAs: mox.Conf.Static.TLS.CertPool,
MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
}
tlsconn := tls.Client(conn, &tlsconfig)
// todo: we could also verify DANE here. not applicable to SMTP delivery.
config := c.tlsConfig(tlsMode)
tlsconn := tls.Client(conn, &config)
if err := tlsconn.HandshakeContext(ctx); err != nil {
return nil, err
}
@ -216,12 +233,25 @@ func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, our
c.tw = moxio.NewTraceWriter(c.log, "LC: ", timeoutWriter{c.conn, 30 * time.Second, c.log})
c.w = bufio.NewWriter(c.tw)
if err := c.hello(ctx, tlsMode, ourHostname, remoteHostname, auth); err != nil {
if err := c.hello(ctx, tlsMode, ehloHostname, auth); err != nil {
return nil, err
}
return c, nil
}
func (c *Client) tlsConfig(tlsMode TLSMode) tls.Config {
if c.daneRecords != nil {
return dane.TLSClientConfig(c.log, c.daneRecords, c.remoteHostname, c.moreRemoteHostnames, c.verifiedRecord)
}
// todo: possibly accept older TLS versions for TLSOpportunistic?
return tls.Config{
ServerName: c.remoteHostname.ASCII,
RootCAs: mox.Conf.Static.TLS.CertPool,
InsecureSkipVerify: tlsMode == TLSOpportunistic || tlsMode == TLSUnverifiedStartTLS,
MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
}
}
// xbotchf generates a temporary error and marks the client as botched. e.g. for
// i/o errors or invalid protocol messages.
func (c *Client) xbotchf(code int, secode string, lastLine, format string, args ...any) {
@ -463,7 +493,7 @@ func (c *Client) recover(rerr *error) {
*rerr = cerr
}
func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ourHostname, remoteHostname dns.Domain, auth []sasl.Client) (rerr error) {
func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Domain, auth []sasl.Client) (rerr error) {
defer c.recover(&rerr)
// perform EHLO handshake, falling back to HELO if server does not appear to
@ -474,7 +504,7 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ourHostname, remote
c.cmds[0] = "ehlo"
c.cmdStart = time.Now()
// Syntax: ../rfc/5321:1827
c.xwritelinef("EHLO %s", ourHostname.ASCII)
c.xwritelinef("EHLO %s", ehloHostname.ASCII)
code, _, lastLine, remains := c.xreadecode(false)
switch code {
// ../rfc/5321:997
@ -486,7 +516,7 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ourHostname, remote
// ../rfc/5321:996
c.cmds[0] = "helo"
c.cmdStart = time.Now()
c.xwritelinef("HELO %s", ourHostname.ASCII)
c.xwritelinef("HELO %s", ehloHostname.ASCII)
code, _, lastLine, _ = c.xreadecode(false)
if code != smtp.C250Completed {
c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250 to HELO, got %d", ErrStatus, code)
@ -536,8 +566,8 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ourHostname, remote
hello(true)
// Attempt TLS if remote understands STARTTLS and we aren't doing immediate TLS or if caller requires it.
if c.extStartTLS && (tlsMode != TLSSkip && tlsMode != TLSStrictImmediate) || tlsMode == TLSStrictStartTLS {
c.log.Debug("starting tls client", mlog.Field("tlsmode", tlsMode), mlog.Field("servername", remoteHostname))
if c.extStartTLS && (tlsMode != TLSSkip && tlsMode != TLSStrictImmediate) || tlsMode == TLSStrictStartTLS || tlsMode == TLSUnverifiedStartTLS {
c.log.Debug("starting tls client", mlog.Field("tlsmode", tlsMode), mlog.Field("servername", c.remoteHostname))
c.cmds[0] = "starttls"
c.cmdStart = time.Now()
c.xwritelinef("STARTTLS")
@ -561,14 +591,8 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ourHostname, remote
// For TLSStrictStartTLS, the Go TLS library performs the checks needed for MTA-STS.
// ../rfc/8461:646
// todo: possibly accept older TLS versions for TLSOpportunistic?
tlsConfig := &tls.Config{
ServerName: remoteHostname.ASCII,
RootCAs: mox.Conf.Static.TLS.CertPool,
InsecureSkipVerify: tlsMode != TLSStrictStartTLS,
MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
}
nconn := tls.Client(conn, tlsConfig)
tlsConfig := c.tlsConfig(tlsMode)
nconn := tls.Client(conn, &tlsConfig)
c.conn = nconn
nctx, cancel := context.WithTimeout(ctx, time.Minute)
@ -584,7 +608,7 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ourHostname, remote
c.w = bufio.NewWriter(c.tw)
tlsversion, ciphersuite := mox.TLSInfo(nconn)
c.log.Debug("starttls client handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", remoteHostname), mlog.Field("insecureskipverify", tlsConfig.InsecureSkipVerify))
c.log.Debug("starttls client handshake done", mlog.Field("tlsmode", tlsMode), mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", c.remoteHostname), mlog.Field("danerecord", c.verifiedRecord))
hello(false)
}
@ -916,3 +940,13 @@ func (c *Client) Close() (rerr error) {
}
return
}
// Conn returns the connection with initialized SMTP session. Once the caller uses
// this connection it is in control, and responsible for closing the connection,
// and other functions on the client must not be called anymore.
func (c *Client) Conn() (net.Conn, error) {
if err := c.conn.SetDeadline(time.Time{}); err != nil {
return nil, fmt.Errorf("clearing io deadlines: %w", err)
}
return c.conn, nil
}

View file

@ -271,7 +271,7 @@ func TestClient(t *testing.T) {
result <- fmt.Errorf("client: %w", fmt.Errorf(format, args...))
panic("stop")
}
c, err := New(ctx, log, clientConn, opts.tlsMode, localhost, opts.tlsHostname, auths)
c, err := New(ctx, log, clientConn, opts.tlsMode, localhost, opts.tlsHostname, auths, nil, nil, nil)
if (err == nil) != (expClientErr == nil) || err != nil && !errors.As(err, reflect.New(reflect.ValueOf(expClientErr).Type()).Interface()) && !errors.Is(err, expClientErr) {
fail("new client: got err %v, expected %#v", err, expClientErr)
}
@ -373,7 +373,7 @@ func TestErrors(t *testing.T) {
run(t, func(s xserver) {
s.writeline("bogus") // Invalid, should be "220 <hostname>".
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
var xerr Error
if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
@ -384,7 +384,7 @@ func TestErrors(t *testing.T) {
run(t, func(s xserver) {
s.conn.Close()
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
var xerr Error
if err == nil || !errors.Is(err, io.ErrUnexpectedEOF) || !errors.As(err, &xerr) || xerr.Permanent {
panic(fmt.Errorf("got %#v (%v), expected ErrUnexpectedEOF without Permanent", err, err))
@ -395,7 +395,7 @@ func TestErrors(t *testing.T) {
run(t, func(s xserver) {
s.writeline("521 not accepting connections")
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
var xerr Error
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
@ -406,7 +406,7 @@ func TestErrors(t *testing.T) {
run(t, func(s xserver) {
s.writeline("2200 mox.example") // Invalid, too many digits.
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
var xerr Error
if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
@ -420,7 +420,7 @@ func TestErrors(t *testing.T) {
s.writeline("250-mox.example")
s.writeline("500 different code") // Invalid.
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
var xerr Error
if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
@ -436,7 +436,7 @@ func TestErrors(t *testing.T) {
s.readline("MAIL FROM:")
s.writeline("550 5.7.0 not allowed")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
if err != nil {
panic(err)
}
@ -456,7 +456,7 @@ func TestErrors(t *testing.T) {
s.readline("MAIL FROM:")
s.writeline("451 bad sender")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
if err != nil {
panic(err)
}
@ -478,7 +478,7 @@ func TestErrors(t *testing.T) {
s.readline("RCPT TO:")
s.writeline("451")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
if err != nil {
panic(err)
}
@ -502,7 +502,7 @@ func TestErrors(t *testing.T) {
s.readline("DATA")
s.writeline("550 no!")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
if err != nil {
panic(err)
}
@ -522,7 +522,7 @@ func TestErrors(t *testing.T) {
s.readline("STARTTLS")
s.writeline("502 command not implemented")
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSStrictStartTLS, localhost, dns.Domain{ASCII: "mox.example"}, nil)
_, err := New(ctx, log, conn, TLSStrictStartTLS, localhost, dns.Domain{ASCII: "mox.example"}, nil, nil, nil, nil)
var xerr Error
if err == nil || !errors.Is(err, ErrTLS) || !errors.As(err, &xerr) || !xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrTLS with Permanent", err))
@ -538,7 +538,7 @@ func TestErrors(t *testing.T) {
s.readline("MAIL FROM:")
s.writeline("451 enough")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSSkip, localhost, dns.Domain{ASCII: "mox.example"}, nil)
c, err := New(ctx, log, conn, TLSSkip, localhost, dns.Domain{ASCII: "mox.example"}, nil, nil, nil, nil)
if err != nil {
panic(err)
}
@ -568,7 +568,7 @@ func TestErrors(t *testing.T) {
s.readline("DATA")
s.writeline("550 not now")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
if err != nil {
panic(err)
}
@ -598,7 +598,7 @@ func TestErrors(t *testing.T) {
s.readline("MAIL FROM:")
s.writeline("550 ok")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
if err != nil {
panic(err)
}

87
smtpclient/dial.go Normal file
View file

@ -0,0 +1,87 @@
package smtpclient
import (
"context"
"fmt"
"net"
"time"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
)
// DialHook can be used during tests to override the regular dialer from being used.
var DialHook func(ctx context.Context, dialer Dialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error)
func dial(ctx context.Context, dialer Dialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
// todo: see if we can remove this function and DialHook in favor of the Dialer interface.
if DialHook != nil {
return DialHook(ctx, dialer, timeout, addr, laddr)
}
// If this is a net.Dialer, use its settings and add the timeout and localaddr.
// This is the typical case, but SOCKS5 support can use a different dialer.
if d, ok := dialer.(*net.Dialer); ok {
nd := *d
nd.Timeout = timeout
nd.LocalAddr = laddr
return nd.DialContext(ctx, "tcp", addr)
}
return dialer.DialContext(ctx, "tcp", addr)
}
// Dialer is used to dial mail servers, an interface to facilitate testing.
type Dialer interface {
DialContext(ctx context.Context, network, addr string) (c net.Conn, err error)
}
// Dial connects to host by dialing ips, taking previous attempts in dialedIPs into
// accounts (for greylisting, blocklisting and ipv4/ipv6).
//
// If the previous attempt used IPv4, this attempt will use IPv6 (in case one of
// the IPs is in a DNSBL).
// The second attempt for an address family we prefer the same IP as earlier, to
// increase our chances if remote is doing greylisting.
//
// Dial updates dialedIPs, callers may want to save it so it can be taken into
// account for future delivery attempts.
//
// If we have fully specified local SMTP listener IPs, we set those for the
// outgoing connection. The admin probably configured these same IPs in SPF, but
// others possibly not.
func Dial(ctx context.Context, log *mlog.Log, dialer Dialer, host dns.IPDomain, ips []net.IP, port int, dialedIPs map[string][]net.IP) (conn net.Conn, ip net.IP, rerr error) {
timeout := 30 * time.Second
if deadline, ok := ctx.Deadline(); ok && len(ips) > 0 {
timeout = time.Until(deadline) / time.Duration(len(ips))
}
var lastErr error
var lastIP net.IP
for _, ip := range ips {
addr := net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port))
log.Debug("dialing host", mlog.Field("addr", addr))
var laddr net.Addr
for _, lip := range mox.Conf.Static.SpecifiedSMTPListenIPs {
ipIs4 := ip.To4() != nil
lipIs4 := lip.To4() != nil
if ipIs4 == lipIs4 {
laddr = &net.TCPAddr{IP: lip}
break
}
}
conn, err := dial(ctx, dialer, timeout, addr, laddr)
if err == nil {
log.Debug("connected to host", mlog.Field("host", host), mlog.Field("addr", addr), mlog.Field("laddr", laddr))
name := host.String()
dialedIPs[name] = append(dialedIPs[name], ip)
return conn, ip, nil
}
log.Debugx("connection attempt", err, mlog.Field("host", host), mlog.Field("addr", addr), mlog.Field("laddr", laddr))
lastErr = err
lastIP = ip
}
// todo: possibly return all errors joined?
return nil, lastIP, lastErr
}

57
smtpclient/dial_test.go Normal file
View file

@ -0,0 +1,57 @@
package smtpclient
import (
"context"
"net"
"reflect"
"testing"
"time"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
)
func TestDialHost(t *testing.T) {
// We mostly want to test that dialing a second time switches to the other address family.
ctxbg := context.Background()
log := mlog.New("smtpclient")
resolver := dns.MockResolver{
A: map[string][]string{
"dualstack.example.": {"10.0.0.1"},
},
AAAA: map[string][]string{
"dualstack.example.": {"2001:db8::1"},
},
}
DialHook = func(ctx context.Context, dialer Dialer, timeout time.Duration, addr string, laddr net.Addr) (net.Conn, error) {
return nil, nil // No error, nil connection isn't used.
}
defer func() {
DialHook = nil
}()
ipdomain := func(s string) dns.IPDomain {
return dns.IPDomain{Domain: dns.Domain{ASCII: s}}
}
dialedIPs := map[string][]net.IP{}
_, _, _, ips, dualstack, err := GatherIPs(ctxbg, log, resolver, ipdomain("dualstack.example"), dialedIPs)
if err != nil || !reflect.DeepEqual(ips, []net.IP{net.ParseIP("10.0.0.1"), net.ParseIP("2001:db8::1")}) || !dualstack {
t.Fatalf("expected err nil, address 10.0.0.1,2001:db8::1, dualstack true, got %v %v %v", err, ips, dualstack)
}
_, ip, err := Dial(ctxbg, log, nil, ipdomain("dualstack.example"), ips, 25, dialedIPs)
if err != nil || ip.String() != "10.0.0.1" {
t.Fatalf("expected err nil, address 10.0.0.1, dualstack true, got %v %v %v", err, ip, dualstack)
}
_, _, _, ips, dualstack, err = GatherIPs(ctxbg, log, resolver, ipdomain("dualstack.example"), dialedIPs)
if err != nil || !reflect.DeepEqual(ips, []net.IP{net.ParseIP("2001:db8::1"), net.ParseIP("10.0.0.1")}) || !dualstack {
t.Fatalf("expected err nil, address 2001:db8::1,10.0.0.1, dualstack true, got %v %v %v", err, ips, dualstack)
}
_, ip, err = Dial(ctxbg, log, nil, ipdomain("dualstack.example"), ips, 25, dialedIPs)
if err != nil || ip.String() != "2001:db8::1" {
t.Fatalf("expected err nil, address 2001:db8::1, dualstack true, got %v %v %v", err, ip, dualstack)
}
}

419
smtpclient/gather.go Normal file
View file

@ -0,0 +1,419 @@
package smtpclient
import (
"context"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"errors"
"fmt"
"net"
"sort"
"strings"
"time"
"github.com/mjl-/adns"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
)
var (
errCNAMELoop = errors.New("cname loop")
errCNAMELimit = errors.New("too many cname records")
errDNS = errors.New("dns lookup error")
errNoMail = errors.New("domain does not accept email as indicated with single dot for mx record")
)
// GatherDestinations looks up the hosts to deliver email to a domain ("next-hop").
// If it is an IP address, it is the only destination to try. Otherwise CNAMEs of
// the domain are followed. Then MX records for the expanded CNAME are looked up.
// If no MX record is present, the original domain is returned. If an MX record is
// present but indicates the domain does not accept email, ErrNoMail is returned.
// If valid MX records were found, the MX target hosts are returned.
//
// haveMX indicates if an MX record was found.
//
// origNextHopAuthentic indicates if the DNS record for the initial domain name was
// DNSSEC secure (CNAME, MX).
//
// expandedNextHopAuthentic indicates if the DNS records after following CNAMEs were
// DNSSEC secure.
//
// These authentic flags are used by DANE, to determine where to look up TLSA
// records, and which names to allow in the remote TLS certificate. If MX records
// were found, both the original and expanded next-hops must be authentic for DANE
// to apply. For a non-IP with no MX records found, the authentic result can be
// used to decide which of the names to use as TLSA base domain.
func GatherDestinations(ctx context.Context, log *mlog.Log, resolver dns.Resolver, origNextHop dns.IPDomain) (haveMX, origNextHopAuthentic, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, hosts []dns.IPDomain, permanent bool, err error) {
// ../rfc/5321:3824
// IP addresses are dialed directly, and don't have TLSA records.
if len(origNextHop.IP) > 0 {
return false, false, false, expandedNextHop, []dns.IPDomain{origNextHop}, false, nil
}
// We start out assuming the result is authentic. Updated with each lookup.
origNextHopAuthentic = true
expandedNextHopAuthentic = true
// We start out delivering to the recipient domain. We follow CNAMEs.
rcptDomain := origNextHop.Domain
// Domain we are actually delivering to, after following CNAME record(s).
expandedNextHop = rcptDomain
// Keep track of CNAMEs we have followed, to detect loops.
domainsSeen := map[string]bool{}
for i := 0; ; i++ {
if domainsSeen[expandedNextHop.ASCII] {
// todo: only mark as permanent failure if TTLs for all records are beyond latest possibly delivery retry we would do.
err := fmt.Errorf("%w: recipient domain %s: already saw %s", errCNAMELoop, rcptDomain, expandedNextHop)
return false, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, false, err
}
domainsSeen[expandedNextHop.ASCII] = true
// note: The Go resolver returns the requested name if the domain has no CNAME
// record but has a host record.
if i == 16 {
// We have a maximum number of CNAME records we follow. There is no hard limit for
// DNS, and you might think folks wouldn't configure CNAME chains at all, but for
// (non-mail) domains, CNAME chains of 10 records have been encountered according
// to the internet.
// todo: only mark as permanent failure if TTLs for all records are beyond latest possibly delivery retry we would do.
err := fmt.Errorf("%w: recipient domain %s, last resolved domain %s", errCNAMELimit, rcptDomain, expandedNextHop)
return false, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, false, err
}
// Do explicit CNAME lookup. Go's LookupMX also resolves CNAMEs, but we want to
// know the final name, and we're interested in learning if the first vs later
// results were DNSSEC-(in)secure.
// ../rfc/5321:3838 ../rfc/3974:197
cctx, ccancel := context.WithTimeout(ctx, 30*time.Second)
defer ccancel()
cname, cnameResult, err := resolver.LookupCNAME(cctx, expandedNextHop.ASCII+".")
ccancel()
if i == 0 {
origNextHopAuthentic = origNextHopAuthentic && cnameResult.Authentic
}
expandedNextHopAuthentic = expandedNextHopAuthentic && cnameResult.Authentic
if err != nil && !dns.IsNotFound(err) {
err = fmt.Errorf("%w: cname lookup for %s: %v", errDNS, expandedNextHop, err)
return false, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, false, err
}
if err == nil && cname != expandedNextHop.ASCII+"." {
d, err := dns.ParseDomain(strings.TrimSuffix(cname, "."))
if err != nil {
// todo: only mark as permanent failure if TTLs for all records are beyond latest possibly delivery retry we would do.
err = fmt.Errorf("%w: parsing cname domain %s: %v", errDNS, expandedNextHop, err)
return false, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, false, err
}
expandedNextHop = d
// Start again with new domain.
continue
}
// Not a CNAME, so lookup MX record.
mctx, mcancel := context.WithTimeout(ctx, 30*time.Second)
defer mcancel()
// Note: LookupMX can return an error and still return records: Invalid records are
// filtered out and an error returned. We must process any records that are valid.
// Only if all are unusable will we return an error. ../rfc/5321:3851
mxl, mxResult, err := resolver.LookupMX(mctx, expandedNextHop.ASCII+".")
mcancel()
if i == 0 {
origNextHopAuthentic = origNextHopAuthentic && mxResult.Authentic
}
expandedNextHopAuthentic = expandedNextHopAuthentic && mxResult.Authentic
if err != nil && len(mxl) == 0 {
if !dns.IsNotFound(err) {
err = fmt.Errorf("%w: mx lookup for %s: %v", errDNS, expandedNextHop, err)
return false, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, false, err
}
// No MX record, attempt delivery directly to host. ../rfc/5321:3842
hosts = []dns.IPDomain{{Domain: expandedNextHop}}
return false, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, false, nil
} else if err != nil {
log.Infox("mx record has some invalid records, keeping only the valid mx records", err)
}
// ../rfc/7505:122
if err == nil && len(mxl) == 1 && mxl[0].Host == "." {
// Note: Depending on MX record TTL, this record may be replaced with a more
// receptive MX record before our final delivery attempt. But it's clearly the
// explicit desire not to be bothered with email delivery attempts, so mark failure
// as permanent.
return true, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, true, errNoMail
}
// The Go resolver already sorts by preference, randomizing records of same
// preference. ../rfc/5321:3885
for _, mx := range mxl {
host, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
if err != nil {
// note: should not happen because Go resolver already filters these out.
err = fmt.Errorf("%w: invalid host name in mx record %q: %v", errDNS, mx.Host, err)
return true, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, true, err
}
hosts = append(hosts, dns.IPDomain{Domain: host})
}
if len(hosts) > 0 {
err = nil
}
return true, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, false, err
}
}
// GatherIPs looks up the IPs to try for connecting to host, with the IPs ordered
// to take previous attempts into account. For use with DANE, the CNAME-expanded
// name is returned, and whether the DNS responses were authentic.
func GatherIPs(ctx context.Context, log *mlog.Log, resolver dns.Resolver, host dns.IPDomain, dialedIPs map[string][]net.IP) (authentic bool, expandedAuthentic bool, expandedHost dns.Domain, ips []net.IP, dualstack bool, rerr error) {
if len(host.IP) > 0 {
return false, false, dns.Domain{}, []net.IP{host.IP}, false, nil
}
authentic = true
expandedAuthentic = true
// The Go resolver automatically follows CNAMEs, which is not allowed for host
// names in MX records, but seems to be accepted and is documented for DANE SMTP
// behaviour. We resolve CNAMEs explicitly, so we can return the final name, which
// DANE needs. ../rfc/7671:246
// ../rfc/5321:3861 ../rfc/2181:661 ../rfc/7672:1382 ../rfc/7671:1030
name := host.Domain.ASCII + "."
for i := 0; ; i++ {
cname, result, err := resolver.LookupCNAME(ctx, name)
if i == 0 {
authentic = result.Authentic
}
expandedAuthentic = expandedAuthentic && result.Authentic
if dns.IsNotFound(err) {
break
} else if err != nil {
return authentic, expandedAuthentic, dns.Domain{}, nil, dualstack, err
} else if strings.TrimSuffix(cname, ".") == strings.TrimSuffix(name, ".") {
break
}
if i > 10 {
return authentic, expandedAuthentic, dns.Domain{}, nil, dualstack, fmt.Errorf("mx lookup: %w", errCNAMELimit)
}
name = strings.TrimSuffix(cname, ".") + "."
}
if name == host.Domain.ASCII+"." {
expandedHost = host.Domain
} else {
var err error
expandedHost, err = dns.ParseDomain(strings.TrimSuffix(name, "."))
if err != nil {
return authentic, expandedAuthentic, dns.Domain{}, nil, dualstack, fmt.Errorf("parsing cname-resolved domain: %w", err)
}
}
ipaddrs, result, err := resolver.LookupIPAddr(ctx, name)
authentic = authentic && result.Authentic
expandedAuthentic = expandedAuthentic && result.Authentic
if err != nil || len(ipaddrs) == 0 {
return authentic, expandedAuthentic, expandedHost, nil, false, fmt.Errorf("looking up %q: %w", name, err)
}
var have4, have6 bool
for _, ipaddr := range ipaddrs {
ips = append(ips, ipaddr.IP)
if ipaddr.IP.To4() == nil {
have6 = true
} else {
have4 = true
}
}
dualstack = have4 && have6
prevIPs := dialedIPs[host.String()]
if len(prevIPs) > 0 {
prevIP := prevIPs[len(prevIPs)-1]
prevIs4 := prevIP.To4() != nil
sameFamily := 0
for _, ip := range prevIPs {
is4 := ip.To4() != nil
if prevIs4 == is4 {
sameFamily++
}
}
preferPrev := sameFamily == 1
// We use stable sort so any preferred/randomized listing from DNS is kept intact.
sort.SliceStable(ips, func(i, j int) bool {
aIs4 := ips[i].To4() != nil
bIs4 := ips[j].To4() != nil
if aIs4 != bIs4 {
// Prefer "i" if it is not same address family.
return aIs4 != prevIs4
}
// Prefer "i" if it is the same as last and we should be preferring it.
return preferPrev && ips[i].Equal(prevIP)
})
log.Debug("ordered ips for dialing", mlog.Field("ips", ips))
}
return
}
// GatherTLSA looks up TLSA record for either expandedHost or host, and returns
// records usable for DANE with SMTP, and host names to allow in DANE-TA
// certificate name verification.
//
// If no records are found, this isn't necessarily an error. It can just indicate
// the domain/host does not opt-in to DANE, and nil records and a nil error are
// returned.
//
// Only usable records are returned. If any record was found, DANE is required and
// this is indicated with daneRequired. If no usable records remain, the caller
// must do TLS, but not verify the remote TLS certificate.
func GatherTLSA(ctx context.Context, log *mlog.Log, resolver dns.Resolver, host dns.Domain, expandedAuthentic bool, expandedHost dns.Domain) (daneRequired bool, daneRecords []adns.TLSA, tlsaBaseDomain dns.Domain, err error) {
// ../rfc/7672:912
// This function is only called when the lookup of host was authentic.
var l []adns.TLSA
if host == expandedHost || !expandedAuthentic {
tlsaBaseDomain = host
l, err = lookupTLSACNAME(ctx, log, resolver, 25, "tcp", host)
} else if expandedAuthentic {
// ../rfc/7672:934
tlsaBaseDomain = expandedHost
l, err = lookupTLSACNAME(ctx, log, resolver, 25, "tcp", expandedHost)
if err == nil && len(l) == 0 {
tlsaBaseDomain = host
l, err = lookupTLSACNAME(ctx, log, resolver, 25, "tcp", host)
}
}
if len(l) == 0 || err != nil {
daneRequired = err != nil
log.Debugx("gathering tlsa records failed", err, mlog.Field("danerequired", daneRequired))
return daneRequired, nil, dns.Domain{}, err
}
daneRequired = len(l) > 0
l = filterUsableTLSARecords(log, l)
log.Debug("tlsa records exist", mlog.Field("danerequired", daneRequired), mlog.Field("records", l), mlog.Field("basedomain", tlsaBaseDomain))
return daneRequired, l, tlsaBaseDomain, err
}
// lookupTLSACNAME composes a TLSA domain name to lookup, follows CNAMEs and looks
// up TLSA records. no TLSA records exist, a nil error is returned as it means
// the host does not opt-in to DANE.
func lookupTLSACNAME(ctx context.Context, log *mlog.Log, resolver dns.Resolver, port int, protocol string, host dns.Domain) (l []adns.TLSA, rerr error) {
name := fmt.Sprintf("_%d._%s.%s", port, protocol, host.ASCII+".")
for i := 0; ; i++ {
cname, result, err := resolver.LookupCNAME(ctx, name)
if dns.IsNotFound(err) {
if !result.Authentic {
log.Debugx("cname nxdomain result during tlsa lookup not authentic, not doing dane for host", err, mlog.Field("host", host), mlog.Field("name", name))
return nil, nil
}
break
} else if err != nil {
return nil, fmt.Errorf("looking up cname for tlsa candidate base domain: %w", err)
} else if !result.Authentic {
log.Debugx("cname result during tlsa lookup not authentic, not doing dane for host", err, mlog.Field("host", host), mlog.Field("name", name))
return nil, nil
}
if i == 10 {
return nil, fmt.Errorf("looking up cname for tlsa candidate base domain: %w", errCNAMELimit)
}
name = strings.TrimSuffix(cname, ".") + "."
}
var result adns.Result
var err error
l, result, err = resolver.LookupTLSA(ctx, 0, "", name)
if dns.IsNotFound(err) || err == nil && len(l) == 0 {
log.Debugx("no tlsa records for host, not doing dane", err, mlog.Field("host", host), mlog.Field("name", name), mlog.Field("authentic", result.Authentic))
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("looking up tlsa records for tlsa candidate base domain: %w", err)
} else if !result.Authentic {
log.Debugx("tlsa lookup not authentic, not doing dane for host", err, mlog.Field("host", host), mlog.Field("name", name))
return nil, err
}
return l, nil
}
func filterUsableTLSARecords(log *mlog.Log, l []adns.TLSA) []adns.TLSA {
// Gather "usable" records. ../rfc/7672:708
o := 0
for _, r := range l {
// A record is not usable when we don't recognize parameters. ../rfc/6698:649
switch r.Usage {
case adns.TLSAUsageDANETA, adns.TLSAUsageDANEEE:
default:
// We can regard PKIX-TA and PKIX-EE as "unusable" with SMTP DANE. ../rfc/7672:1304
continue
}
switch r.Selector {
case adns.TLSASelectorCert, adns.TLSASelectorSPKI:
default:
continue
}
switch r.MatchType {
case adns.TLSAMatchTypeFull:
if r.Selector == adns.TLSASelectorCert {
if _, err := x509.ParseCertificate(r.CertAssoc); err != nil {
log.Debugx("parsing certificate in dane tlsa record, ignoring", err)
continue
}
} else if r.Selector == adns.TLSASelectorSPKI {
if _, err := x509.ParsePKIXPublicKey(r.CertAssoc); err != nil {
log.Debugx("parsing certificate in dane tlsa record, ignoring", err)
continue
}
}
case adns.TLSAMatchTypeSHA256:
if len(r.CertAssoc) != sha256.Size {
log.Debug("dane tlsa record with wrong data size for sha2-256", mlog.Field("got", len(r.CertAssoc)), mlog.Field("expect", sha256.Size))
continue
}
case adns.TLSAMatchTypeSHA512:
if len(r.CertAssoc) != sha512.Size {
log.Debug("dane tlsa record with wrong data size for sha2-512", mlog.Field("got", len(r.CertAssoc)), mlog.Field("expect", sha512.Size))
continue
}
default:
continue
}
l[o] = r
o++
}
return l[:o]
}
// GatherTLSANames returns the allowed names in TLS certificates for verification
// with PKIX-* or DANE-TA. The first name should be used for SNI.
//
// If there was no MX record, the next-hop domain parameters (i.e. the original
// email destination host, and its CNAME-expanded host, that has MX records) are
// ignored and only the base domain parameters are taken into account.
func GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedTLSABaseDomainAuthentic bool, origNextHop, expandedNextHop, origTLSABaseDomain, expandedTLSABaseDomain dns.Domain) []dns.Domain {
// Gather the names to check against TLS certificate. ../rfc/7672:1318
if !haveMX {
// ../rfc/7672:1336
if !expandedTLSABaseDomainAuthentic || origTLSABaseDomain == expandedTLSABaseDomain {
return []dns.Domain{origTLSABaseDomain}
}
return []dns.Domain{expandedTLSABaseDomain, origTLSABaseDomain}
} else if expandedNextHopAuthentic {
// ../rfc/7672:1326
var l []dns.Domain
if expandedTLSABaseDomainAuthentic {
l = []dns.Domain{expandedTLSABaseDomain}
}
if expandedTLSABaseDomain != origTLSABaseDomain {
l = append(l, origTLSABaseDomain)
}
l = append(l, origNextHop)
if origNextHop != expandedNextHop {
l = append(l, expandedNextHop)
}
return l
} else {
// We don't attempt DANE after insecure MX, but behaviour for it is specified.
// ../rfc/7672:1332
return []dns.Domain{origNextHop}
}
}

309
smtpclient/gather_test.go Normal file
View file

@ -0,0 +1,309 @@
package smtpclient
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"net"
"reflect"
"testing"
"github.com/mjl-/adns"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
)
func domain(s string) dns.Domain {
d, err := dns.ParseDomain(s)
if err != nil {
panic("parse domain: " + err.Error())
}
return d
}
func ipdomain(s string) dns.IPDomain {
ip := net.ParseIP(s)
if ip != nil {
return dns.IPDomain{IP: ip}
}
d, err := dns.ParseDomain(s)
if err != nil {
panic(fmt.Sprintf("parse domain %q: %v", s, err))
}
return dns.IPDomain{Domain: d}
}
func ipdomains(s ...string) (l []dns.IPDomain) {
for _, e := range s {
l = append(l, ipdomain(e))
}
return
}
// Test basic MX lookup case, but also following CNAME, detecting CNAME loops and
// having a CNAME limit, connecting directly to a host, and domain that does not
// exist or has temporary error.
func TestGatherDestinations(t *testing.T) {
ctxbg := context.Background()
log := mlog.New("smtpclient")
resolver := dns.MockResolver{
MX: map[string][]*net.MX{
"basic.example.": {{Host: "mail.basic.example.", Pref: 10}},
"multimx.example.": {{Host: "mail1.multimx.example.", Pref: 10}, {Host: "mail2.multimx.example.", Pref: 10}},
"nullmx.example.": {{Host: ".", Pref: 10}},
"temperror-mx.example.": {{Host: "absent.example.", Pref: 10}},
},
A: map[string][]string{
"mail.basic.example": {"10.0.0.1"},
"justhost.example.": {"10.0.0.1"}, // No MX record for domain, only an A record.
"temperror-a.example.": {"10.0.0.1"},
},
AAAA: map[string][]string{
"justhost6.example.": {"2001:db8::1"}, // No MX record for domain, only an AAAA record.
},
CNAME: map[string]string{
"cname.example.": "basic.example.",
"cname-to-inauthentic.example.": "cnameinauthentic.example.",
"cnameinauthentic.example.": "basic.example.",
"cnameloop.example.": "cnameloop2.example.",
"cnameloop2.example.": "cnameloop.example.",
"danglingcname.example.": "absent.example.", // Points to missing name.
"temperror-cname.example.": "absent.example.",
},
Fail: map[dns.Mockreq]struct{}{
{Type: "mx", Name: "temperror-mx.example."}: {},
{Type: "host", Name: "temperror-a.example."}: {},
{Type: "cname", Name: "temperror-cname.example."}: {},
},
Inauthentic: []string{"cname cnameinauthentic.example."},
}
for i := 0; i <= 16; i++ {
s := fmt.Sprintf("cnamelimit%d.example.", i)
next := fmt.Sprintf("cnamelimit%d.example.", i+1)
resolver.CNAME[s] = next
}
test := func(ipd dns.IPDomain, expHosts []dns.IPDomain, expDomain dns.Domain, expPerm, expAuthic, expExpAuthic bool, expErr error) {
t.Helper()
_, authic, authicExp, ed, hosts, perm, err := GatherDestinations(ctxbg, log, resolver, ipd)
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
// todo: could also check the individual errors? code currently does not have structured errors.
t.Fatalf("gather hosts: %v, expected %v", err, expErr)
}
if err != nil {
return
}
if !reflect.DeepEqual(hosts, expHosts) || ed != expDomain || perm != expPerm || authic != expAuthic || authicExp != expExpAuthic {
t.Fatalf("got hosts %#v, effectiveDomain %#v, permanent %#v, authic %v %v, expected %#v %#v %#v %v %v", hosts, ed, perm, authic, authicExp, expHosts, expDomain, expPerm, expAuthic, expExpAuthic)
}
}
var zerodom dns.Domain
for i := 0; i < 2; i++ {
authic := i == 1
resolver.AllAuthentic = authic
// Basic with simple MX.
test(ipdomain("basic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, authic, authic, nil)
test(ipdomain("multimx.example"), ipdomains("mail1.multimx.example", "mail2.multimx.example"), domain("multimx.example"), false, authic, authic, nil)
// Only an A record.
test(ipdomain("justhost.example"), ipdomains("justhost.example"), domain("justhost.example"), false, authic, authic, nil)
// Only an AAAA record.
test(ipdomain("justhost6.example"), ipdomains("justhost6.example"), domain("justhost6.example"), false, authic, authic, nil)
// Follow CNAME.
test(ipdomain("cname.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, authic, authic, nil)
// No MX/CNAME, non-existence of host will be found out later.
test(ipdomain("absent.example"), ipdomains("absent.example"), domain("absent.example"), false, authic, authic, nil)
// Followed CNAME, has no MX, non-existence of host will be found out later.
test(ipdomain("danglingcname.example"), ipdomains("absent.example"), domain("absent.example"), false, authic, authic, nil)
test(ipdomain("cnamelimit1.example"), nil, zerodom, true, authic, authic, errCNAMELimit)
test(ipdomain("cnameloop.example"), nil, zerodom, true, authic, authic, errCNAMELoop)
test(ipdomain("nullmx.example"), nil, zerodom, true, authic, authic, errNoMail)
test(ipdomain("temperror-mx.example"), nil, zerodom, false, authic, authic, errDNS)
test(ipdomain("temperror-cname.example"), nil, zerodom, false, authic, authic, errDNS)
}
test(ipdomain("10.0.0.1"), ipdomains("10.0.0.1"), zerodom, false, false, false, nil)
test(ipdomain("cnameinauthentic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, false, false, nil)
test(ipdomain("cname-to-inauthentic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, true, false, nil)
}
func TestGatherIPs(t *testing.T) {
ctxbg := context.Background()
log := mlog.New("smtpclient")
resolver := dns.MockResolver{
A: map[string][]string{
"host1.example.": {"10.0.0.1"},
"host2.example.": {"10.0.0.2"},
"temperror-a.example.": {"10.0.0.3"},
},
AAAA: map[string][]string{
"host2.example.": {"2001:db8::1"},
},
CNAME: map[string]string{
"cname1.example.": "host1.example.",
"cname-to-inauthentic.example.": "cnameinauthentic.example.",
"cnameinauthentic.example.": "host1.example.",
"cnameloop.example.": "cnameloop2.example.",
"cnameloop2.example.": "cnameloop.example.",
"danglingcname.example.": "absent.example.", // Points to missing name.
"temperror-cname.example.": "absent.example.",
},
Fail: map[dns.Mockreq]struct{}{
{Type: "host", Name: "temperror-a.example."}: {},
{Type: "cname", Name: "temperror-cname.example."}: {},
},
Inauthentic: []string{"cname cnameinauthentic.example."},
}
test := func(host dns.IPDomain, expAuthic, expAuthicExp bool, expHostExp dns.Domain, expIPs []net.IP, expErr any) {
t.Helper()
authic, authicExp, hostExp, ips, _, err := GatherIPs(ctxbg, log, resolver, host, nil)
if (err == nil) != (expErr == nil) || err != nil && !(errors.Is(err, expErr.(error)) || errors.As(err, &expErr)) {
// todo: could also check the individual errors?
t.Fatalf("gather hosts: %v, expected %v", err, expErr)
}
if err != nil {
return
}
if expHostExp == zerohost {
expHostExp = host.Domain
}
if authic != expAuthic || authicExp != expAuthicExp || hostExp != expHostExp || !reflect.DeepEqual(ips, expIPs) {
t.Fatalf("got authic %v %v, host %v, ips %v, expected %v %v %v %v", authic, authicExp, hostExp, ips, expAuthic, expAuthicExp, expHostExp, expIPs)
}
}
ips := func(l ...string) (r []net.IP) {
for _, s := range l {
r = append(r, net.ParseIP(s))
}
return r
}
for i := 0; i < 2; i++ {
authic := i == 1
resolver.AllAuthentic = authic
test(ipdomain("host1.example"), authic, authic, zerohost, ips("10.0.0.1"), nil)
test(ipdomain("host2.example"), authic, authic, zerohost, ips("10.0.0.2", "2001:db8::1"), nil)
test(ipdomain("cname-to-inauthentic.example"), authic, false, domain("host1.example"), ips("10.0.0.1"), nil)
test(ipdomain("cnameloop.example"), authic, authic, zerohost, nil, errCNAMELimit)
test(ipdomain("bogus.example"), authic, authic, zerohost, nil, &adns.DNSError{})
test(ipdomain("danglingcname.example"), authic, authic, zerohost, nil, &adns.DNSError{})
test(ipdomain("temperror-a.example"), authic, authic, zerohost, nil, &adns.DNSError{})
test(ipdomain("temperror-cname.example"), authic, authic, zerohost, nil, &adns.DNSError{})
}
test(ipdomain("cnameinauthentic.example"), false, false, domain("host1.example"), ips("10.0.0.1"), nil)
test(ipdomain("cname-to-inauthentic.example"), true, false, domain("host1.example"), ips("10.0.0.1"), nil)
}
func TestGatherTLSA(t *testing.T) {
ctxbg := context.Background()
log := mlog.New("smtpclient")
record := func(usage, selector, matchType uint8) adns.TLSA {
return adns.TLSA{
Usage: adns.TLSAUsage(usage),
Selector: adns.TLSASelector(selector),
MatchType: adns.TLSAMatchType(matchType),
CertAssoc: make([]byte, sha256.Size), // Assume sha256.
}
}
records := func(l ...adns.TLSA) []adns.TLSA {
return l
}
record0 := record(3, 1, 1)
list0 := records(record0)
record1 := record(3, 0, 1)
list1 := records(record1)
resolver := dns.MockResolver{
TLSA: map[string][]adns.TLSA{
"_25._tcp.host0.example.": list0,
"_25._tcp.host1.example.": list1,
"_25._tcp.inauthentic.example.": list1,
"_25._tcp.temperror-cname.example.": list1,
},
CNAME: map[string]string{
"_25._tcp.cname.example.": "_25._tcp.host1.example.",
"_25._tcp.cnameloop.example.": "_25._tcp.cnameloop2.example.",
"_25._tcp.cnameloop2.example.": "_25._tcp.cnameloop.example.",
"_25._tcp.cname-to-inauthentic.example.": "_25._tcp.cnameinauthentic.example.",
"_25._tcp.cnameinauthentic.example.": "_25._tcp.host1.example.",
"_25._tcp.danglingcname.example.": "_25._tcp.absent.example.", // Points to missing name.
},
Fail: map[dns.Mockreq]struct{}{
{Type: "cname", Name: "_25._tcp.temperror-cname.example."}: {},
},
Inauthentic: []string{
"cname _25._tcp.cnameinauthentic.example.",
"tlsa _25._tcp.inauthentic.example.",
},
}
test := func(host dns.Domain, expandedAuthentic bool, expandedHost dns.Domain, expDANERequired bool, expRecords []adns.TLSA, expBaseDom dns.Domain, expErr any) {
t.Helper()
daneReq, records, baseDom, err := GatherTLSA(ctxbg, log, resolver, host, expandedAuthentic, expandedHost)
if (err == nil) != (expErr == nil) || err != nil && !(errors.Is(err, expErr.(error)) || errors.As(err, &expErr)) {
// todo: could also check the individual errors?
t.Fatalf("gather tlsa: %v, expected %v", err, expErr)
}
if daneReq != expDANERequired {
t.Fatalf("got daneRequired %v, expected %v", daneReq, expDANERequired)
}
if err != nil {
return
}
if !reflect.DeepEqual(records, expRecords) || baseDom != expBaseDom {
t.Fatalf("got records, baseDomain %v %v, expected %v %v", records, baseDom, expRecords, expBaseDom)
}
}
resolver.AllAuthentic = true
test(domain("host1.example"), false, domain("host1.example"), true, list1, domain("host1.example"), nil)
test(domain("host1.example"), true, domain("host1.example"), true, list1, domain("host1.example"), nil)
test(domain("host0.example"), true, domain("host1.example"), true, list1, domain("host1.example"), nil)
test(domain("host0.example"), false, domain("host1.example"), true, list0, domain("host0.example"), nil)
// CNAME for TLSA at cname.example should be followed.
test(domain("host0.example"), true, domain("cname.example"), true, list1, domain("cname.example"), nil)
// TLSA records at original domain should be followed.
test(domain("host0.example"), false, domain("cname.example"), true, list0, domain("host0.example"), nil)
test(domain("cnameloop.example"), false, domain("cnameloop.example"), true, nil, zerohost, errCNAMELimit)
test(domain("host0.example"), false, domain("inauthentic.example"), true, list0, domain("host0.example"), nil)
test(domain("inauthentic.example"), false, domain("inauthentic.example"), false, nil, zerohost, nil)
test(domain("temperror-cname.example"), false, domain("temperror-cname.example"), true, nil, zerohost, &adns.DNSError{})
test(domain("host1.example"), true, domain("cname-to-inauthentic.example"), true, list1, domain("host1.example"), nil)
test(domain("host1.example"), true, domain("danglingcname.example"), true, list1, domain("host1.example"), nil)
test(domain("danglingcname.example"), true, domain("danglingcname.example"), false, nil, zerohost, nil)
}
func TestGatherTLSANames(t *testing.T) {
a, b, c, d := domain("nexthop.example"), domain("nexthopexpanded.example"), domain("base.example"), domain("baseexpanded.example")
test := func(haveMX, nexthopExpAuth, tlsabaseExpAuth bool, expDoms ...dns.Domain) {
t.Helper()
doms := GatherTLSANames(haveMX, nexthopExpAuth, tlsabaseExpAuth, a, b, c, d)
if !reflect.DeepEqual(doms, expDoms) {
t.Fatalf("got domains %v, expected %v", doms, expDoms)
}
}
test(false, false, false, c)
test(false, false, true, d, c)
test(true, true, true, d, c, a, b)
test(true, true, false, c, a, b)
test(true, false, false, a)
}

View file

@ -11,7 +11,7 @@ import (
// i.e. if it has no null mx record, regular mx records or resolve to an address.
func checkMXRecords(ctx context.Context, resolver dns.Resolver, d dns.Domain) (bool, error) {
// Note: LookupMX can return an error and still return records.
mx, err := resolver.LookupMX(ctx, d.ASCII+".")
mx, _, err := resolver.LookupMX(ctx, d.ASCII+".")
if err == nil && len(mx) == 1 && mx[0].Host == "." {
// Null MX record, explicit signal that remote does not accept email.
return false, nil
@ -25,7 +25,7 @@ func checkMXRecords(ctx context.Context, resolver dns.Resolver, d dns.Domain) (b
}
var lastErr error
for _, x := range mx {
ips, err := resolver.LookupIPAddr(ctx, x.Host)
ips, _, err := resolver.LookupIPAddr(ctx, x.Host)
if len(ips) > 0 {
return true, nil
}

View file

@ -789,7 +789,7 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
// Verify a remote domain name has an A or AAAA record, CNAME not allowed. ../rfc/5321:722
cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
ctx, cancel := context.WithTimeout(cidctx, time.Minute)
_, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
_, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
cancel()
if dns.IsNotFound(err) {
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
@ -1413,7 +1413,7 @@ func (c *conn) cmdRcpt(p *parser) {
cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
defer spfcancel()
receivedSPF, _, _, err := spf.Verify(spfctx, c.resolver, spfArgs)
receivedSPF, _, _, _, err := spf.Verify(spfctx, c.resolver, spfArgs)
spfcancel()
if err != nil {
c.log.Errorx("spf verify for multiple recipients", err)
@ -1573,6 +1573,7 @@ func (c *conn) cmdData(p *parser) {
// ../rfc/5321:3311 ../rfc/6531:578
var recvFrom string
var iprevStatus iprev.Status // Only for delivery, not submission.
var iprevAuthentic bool
if c.submission {
// Hide internal hosts.
// todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see ../rfc/5321:4321
@ -1588,7 +1589,7 @@ func (c *conn) cmdData(p *parser) {
iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
var revName string
var revNames []string
iprevStatus, revName, revNames, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
iprevcancel()
if err != nil {
c.log.Infox("reverse-forward lookup", err, mlog.Field("remoteip", c.remoteIP))
@ -1655,7 +1656,7 @@ func (c *conn) cmdData(p *parser) {
if c.submission {
c.submit(cmdctx, recvHdrFor, msgWriter, &dataFile)
} else {
c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, &dataFile)
c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, &dataFile)
}
}
@ -1859,7 +1860,7 @@ func (c *conn) xlocalserveError(lp smtp.Localpart) {
// deliver is called for incoming messages from external, typically untrusted
// sources. i.e. not submitted by authenticated users.
func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, pdataFile **os.File) {
func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, pdataFile **os.File) {
dataFile := *pdataFile
// todo: in decision making process, if we run into (some) temporary errors, attempt to continue. if we decide to accept, all good. if we decide to reject, we'll make it a temporary reject.
@ -1879,12 +1880,20 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
}
commentAuthentic := func(v bool) string {
if v {
return "with dnssec"
}
return "without dnssec"
}
// Reverse IP lookup results.
// todo future: how useful is this?
// ../rfc/5321:2481
authResults.Methods = append(authResults.Methods, message.AuthMethod{
Method: "iprev",
Result: string(iprevStatus),
Method: "iprev",
Result: string(iprevStatus),
Comment: commentAuthentic(iprevAuthentic),
Props: []message.AuthProp{
message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
},
@ -1923,6 +1932,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
var receivedSPF spf.Received
var spfDomain dns.Domain
var spfExpl string
var spfAuthentic bool
var spfErr error
spfArgs := spf.Args{
RemoteIP: c.remoteIP,
@ -1945,7 +1955,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
defer wg.Done()
spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
defer spfcancel()
receivedSPF, spfDomain, spfExpl, spfErr = spf.Verify(spfctx, c.resolver, spfArgs)
receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.resolver, spfArgs)
spfcancel()
if spfErr != nil {
c.log.Infox("spf verify", spfErr)
@ -2003,7 +2013,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
// todo future: also specify whether dns record was dnssec-signed.
if r.Record != nil && r.Record.PublicKey != nil {
if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
comment = fmt.Sprintf("%d bit rsa", pubkey.N.BitLen())
comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
}
}
@ -2021,6 +2031,11 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
identity = r.Sig.Identity
}
if r.RecordAuthentic {
comment += "with dnssec"
} else {
comment += "without dnssec"
}
}
var errmsg string
if r.Err != nil {
@ -2048,10 +2063,17 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
if spfIdentity != nil {
props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.smtputf8), true, spfIdentity.ASCIIExtra(c.smtputf8))}
}
var spfComment string
if spfAuthentic {
spfComment = "with dnssec"
} else {
spfComment = "without dnssec"
}
authResults.Methods = append(authResults.Methods, message.AuthMethod{
Method: "spf",
Result: string(receivedSPF.Result),
Props: props,
Method: "spf",
Result: string(receivedSPF.Result),
Comment: spfComment,
Props: props,
})
switch receivedSPF.Result {
case spf.StatusPass:
@ -2105,9 +2127,16 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
defer dmarccancel()
dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
dmarccancel()
var comment string
if dmarcResult.RecordAuthentic {
comment = "with dnssec"
} else {
comment = "without dnssec"
}
dmarcMethod = message.AuthMethod{
Method: "dmarc",
Result: string(dmarcResult.Status),
Method: "dmarc",
Result: string(dmarcResult.Status),
Comment: comment,
Props: []message.AuthProp{
// ../rfc/7489:1489
message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.smtputf8)),

View file

@ -158,7 +158,7 @@ func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
ourHostname := mox.Conf.Static.HostnameDomain
remoteHostname := dns.Domain{ASCII: "mox.example"}
client, err := smtpclient.New(ctxbg, xlog.WithCid(ts.cid-1), clientConn, ts.tlsmode, ourHostname, remoteHostname, auth)
client, err := smtpclient.New(ctxbg, xlog.WithCid(ts.cid-1), clientConn, ts.tlsmode, ourHostname, remoteHostname, auth, nil, nil, nil)
if err != nil {
clientConn.Close()
} else {

View file

@ -127,7 +127,9 @@ type Args struct {
var timeNow = time.Now
// Lookup looks up and parses an SPF TXT record for domain.
func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rstatus Status, rtxt string, rrecord *Record, rerr error) {
//
// authentic indicates if the DNS results were DNSSEC-verified.
func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rstatus Status, rtxt string, rrecord *Record, authentic bool, rerr error) {
log := xlog.WithContext(ctx)
start := time.Now()
defer func() {
@ -137,15 +139,15 @@ func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rsta
// ../rfc/7208:586
host := domain.ASCII + "."
if err := validateDNS(host); err != nil {
return StatusNone, "", nil, fmt.Errorf("%w: %s: %s", ErrName, domain, err)
return StatusNone, "", nil, false, fmt.Errorf("%w: %s: %s", ErrName, domain, err)
}
// Lookup spf record.
txts, err := dns.WithPackage(resolver, "spf").LookupTXT(ctx, host)
txts, result, err := dns.WithPackage(resolver, "spf").LookupTXT(ctx, host)
if dns.IsNotFound(err) {
return StatusNone, "", nil, fmt.Errorf("%w for %s", ErrNoRecord, host)
return StatusNone, "", nil, result.Authentic, fmt.Errorf("%w for %s", ErrNoRecord, host)
} else if err != nil {
return StatusTemperror, "", nil, fmt.Errorf("%w: %s: %s", ErrDNS, host, err)
return StatusTemperror, "", nil, result.Authentic, fmt.Errorf("%w: %s: %s", ErrDNS, host, err)
}
// Parse the records. We only handle those that look like spf records.
@ -159,20 +161,20 @@ func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rsta
continue
} else if err != nil {
// ../rfc/7208:852
return StatusPermerror, txt, nil, fmt.Errorf("%w: %s", ErrRecordSyntax, err)
return StatusPermerror, txt, nil, result.Authentic, fmt.Errorf("%w: %s", ErrRecordSyntax, err)
}
if record != nil {
// ../rfc/7208:576
return StatusPermerror, "", nil, ErrMultipleRecords
return StatusPermerror, "", nil, result.Authentic, ErrMultipleRecords
}
text = txt
record = r
}
if record == nil {
// ../rfc/7208:837
return StatusNone, "", nil, ErrNoRecord
return StatusNone, "", nil, result.Authentic, ErrNoRecord
}
return StatusNone, text, record, nil
return StatusNone, text, record, result.Authentic, nil
}
// Verify checks if a remote IP is allowed to send email for a domain.
@ -190,7 +192,9 @@ func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rsta
//
// Verify takes the maximum number of 10 DNS requests into account, and the maximum
// of 2 lookups resulting in no records ("void lookups").
func Verify(ctx context.Context, resolver dns.Resolver, args Args) (received Received, domain dns.Domain, explanation string, rerr error) {
//
// authentic indicates if the DNS results were DNSSEC-verified.
func Verify(ctx context.Context, resolver dns.Resolver, args Args) (received Received, domain dns.Domain, explanation string, authentic bool, rerr error) {
log := xlog.WithContext(ctx)
start := time.Now()
defer func() {
@ -208,10 +212,10 @@ func Verify(ctx context.Context, resolver dns.Resolver, args Args) (received Rec
Helo: args.HelloDomain,
Receiver: args.LocalHostname.ASCII,
}
return received, dns.Domain{}, "", nil
return received, dns.Domain{}, "", false, nil
}
status, mechanism, expl, err := checkHost(ctx, resolver, args)
status, mechanism, expl, authentic, err := checkHost(ctx, resolver, args)
comment := fmt.Sprintf("domain %s", args.domain.ASCII)
if isHello {
comment += ", from ehlo because mailfrom is empty"
@ -233,7 +237,7 @@ func Verify(ctx context.Context, resolver dns.Resolver, args Args) (received Rec
} else {
received.Identity = "mailfrom"
}
return received, args.domain, expl, err
return received, args.domain, expl, authentic, err
}
// prepare args, setting fields sender* and domain as required for checkHost.
@ -268,26 +272,29 @@ func prepare(args *Args) (isHello bool, ok bool) {
}
// lookup spf record, then evaluate args against it.
func checkHost(ctx context.Context, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rerr error) {
status, _, record, err := Lookup(ctx, resolver, args.domain)
func checkHost(ctx context.Context, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
status, _, record, rauthentic, err := Lookup(ctx, resolver, args.domain)
if err != nil {
return status, "", "", err
return status, "", "", rauthentic, err
}
return evaluate(ctx, record, resolver, args)
var evalAuthentic bool
rstatus, mechanism, rexplanation, evalAuthentic, rerr = evaluate(ctx, record, resolver, args)
rauthentic = rauthentic && evalAuthentic
return
}
// Evaluate evaluates the IP and names from args against the SPF DNS record for the domain.
func Evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rerr error) {
func Evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
_, ok := prepare(&args)
if !ok {
return StatusNone, "default", "", fmt.Errorf("no domain name to validate")
return StatusNone, "default", "", false, fmt.Errorf("no domain name to validate")
}
return evaluate(ctx, record, resolver, args)
}
// evaluate RemoteIP against domain from args, given record.
func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rerr error) {
func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
log := xlog.WithContext(ctx)
start := time.Now()
defer func() {
@ -301,6 +308,9 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
args.voidLookups = new(int)
}
// Response is authentic until we find a non-authentic DNS response.
rauthentic = true
// To4 returns nil for an IPv6 address. To16 will return an IPv4-to-IPv6-mapped address.
var remote6 net.IP
remote4 := args.RemoteIP.To4()
@ -338,7 +348,8 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
// Used for "a" and "mx".
checkHostIP := func(domain dns.Domain, d Directive, args *Args) (bool, Status, error) {
ips, err := resolver.LookupIP(ctx, "ip", domain.ASCII+".")
ips, result, err := resolver.LookupIP(ctx, "ip", domain.ASCII+".")
rauthentic = rauthentic && result.Authentic
trackVoidLookup(err, args)
// If "not found", we must ignore the error and treat as zero records in answer. ../rfc/7208:1116
if err != nil && !dns.IsNotFound(err) {
@ -358,7 +369,7 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
switch d.Mechanism {
case "include", "a", "mx", "ptr", "exists":
if err := trackLookupLimits(&args); err != nil {
return StatusPermerror, d.MechanismString(), "", err
return StatusPermerror, d.MechanismString(), "", rauthentic, err
}
}
@ -369,22 +380,24 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
case "include":
// ../rfc/7208:1143
name, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
name, authentic, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
rauthentic = rauthentic && authentic
if err != nil {
return StatusPermerror, d.MechanismString(), "", fmt.Errorf("expanding domain-spec for include: %w", err)
return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("expanding domain-spec for include: %w", err)
}
nargs := args
nargs.domain = dns.Domain{ASCII: strings.TrimSuffix(name, ".")}
nargs.explanation = &record.Explanation // ../rfc/7208:1548
status, _, _, err := checkHost(ctx, resolver, nargs)
status, _, _, authentic, err := checkHost(ctx, resolver, nargs)
rauthentic = rauthentic && authentic
// ../rfc/7208:1202
switch status {
case StatusPass:
match = true
case StatusTemperror:
return StatusTemperror, d.MechanismString(), "", fmt.Errorf("include %q: %w", name, err)
return StatusTemperror, d.MechanismString(), "", rauthentic, fmt.Errorf("include %q: %w", name, err)
case StatusPermerror, StatusNone:
return StatusPermerror, d.MechanismString(), "", fmt.Errorf("include %q resulted in status %q: %w", name, status, err)
return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("include %q resulted in status %q: %w", name, status, err)
}
case "a":
@ -396,11 +409,11 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
// mechanism for which it isn't specified.
host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
if err != nil {
return StatusPermerror, d.MechanismString(), "", err
return StatusPermerror, d.MechanismString(), "", rauthentic, err
}
hmatch, status, err := checkHostIP(host, d, &args)
if err != nil {
return status, d.MechanismString(), "", err
return status, d.MechanismString(), "", rauthentic, err
}
match = hmatch
@ -408,14 +421,15 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
// ../rfc/7208:1262
host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
if err != nil {
return StatusPermerror, d.MechanismString(), "", err
return StatusPermerror, d.MechanismString(), "", rauthentic, err
}
// Note: LookupMX can return an error and still return MX records.
mxs, err := resolver.LookupMX(ctx, host.ASCII+".")
mxs, result, err := resolver.LookupMX(ctx, host.ASCII+".")
rauthentic = rauthentic && result.Authentic
trackVoidLookup(err, &args)
// note: we handle "not found" simply as a result of zero mx records.
if err != nil && !dns.IsNotFound(err) {
return StatusTemperror, d.MechanismString(), "", err
return StatusTemperror, d.MechanismString(), "", rauthentic, err
}
if err == nil && len(mxs) == 1 && mxs[0].Host == "." {
// Explicitly no MX.
@ -428,15 +442,15 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
// found no match before the 11th name.
// ../rfc/7208:945
if i >= 10 {
return StatusPermerror, d.MechanismString(), "", ErrTooManyDNSRequests
return StatusPermerror, d.MechanismString(), "", rauthentic, ErrTooManyDNSRequests
}
mxd, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
if err != nil {
return StatusPermerror, d.MechanismString(), "", err
return StatusPermerror, d.MechanismString(), "", rauthentic, err
}
hmatch, status, err := checkHostIP(mxd, d, &args)
if err != nil {
return status, d.MechanismString(), "", err
return status, d.MechanismString(), "", rauthentic, err
}
if hmatch {
match = hmatch
@ -448,13 +462,14 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
// ../rfc/7208:1281
host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
if err != nil {
return StatusPermerror, d.MechanismString(), "", err
return StatusPermerror, d.MechanismString(), "", rauthentic, err
}
rnames, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
rnames, result, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
rauthentic = rauthentic && result.Authentic
trackVoidLookup(err, &args)
if err != nil && !dns.IsNotFound(err) {
return StatusTemperror, d.MechanismString(), "", err
return StatusTemperror, d.MechanismString(), "", rauthentic, err
}
lookups := 0
ptrnames:
@ -474,7 +489,8 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
break
}
lookups++
ips, err := resolver.LookupIP(ctx, "ip", rd.ASCII+".")
ips, result, err := resolver.LookupIP(ctx, "ip", rd.ASCII+".")
rauthentic = rauthentic && result.Authentic
trackVoidLookup(err, &args)
for _, ip := range ips {
if checkIP(ip, d) {
@ -496,22 +512,24 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
case "exists":
// ../rfc/7208:1382
name, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
name, authentic, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
rauthentic = rauthentic && authentic
if err != nil {
return StatusPermerror, d.MechanismString(), "", fmt.Errorf("expanding domain-spec for exists: %w", err)
return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("expanding domain-spec for exists: %w", err)
}
ips, err := resolver.LookupIP(ctx, "ip4", ensureAbsDNS(name))
ips, result, err := resolver.LookupIP(ctx, "ip4", ensureAbsDNS(name))
rauthentic = rauthentic && result.Authentic
// Note: we do count this for void lookups, as that is an anti-abuse mechanism.
// ../rfc/7208:1382 does not say anything special, so ../rfc/7208:984 applies.
trackVoidLookup(err, &args)
if err != nil && !dns.IsNotFound(err) {
return StatusTemperror, d.MechanismString(), "", err
return StatusTemperror, d.MechanismString(), "", rauthentic, err
}
match = len(ips) > 0
default:
return StatusNone, d.MechanismString(), "", fmt.Errorf("internal error, unexpected mechanism %q", d.Mechanism)
return StatusNone, d.MechanismString(), "", rauthentic, fmt.Errorf("internal error, unexpected mechanism %q", d.Mechanism)
}
if !match {
@ -519,40 +537,43 @@ func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args A
}
switch d.Qualifier {
case "", "+":
return StatusPass, d.MechanismString(), "", nil
return StatusPass, d.MechanismString(), "", rauthentic, nil
case "?":
return StatusNeutral, d.MechanismString(), "", nil
return StatusNeutral, d.MechanismString(), "", rauthentic, nil
case "-":
nargs := args
// ../rfc/7208:1489
expl := explanation(ctx, resolver, record, nargs)
return StatusFail, d.MechanismString(), expl, nil
authentic, expl := explanation(ctx, resolver, record, nargs)
rauthentic = rauthentic && authentic
return StatusFail, d.MechanismString(), expl, rauthentic, nil
case "~":
return StatusSoftfail, d.MechanismString(), "", nil
return StatusSoftfail, d.MechanismString(), "", rauthentic, nil
}
return StatusNone, d.MechanismString(), "", fmt.Errorf("internal error, unexpected qualifier %q", d.Qualifier)
return StatusNone, d.MechanismString(), "", rauthentic, fmt.Errorf("internal error, unexpected qualifier %q", d.Qualifier)
}
if record.Redirect != "" {
// We only know "redirect" for evaluating purposes, ignoring any others. ../rfc/7208:1423
// ../rfc/7208:1440
name, err := expandDomainSpecDNS(ctx, resolver, record.Redirect, args)
name, authentic, err := expandDomainSpecDNS(ctx, resolver, record.Redirect, args)
rauthentic = rauthentic && authentic
if err != nil {
return StatusPermerror, "", "", fmt.Errorf("expanding domain-spec: %w", err)
return StatusPermerror, "", "", rauthentic, fmt.Errorf("expanding domain-spec: %w", err)
}
nargs := args
nargs.domain = dns.Domain{ASCII: strings.TrimSuffix(name, ".")}
nargs.explanation = nil // ../rfc/7208:1548
status, mechanism, expl, err := checkHost(ctx, resolver, nargs)
status, mechanism, expl, authentic, err := checkHost(ctx, resolver, nargs)
rauthentic = rauthentic && authentic
if status == StatusNone {
return StatusPermerror, mechanism, "", err
return StatusPermerror, mechanism, "", rauthentic, err
}
return status, mechanism, expl, err
return status, mechanism, expl, rauthentic, err
}
// ../rfc/7208:996 ../rfc/7208:2095
return StatusNeutral, "default", "", nil
return StatusNeutral, "default", "", rauthentic, nil
}
// evaluateDomainSpec returns the parsed dns domain for spec if non-empty, and
@ -569,11 +590,11 @@ func evaluateDomainSpec(spec string, d dns.Domain) (dns.Domain, error) {
return d, nil
}
func expandDomainSpecDNS(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, error) {
func expandDomainSpecDNS(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, bool, error) {
return expandDomainSpec(ctx, resolver, domainSpec, args, true)
}
func expandDomainSpecExp(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, error) {
func expandDomainSpecExp(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, bool, error) {
return expandDomainSpec(ctx, resolver, domainSpec, args, false)
}
@ -582,9 +603,11 @@ func expandDomainSpecExp(ctx context.Context, resolver dns.Resolver, domainSpec
// Caller should typically treat failures as StatusPermerror. ../rfc/7208:1641
// ../rfc/7208:1639
// ../rfc/7208:1047
func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args, dns bool) (string, error) {
func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args, dns bool) (string, bool, error) {
exp := !dns
rauthentic := true // Until non-authentic record is found.
s := domainSpec
b := &strings.Builder{}
@ -599,7 +622,7 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
}
if i >= n {
return "", fmt.Errorf("%w: trailing bare %%", ErrMacroSyntax)
return "", rauthentic, fmt.Errorf("%w: trailing bare %%", ErrMacroSyntax)
}
c = s[i]
i++
@ -613,11 +636,11 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
b.WriteString("%20")
continue
} else if c != '{' {
return "", fmt.Errorf("%w: invalid macro opening %%%c", ErrMacroSyntax, c)
return "", rauthentic, fmt.Errorf("%w: invalid macro opening %%%c", ErrMacroSyntax, c)
}
if i >= n {
return "", fmt.Errorf("%w: missing macro ending }", ErrMacroSyntax)
return "", rauthentic, fmt.Errorf("%w: missing macro ending }", ErrMacroSyntax)
}
c = s[i]
i++
@ -645,9 +668,10 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
case 'p':
// ../rfc/7208:937
if err := trackLookupLimits(&args); err != nil {
return "", err
return "", rauthentic, err
}
names, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
names, result, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
rauthentic = rauthentic && result.Authentic
trackVoidLookup(err, &args)
if len(names) == 0 || err != nil {
// ../rfc/7208:1709
@ -661,7 +685,8 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
if !matchfn(name) {
continue
}
ips, err := resolver.LookupIP(ctx, "ip", name)
ips, result, err := resolver.LookupIP(ctx, "ip", name)
rauthentic = rauthentic && result.Authentic
trackVoidLookup(err, &args)
// ../rfc/7208:1714, we don't have to check other errors.
for _, ip := range ips {
@ -678,18 +703,18 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
dotdomain := "." + domain
v, err = verify(func(name string) bool { return name == domain })
if err != nil {
return "", err
return "", rauthentic, err
}
if v == "" {
v, err = verify(func(name string) bool { return strings.HasSuffix(name, dotdomain) })
if err != nil {
return "", err
return "", rauthentic, err
}
}
if v == "" {
v, err = verify(func(name string) bool { return name != domain && !strings.HasSuffix(name, dotdomain) })
if err != nil {
return "", err
return "", rauthentic, err
}
}
if v == "" {
@ -712,7 +737,7 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
}
case 'c', 'r', 't':
if !exp {
return "", fmt.Errorf("%w: macro letter %c only allowed in exp", ErrMacroSyntax, c)
return "", rauthentic, fmt.Errorf("%w: macro letter %c only allowed in exp", ErrMacroSyntax, c)
}
switch c {
case 'c':
@ -723,7 +748,7 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
v = fmt.Sprintf("%d", timeNow().Unix())
}
default:
return "", fmt.Errorf("%w: unknown macro letter %c", ErrMacroSyntax, c)
return "", rauthentic, fmt.Errorf("%w: unknown macro letter %c", ErrMacroSyntax, c)
}
digits := ""
@ -735,11 +760,11 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
if digits != "" {
v, err := strconv.Atoi(digits)
if err != nil {
return "", fmt.Errorf("%w: bad macro transformer digits %q: %s", ErrMacroSyntax, digits, err)
return "", rauthentic, fmt.Errorf("%w: bad macro transformer digits %q: %s", ErrMacroSyntax, digits, err)
}
nlabels = v
if nlabels == 0 {
return "", fmt.Errorf("%w: zero labels for digits transformer", ErrMacroSyntax)
return "", rauthentic, fmt.Errorf("%w: zero labels for digits transformer", ErrMacroSyntax)
}
}
@ -764,7 +789,7 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
}
if i >= n || s[i] != '}' {
return "", fmt.Errorf("%w: missing closing } for macro", ErrMacroSyntax)
return "", rauthentic, fmt.Errorf("%w: missing closing } for macro", ErrMacroSyntax)
}
i++
@ -801,14 +826,14 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
isAbs := strings.HasSuffix(r, ".")
r = ensureAbsDNS(r)
if err := validateDNS(r); err != nil {
return "", fmt.Errorf("invalid dns name: %s", err)
return "", rauthentic, fmt.Errorf("invalid dns name: %s", err)
}
// If resulting name is too large, cut off labels on the left until it fits. ../rfc/7208:1749
if len(r) > 253+1 {
labels := strings.Split(r, ".")
for i := range labels {
if i == len(labels)-1 {
return "", fmt.Errorf("expanded dns name too long")
return "", rauthentic, fmt.Errorf("expanded dns name too long")
}
s := strings.Join(labels[i+1:], ".")
if len(s) <= 254 {
@ -821,7 +846,7 @@ func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec str
r = r[:len(r)-1]
}
}
return r, nil
return r, rauthentic, nil
}
func expandIP(ip net.IP) string {
@ -883,7 +908,7 @@ func split(v, delim string) (r []string) {
// explanation does a best-effort attempt to fetch an explanation for a StatusFail response.
// If no explanation could be composed, an empty string is returned.
func explanation(ctx context.Context, resolver dns.Resolver, r *Record, args Args) string {
func explanation(ctx context.Context, resolver dns.Resolver, r *Record, args Args) (bool, string) {
// ../rfc/7208:1485
// If this record is the result of an "include", we have to use the explanation
@ -896,27 +921,29 @@ func explanation(ctx context.Context, resolver dns.Resolver, r *Record, args Arg
// ../rfc/7208:1491
if expl == "" {
return ""
return true, ""
}
// Limits for dns requests and void lookups should not be taken into account.
// Starting with zero ensures they aren't triggered.
args.dnsRequests = new(int)
args.voidLookups = new(int)
name, err := expandDomainSpecDNS(ctx, resolver, r.Explanation, args)
name, authentic, err := expandDomainSpecDNS(ctx, resolver, r.Explanation, args)
if err != nil || name == "" {
return ""
return authentic, ""
}
txts, err := resolver.LookupTXT(ctx, ensureAbsDNS(name))
txts, result, err := resolver.LookupTXT(ctx, ensureAbsDNS(name))
authentic = authentic && result.Authentic
if err != nil || len(txts) == 0 {
return ""
return authentic, ""
}
txt := strings.Join(txts, "")
s, err := expandDomainSpecExp(ctx, resolver, txt, args)
s, exauthentic, err := expandDomainSpecExp(ctx, resolver, txt, args)
authentic = authentic && exauthentic
if err != nil {
return ""
return authentic, ""
}
return s
return authentic, s
}
func ensureAbsDNS(s string) string {

View file

@ -31,7 +31,7 @@ func TestLookup(t *testing.T) {
t.Helper()
d := dns.Domain{ASCII: domain}
status, txt, record, err := Lookup(context.Background(), resolver, d)
status, txt, record, _, err := Lookup(context.Background(), resolver, d)
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
t.Fatalf("got err %v, expected err %v", err, expErr)
}
@ -99,7 +99,7 @@ func TestExpand(t *testing.T) {
args.RemoteIP = mustParseIP(ip)
}
r, err := expandDomainSpec(ctx, resolver, macro, args, dns)
r, _, err := expandDomainSpec(ctx, resolver, macro, args, dns)
if (err == nil) != (exp != "") {
t.Fatalf("got err %v, expected expansion %q, for macro %q", err, exp, macro)
}
@ -260,7 +260,7 @@ func TestVerify(t *testing.T) {
LocalIP: xip("127.0.0.1"),
LocalHostname: dns.Domain{ASCII: "localhost"},
}
received, _, _, err := Verify(ctx, r, args)
received, _, _, _, err := Verify(ctx, r, args)
if received.Result != status {
t.Fatalf("got status %q, expected %q, for ip %q (err %v)", received.Result, status, ip, err)
}
@ -346,7 +346,7 @@ func TestVerifyMultipleDomain(t *testing.T) {
LocalIP: net.ParseIP("127.0.0.1"),
LocalHostname: dns.Domain{ASCII: "localhost"},
}
received, _, _, err := Verify(context.Background(), resolver, args)
received, _, _, _, err := Verify(context.Background(), resolver, args)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
@ -371,7 +371,7 @@ func TestVerifyScenarios(t *testing.T) {
test := func(resolver dns.Resolver, args Args, expStatus Status, expDomain string, expExpl string, expErr error) {
t.Helper()
recv, d, expl, err := Verify(context.Background(), resolver, args)
recv, d, expl, _, err := Verify(context.Background(), resolver, args)
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
t.Fatalf("got err %v, expected %v", err, expErr)
}
@ -506,7 +506,7 @@ func TestEvaluate(t *testing.T) {
record := &Record{}
resolver := dns.MockResolver{}
args := Args{}
status, _, _, _ := Evaluate(context.Background(), record, resolver, args)
status, _, _, _, _ := Evaluate(context.Background(), record, resolver, args)
if status != StatusNone {
t.Fatalf("got status %q, expected none", status)
}
@ -514,7 +514,7 @@ func TestEvaluate(t *testing.T) {
args = Args{
HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "test.example"}},
}
status, mechanism, _, err := Evaluate(context.Background(), record, resolver, args)
status, mechanism, _, _, err := Evaluate(context.Background(), record, resolver, args)
if status != StatusNeutral || mechanism != "default" || err != nil {
t.Fatalf("got status %q, mechanism %q, err %v, expected neutral, default, no error", status, mechanism, err)
}

View file

@ -58,7 +58,7 @@ func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rrec
}()
name := "_smtp._tls." + domain.ASCII + "."
txts, err := dns.WithPackage(resolver, "tlsrpt").LookupTXT(ctx, name)
txts, _, err := dns.WithPackage(resolver, "tlsrpt").LookupTXT(ctx, name)
if dns.IsNotFound(err) {
return nil, "", ErrNoRecord
} else if err != nil {

View file

@ -96,7 +96,7 @@ func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rver
nctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
name := "_updates." + domain.ASCII + "."
txts, err := dns.WithPackage(resolver, "updates").LookupTXT(nctx, name)
txts, _, err := dns.WithPackage(resolver, "updates").LookupTXT(nctx, name)
if dns.IsNotFound(err) {
return Version{}, nil, ErrNoRecord
} else if err != nil {

1
vendor/github.com/mjl-/adns/.gitignore generated vendored Normal file
View file

@ -0,0 +1 @@
/cover.*

27
vendor/github.com/mjl-/adns/LICENSE generated vendored Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

33
vendor/github.com/mjl-/adns/Makefile generated vendored Normal file
View file

@ -0,0 +1,33 @@
build:
go build
go vet ./...
test:
go test -race -shuffle=on -coverprofile cover.out -covermode atomic
go tool cover -html=cover.out -o cover.html
check:
GOARCH=386 go vet
staticcheck ./...
# having "err" shadowed is common, best to not have others
check-shadow:
go vet -vettool=$$(which shadow) ./... 2>&1 | grep -v '"err"'
buildall:
GOOS=linux GOARCH=arm go build
GOOS=linux GOARCH=arm64 go build
GOOS=linux GOARCH=amd64 go build
GOOS=linux GOARCH=386 go build
GOOS=openbsd GOARCH=amd64 go build
GOOS=freebsd GOARCH=amd64 go build
GOOS=netbsd GOARCH=amd64 go build
GOOS=darwin GOARCH=amd64 go build
GOOS=dragonfly GOARCH=amd64 go build
GOOS=illumos GOARCH=amd64 go build
GOOS=solaris GOARCH=amd64 go build
GOOS=aix GOARCH=ppc64 go build
# no windows or plan9 for now
fmt:
gofmt -w -s *.go */*/*.go

5
vendor/github.com/mjl-/adns/README.md generated vendored Normal file
View file

@ -0,0 +1,5 @@
adns - copy of pure Go resolver from Go standard library, with modifications to facilitate use with DNSSEC.
Documentation: https://pkg.go.dev/github.com/mjl-/adns
License: Go's BSD license, see LICENSE.

377
vendor/github.com/mjl-/adns/addrselect.go generated vendored Normal file
View file

@ -0,0 +1,377 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Minimal RFC 6724 address selection.
package adns
import (
"net"
"net/netip"
"sort"
)
func sortByRFC6724(addrs []net.IPAddr) {
if len(addrs) < 2 {
return
}
sortByRFC6724withSrcs(addrs, srcAddrs(addrs))
}
func sortByRFC6724withSrcs(addrs []net.IPAddr, srcs []netip.Addr) {
if len(addrs) != len(srcs) {
panic("internal error")
}
addrAttr := make([]ipAttr, len(addrs))
srcAttr := make([]ipAttr, len(srcs))
for i, v := range addrs {
addrAttrIP, _ := netip.AddrFromSlice(v.IP)
addrAttr[i] = ipAttrOf(addrAttrIP)
srcAttr[i] = ipAttrOf(srcs[i])
}
sort.Stable(&byRFC6724{
addrs: addrs,
addrAttr: addrAttr,
srcs: srcs,
srcAttr: srcAttr,
})
}
// srcAddrs tries to UDP-connect to each address to see if it has a
// route. (This doesn't send any packets). The destination port
// number is irrelevant.
func srcAddrs(addrs []net.IPAddr) []netip.Addr {
srcs := make([]netip.Addr, len(addrs))
dst := net.UDPAddr{Port: 9}
for i := range addrs {
dst.IP = addrs[i].IP
dst.Zone = addrs[i].Zone
c, err := net.DialUDP("udp", nil, &dst)
if err == nil {
if src, ok := c.LocalAddr().(*net.UDPAddr); ok {
srcs[i], _ = netip.AddrFromSlice(src.IP)
}
c.Close()
}
}
return srcs
}
type ipAttr struct {
Scope scope
Precedence uint8
Label uint8
}
func ipAttrOf(ip netip.Addr) ipAttr {
if !ip.IsValid() {
return ipAttr{}
}
match := rfc6724policyTable.Classify(ip)
return ipAttr{
Scope: classifyScope(ip),
Precedence: match.Precedence,
Label: match.Label,
}
}
type byRFC6724 struct {
addrs []net.IPAddr // addrs to sort
addrAttr []ipAttr
srcs []netip.Addr // or not valid addr if unreachable
srcAttr []ipAttr
}
func (s *byRFC6724) Len() int { return len(s.addrs) }
func (s *byRFC6724) Swap(i, j int) {
s.addrs[i], s.addrs[j] = s.addrs[j], s.addrs[i]
s.srcs[i], s.srcs[j] = s.srcs[j], s.srcs[i]
s.addrAttr[i], s.addrAttr[j] = s.addrAttr[j], s.addrAttr[i]
s.srcAttr[i], s.srcAttr[j] = s.srcAttr[j], s.srcAttr[i]
}
// Less reports whether i is a better destination address for this
// host than j.
//
// The algorithm and variable names comes from RFC 6724 section 6.
func (s *byRFC6724) Less(i, j int) bool {
DA := s.addrs[i].IP
DB := s.addrs[j].IP
SourceDA := s.srcs[i]
SourceDB := s.srcs[j]
attrDA := &s.addrAttr[i]
attrDB := &s.addrAttr[j]
attrSourceDA := &s.srcAttr[i]
attrSourceDB := &s.srcAttr[j]
const preferDA = true
const preferDB = false
// Rule 1: Avoid unusable destinations.
// If DB is known to be unreachable or if Source(DB) is undefined, then
// prefer DA. Similarly, if DA is known to be unreachable or if
// Source(DA) is undefined, then prefer DB.
if !SourceDA.IsValid() && !SourceDB.IsValid() {
return false // "equal"
}
if !SourceDB.IsValid() {
return preferDA
}
if !SourceDA.IsValid() {
return preferDB
}
// Rule 2: Prefer matching scope.
// If Scope(DA) = Scope(Source(DA)) and Scope(DB) <> Scope(Source(DB)),
// then prefer DA. Similarly, if Scope(DA) <> Scope(Source(DA)) and
// Scope(DB) = Scope(Source(DB)), then prefer DB.
if attrDA.Scope == attrSourceDA.Scope && attrDB.Scope != attrSourceDB.Scope {
return preferDA
}
if attrDA.Scope != attrSourceDA.Scope && attrDB.Scope == attrSourceDB.Scope {
return preferDB
}
// Rule 3: Avoid deprecated addresses.
// If Source(DA) is deprecated and Source(DB) is not, then prefer DB.
// Similarly, if Source(DA) is not deprecated and Source(DB) is
// deprecated, then prefer DA.
// TODO(bradfitz): implement? low priority for now.
// Rule 4: Prefer home addresses.
// If Source(DA) is simultaneously a home address and care-of address
// and Source(DB) is not, then prefer DA. Similarly, if Source(DB) is
// simultaneously a home address and care-of address and Source(DA) is
// not, then prefer DB.
// TODO(bradfitz): implement? low priority for now.
// Rule 5: Prefer matching label.
// If Label(Source(DA)) = Label(DA) and Label(Source(DB)) <> Label(DB),
// then prefer DA. Similarly, if Label(Source(DA)) <> Label(DA) and
// Label(Source(DB)) = Label(DB), then prefer DB.
if attrSourceDA.Label == attrDA.Label &&
attrSourceDB.Label != attrDB.Label {
return preferDA
}
if attrSourceDA.Label != attrDA.Label &&
attrSourceDB.Label == attrDB.Label {
return preferDB
}
// Rule 6: Prefer higher precedence.
// If Precedence(DA) > Precedence(DB), then prefer DA. Similarly, if
// Precedence(DA) < Precedence(DB), then prefer DB.
if attrDA.Precedence > attrDB.Precedence {
return preferDA
}
if attrDA.Precedence < attrDB.Precedence {
return preferDB
}
// Rule 7: Prefer native transport.
// If DA is reached via an encapsulating transition mechanism (e.g.,
// IPv6 in IPv4) and DB is not, then prefer DB. Similarly, if DB is
// reached via encapsulation and DA is not, then prefer DA.
// TODO(bradfitz): implement? low priority for now.
// Rule 8: Prefer smaller scope.
// If Scope(DA) < Scope(DB), then prefer DA. Similarly, if Scope(DA) >
// Scope(DB), then prefer DB.
if attrDA.Scope < attrDB.Scope {
return preferDA
}
if attrDA.Scope > attrDB.Scope {
return preferDB
}
// Rule 9: Use the longest matching prefix.
// When DA and DB belong to the same address family (both are IPv6 or
// both are IPv4 [but see below]): If CommonPrefixLen(Source(DA), DA) >
// CommonPrefixLen(Source(DB), DB), then prefer DA. Similarly, if
// CommonPrefixLen(Source(DA), DA) < CommonPrefixLen(Source(DB), DB),
// then prefer DB.
//
// However, applying this rule to IPv4 addresses causes
// problems (see issues 13283 and 18518), so limit to IPv6.
if DA.To4() == nil && DB.To4() == nil {
commonA := commonPrefixLen(SourceDA, DA)
commonB := commonPrefixLen(SourceDB, DB)
if commonA > commonB {
return preferDA
}
if commonA < commonB {
return preferDB
}
}
// Rule 10: Otherwise, leave the order unchanged.
// If DA preceded DB in the original list, prefer DA.
// Otherwise, prefer DB.
return false // "equal"
}
type policyTableEntry struct {
Prefix netip.Prefix
Precedence uint8
Label uint8
}
type policyTable []policyTableEntry
// RFC 6724 section 2.1.
// Items are sorted by the size of their Prefix.Mask.Size,
var rfc6724policyTable = policyTable{
{
// "::1/128"
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}), 128),
Precedence: 50,
Label: 0,
},
{
// "::ffff:0:0/96"
// IPv4-compatible, etc.
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}), 96),
Precedence: 35,
Label: 4,
},
{
// "::/96"
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 96),
Precedence: 1,
Label: 3,
},
{
// "2001::/32"
// Teredo
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01}), 32),
Precedence: 5,
Label: 5,
},
{
// "2002::/16"
// 6to4
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x02}), 16),
Precedence: 30,
Label: 2,
},
{
// "3ffe::/16"
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x3f, 0xfe}), 16),
Precedence: 1,
Label: 12,
},
{
// "fec0::/10"
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfe, 0xc0}), 10),
Precedence: 1,
Label: 11,
},
{
// "fc00::/7"
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfc}), 7),
Precedence: 3,
Label: 13,
},
{
// "::/0"
Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
Precedence: 40,
Label: 1,
},
}
// Classify returns the policyTableEntry of the entry with the longest
// matching prefix that contains ip.
// The table t must be sorted from largest mask size to smallest.
func (t policyTable) Classify(ip netip.Addr) policyTableEntry {
// Prefix.Contains() will not match an IPv6 prefix for an IPv4 address.
if ip.Is4() {
ip = netip.AddrFrom16(ip.As16())
}
for _, ent := range t {
if ent.Prefix.Contains(ip) {
return ent
}
}
return policyTableEntry{}
}
// RFC 6724 section 3.1.
type scope uint8
const (
scopeInterfaceLocal scope = 0x1
scopeLinkLocal scope = 0x2
scopeAdminLocal scope = 0x4
scopeSiteLocal scope = 0x5
scopeOrgLocal scope = 0x8
scopeGlobal scope = 0xe
)
func classifyScope(ip netip.Addr) scope {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
return scopeLinkLocal
}
ipv6 := ip.Is6() && !ip.Is4In6()
ipv6AsBytes := ip.As16()
if ipv6 && ip.IsMulticast() {
return scope(ipv6AsBytes[1] & 0xf)
}
// Site-local addresses are defined in RFC 3513 section 2.5.6
// (and deprecated in RFC 3879).
if ipv6 && ipv6AsBytes[0] == 0xfe && ipv6AsBytes[1]&0xc0 == 0xc0 {
return scopeSiteLocal
}
return scopeGlobal
}
// commonPrefixLen reports the length of the longest prefix (looking
// at the most significant, or leftmost, bits) that the
// two addresses have in common, up to the length of a's prefix (i.e.,
// the portion of the address not including the interface ID).
//
// If a or b is an IPv4 address as an IPv6 address, the IPv4 addresses
// are compared (with max common prefix length of 32).
// If a and b are different IP versions, 0 is returned.
//
// See https://tools.ietf.org/html/rfc6724#section-2.2
func commonPrefixLen(a netip.Addr, b net.IP) (cpl int) {
if b4 := b.To4(); b4 != nil {
b = b4
}
aAsSlice := a.AsSlice()
if len(aAsSlice) != len(b) {
return 0
}
// If IPv6, only up to the prefix (first 64 bits)
if len(aAsSlice) > 8 {
aAsSlice = aAsSlice[:8]
b = b[:8]
}
for len(aAsSlice) > 0 {
if aAsSlice[0] == b[0] {
cpl += 8
aAsSlice = aAsSlice[1:]
b = b[1:]
continue
}
bits := 8
ab, bb := aAsSlice[0], b[0]
for {
ab >>= 1
bb >>= 1
bits--
if ab == bb {
cpl += bits
return
}
}
}
return
}

17
vendor/github.com/mjl-/adns/authentic.go generated vendored Normal file
View file

@ -0,0 +1,17 @@
package adns
// Result has additional information about a DNS lookup.
type Result struct {
// Authentic indicates whether the response was DNSSEC-signed and verified.
// This package is a security-aware non-validating stub-resolver, sending requests
// with the "authentic data" bit set to its recursive resolvers, but only if the
// resolvers are trusted. Resolvers are trusted either if explicitly marked with
// "options trust-ad" in /etc/resolv.conf, or if all resolver IP addresses are
// loopback IP's. If the response from the resolver has the "authentic data" bit
// set, the DNS name and all indirections towards the name, were signed and the
// recursive resolver has verified them.
Authentic bool
// todo: possibly add followed cname's
// todo: possibly add lowest TTL encountered in lookup (gathered after following cname's)
}

508
vendor/github.com/mjl-/adns/conf.go generated vendored Normal file
View file

@ -0,0 +1,508 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !js
package adns
import (
"errors"
"io/fs"
"os"
"runtime"
"sync"
"syscall"
"github.com/mjl-/adns/internal/bytealg"
)
// The net package's name resolution is rather complicated.
// There are two main approaches, go and cgo.
// The cgo resolver uses C functions like getaddrinfo.
// The go resolver reads system files directly and
// sends DNS packets directly to servers.
//
// The netgo build tag prefers the go resolver.
// The netcgo build tag prefers the cgo resolver.
//
// The netgo build tag also prohibits the use of the cgo tool.
// However, on Darwin, Plan 9, and Windows the cgo resolver is still available.
// On those systems the cgo resolver does not require the cgo tool.
// (The term "cgo resolver" was locked in by GODEBUG settings
// at a time when the cgo resolver did require the cgo tool.)
//
// Adding netdns=go to GODEBUG will prefer the go resolver.
// Adding netdns=cgo to GODEBUG will prefer the cgo resolver.
//
// The Resolver struct has a PreferGo field that user code
// may set to prefer the go resolver. It is documented as being
// equivalent to adding netdns=go to GODEBUG.
//
// When deciding which resolver to use, we first check the PreferGo field.
// If that is not set, we check the GODEBUG setting.
// If that is not set, we check the netgo or netcgo build tag.
// If none of those are set, we normally prefer the go resolver by default.
// However, if the cgo resolver is available,
// there is a complex set of conditions for which we prefer the cgo resolver.
//
// Other files define the netGoBuildTag, netCgoBuildTag, and cgoAvailable
// constants.
// conf is used to determine name resolution configuration.
type conf struct {
netGo bool // prefer go approach, based on build tag and GODEBUG
netCgo bool // prefer cgo approach, based on build tag and GODEBUG
dnsDebugLevel int // from GODEBUG
preferCgo bool // if no explicit preference, use cgo
goos string // copy of runtime.GOOS, used for testing
mdnsTest mdnsTest // assume /etc/mdns.allow exists, for testing
}
// mdnsTest is for testing only.
type mdnsTest int
const (
mdnsFromSystem mdnsTest = iota
mdnsAssumeExists
mdnsAssumeDoesNotExist
)
var (
confOnce sync.Once // guards init of confVal via initConfVal
confVal = &conf{goos: runtime.GOOS}
)
// systemConf returns the machine's network configuration.
func systemConf() *conf {
confOnce.Do(initConfVal)
return confVal
}
var netGoBuildTag = true
var netCgoBuildTag = false
var cgoAvailable = false
// initConfVal initializes confVal based on the environment
// that will not change during program execution.
func initConfVal() {
dnsMode, debugLevel := goDebugNetDNS()
confVal.netGo = netGoBuildTag || dnsMode == "go"
confVal.netCgo = netCgoBuildTag || dnsMode == "cgo"
confVal.dnsDebugLevel = debugLevel
if confVal.dnsDebugLevel > 0 {
defer func() {
if confVal.dnsDebugLevel > 1 {
println("go package net: confVal.netCgo =", confVal.netCgo, " netGo =", confVal.netGo)
}
switch {
case confVal.netGo:
if netGoBuildTag {
println("go package net: built with netgo build tag; using Go's DNS resolver")
} else {
println("go package net: GODEBUG setting forcing use of Go's resolver")
}
case !cgoAvailable:
println("go package net: cgo resolver not supported; using Go's DNS resolver")
case confVal.netCgo || confVal.preferCgo:
println("go package net: using cgo DNS resolver")
default:
println("go package net: dynamic selection of DNS resolver")
}
}()
}
// The remainder of this function sets preferCgo based on
// conditions that will not change during program execution.
// By default, prefer the go resolver.
confVal.preferCgo = false
// If the cgo resolver is not available, we can't prefer it.
if !cgoAvailable {
return
}
// Some operating systems always prefer the cgo resolver.
if goosPrefersCgo() {
confVal.preferCgo = true
return
}
// The remaining checks are specific to Unix systems.
switch runtime.GOOS {
case "plan9", "windows", "js", "wasip1":
return
}
// If any environment-specified resolver options are specified,
// prefer the cgo resolver.
// Note that LOCALDOMAIN can change behavior merely by being
// specified with the empty string.
_, localDomainDefined := syscall.Getenv("LOCALDOMAIN")
if localDomainDefined || os.Getenv("RES_OPTIONS") != "" || os.Getenv("HOSTALIASES") != "" {
confVal.preferCgo = true
return
}
// OpenBSD apparently lets you override the location of resolv.conf
// with ASR_CONFIG. If we notice that, defer to libc.
if runtime.GOOS == "openbsd" && os.Getenv("ASR_CONFIG") != "" {
confVal.preferCgo = true
return
}
}
// goosPreferCgo reports whether the GOOS value passed in prefers
// the cgo resolver.
func goosPrefersCgo() bool {
switch runtime.GOOS {
// Historically on Windows and Plan 9 we prefer the
// cgo resolver (which doesn't use the cgo tool) rather than
// the go resolver. This is because originally these
// systems did not support the go resolver.
// Keep it this way for better compatibility.
// Perhaps we can revisit this some day.
case "windows", "plan9":
return true
// Darwin pops up annoying dialog boxes if programs try to
// do their own DNS requests, so prefer cgo.
case "darwin", "ios":
return true
// DNS requests don't work on Android, so prefer the cgo resolver.
// Issue #10714.
case "android":
return true
default:
return false
}
}
// mustUseGoResolver reports whether a DNS lookup of any sort is
// required to use the go resolver. The provided Resolver is optional.
// This will report true if the cgo resolver is not available.
func (c *conf) mustUseGoResolver(r *Resolver) bool {
return c.netGo || r.preferGo() || !cgoAvailable
}
// addrLookupOrder determines which strategy to use to resolve addresses.
// The provided Resolver is optional. nil means to not consider its options.
// It also returns dnsConfig when it was used to determine the lookup order.
func (c *conf) addrLookupOrder(r *Resolver, addr string) (ret hostLookupOrder, dnsConf *dnsConfig) {
if c.dnsDebugLevel > 1 {
defer func() {
print("go package net: addrLookupOrder(", addr, ") = ", ret.String(), "\n")
}()
}
return c.lookupOrder(r, "")
}
// hostLookupOrder determines which strategy to use to resolve hostname.
// The provided Resolver is optional. nil means to not consider its options.
// It also returns dnsConfig when it was used to determine the lookup order.
func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder, dnsConf *dnsConfig) {
if c.dnsDebugLevel > 1 {
defer func() {
print("go package net: hostLookupOrder(", hostname, ") = ", ret.String(), "\n")
}()
}
return c.lookupOrder(r, hostname)
}
func (c *conf) lookupOrder(r *Resolver, hostname string) (ret hostLookupOrder, dnsConf *dnsConfig) {
// fallbackOrder is the order we return if we can't figure it out.
var fallbackOrder hostLookupOrder
var canUseCgo bool
if c.mustUseGoResolver(r) {
// Go resolver was explicitly requested
// or cgo resolver is not available.
// Figure out the order below.
switch c.goos {
case "windows":
// TODO(bradfitz): implement files-based
// lookup on Windows too? I guess /etc/hosts
// kinda exists on Windows. But for now, only
// do DNS.
fallbackOrder = hostLookupDNS
default:
fallbackOrder = hostLookupFilesDNS
}
canUseCgo = false
} else if c.netCgo {
// Cgo resolver was explicitly requested.
return hostLookupCgo, nil
} else if c.preferCgo {
// Given a choice, we prefer the cgo resolver.
return hostLookupCgo, nil
} else {
// Neither resolver was explicitly requested
// and we have no preference.
if bytealg.IndexByteString(hostname, '\\') != -1 || bytealg.IndexByteString(hostname, '%') != -1 {
// Don't deal with special form hostnames
// with backslashes or '%'.
return hostLookupCgo, nil
}
// If something is unrecognized, use cgo.
fallbackOrder = hostLookupCgo
canUseCgo = true
}
// On systems that don't use /etc/resolv.conf or /etc/nsswitch.conf, we are done.
switch c.goos {
case "windows", "plan9", "android", "ios":
return fallbackOrder, nil
}
// Try to figure out the order to use for searches.
// If we don't recognize something, use fallbackOrder.
// That will use cgo unless the Go resolver was explicitly requested.
// If we do figure out the order, return something other
// than fallbackOrder to use the Go resolver with that order.
dnsConf = getSystemDNSConfig()
if canUseCgo && dnsConf.err != nil && !errors.Is(dnsConf.err, fs.ErrNotExist) && !errors.Is(dnsConf.err, fs.ErrPermission) {
// We can't read the resolv.conf file, so use cgo if we can.
return hostLookupCgo, dnsConf
}
if canUseCgo && dnsConf.unknownOpt {
// We didn't recognize something in resolv.conf,
// so use cgo if we can.
return hostLookupCgo, dnsConf
}
// OpenBSD is unique and doesn't use nsswitch.conf.
// It also doesn't support mDNS.
if c.goos == "openbsd" {
// OpenBSD's resolv.conf manpage says that a
// non-existent resolv.conf means "lookup" defaults
// to only "files", without DNS lookups.
if errors.Is(dnsConf.err, fs.ErrNotExist) {
return hostLookupFiles, dnsConf
}
lookup := dnsConf.lookup
if len(lookup) == 0 {
// https://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man5/resolv.conf.5
// "If the lookup keyword is not used in the
// system's resolv.conf file then the assumed
// order is 'bind file'"
return hostLookupDNSFiles, dnsConf
}
if len(lookup) < 1 || len(lookup) > 2 {
// We don't recognize this format.
return fallbackOrder, dnsConf
}
switch lookup[0] {
case "bind":
if len(lookup) == 2 {
if lookup[1] == "file" {
return hostLookupDNSFiles, dnsConf
}
// Unrecognized.
return fallbackOrder, dnsConf
}
return hostLookupDNS, dnsConf
case "file":
if len(lookup) == 2 {
if lookup[1] == "bind" {
return hostLookupFilesDNS, dnsConf
}
// Unrecognized.
return fallbackOrder, dnsConf
}
return hostLookupFiles, dnsConf
default:
// Unrecognized.
return fallbackOrder, dnsConf
}
// We always return before this point.
// The code below is for non-OpenBSD.
}
// Canonicalize the hostname by removing any trailing dot.
if stringsHasSuffix(hostname, ".") {
hostname = hostname[:len(hostname)-1]
}
if canUseCgo && stringsHasSuffixFold(hostname, ".local") {
// Per RFC 6762, the ".local" TLD is special. And
// because Go's native resolver doesn't do mDNS or
// similar local resolution mechanisms, assume that
// libc might (via Avahi, etc) and use cgo.
return hostLookupCgo, dnsConf
}
nss := getSystemNSS()
srcs := nss.sources["hosts"]
// If /etc/nsswitch.conf doesn't exist or doesn't specify any
// sources for "hosts", assume Go's DNS will work fine.
if errors.Is(nss.err, fs.ErrNotExist) || (nss.err == nil && len(srcs) == 0) {
if canUseCgo && c.goos == "solaris" {
// illumos defaults to
// "nis [NOTFOUND=return] files",
// which the go resolver doesn't support.
return hostLookupCgo, dnsConf
}
return hostLookupFilesDNS, dnsConf
}
if nss.err != nil {
// We failed to parse or open nsswitch.conf, so
// we have nothing to base an order on.
return fallbackOrder, dnsConf
}
var hasDNSSource bool
var hasDNSSourceChecked bool
var filesSource, dnsSource bool
var first string
for i, src := range srcs {
if src.source == "files" || src.source == "dns" {
if canUseCgo && !src.standardCriteria() {
// non-standard; let libc deal with it.
return hostLookupCgo, dnsConf
}
if src.source == "files" {
filesSource = true
} else {
hasDNSSource = true
hasDNSSourceChecked = true
dnsSource = true
}
if first == "" {
first = src.source
}
continue
}
if canUseCgo {
switch {
case hostname != "" && src.source == "myhostname":
// Let the cgo resolver handle myhostname
// if we are looking up the local hostname.
if isLocalhost(hostname) || isGateway(hostname) || isOutbound(hostname) {
return hostLookupCgo, dnsConf
}
hn, err := getHostname()
if err != nil || stringsEqualFold(hostname, hn) {
return hostLookupCgo, dnsConf
}
continue
case hostname != "" && stringsHasPrefix(src.source, "mdns"):
// e.g. "mdns4", "mdns4_minimal"
// We already returned true before if it was *.local.
// libc wouldn't have found a hit on this anyway.
// We don't parse mdns.allow files. They're rare. If one
// exists, it might list other TLDs (besides .local) or even
// '*', so just let libc deal with it.
var haveMDNSAllow bool
switch c.mdnsTest {
case mdnsFromSystem:
_, err := os.Stat("/etc/mdns.allow")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
// Let libc figure out what is going on.
return hostLookupCgo, dnsConf
}
haveMDNSAllow = err == nil
case mdnsAssumeExists:
haveMDNSAllow = true
case mdnsAssumeDoesNotExist:
haveMDNSAllow = false
}
if haveMDNSAllow {
return hostLookupCgo, dnsConf
}
continue
default:
// Some source we don't know how to deal with.
return hostLookupCgo, dnsConf
}
}
if !hasDNSSourceChecked {
hasDNSSourceChecked = true
for _, v := range srcs[i+1:] {
if v.source == "dns" {
hasDNSSource = true
break
}
}
}
// If we saw a source we don't recognize, which can only
// happen if we can't use the cgo resolver, treat it as DNS,
// but only when there is no dns in all other sources.
if !hasDNSSource {
dnsSource = true
if first == "" {
first = "dns"
}
}
}
// Cases where Go can handle it without cgo and C thread overhead,
// or where the Go resolver has been forced.
switch {
case filesSource && dnsSource:
if first == "files" {
return hostLookupFilesDNS, dnsConf
} else {
return hostLookupDNSFiles, dnsConf
}
case filesSource:
return hostLookupFiles, dnsConf
case dnsSource:
return hostLookupDNS, dnsConf
}
// Something weird. Fallback to the default.
return fallbackOrder, dnsConf
}
// goDebugNetDNS parses the value of the GODEBUG "netdns" value.
// The netdns value can be of the form:
//
// 1 // debug level 1
// 2 // debug level 2
// cgo // use cgo for DNS lookups
// go // use go for DNS lookups
// cgo+1 // use cgo for DNS lookups + debug level 1
// 1+cgo // same
// cgo+2 // same, but debug level 2
//
// etc.
func goDebugNetDNS() (dnsMode string, debugLevel int) {
return "go", 0
}
// isLocalhost reports whether h should be considered a "localhost"
// name for the myhostname NSS module.
func isLocalhost(h string) bool {
return stringsEqualFold(h, "localhost") || stringsEqualFold(h, "localhost.localdomain") || stringsHasSuffixFold(h, ".localhost") || stringsHasSuffixFold(h, ".localhost.localdomain")
}
// isGateway reports whether h should be considered a "gateway"
// name for the myhostname NSS module.
func isGateway(h string) bool {
return stringsEqualFold(h, "_gateway")
}
// isOutbound reports whether h should be considered a "outbound"
// name for the myhostname NSS module.
func isOutbound(h string) bool {
return stringsEqualFold(h, "_outbound")
}

42
vendor/github.com/mjl-/adns/dial.go generated vendored Normal file
View file

@ -0,0 +1,42 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
"context"
"net"
)
func parseNetwork(ctx context.Context, network string, needsProto bool) (afnet string, proto int, err error) {
i := last(network, ':')
if i < 0 { // no colon
switch network {
case "tcp", "tcp4", "tcp6":
case "udp", "udp4", "udp6":
case "ip", "ip4", "ip6":
if needsProto {
return "", 0, net.UnknownNetworkError(network)
}
case "unix", "unixgram", "unixpacket":
default:
return "", 0, net.UnknownNetworkError(network)
}
return network, 0, nil
}
afnet = network[:i]
switch afnet {
case "ip", "ip4", "ip6":
protostr := network[i+1:]
proto, i, ok := dtoi(protostr)
if !ok || i != len(protostr) {
proto, err = lookupProtocol(ctx, protostr)
if err != nil {
return "", 0, err
}
}
return afnet, proto, nil
}
return "", 0, net.UnknownNetworkError(network)
}

209
vendor/github.com/mjl-/adns/dnsclient.go generated vendored Normal file
View file

@ -0,0 +1,209 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
mathrand "math/rand"
"net"
"sort"
"golang.org/x/net/dns/dnsmessage"
"github.com/mjl-/adns/internal/bytealg"
"github.com/mjl-/adns/internal/itoa"
)
func randInt() int {
return mathrand.Int()
}
func randIntn(n int) int {
return randInt() % n
}
// reverseaddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP
// address addr suitable for rDNS (PTR) record lookup or an error if it fails
// to parse the IP address.
func reverseaddr(addr string) (arpa string, err error) {
ip := net.ParseIP(addr)
if ip == nil {
return "", &DNSError{Err: "unrecognized address", Name: addr}
}
if ip.To4() != nil {
return itoa.Uitoa(uint(ip[15])) + "." + itoa.Uitoa(uint(ip[14])) + "." + itoa.Uitoa(uint(ip[13])) + "." + itoa.Uitoa(uint(ip[12])) + ".in-addr.arpa.", nil
}
// Must be IPv6
buf := make([]byte, 0, len(ip)*4+len("ip6.arpa."))
// Add it, in reverse, to the buffer
for i := len(ip) - 1; i >= 0; i-- {
v := ip[i]
buf = append(buf, hexDigit[v&0xF],
'.',
hexDigit[v>>4],
'.')
}
// Append "ip6.arpa." and return (buf already has the final .)
buf = append(buf, "ip6.arpa."...)
return string(buf), nil
}
func equalASCIIName(x, y dnsmessage.Name) bool {
if x.Length != y.Length {
return false
}
for i := 0; i < int(x.Length); i++ {
a := x.Data[i]
b := y.Data[i]
if 'A' <= a && a <= 'Z' {
a += 0x20
}
if 'A' <= b && b <= 'Z' {
b += 0x20
}
if a != b {
return false
}
}
return true
}
// isDomainName checks if a string is a presentation-format domain name
// (currently restricted to hostname-compatible "preferred name" LDH labels and
// SRV-like "underscore labels"; see golang.org/issue/12421).
func isDomainName(s string) bool {
// The root domain name is valid. See golang.org/issue/45715.
if s == "." {
return true
}
// See RFC 1035, RFC 3696.
// Presentation format has dots before every label except the first, and the
// terminal empty label is optional here because we assume fully-qualified
// (absolute) input. We must therefore reserve space for the first and last
// labels' length octets in wire format, where they are necessary and the
// maximum total length is 255.
// So our _effective_ maximum is 253, but 254 is not rejected if the last
// character is a dot.
l := len(s)
if l == 0 || l > 254 || l == 254 && s[l-1] != '.' {
return false
}
last := byte('.')
nonNumeric := false // true once we've seen a letter or hyphen
partlen := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch {
default:
return false
case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_':
nonNumeric = true
partlen++
case '0' <= c && c <= '9':
// fine
partlen++
case c == '-':
// Byte before dash cannot be dot.
if last == '.' {
return false
}
partlen++
nonNumeric = true
case c == '.':
// Byte before dot cannot be dot, dash.
if last == '.' || last == '-' {
return false
}
if partlen > 63 || partlen == 0 {
return false
}
partlen = 0
}
last = c
}
if last == '-' || partlen > 63 {
return false
}
return nonNumeric
}
// absDomainName returns an absolute domain name which ends with a
// trailing dot to match pure Go reverse resolver and all other lookup
// routines.
// See golang.org/issue/12189.
// But we don't want to add dots for local names from /etc/hosts.
// It's hard to tell so we settle on the heuristic that names without dots
// (like "localhost" or "myhost") do not get trailing dots, but any other
// names do.
func absDomainName(s string) string {
if bytealg.IndexByteString(s, '.') != -1 && s[len(s)-1] != '.' {
s += "."
}
return s
}
// byPriorityWeight sorts SRV records by ascending priority and weight.
type byPriorityWeight []*net.SRV
func (s byPriorityWeight) Len() int { return len(s) }
func (s byPriorityWeight) Less(i, j int) bool {
return s[i].Priority < s[j].Priority || (s[i].Priority == s[j].Priority && s[i].Weight < s[j].Weight)
}
func (s byPriorityWeight) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// shuffleByWeight shuffles SRV records by weight using the algorithm
// described in RFC 2782.
func (addrs byPriorityWeight) shuffleByWeight() {
sum := 0
for _, addr := range addrs {
sum += int(addr.Weight)
}
for sum > 0 && len(addrs) > 1 {
s := 0
n := randIntn(sum)
for i := range addrs {
s += int(addrs[i].Weight)
if s > n {
if i > 0 {
addrs[0], addrs[i] = addrs[i], addrs[0]
}
break
}
}
sum -= int(addrs[0].Weight)
addrs = addrs[1:]
}
}
// sort reorders SRV records as specified in RFC 2782.
func (addrs byPriorityWeight) sort() {
sort.Sort(addrs)
i := 0
for j := 1; j < len(addrs); j++ {
if addrs[i].Priority != addrs[j].Priority {
addrs[i:j].shuffleByWeight()
i = j
}
}
addrs[i:].shuffleByWeight()
}
// byPref implements sort.Interface to sort MX records by preference
type byPref []*net.MX
func (s byPref) Len() int { return len(s) }
func (s byPref) Less(i, j int) bool { return s[i].Pref < s[j].Pref }
func (s byPref) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// sort reorders MX records as specified in RFC 5321.
func (s byPref) sort() {
for i := range s {
j := randIntn(i + 1)
s[i], s[j] = s[j], s[i]
}
sort.Sort(s)
}

947
vendor/github.com/mjl-/adns/dnsclient_unix.go generated vendored Normal file
View file

@ -0,0 +1,947 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !js
// DNS client: see RFC 1035.
// Has to be linked into package net for Dial.
// TODO(rsc):
// Could potentially handle many outstanding lookups faster.
// Random UDP source port (net.Dial should do that for us).
// Random request IDs.
package adns
import (
"bytes"
"context"
cryptorand "crypto/rand"
"errors"
"io"
"net"
"os"
"runtime"
"sync"
"sync/atomic"
"time"
"golang.org/x/net/dns/dnsmessage"
"github.com/mjl-/adns/internal/itoa"
)
const (
// to be used as a useTCP parameter to exchange
useTCPOnly = true
useUDPOrTCP = false
// Maximum DNS packet size.
// Value taken from https://dnsflagday.net/2020/.
maxDNSPacketSize = 1232
)
var (
errLameReferral = errors.New("lame referral")
errCannotUnmarshalDNSMessage = errors.New("cannot unmarshal DNS message")
errCannotMarshalDNSMessage = errors.New("cannot marshal DNS message")
errServerMisbehaving = errors.New("server misbehaving")
errInvalidDNSResponse = errors.New("invalid DNS response")
errNoAnswerFromDNSServer = errors.New("no answer from DNS server")
// errServerTemporarilyMisbehaving is like errServerMisbehaving, except
// that when it gets translated to a DNSError, the IsTemporary field
// gets set to true.
errServerTemporarilyMisbehaving = errors.New("server misbehaving")
)
func newRequest(q dnsmessage.Question, ad bool) (id uint16, udpReq, tcpReq []byte, err error) {
var idbuf [2]byte
_, err = cryptorand.Read(idbuf[:])
if err != nil {
return 0, nil, nil, err
}
id = uint16(idbuf[0])<<8 | uint16(idbuf[1])
b := dnsmessage.NewBuilder(make([]byte, 2, 514), dnsmessage.Header{ID: id, RecursionDesired: true, AuthenticData: ad})
if err := b.StartQuestions(); err != nil {
return 0, nil, nil, err
}
if err := b.Question(q); err != nil {
return 0, nil, nil, err
}
// Accept packets up to maxDNSPacketSize. RFC 6891.
if err := b.StartAdditionals(); err != nil {
return 0, nil, nil, err
}
var rh dnsmessage.ResourceHeader
if err := rh.SetEDNS0(maxDNSPacketSize, dnsmessage.RCodeSuccess, false); err != nil {
return 0, nil, nil, err
}
if err := b.OPTResource(rh, dnsmessage.OPTResource{}); err != nil {
return 0, nil, nil, err
}
tcpReq, err = b.Finish()
if err != nil {
return 0, nil, nil, err
}
udpReq = tcpReq[2:]
l := len(tcpReq) - 2
tcpReq[0] = byte(l >> 8)
tcpReq[1] = byte(l)
return id, udpReq, tcpReq, nil
}
func checkResponse(reqID uint16, reqQues dnsmessage.Question, respHdr dnsmessage.Header, respQues dnsmessage.Question) bool {
if !respHdr.Response {
return false
}
if reqID != respHdr.ID {
return false
}
if reqQues.Type != respQues.Type || reqQues.Class != respQues.Class || !equalASCIIName(reqQues.Name, respQues.Name) {
return false
}
return true
}
func dnsPacketRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) {
if _, err := c.Write(b); err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
b = make([]byte, maxDNSPacketSize)
for {
n, err := c.Read(b)
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
var p dnsmessage.Parser
// Ignore invalid responses as they may be malicious
// forgery attempts. Instead continue waiting until
// timeout. See golang.org/issue/13281.
h, err := p.Start(b[:n])
if err != nil {
continue
}
q, err := p.Question()
if err != nil || !checkResponse(id, query, h, q) {
continue
}
return p, h, nil
}
}
func dnsStreamRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) {
if _, err := c.Write(b); err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
b = make([]byte, 1280) // 1280 is a reasonable initial size for IP over Ethernet, see RFC 4035
if _, err := io.ReadFull(c, b[:2]); err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
l := int(b[0])<<8 | int(b[1])
if l > len(b) {
b = make([]byte, l)
}
n, err := io.ReadFull(c, b[:l])
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
var p dnsmessage.Parser
h, err := p.Start(b[:n])
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage
}
q, err := p.Question()
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage
}
if !checkResponse(id, query, h, q) {
return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse
}
return p, h, nil
}
// exchange sends a query on the connection and hopes for a response.
func (r *Resolver) exchange(ctx context.Context, server string, q dnsmessage.Question, timeout time.Duration, useTCP, ad bool) (dnsmessage.Parser, dnsmessage.Header, error) {
q.Class = dnsmessage.ClassINET
id, udpReq, tcpReq, err := newRequest(q, ad)
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotMarshalDNSMessage
}
var networks []string
if useTCP {
networks = []string{"tcp"}
} else {
networks = []string{"udp", "tcp"}
}
for _, network := range networks {
nctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
defer cancel()
c, err := r.dial(nctx, network, server)
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
if d, ok := nctx.Deadline(); ok && !d.IsZero() {
c.SetDeadline(d)
}
var p dnsmessage.Parser
var h dnsmessage.Header
if _, ok := c.(net.PacketConn); ok {
p, h, err = dnsPacketRoundTrip(c, id, q, udpReq)
} else {
p, h, err = dnsStreamRoundTrip(c, id, q, tcpReq)
}
c.Close()
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, mapErr(err)
}
if err := p.SkipQuestion(); err != dnsmessage.ErrSectionDone {
return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse
}
if h.Truncated { // see RFC 5966
continue
}
return p, h, nil
}
return dnsmessage.Parser{}, dnsmessage.Header{}, errNoAnswerFromDNSServer
}
// checkHeader performs basic sanity checks on the header.
func checkHeader(p *dnsmessage.Parser, h dnsmessage.Header) error {
if h.RCode == dnsmessage.RCodeNameError {
return errNoSuchHost
}
_, err := p.AnswerHeader()
if err != nil && err != dnsmessage.ErrSectionDone {
return errCannotUnmarshalDNSMessage
}
// libresolv continues to the next server when it receives
// an invalid referral response. See golang.org/issue/15434.
if h.RCode == dnsmessage.RCodeSuccess && !h.Authoritative && !h.RecursionAvailable && err == dnsmessage.ErrSectionDone {
return errLameReferral
}
if h.RCode != dnsmessage.RCodeSuccess && h.RCode != dnsmessage.RCodeNameError {
// None of the error codes make sense
// for the query we sent. If we didn't get
// a name error and we didn't get success,
// the server is behaving incorrectly or
// having temporary trouble.
if h.RCode == dnsmessage.RCodeServerFailure {
// Look for Extended DNS Error (EDE), RFC 8914.
if p.SkipAllAnswers() != nil || p.SkipAllAuthorities() != nil {
return errServerTemporarilyMisbehaving
}
var haveOPT bool
for {
rh, err := p.AdditionalHeader()
if err == dnsmessage.ErrSectionDone {
break
} else if err != nil {
return errServerTemporarilyMisbehaving
}
if rh.Type != dnsmessage.TypeOPT {
p.SkipAdditional()
continue
}
// Only one OPT record is allowed. With multiple we MUST return an error. See RFC
// 6891, section 6.1.1, page 6, last paragraph.
if haveOPT {
return errInvalidDNSResponse
}
haveOPT = true
opt, err := p.OPTResource()
if err != nil {
return errInvalidDNSResponse
}
for _, o := range opt.Options {
if o.Code == 15 {
if len(o.Data) < 2 {
return errInvalidDNSResponse
}
infoCode := ErrorCode(uint16(o.Data[0])<<8 | uint16(o.Data[1]<<0))
extraText := string(bytes.TrimRight(o.Data[2:], "\u0000"))
return ExtendedError{infoCode, extraText}
}
}
}
return errServerTemporarilyMisbehaving
}
return errServerMisbehaving
}
return nil
}
func skipToAnswer(p *dnsmessage.Parser, qtype dnsmessage.Type) error {
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
return errNoSuchHost
}
if err != nil {
return errCannotUnmarshalDNSMessage
}
if h.Type == qtype {
return nil
}
if err := p.SkipAnswer(); err != nil {
return errCannotUnmarshalDNSMessage
}
}
}
// Do a lookup for a single name, which must be rooted
// (otherwise answer will not find the answers).
func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, Result, error) {
var lastErr error
var lastResult Result
serverOffset := cfg.serverOffset()
sLen := uint32(len(cfg.servers))
n, err := dnsmessage.NewName(name)
if err != nil {
return dnsmessage.Parser{}, "", Result{}, errCannotMarshalDNSMessage
}
q := dnsmessage.Question{
Name: n,
Type: qtype,
Class: dnsmessage.ClassINET,
}
for i := 0; i < cfg.attempts; i++ {
for j := uint32(0); j < sLen; j++ {
server := cfg.servers[(serverOffset+j)%sLen]
p, h, err := r.exchange(ctx, server, q, cfg.timeout, cfg.useTCP, cfg.trustAD)
if err != nil {
dnsErr := &DNSError{
Err: err.Error(),
Name: name,
Server: server,
}
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
dnsErr.IsTimeout = true
}
// Set IsTemporary for socket-level errors. Note that this flag
// may also be used to indicate a SERVFAIL response.
if _, ok := err.(*net.OpError); ok {
dnsErr.IsTemporary = true
}
lastErr = dnsErr
lastResult = Result{}
continue
}
useAD := h.RCode == dnsmessage.RCodeSuccess || h.RCode == dnsmessage.RCodeNameError
result := Result{Authentic: cfg.trustAD && h.AuthenticData && useAD}
if err := checkHeader(&p, h); err != nil {
dnsErr := &DNSError{
Underlying: err,
Err: err.Error(),
Name: name,
Server: server,
}
if err == errServerTemporarilyMisbehaving {
dnsErr.IsTemporary = true
} else if edeErr, isEDE := err.(ExtendedError); isEDE && edeErr.IsTemporary() {
dnsErr.IsTemporary = true
} else if isEDE {
// Something wrong with the zone, no point asking another server or retrying.
return p, server, result, dnsErr
}
if err == errNoSuchHost {
// The name does not exist, so trying
// another server won't help.
dnsErr.IsNotFound = true
return p, server, result, dnsErr
}
lastErr = dnsErr
lastResult = result
continue
}
err = skipToAnswer(&p, qtype)
if err == nil {
return p, server, result, nil
}
lastErr = &DNSError{
Err: err.Error(),
Name: name,
Server: server,
}
lastResult = result
if err == errNoSuchHost {
// The name does not exist, so trying another
// server won't help.
lastErr.(*DNSError).IsNotFound = true
return p, server, lastResult, lastErr
}
}
}
return dnsmessage.Parser{}, "", lastResult, lastErr
}
// A resolverConfig represents a DNS stub resolver configuration.
type resolverConfig struct {
initOnce sync.Once // guards init of resolverConfig
// ch is used as a semaphore that only allows one lookup at a
// time to recheck resolv.conf.
ch chan struct{} // guards lastChecked and modTime
lastChecked time.Time // last time resolv.conf was checked
dnsConfig atomic.Pointer[dnsConfig] // parsed resolv.conf structure used in lookups
}
var resolvConf resolverConfig
func getSystemDNSConfig() *dnsConfig {
resolvConf.tryUpdate("/etc/resolv.conf")
return resolvConf.dnsConfig.Load()
}
// init initializes conf and is only called via conf.initOnce.
func (conf *resolverConfig) init() {
// Set dnsConfig and lastChecked so we don't parse
// resolv.conf twice the first time.
conf.dnsConfig.Store(dnsReadConfig("/etc/resolv.conf"))
conf.lastChecked = time.Now()
// Prepare ch so that only one update of resolverConfig may
// run at once.
conf.ch = make(chan struct{}, 1)
}
// tryUpdate tries to update conf with the named resolv.conf file.
// The name variable only exists for testing. It is otherwise always
// "/etc/resolv.conf".
func (conf *resolverConfig) tryUpdate(name string) {
conf.initOnce.Do(conf.init)
if conf.dnsConfig.Load().noReload {
return
}
// Ensure only one update at a time checks resolv.conf.
if !conf.tryAcquireSema() {
return
}
defer conf.releaseSema()
now := time.Now()
if conf.lastChecked.After(now.Add(-5 * time.Second)) {
return
}
conf.lastChecked = now
switch runtime.GOOS {
case "windows":
// There's no file on disk, so don't bother checking
// and failing.
//
// The Windows implementation of dnsReadConfig (called
// below) ignores the name.
default:
var mtime time.Time
if fi, err := os.Stat(name); err == nil {
mtime = fi.ModTime()
}
if mtime.Equal(conf.dnsConfig.Load().mtime) {
return
}
}
dnsConf := dnsReadConfig(name)
conf.dnsConfig.Store(dnsConf)
}
func (conf *resolverConfig) tryAcquireSema() bool {
select {
case conf.ch <- struct{}{}:
return true
default:
return false
}
}
func (conf *resolverConfig) releaseSema() {
<-conf.ch
}
func (r *Resolver) lookup(ctx context.Context, name string, qtype dnsmessage.Type, conf *dnsConfig) (dnsmessage.Parser, string, Result, error) {
if !isDomainName(name) {
// We used to use "invalid domain name" as the error,
// but that is a detail of the specific lookup mechanism.
// Other lookups might allow broader name syntax
// (for example Multicast DNS allows UTF-8; see RFC 6762).
// For consistency with libc resolvers, report no such host.
return dnsmessage.Parser{}, "", Result{}, &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true}
}
if conf == nil {
conf = getSystemDNSConfig()
}
var (
p dnsmessage.Parser
server string
result Result
err error
)
for _, fqdn := range conf.nameList(name) {
p, server, result, err = r.tryOneName(ctx, conf, fqdn, qtype)
if err == nil {
break
}
var edeErr ExtendedError
if nerr, ok := err.(net.Error); ok && nerr.Temporary() && r.strictErrors() || errors.As(err, &edeErr) && !edeErr.IsTemporary() {
// If we hit a temporary error with StrictErrors enabled,
// stop immediately instead of trying more names.
break
}
}
if err == nil {
return p, server, result, nil
}
if err, ok := err.(*DNSError); ok {
// Show original name passed to lookup, not suffixed one.
// In general we might have tried many suffixes; showing
// just one is misleading. See also golang.org/issue/6324.
err.Name = name
}
return dnsmessage.Parser{}, "", result, err
}
// avoidDNS reports whether this is a hostname for which we should not
// use DNS. Currently this includes only .onion, per RFC 7686. See
// golang.org/issue/13705. Does not cover .local names (RFC 6762),
// see golang.org/issue/16739.
func avoidDNS(name string) bool {
if name == "" {
return true
}
if name[len(name)-1] == '.' {
name = name[:len(name)-1]
}
return stringsHasSuffixFold(name, ".onion")
}
// nameList returns a list of names for sequential DNS queries.
func (conf *dnsConfig) nameList(name string) []string {
if avoidDNS(name) {
return nil
}
// Check name length (see isDomainName).
l := len(name)
rooted := l > 0 && name[l-1] == '.'
if l > 254 || l == 254 && !rooted {
return nil
}
// If name is rooted (trailing dot), try only that name.
if rooted {
return []string{name}
}
hasNdots := count(name, '.') >= conf.ndots
name += "."
l++
// Build list of search choices.
names := make([]string, 0, 1+len(conf.search))
// If name has enough dots, try unsuffixed first.
if hasNdots {
names = append(names, name)
}
// Try suffixes that are not too long (see isDomainName).
for _, suffix := range conf.search {
if l+len(suffix) <= 254 {
names = append(names, name+suffix)
}
}
// Try unsuffixed, if not tried first above.
if !hasNdots {
names = append(names, name)
}
return names
}
// hostLookupOrder specifies the order of LookupHost lookup strategies.
// It is basically a simplified representation of nsswitch.conf.
// "files" means /etc/hosts.
type hostLookupOrder int
const (
// hostLookupCgo means defer to cgo.
hostLookupCgo hostLookupOrder = iota
hostLookupFilesDNS // files first
hostLookupDNSFiles // dns first
hostLookupFiles // only files
hostLookupDNS // only DNS
)
var lookupOrderName = map[hostLookupOrder]string{
hostLookupCgo: "cgo",
hostLookupFilesDNS: "files,dns",
hostLookupDNSFiles: "dns,files",
hostLookupFiles: "files",
hostLookupDNS: "dns",
}
func (o hostLookupOrder) String() string {
if s, ok := lookupOrderName[o]; ok {
return s
}
return "hostLookupOrder=" + itoa.Itoa(int(o)) + "??"
}
func (r *Resolver) goLookupHostOrder(ctx context.Context, name string, order hostLookupOrder, conf *dnsConfig) (addrs []string, result Result, err error) {
if order == hostLookupFilesDNS || order == hostLookupFiles {
// Use entries from /etc/hosts if they match.
addrs, _ = lookupStaticHost(name)
if len(addrs) > 0 {
return
}
if order == hostLookupFiles {
return nil, result, &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true}
}
}
ips, _, result, err := r.goLookupIPCNAMEOrder(ctx, "ip", name, order, conf)
if err != nil {
return
}
addrs = make([]string, 0, len(ips))
for _, ip := range ips {
addrs = append(addrs, ip.String())
}
return
}
// lookup entries from /etc/hosts
func goLookupIPFiles(name string) (addrs []net.IPAddr, canonical string) {
addr, canonical := lookupStaticHost(name)
for _, haddr := range addr {
xhaddr, zone := splitHostZone(haddr)
if ip := net.ParseIP(xhaddr); ip != nil {
addr := net.IPAddr{IP: ip, Zone: zone}
addrs = append(addrs, addr)
}
}
sortByRFC6724(addrs)
return addrs, canonical
}
// goLookupIP is the native Go implementation of LookupIP.
// The libc versions are in cgo_*.go.
func (r *Resolver) goLookupIP(ctx context.Context, network, host string) (addrs []net.IPAddr, result Result, err error) {
order, conf := systemConf().hostLookupOrder(r, host)
addrs, _, result, err = r.goLookupIPCNAMEOrder(ctx, network, host, order, conf)
return
}
func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name string, order hostLookupOrder, conf *dnsConfig) (addrs []net.IPAddr, cname dnsmessage.Name, result Result, err error) {
if order == hostLookupFilesDNS || order == hostLookupFiles {
var canonical string
addrs, canonical = goLookupIPFiles(name)
if len(addrs) > 0 {
var err error
cname, err = dnsmessage.NewName(canonical)
if err != nil {
return nil, dnsmessage.Name{}, result, err
}
return addrs, cname, result, nil
}
if order == hostLookupFiles {
return nil, dnsmessage.Name{}, result, &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true}
}
}
if !isDomainName(name) {
// See comment in func lookup above about use of errNoSuchHost.
return nil, dnsmessage.Name{}, result, &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true}
}
type result0 struct {
p dnsmessage.Parser
server string
result Result
error
}
if conf == nil {
conf = getSystemDNSConfig()
}
lane := make(chan result0, 1)
qtypes := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
if network == "CNAME" {
qtypes = append(qtypes, dnsmessage.TypeCNAME)
}
switch ipVersion(network) {
case '4':
qtypes = []dnsmessage.Type{dnsmessage.TypeA}
case '6':
qtypes = []dnsmessage.Type{dnsmessage.TypeAAAA}
}
var queryFn func(fqdn string, qtype dnsmessage.Type)
var responseFn func(fqdn string, qtype dnsmessage.Type) result0
if conf.singleRequest {
queryFn = func(fqdn string, qtype dnsmessage.Type) {}
responseFn = func(fqdn string, qtype dnsmessage.Type) result0 {
dnsWaitGroup.Add(1)
defer dnsWaitGroup.Done()
p, server, res, err := r.tryOneName(ctx, conf, fqdn, qtype)
return result0{p, server, res, err}
}
} else {
queryFn = func(fqdn string, qtype dnsmessage.Type) {
dnsWaitGroup.Add(1)
go func(qtype dnsmessage.Type) {
p, server, res, err := r.tryOneName(ctx, conf, fqdn, qtype)
lane <- result0{p, server, res, err}
dnsWaitGroup.Done()
}(qtype)
}
responseFn = func(fqdn string, qtype dnsmessage.Type) result0 {
return <-lane
}
}
var lastErr error
var lastResult Result
for _, fqdn := range conf.nameList(name) {
for _, qtype := range qtypes {
queryFn(fqdn, qtype)
}
hitStrictError := false
for _, qtype := range qtypes {
result0 := responseFn(fqdn, qtype)
if result0.error != nil {
if nerr, ok := result0.error.(net.Error); ok && nerr.Temporary() && r.strictErrors() {
// This error will abort the nameList loop.
hitStrictError = true
lastErr = result0.error
lastResult = result0.result
} else if lastErr == nil || fqdn == name+"." {
// Prefer error for original name.
lastErr = result0.error
lastResult = result0.result
}
continue
}
result = result0.result
// Presotto says it's okay to assume that servers listed in
// /etc/resolv.conf are recursive resolvers.
//
// We asked for recursion, so it should have included all the
// answers we need in this one packet.
//
// Further, RFC 1034 section 4.3.1 says that "the recursive
// response to a query will be... The answer to the query,
// possibly preface by one or more CNAME RRs that specify
// aliases encountered on the way to an answer."
//
// Therefore, we should be able to assume that we can ignore
// CNAMEs and that the A and AAAA records we requested are
// for the canonical name.
loop:
for {
h, err := result0.p.AnswerHeader()
if err != nil && err != dnsmessage.ErrSectionDone {
lastErr = &DNSError{
Err: "cannot marshal DNS message",
Name: name,
Server: result0.server,
}
}
if err != nil {
break
}
switch h.Type {
case dnsmessage.TypeA:
a, err := result0.p.AResource()
if err != nil {
lastErr = &DNSError{
Err: "cannot marshal DNS message",
Name: name,
Server: result0.server,
}
break loop
}
addrs = append(addrs, net.IPAddr{IP: net.IP(a.A[:])})
if cname.Length == 0 && h.Name.Length != 0 {
cname = h.Name
}
case dnsmessage.TypeAAAA:
aaaa, err := result0.p.AAAAResource()
if err != nil {
lastErr = &DNSError{
Err: "cannot marshal DNS message",
Name: name,
Server: result0.server,
}
break loop
}
addrs = append(addrs, net.IPAddr{IP: net.IP(aaaa.AAAA[:])})
if cname.Length == 0 && h.Name.Length != 0 {
cname = h.Name
}
case dnsmessage.TypeCNAME:
c, err := result0.p.CNAMEResource()
if err != nil {
lastErr = &DNSError{
Err: "cannot marshal DNS message",
Name: name,
Server: result0.server,
}
break loop
}
if cname.Length == 0 && c.CNAME.Length > 0 {
cname = c.CNAME
}
default:
if err := result0.p.SkipAnswer(); err != nil {
lastErr = &DNSError{
Err: "cannot marshal DNS message",
Name: name,
Server: result0.server,
}
break loop
}
continue
}
}
}
if hitStrictError {
// If either family hit an error with StrictErrors enabled,
// discard all addresses. This ensures that network flakiness
// cannot turn a dualstack hostname IPv4/IPv6-only.
addrs = nil
break
}
if len(addrs) > 0 || network == "CNAME" && cname.Length > 0 {
break
}
}
if lastErr, ok := lastErr.(*DNSError); ok {
// Show original name passed to lookup, not suffixed one.
// In general we might have tried many suffixes; showing
// just one is misleading. See also golang.org/issue/6324.
lastErr.Name = name
}
sortByRFC6724(addrs)
if len(addrs) == 0 && !(network == "CNAME" && cname.Length > 0) {
if order == hostLookupDNSFiles {
var canonical string
addrs, canonical = goLookupIPFiles(name)
if len(addrs) > 0 {
var err error
cname, err = dnsmessage.NewName(canonical)
if err != nil {
return nil, dnsmessage.Name{}, result, err
}
return addrs, cname, result, nil
}
}
if lastErr != nil {
return nil, dnsmessage.Name{}, lastResult, lastErr
}
}
return addrs, cname, result, nil
}
// goLookupCNAME is the native Go (non-cgo) implementation of LookupCNAME.
func (r *Resolver) goLookupCNAME(ctx context.Context, host string, order hostLookupOrder, conf *dnsConfig) (string, Result, error) {
_, cname, result, err := r.goLookupIPCNAMEOrder(ctx, "CNAME", host, order, conf)
return cname.String(), result, err
}
// goLookupPTR is the native Go implementation of LookupAddr.
func (r *Resolver) goLookupPTR(ctx context.Context, addr string, order hostLookupOrder, conf *dnsConfig) ([]string, Result, error) {
if order == hostLookupFiles || order == hostLookupFilesDNS {
names := lookupStaticAddr(addr)
if len(names) > 0 {
return names, Result{}, nil
}
if order == hostLookupFiles {
return nil, Result{}, &DNSError{Err: errNoSuchHost.Error(), Name: addr, IsNotFound: true}
}
}
arpa, err := reverseaddr(addr)
if err != nil {
return nil, Result{}, err
}
p, server, result, err := r.lookup(ctx, arpa, dnsmessage.TypePTR, conf)
if err != nil {
var dnsErr *DNSError
if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
if order == hostLookupDNSFiles {
names := lookupStaticAddr(addr)
if len(names) > 0 {
return names, result, nil
}
}
}
return nil, result, err
}
var ptrs []string
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return nil, result, &DNSError{
Err: "cannot marshal DNS message",
Name: addr,
Server: server,
}
}
if h.Type != dnsmessage.TypePTR {
err := p.SkipAnswer()
if err != nil {
return nil, result, &DNSError{
Err: "cannot marshal DNS message",
Name: addr,
Server: server,
}
}
continue
}
ptr, err := p.PTRResource()
if err != nil {
return nil, result, &DNSError{
Err: "cannot marshal DNS message",
Name: addr,
Server: server,
}
}
ptrs = append(ptrs, ptr.PTR.String())
}
return ptrs, result, nil
}

45
vendor/github.com/mjl-/adns/dnsconfig.go generated vendored Normal file
View file

@ -0,0 +1,45 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
"os"
"sync/atomic"
"time"
)
var (
defaultNS = []string{"127.0.0.1:53", "[::1]:53"}
getHostname = os.Hostname // variable for testing
)
type dnsConfig struct {
servers []string // server addresses (in host:port form) to use
search []string // rooted suffixes to append to local name
ndots int // number of dots in name to trigger absolute lookup
timeout time.Duration // wait before giving up on a query, including retries
attempts int // lost packets before giving up on server
rotate bool // round robin among servers
unknownOpt bool // anything unknown was encountered
lookup []string // OpenBSD top-level database "lookup" order
err error // any error that occurs during open of resolv.conf
mtime time.Time // time of resolv.conf modification
soffset uint32 // used by serverOffset
singleRequest bool // use sequential A and AAAA queries instead of parallel queries
useTCP bool // force usage of TCP for DNS resolutions
trustAD bool // add AD flag to queries
noReload bool // do not check for config file updates
}
// serverOffset returns an offset that can be used to determine
// indices of servers in c.servers when making queries.
// When the rotate option is enabled, this offset increases.
// Otherwise it is always 0.
func (c *dnsConfig) serverOffset() uint32 {
if c.rotate {
return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start
}
return 0
}

188
vendor/github.com/mjl-/adns/dnsconfig_unix.go generated vendored Normal file
View file

@ -0,0 +1,188 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !js && !windows
// Read system DNS config from /etc/resolv.conf
package adns
import (
"net"
"net/netip"
"time"
"github.com/mjl-/adns/internal/bytealg"
)
// See resolv.conf(5) on a Linux machine.
func dnsReadConfig(filename string) *dnsConfig {
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
}
file, err := open(filename)
if err != nil {
conf.servers = defaultNS
conf.trustAD = true
conf.search = dnsDefaultSearch()
conf.err = err
return conf
}
defer file.close()
if fi, err := file.file.Stat(); err == nil {
conf.mtime = fi.ModTime()
} else {
conf.servers = defaultNS
conf.trustAD = true
conf.search = dnsDefaultSearch()
conf.err = err
return conf
}
for line, ok := file.readLine(); ok; line, ok = file.readLine() {
if len(line) > 0 && (line[0] == ';' || line[0] == '#') {
// comment.
continue
}
f := getFields(line)
if len(f) < 1 {
continue
}
switch f[0] {
case "nameserver": // add one name server
if len(f) > 1 && len(conf.servers) < 3 { // small, but the standard limit
// One more check: make sure server name is
// just an IP address. Otherwise we need DNS
// to look it up.
if _, err := netip.ParseAddr(f[1]); err == nil {
conf.servers = append(conf.servers, JoinHostPort(f[1], "53"))
}
}
case "domain": // set search path to just this domain
if len(f) > 1 {
conf.search = []string{ensureRooted(f[1])}
}
case "search": // set search path to given servers
conf.search = make([]string, 0, len(f)-1)
for i := 1; i < len(f); i++ {
name := ensureRooted(f[i])
if name == "." {
continue
}
conf.search = append(conf.search, name)
}
case "options": // magic options
for _, s := range f[1:] {
switch {
case hasPrefix(s, "ndots:"):
n, _, _ := dtoi(s[6:])
if n < 0 {
n = 0
} else if n > 15 {
n = 15
}
conf.ndots = n
case hasPrefix(s, "timeout:"):
n, _, _ := dtoi(s[8:])
if n < 1 {
n = 1
}
conf.timeout = time.Duration(n) * time.Second
case hasPrefix(s, "attempts:"):
n, _, _ := dtoi(s[9:])
if n < 1 {
n = 1
}
conf.attempts = n
case s == "rotate":
conf.rotate = true
case s == "single-request" || s == "single-request-reopen":
// Linux option:
// http://man7.org/linux/man-pages/man5/resolv.conf.5.html
// "By default, glibc performs IPv4 and IPv6 lookups in parallel [...]
// This option disables the behavior and makes glibc
// perform the IPv6 and IPv4 requests sequentially."
conf.singleRequest = true
case s == "use-vc" || s == "usevc" || s == "tcp":
// Linux (use-vc), FreeBSD (usevc) and OpenBSD (tcp) option:
// http://man7.org/linux/man-pages/man5/resolv.conf.5.html
// "Sets RES_USEVC in _res.options.
// This option forces the use of TCP for DNS resolutions."
// https://www.freebsd.org/cgi/man.cgi?query=resolv.conf&sektion=5&manpath=freebsd-release-ports
// https://man.openbsd.org/resolv.conf.5
conf.useTCP = true
case s == "trust-ad":
conf.trustAD = true
case s == "edns0":
// We use EDNS by default.
// Ignore this option.
case s == "no-reload":
conf.noReload = true
default:
conf.unknownOpt = true
}
}
case "lookup":
// OpenBSD option:
// https://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man5/resolv.conf.5
// "the legal space-separated values are: bind, file, yp"
conf.lookup = f[1:]
default:
conf.unknownOpt = true
}
}
if len(conf.servers) == 0 {
conf.servers = defaultNS
}
if !conf.trustAD {
// If we only have name servers on loopback IP, we trust them.
// As mentioned in RFC 6698, section A.3 point 2 (line 1693).
conf.trustAD = true
for _, addr := range conf.servers {
host, _, err := net.SplitHostPort(addr)
if err != nil {
conf.trustAD = false
break
}
ip := net.ParseIP(host)
if ip == nil || !ip.IsLoopback() {
conf.trustAD = false
break
}
}
}
if len(conf.search) == 0 {
conf.search = dnsDefaultSearch()
}
return conf
}
func dnsDefaultSearch() []string {
hn, err := getHostname()
if err != nil {
// best effort
return nil
}
if i := bytealg.IndexByteString(hn, '.'); i >= 0 && i < len(hn)-1 {
return []string{ensureRooted(hn[i+1:])}
}
return nil
}
func hasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
func ensureRooted(s string) string {
if len(s) > 0 && s[len(s)-1] == '.' {
return s
}
return s + "."
}

20
vendor/github.com/mjl-/adns/doc.go generated vendored Normal file
View file

@ -0,0 +1,20 @@
/*
adns is a copy of the Go standard library, modified to provide details about
the DNSSEC status of responses.
The MX, NS, SRV types from the "net" package are used to make to prevent churn
when switching from net to adns.
Modifications
- Each Lookup* also returns a Result with the "Authentic" field representing if
the response had the "authentic data" bit (and is trusted), i.e. was
DNSSEC-signed according to the recursive resolver.
- Resolver are also trusted if all name servers have loopback IPs. Resolvers
are still also trusted if /etc/resolv.conf has "trust-ad" in the "options".
- New function LookupTLSA, to support DANE which uses DNS records of type TLSA.
- Support Extended DNS Errors (EDE) for details about DNSSEC errors.
- adns uses its own DNSError type, with an additional "Underlying error" field
and Unwrap function, so callers can check for the new ExtendedError type.
*/
package adns

168
vendor/github.com/mjl-/adns/ede.go generated vendored Normal file
View file

@ -0,0 +1,168 @@
package adns
import (
"fmt"
)
// ExtendedError is an RFC 8914 Extended DNS Error (EDE).
type ExtendedError struct {
InfoCode ErrorCode
ExtraText string // Human-readable error message, optional.
}
// IsTemporary indicates whether an error is a temporary server error, and
// retries might give a different result.
func (e ExtendedError) IsTemporary() bool {
return e.InfoCode.IsTemporary()
}
// Unwrap returns the underlying ErrorCode error.
func (e ExtendedError) Unwrap() error {
return e.InfoCode
}
// Error returns a string representing the InfoCode, and either the extra text or
// more details for the info code.
func (e ExtendedError) Error() string {
s := e.InfoCode.Error()
if e.ExtraText != "" {
return s + ": " + e.ExtraText
}
if int(e.InfoCode) >= len(errorCodeDetails) {
return s
}
return s + ": " + errorCodeDetails[e.InfoCode]
}
// ErrorCode is an InfoCode from Extended DNS Errors, RFC 8914.
type ErrorCode uint16
const (
ErrOtherErrorCode ErrorCode = 0
ErrUnsupportedDNSKEYAlgorithm ErrorCode = 1
ErrUnsupportedDSDigestType ErrorCode = 2
ErrStaleAnswer ErrorCode = 3
ErrForgedAnswer ErrorCode = 4
ErrDNSSECIndeterminate ErrorCode = 5
ErrDNSSECBogus ErrorCode = 6
ErrSignatureExpired ErrorCode = 7
ErrSignatureNotYetValid ErrorCode = 8
ErrDNSKEYMissing ErrorCode = 9
ErrRRSIGMissing ErrorCode = 10
ErrNoZoneKeyBitSet ErrorCode = 11
ErrNSECMissing ErrorCode = 12
ErrCachedError ErrorCode = 13
ErrNotReady ErrorCode = 14
ErrBlocked ErrorCode = 15
ErrCensored ErrorCode = 16
ErrFiltered ErrorCode = 17
ErrProhibited ErrorCode = 18
ErrStaleNXDOMAINAnswer ErrorCode = 19
ErrNotAuthoritative ErrorCode = 20
ErrNotSupported ErrorCode = 21
ErrNoReachableAuthority ErrorCode = 22
ErrNetworkError ErrorCode = 23
ErrInvalidData ErrorCode = 24
)
// IsTemporary returns whether the error is temporary and has a chance of
// succeeding on a retry.
func (e ErrorCode) IsTemporary() bool {
switch e {
case ErrOtherErrorCode,
ErrStaleAnswer,
ErrCachedError,
ErrNotReady,
ErrStaleNXDOMAINAnswer,
ErrNoReachableAuthority,
ErrNetworkError:
return true
}
return false
}
// IsAuthentication returns whether the error is related to authentication,
// e.g. bogus DNSSEC, missing DS/DNSKEY/RRSIG records, etc, or an other
// DNSSEC-related error.
func (e ErrorCode) IsAuthentication() bool {
switch e {
case ErrUnsupportedDNSKEYAlgorithm,
ErrUnsupportedDSDigestType,
ErrDNSSECIndeterminate,
ErrDNSSECBogus,
ErrSignatureExpired,
ErrSignatureNotYetValid,
ErrDNSKEYMissing,
ErrRRSIGMissing,
ErrNoZoneKeyBitSet,
ErrNSECMissing:
return true
}
return false
}
// Error includes a human-readable short string for the info code.
func (e ErrorCode) Error() string {
if int(e) >= len(errorCodeStrings) {
return fmt.Sprintf("unknown error code from name server: %d", e)
}
return fmt.Sprintf("error from name server: %s", errorCodeStrings[e])
}
// short strings, always included in error messages.
var errorCodeStrings = []string{
"other",
"unsupported dnskey algorithm",
"unsupported ds digest type",
"stale answer",
"forged answer",
"dnssec indeterminate",
"dnssec bogus",
"signature expired",
"signature not yet valid",
"dnskey missing",
"rrsigs missing",
"no zone key bit set",
"nsec missing",
"cached error",
"not ready",
"blocked",
"censored",
"filtered",
"prohibited",
"stale nxdomain answer",
"not authoritative",
"not supported",
"no reachable authority",
"network error",
"invalid data",
}
// more detailed string, only included if there is no detail text in the response.
var errorCodeDetails = []string{
"unspecified error",
"only found unsupported algorithms in DNSKEY records",
"only found unsupported types in DS records",
"unable to resolve within deadline, stale data served",
"answer was forged for policy reason",
"dnssec validation ended in interderminate state",
"dnssec validation ended in bogus status",
"only expired dnssec signatures found",
"only signatures found that are not yet valid",
"ds key exists at a parent, but no supported matching dnskey found",
"dnssec validation attempted, but no rrsig found",
"no zone key bit found in a dnskey",
"dnssec validation found missing data without nsec/nsec3 record",
"failure served from cache",
"not yet fully functional to resolve query",
"domain is on blocklist due to internal security policy",
"domain is on blocklist due to external entity",
"domain is on client-requested blocklist",
"refusing to serve request",
"stale nxdomain served from cache",
"unexpected authoritativeness of query",
"query or operation not supported",
"no authoritative name server could be reached",
"unrecoverable network error",
"zone data not valid",
}

22
vendor/github.com/mjl-/adns/hook.go generated vendored Normal file
View file

@ -0,0 +1,22 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
"context"
"net"
)
var (
testHookHostsPath = "/etc/hosts"
testHookLookupIP = func(
ctx context.Context,
fn func(context.Context, string, string) ([]net.IPAddr, Result, error),
network string,
host string,
) ([]net.IPAddr, Result, error) {
return fn(ctx, network, host)
}
)

166
vendor/github.com/mjl-/adns/hosts.go generated vendored Normal file
View file

@ -0,0 +1,166 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
"errors"
"io/fs"
"net/netip"
"sync"
"time"
"github.com/mjl-/adns/internal/bytealg"
)
const cacheMaxAge = 5 * time.Second
func parseLiteralIP(addr string) string {
ip, err := netip.ParseAddr(addr)
if err != nil {
return ""
}
return ip.String()
}
type byName struct {
addrs []string
canonicalName string
}
// hosts contains known host entries.
var hosts struct {
sync.Mutex
// Key for the list of literal IP addresses must be a host
// name. It would be part of DNS labels, a FQDN or an absolute
// FQDN.
// For now the key is converted to lower case for convenience.
byName map[string]byName
// Key for the list of host names must be a literal IP address
// including IPv6 address with zone identifier.
// We don't support old-classful IP address notation.
byAddr map[string][]string
expire time.Time
path string
mtime time.Time
size int64
}
func readHosts() {
now := time.Now()
hp := testHookHostsPath
if now.Before(hosts.expire) && hosts.path == hp && len(hosts.byName) > 0 {
return
}
mtime, size, err := stat(hp)
if err == nil && hosts.path == hp && hosts.mtime.Equal(mtime) && hosts.size == size {
hosts.expire = now.Add(cacheMaxAge)
return
}
hs := make(map[string]byName)
is := make(map[string][]string)
file, err := open(hp)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrPermission) {
return
}
}
if file != nil {
defer file.close()
for line, ok := file.readLine(); ok; line, ok = file.readLine() {
if i := bytealg.IndexByteString(line, '#'); i >= 0 {
// Discard comments.
line = line[0:i]
}
f := getFields(line)
if len(f) < 2 {
continue
}
addr := parseLiteralIP(f[0])
if addr == "" {
continue
}
var canonical string
for i := 1; i < len(f); i++ {
name := absDomainName(f[i])
h := []byte(f[i])
lowerASCIIBytes(h)
key := absDomainName(string(h))
if i == 1 {
canonical = key
}
is[addr] = append(is[addr], name)
if v, ok := hs[key]; ok {
hs[key] = byName{
addrs: append(v.addrs, addr),
canonicalName: v.canonicalName,
}
continue
}
hs[key] = byName{
addrs: []string{addr},
canonicalName: canonical,
}
}
}
}
// Update the data cache.
hosts.expire = now.Add(cacheMaxAge)
hosts.path = hp
hosts.byName = hs
hosts.byAddr = is
hosts.mtime = mtime
hosts.size = size
}
// lookupStaticHost looks up the addresses and the canonical name for the given host from /etc/hosts.
func lookupStaticHost(host string) ([]string, string) {
hosts.Lock()
defer hosts.Unlock()
readHosts()
if len(hosts.byName) != 0 {
if hasUpperCase(host) {
lowerHost := []byte(host)
lowerASCIIBytes(lowerHost)
host = string(lowerHost)
}
if byName, ok := hosts.byName[absDomainName(host)]; ok {
ipsCp := make([]string, len(byName.addrs))
copy(ipsCp, byName.addrs)
return ipsCp, byName.canonicalName
}
}
return nil, ""
}
// lookupStaticAddr looks up the hosts for the given address from /etc/hosts.
func lookupStaticAddr(addr string) []string {
hosts.Lock()
defer hosts.Unlock()
readHosts()
addr = parseLiteralIP(addr)
if addr == "" {
return nil
}
if len(hosts.byAddr) != 0 {
if hosts, ok := hosts.byAddr[addr]; ok {
hostsCp := make([]string, len(hosts))
copy(hostsCp, hosts)
return hostsCp
}
}
return nil
}

View file

@ -0,0 +1,9 @@
package bytealg
import (
"strings"
)
func IndexByteString(s string, b byte) int {
return strings.IndexByte(s, b)
}

33
vendor/github.com/mjl-/adns/internal/itoa/itoa.go generated vendored Normal file
View file

@ -0,0 +1,33 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Simple conversions to avoid depending on strconv.
package itoa
// Itoa converts val to a decimal string.
func Itoa(val int) string {
if val < 0 {
return "-" + Uitoa(uint(-val))
}
return Uitoa(uint(val))
}
// Uitoa converts val to a decimal string.
func Uitoa(val uint) string {
if val == 0 { // avoid string allocation
return "0"
}
var buf [20]byte // big enough for 64bit value base 10
i := len(buf) - 1
for val >= 10 {
q := val / 10
buf[i] = byte('0' + val - q*10)
i--
val = q
}
// val < 10
buf[i] = byte('0' + val)
return string(buf[i:])
}

View file

@ -0,0 +1,123 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package singleflight provides a duplicate function call suppression
// mechanism.
package singleflight
import "sync"
// call is an in-flight or completed singleflight.Do call
type call struct {
wg sync.WaitGroup
// These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
val any
err error
// These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done.
dups int
chans []chan<- Result
}
// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
// Result holds the results of Do, so they can be passed
// on a channel.
type Result struct {
Val any
Err error
Shared bool
}
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
func (g *Group) Do(key string, fn func() (any, error)) (v any, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err, true
}
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}
// DoChan is like Do but returns a channel that will receive the
// results when they are ready.
func (g *Group) DoChan(key string, fn func() (any, error)) <-chan Result {
ch := make(chan Result, 1)
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
c.chans = append(c.chans, ch)
g.mu.Unlock()
return ch
}
c := &call{chans: []chan<- Result{ch}}
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
go g.doCall(c, key, fn)
return ch
}
// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (any, error)) {
c.val, c.err = fn()
g.mu.Lock()
c.wg.Done()
if g.m[key] == c {
delete(g.m, key)
}
for _, ch := range c.chans {
ch <- Result{c.val, c.err, c.dups > 0}
}
g.mu.Unlock()
}
// ForgetUnshared tells the singleflight to forget about a key if it is not
// shared with any other goroutines. Future calls to Do for a forgotten key
// will call the function rather than waiting for an earlier call to complete.
// Returns whether the key was forgotten or unknown--that is, whether no
// other goroutines are waiting for the result.
func (g *Group) ForgetUnshared(key string) bool {
g.mu.Lock()
defer g.mu.Unlock()
c, ok := g.m[key]
if !ok {
return true
}
if c.dups == 0 {
delete(g.m, key)
return true
}
return false
}

205
vendor/github.com/mjl-/adns/ipsock.go generated vendored Normal file
View file

@ -0,0 +1,205 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
"context"
"net"
"github.com/mjl-/adns/internal/bytealg"
)
// BUG(rsc,mikio): On DragonFly BSD and OpenBSD, listening on the
// "tcp" and "udp" networks does not listen for both IPv4 and IPv6
// connections. This is due to the fact that IPv4 traffic will not be
// routed to an IPv6 socket - two separate sockets are required if
// both address families are to be supported.
// See inet6(4) for details.
// An addrList represents a list of network endpoint addresses.
type addrList []net.Addr
// filterAddrList applies a filter to a list of IP addresses,
// yielding a list of Addr objects. Known filters are nil, ipv4only,
// and ipv6only. It returns every address when the filter is nil.
// The result contains at least one address when error is nil.
func filterAddrList(filter func(net.IPAddr) bool, ips []net.IPAddr, inetaddr func(net.IPAddr) net.Addr, originalAddr string) (addrList, error) {
var addrs addrList
for _, ip := range ips {
if filter == nil || filter(ip) {
addrs = append(addrs, inetaddr(ip))
}
}
if len(addrs) == 0 {
return nil, &net.AddrError{Err: errNoSuitableAddress.Error(), Addr: originalAddr}
}
return addrs, nil
}
// ipv4only reports whether addr is an IPv4 address.
func ipv4only(addr net.IPAddr) bool {
return addr.IP.To4() != nil
}
// ipv6only reports whether addr is an IPv6 address except IPv4-mapped IPv6 address.
func ipv6only(addr net.IPAddr) bool {
return len(addr.IP) == net.IPv6len && addr.IP.To4() == nil
}
// SplitHostPort splits a network address of the form "host:port",
// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or
// host%zone and port.
//
// A literal IPv6 address in hostport must be enclosed in square
// brackets, as in "[::1]:80", "[::1%lo0]:80".
//
// See func Dial for a description of the hostport parameter, and host
// and port results.
func SplitHostPort(hostport string) (host, port string, err error) {
const (
missingPort = "missing port in address"
tooManyColons = "too many colons in address"
)
addrErr := func(addr, why string) (host, port string, err error) {
return "", "", &net.AddrError{Err: why, Addr: addr}
}
j, k := 0, 0
// The port starts after the last colon.
i := last(hostport, ':')
if i < 0 {
return addrErr(hostport, missingPort)
}
if hostport[0] == '[' {
// Expect the first ']' just before the last ':'.
end := bytealg.IndexByteString(hostport, ']')
if end < 0 {
return addrErr(hostport, "missing ']' in address")
}
switch end + 1 {
case len(hostport):
// There can't be a ':' behind the ']' now.
return addrErr(hostport, missingPort)
case i:
// The expected result.
default:
// Either ']' isn't followed by a colon, or it is
// followed by a colon that is not the last one.
if hostport[end+1] == ':' {
return addrErr(hostport, tooManyColons)
}
return addrErr(hostport, missingPort)
}
host = hostport[1:end]
j, k = 1, end+1 // there can't be a '[' resp. ']' before these positions
} else {
host = hostport[:i]
if bytealg.IndexByteString(host, ':') >= 0 {
return addrErr(hostport, tooManyColons)
}
}
if bytealg.IndexByteString(hostport[j:], '[') >= 0 {
return addrErr(hostport, "unexpected '[' in address")
}
if bytealg.IndexByteString(hostport[k:], ']') >= 0 {
return addrErr(hostport, "unexpected ']' in address")
}
port = hostport[i+1:]
return host, port, nil
}
func splitHostZone(s string) (host, zone string) {
// The IPv6 scoped addressing zone identifier starts after the
// last percent sign.
if i := last(s, '%'); i > 0 {
host, zone = s[:i], s[i+1:]
} else {
host = s
}
return
}
// JoinHostPort combines host and port into a network address of the
// form "host:port". If host contains a colon, as found in literal
// IPv6 addresses, then JoinHostPort returns "[host]:port".
//
// See func Dial for a description of the host and port parameters.
func JoinHostPort(host, port string) string {
// We assume that host is a literal IPv6 address if host has
// colons.
if bytealg.IndexByteString(host, ':') >= 0 {
return "[" + host + "]:" + port
}
return host + ":" + port
}
// internetAddrList resolves addr, which may be a literal IP
// address or a DNS name, and returns a list of internet protocol
// family addresses. The result contains at least one address when
// error is nil.
func (r *Resolver) internetAddrList(ctx context.Context, network, addr string) (addrList, Result, error) {
var (
err error
host, port string
portnum int
)
switch network {
case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
if addr != "" {
if host, port, err = SplitHostPort(addr); err != nil {
return nil, Result{}, err
}
if portnum, err = r.LookupPort(ctx, network, port); err != nil {
return nil, Result{}, err
}
}
case "ip", "ip4", "ip6":
if addr != "" {
host = addr
}
default:
return nil, Result{}, net.UnknownNetworkError(network)
}
inetaddr := func(ip net.IPAddr) net.Addr {
switch network {
case "tcp", "tcp4", "tcp6":
return &net.TCPAddr{IP: ip.IP, Port: portnum, Zone: ip.Zone}
case "udp", "udp4", "udp6":
return &net.UDPAddr{IP: ip.IP, Port: portnum, Zone: ip.Zone}
case "ip", "ip4", "ip6":
return &net.IPAddr{IP: ip.IP, Zone: ip.Zone}
default:
panic("unexpected network: " + network)
}
}
if host == "" {
return addrList{inetaddr(net.IPAddr{})}, Result{}, nil
}
// Try as a literal IP address, then as a DNS name.
ips, result, err := r.lookupIPAddr(ctx, network, host)
if err != nil {
return nil, result, err
}
// Issue 18806: if the machine has halfway configured
// IPv6 such that it can bind on "::" (IPv6unspecified)
// but not connect back to that same address, fall
// back to dialing 0.0.0.0.
if len(ips) == 1 && ips[0].IP.Equal(net.IPv6unspecified) {
ips = append(ips, net.IPAddr{IP: net.IPv4zero})
}
var filter func(net.IPAddr) bool
if network != "" && network[len(network)-1] == '4' {
filter = ipv4only
}
if network != "" && network[len(network)-1] == '6' {
filter = ipv6only
}
addrs, err := filterAddrList(filter, ips, inetaddr, host)
return addrs, result, err
}

973
vendor/github.com/mjl-/adns/lookup.go generated vendored Normal file
View file

@ -0,0 +1,973 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
"context"
"fmt"
"net"
"net/netip"
"sync"
"golang.org/x/net/dns/dnsmessage"
"github.com/mjl-/adns/internal/singleflight"
)
// protocols contains minimal mappings between internet protocol
// names and numbers for platforms that don't have a complete list of
// protocol numbers.
//
// See https://www.iana.org/assignments/protocol-numbers
//
// On Unix, this map is augmented by readProtocols via lookupProtocol.
var protocols = map[string]int{
"icmp": 1,
"igmp": 2,
"tcp": 6,
"udp": 17,
"ipv6-icmp": 58,
}
// services contains minimal mappings between services names and port
// numbers for platforms that don't have a complete list of port numbers.
//
// See https://www.iana.org/assignments/service-names-port-numbers
//
// On Unix, this map is augmented by readServices via goLookupPort.
var services = map[string]map[string]int{
"udp": {
"domain": 53,
},
"tcp": {
"ftp": 21,
"ftps": 990,
"gopher": 70, // ʕ◔ϖ◔ʔ
"http": 80,
"https": 443,
"imap2": 143,
"imap3": 220,
"imaps": 993,
"pop3": 110,
"pop3s": 995,
"smtp": 25,
"ssh": 22,
"telnet": 23,
},
}
// dnsWaitGroup can be used by tests to wait for all DNS goroutines to
// complete. This avoids races on the test hooks.
var dnsWaitGroup sync.WaitGroup
const maxProtoLength = len("RSVP-E2E-IGNORE") + 10 // with room to grow
func lookupProtocolMap(name string) (int, error) {
var lowerProtocol [maxProtoLength]byte
n := copy(lowerProtocol[:], name)
lowerASCIIBytes(lowerProtocol[:n])
proto, found := protocols[string(lowerProtocol[:n])]
if !found || n != len(name) {
return 0, &net.AddrError{Err: "unknown IP protocol specified", Addr: name}
}
return proto, nil
}
// maxPortBufSize is the longest reasonable name of a service
// (non-numeric port).
// Currently the longest known IANA-unregistered name is
// "mobility-header", so we use that length, plus some slop in case
// something longer is added in the future.
const maxPortBufSize = len("mobility-header") + 10
func lookupPortMap(network, service string) (port int, error error) {
switch network {
case "tcp4", "tcp6":
network = "tcp"
case "udp4", "udp6":
network = "udp"
}
if m, ok := services[network]; ok {
var lowerService [maxPortBufSize]byte
n := copy(lowerService[:], service)
lowerASCIIBytes(lowerService[:n])
if port, ok := m[string(lowerService[:n])]; ok && n == len(service) {
return port, nil
}
}
return 0, &net.AddrError{Err: "unknown port", Addr: network + "/" + service}
}
// ipVersion returns the provided network's IP version: '4', '6' or 0
// if network does not end in a '4' or '6' byte.
func ipVersion(network string) byte {
if network == "" {
return 0
}
n := network[len(network)-1]
if n != '4' && n != '6' {
n = 0
}
return n
}
// DefaultResolver is the resolver used by the package-level Lookup
// functions and by Dialers without a specified Resolver.
var DefaultResolver = &Resolver{}
// A Resolver looks up names and numbers.
//
// A nil *Resolver is equivalent to a zero Resolver.
type Resolver struct {
// PreferGo controls whether Go's built-in DNS resolver is preferred
// on platforms where it's available. It is equivalent to setting
// GODEBUG=netdns=go, but scoped to just this resolver.
PreferGo bool
// StrictErrors controls the behavior of temporary errors
// (including timeout, socket errors, and SERVFAIL) when using
// Go's built-in resolver. For a query composed of multiple
// sub-queries (such as an A+AAAA address lookup, or walking the
// DNS search list), this option causes such errors to abort the
// whole query instead of returning a partial result. This is
// not enabled by default because it may affect compatibility
// with resolvers that process AAAA queries incorrectly.
StrictErrors bool
// Dial optionally specifies an alternate dialer for use by
// Go's built-in DNS resolver to make TCP and UDP connections
// to DNS services. The host in the address parameter will
// always be a literal IP address and not a host name, and the
// port in the address parameter will be a literal port number
// and not a service name.
// If the Conn returned is also a PacketConn, sent and received DNS
// messages must adhere to RFC 1035 section 4.2.1, "UDP usage".
// Otherwise, DNS messages transmitted over Conn must adhere
// to RFC 7766 section 5, "Transport Protocol Selection".
// If nil, the default dialer is used.
Dial func(ctx context.Context, network, address string) (net.Conn, error)
// lookupGroup merges LookupIPAddr calls together for lookups for the same
// host. The lookupGroup key is the LookupIPAddr.host argument.
// The return values are ([]IPAddr, error).
lookupGroup singleflight.Group
// TODO(bradfitz): optional interface impl override hook
// TODO(bradfitz): Timeout time.Duration?
}
func (r *Resolver) preferGo() bool { return r != nil && r.PreferGo }
func (r *Resolver) strictErrors() bool { return r != nil && r.StrictErrors }
func (r *Resolver) getLookupGroup() *singleflight.Group {
if r == nil {
return &DefaultResolver.lookupGroup
}
return &r.lookupGroup
}
// LookupHost looks up the given host using the local resolver.
// It returns a slice of that host's addresses.
//
// LookupHost uses context.Background internally; to specify the context, use
// Resolver.LookupHost.
func LookupHost(host string) (addrs []string, result Result, err error) {
return DefaultResolver.LookupHost(context.Background(), host)
}
// LookupHost looks up the given host using the local resolver.
// It returns a slice of that host's addresses.
func (r *Resolver) LookupHost(ctx context.Context, host string) (addrs []string, result Result, err error) {
// Make sure that no matter what we do later, host=="" is rejected.
if host == "" {
return nil, result, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
if _, err := netip.ParseAddr(host); err == nil {
return []string{host}, result, nil
}
return r.lookupHost(ctx, host)
}
// LookupIP looks up host using the local resolver.
// It returns a slice of that host's IPv4 and IPv6 addresses.
func LookupIP(host string) ([]net.IP, Result, error) {
addrs, result, err := DefaultResolver.LookupIPAddr(context.Background(), host)
if err != nil {
return nil, result, err
}
ips := make([]net.IP, len(addrs))
for i, ia := range addrs {
ips[i] = ia.IP
}
return ips, result, nil
}
// LookupIPAddr looks up host using the local resolver.
// It returns a slice of that host's IPv4 and IPv6 addresses.
func (r *Resolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, Result, error) {
return r.lookupIPAddr(ctx, "ip", host)
}
// LookupIP looks up host for the given network using the local resolver.
// It returns a slice of that host's IP addresses of the type specified by
// network.
// network must be one of "ip", "ip4" or "ip6".
func (r *Resolver) LookupIP(ctx context.Context, network, host string) ([]net.IP, Result, error) {
afnet, _, err := parseNetwork(ctx, network, false)
if err != nil {
return nil, Result{}, err
}
switch afnet {
case "ip", "ip4", "ip6":
default:
return nil, Result{}, net.UnknownNetworkError(network)
}
if host == "" {
return nil, Result{}, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
addrs, result, err := r.internetAddrList(ctx, afnet, host)
if err != nil {
return nil, result, err
}
ips := make([]net.IP, 0, len(addrs))
for _, addr := range addrs {
ips = append(ips, addr.(*net.IPAddr).IP)
}
return ips, result, nil
}
// LookupNetIP looks up host using the local resolver.
// It returns a slice of that host's IP addresses of the type specified by
// network.
// The network must be one of "ip", "ip4" or "ip6".
func (r *Resolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, Result, error) {
// TODO(bradfitz): make this efficient, making the internal net package
// type throughout be netip.Addr and only converting to the net.IP slice
// version at the edge. But for now (2021-10-20), this is a wrapper around
// the old way.
ips, result, err := r.LookupIP(ctx, network, host)
if err != nil {
return nil, result, err
}
ret := make([]netip.Addr, 0, len(ips))
for _, ip := range ips {
if a, ok := netip.AddrFromSlice(ip); ok {
ret = append(ret, a)
}
}
return ret, result, nil
}
// onlyValuesCtx is a context that uses an underlying context
// for value lookup if the underlying context hasn't yet expired.
type onlyValuesCtx struct {
context.Context
lookupValues context.Context
}
var _ context.Context = (*onlyValuesCtx)(nil)
// Value performs a lookup if the original context hasn't expired.
func (ovc *onlyValuesCtx) Value(key any) any {
select {
case <-ovc.lookupValues.Done():
return nil
default:
return ovc.lookupValues.Value(key)
}
}
// withUnexpiredValuesPreserved returns a context.Context that only uses lookupCtx
// for its values, otherwise it is never canceled and has no deadline.
// If the lookup context expires, any looked up values will return nil.
// See Issue 28600.
func withUnexpiredValuesPreserved(lookupCtx context.Context) context.Context {
return &onlyValuesCtx{Context: context.Background(), lookupValues: lookupCtx}
}
// lookupIPAddr looks up host using the local resolver and particular network.
// It returns a slice of that host's IPv4 and IPv6 addresses.
func (r *Resolver) lookupIPAddr(ctx context.Context, network, host string) ([]net.IPAddr, Result, error) {
// Make sure that no matter what we do later, host=="" is rejected.
if host == "" {
return nil, Result{}, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
if ip, err := netip.ParseAddr(host); err == nil {
return []net.IPAddr{{IP: net.IP(ip.AsSlice()).To16(), Zone: ip.Zone()}}, Result{}, nil
}
// The underlying resolver func is lookupIP by default but it
// can be overridden by tests. This is needed by net/http, so it
// uses a context key instead of unexported variables.
resolverFunc := r.lookupIP
// We don't want a cancellation of ctx to affect the
// lookupGroup operation. Otherwise if our context gets
// canceled it might cause an error to be returned to a lookup
// using a completely different context. However we need to preserve
// only the values in context. See Issue 28600.
lookupGroupCtx, lookupGroupCancel := context.WithCancel(withUnexpiredValuesPreserved(ctx))
type Tuple struct {
ips []net.IPAddr
result Result
}
lookupKey := network + "\000" + host
dnsWaitGroup.Add(1)
ch := r.getLookupGroup().DoChan(lookupKey, func() (any, error) {
ips, result, err := testHookLookupIP(lookupGroupCtx, resolverFunc, network, host)
return Tuple{ips, result}, err
})
dnsWaitGroupDone := func(ch <-chan singleflight.Result, cancelFn context.CancelFunc) {
<-ch
dnsWaitGroup.Done()
cancelFn()
}
select {
case <-ctx.Done():
// Our context was canceled. If we are the only
// goroutine looking up this key, then drop the key
// from the lookupGroup and cancel the lookup.
// If there are other goroutines looking up this key,
// let the lookup continue uncanceled, and let later
// lookups with the same key share the result.
// See issues 8602, 20703, 22724.
if r.getLookupGroup().ForgetUnshared(lookupKey) {
lookupGroupCancel()
go dnsWaitGroupDone(ch, func() {})
} else {
go dnsWaitGroupDone(ch, lookupGroupCancel)
}
ctxErr := ctx.Err()
err := &DNSError{
Err: mapErr(ctxErr).Error(),
Name: host,
IsTimeout: ctxErr == context.DeadlineExceeded,
}
return nil, Result{}, err
case r := <-ch:
dnsWaitGroup.Done()
lookupGroupCancel()
err := r.Err
if err != nil {
if _, ok := err.(*DNSError); !ok {
isTimeout := false
if err == context.DeadlineExceeded {
isTimeout = true
} else if terr, ok := err.(timeout); ok {
isTimeout = terr.Timeout()
}
err = &DNSError{
Err: err.Error(),
Name: host,
IsTimeout: isTimeout,
}
}
}
tuple := r.Val.(Tuple)
if err != nil {
return nil, tuple.result, err
}
ips := lookupIPReturn(tuple.ips, r.Shared)
return ips, tuple.result, nil
}
}
// lookupIPReturn turns the return values from singleflight.Do into
// the return values from LookupIP.
func lookupIPReturn(addrs []net.IPAddr, shared bool) []net.IPAddr {
if shared {
clone := make([]net.IPAddr, len(addrs))
copy(clone, addrs)
addrs = clone
}
return addrs
}
// LookupPort looks up the port for the given network and service.
//
// LookupPort uses context.Background internally; to specify the context, use
// Resolver.LookupPort.
func LookupPort(network, service string) (port int, err error) {
return DefaultResolver.LookupPort(context.Background(), network, service)
}
// LookupPort looks up the port for the given network and service.
func (r *Resolver) LookupPort(ctx context.Context, network, service string) (port int, err error) {
port, needsLookup := parsePort(service)
if needsLookup {
switch network {
case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
case "": // a hint wildcard for Go 1.0 undocumented behavior
network = "ip"
default:
return 0, &net.AddrError{Err: "unknown network", Addr: network}
}
port, err = r.lookupPort(ctx, network, service)
if err != nil {
return 0, err
}
}
if 0 > port || port > 65535 {
return 0, &net.AddrError{Err: "invalid port", Addr: service}
}
return port, nil
}
// LookupCNAME returns the canonical name for the given host.
// Callers that do not care about the canonical name can call
// LookupHost or LookupIP directly; both take care of resolving
// the canonical name as part of the lookup.
//
// A canonical name is the final name after following zero
// or more CNAME records.
// LookupCNAME does not return an error if host does not
// contain DNS "CNAME" records, as long as host resolves to
// address records.
//
// The returned canonical name is validated to be a properly
// formatted presentation-format domain name.
//
// LookupCNAME uses context.Background internally; to specify the context, use
// Resolver.LookupCNAME.
func LookupCNAME(host string) (cname string, result Result, err error) {
return DefaultResolver.LookupCNAME(context.Background(), host)
}
// LookupCNAME returns the canonical name for the given host.
// Callers that do not care about the canonical name can call
// LookupHost or LookupIP directly; both take care of resolving
// the canonical name as part of the lookup.
//
// A canonical name is the final name after following zero
// or more CNAME records.
// LookupCNAME does not return an error if host does not
// contain DNS "CNAME" records, as long as host resolves to
// address records.
//
// The returned canonical name is validated to be a properly
// formatted presentation-format domain name.
func (r *Resolver) LookupCNAME(ctx context.Context, host string) (string, Result, error) {
cname, result, err := r.lookupCNAME(ctx, host)
if err != nil {
return "", result, err
}
if !isDomainName(cname) {
return "", result, &DNSError{Err: errMalformedDNSRecordsDetail, Name: host}
}
return cname, result, nil
}
// LookupSRV tries to resolve an SRV query of the given service,
// protocol, and domain name. The proto is "tcp" or "udp".
// The returned records are sorted by priority and randomized
// by weight within a priority.
//
// LookupSRV constructs the DNS name to look up following RFC 2782.
// That is, it looks up _service._proto.name. To accommodate services
// publishing SRV records under non-standard names, if both service
// and proto are empty strings, LookupSRV looks up name directly.
//
// The returned service names are validated to be properly
// formatted presentation-format domain names. If the response contains
// invalid names, those records are filtered out and an error
// will be returned alongside the remaining results, if any.
func LookupSRV(service, proto, name string) (cname string, addrs []*net.SRV, result Result, err error) {
return DefaultResolver.LookupSRV(context.Background(), service, proto, name)
}
// LookupSRV tries to resolve an SRV query of the given service,
// protocol, and domain name. The proto is "tcp" or "udp".
// The returned records are sorted by priority and randomized
// by weight within a priority.
//
// LookupSRV constructs the DNS name to look up following RFC 2782.
// That is, it looks up _service._proto.name. To accommodate services
// publishing SRV records under non-standard names, if both service
// and proto are empty strings, LookupSRV looks up name directly.
//
// The returned service names are validated to be properly
// formatted presentation-format domain names. If the response contains
// invalid names, those records are filtered out and an error
// will be returned alongside the remaining results, if any.
func (r *Resolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, Result, error) {
cname, addrs, result, err := r.lookupSRV(ctx, service, proto, name)
if err != nil {
return "", nil, result, err
}
if cname != "" && !isDomainName(cname) {
return "", nil, result, &DNSError{Err: "SRV header name is invalid", Name: name}
}
filteredAddrs := make([]*net.SRV, 0, len(addrs))
for _, addr := range addrs {
if addr == nil {
continue
}
if !isDomainName(addr.Target) {
continue
}
filteredAddrs = append(filteredAddrs, addr)
}
if len(addrs) != len(filteredAddrs) {
return cname, filteredAddrs, result, &DNSError{Err: errMalformedDNSRecordsDetail, Name: name}
}
return cname, filteredAddrs, result, nil
}
// LookupMX returns the DNS MX records for the given domain name sorted by preference.
//
// The returned mail server names are validated to be properly
// formatted presentation-format domain names. If the response contains
// invalid names, those records are filtered out and an error
// will be returned alongside the remaining results, if any.
//
// LookupMX uses context.Background internally; to specify the context, use
// Resolver.LookupMX.
func LookupMX(name string) ([]*net.MX, Result, error) {
return DefaultResolver.LookupMX(context.Background(), name)
}
// LookupMX returns the DNS MX records for the given domain name sorted by preference.
//
// The returned mail server names are validated to be properly
// formatted presentation-format domain names. If the response contains
// invalid names, those records are filtered out and an error
// will be returned alongside the remaining results, if any.
func (r *Resolver) LookupMX(ctx context.Context, name string) ([]*net.MX, Result, error) {
records, result, err := r.lookupMX(ctx, name)
if err != nil {
return nil, result, err
}
filteredMX := make([]*net.MX, 0, len(records))
for _, mx := range records {
if mx == nil {
continue
}
if !isDomainName(mx.Host) {
continue
}
filteredMX = append(filteredMX, mx)
}
if len(records) != len(filteredMX) {
return filteredMX, result, &DNSError{Err: errMalformedDNSRecordsDetail, Name: name}
}
return filteredMX, result, nil
}
// LookupNS returns the DNS NS records for the given domain name.
//
// The returned name server names are validated to be properly
// formatted presentation-format domain names. If the response contains
// invalid names, those records are filtered out and an error
// will be returned alongside the remaining results, if any.
//
// LookupNS uses context.Background internally; to specify the context, use
// Resolver.LookupNS.
func LookupNS(name string) ([]*net.NS, Result, error) {
return DefaultResolver.LookupNS(context.Background(), name)
}
// LookupNS returns the DNS NS records for the given domain name.
//
// The returned name server names are validated to be properly
// formatted presentation-format domain names. If the response contains
// invalid names, those records are filtered out and an error
// will be returned alongside the remaining results, if any.
func (r *Resolver) LookupNS(ctx context.Context, name string) ([]*net.NS, Result, error) {
records, result, err := r.lookupNS(ctx, name)
if err != nil {
return nil, result, err
}
filteredNS := make([]*net.NS, 0, len(records))
for _, ns := range records {
if ns == nil {
continue
}
if !isDomainName(ns.Host) {
continue
}
filteredNS = append(filteredNS, ns)
}
if len(records) != len(filteredNS) {
return filteredNS, result, &DNSError{Err: errMalformedDNSRecordsDetail, Name: name}
}
return filteredNS, result, nil
}
// LookupTXT returns the DNS TXT records for the given domain name.
//
// LookupTXT uses context.Background internally; to specify the context, use
// Resolver.LookupTXT.
func LookupTXT(name string) ([]string, Result, error) {
return DefaultResolver.lookupTXT(context.Background(), name)
}
// LookupTXT returns the DNS TXT records for the given domain name.
func (r *Resolver) LookupTXT(ctx context.Context, name string) ([]string, Result, error) {
return r.lookupTXT(ctx, name)
}
// LookupAddr performs a reverse lookup for the given address, returning a list
// of names mapping to that address.
//
// The returned names are validated to be properly formatted presentation-format
// domain names. If the response contains invalid names, those records are filtered
// out and an error will be returned alongside the remaining results, if any.
//
// When using the host C library resolver, at most one result will be
// returned. To bypass the host resolver, use a custom Resolver.
//
// LookupAddr uses context.Background internally; to specify the context, use
// Resolver.LookupAddr.
func LookupAddr(addr string) (names []string, result Result, err error) {
return DefaultResolver.LookupAddr(context.Background(), addr)
}
// LookupAddr performs a reverse lookup for the given address, returning a list
// of names mapping to that address.
//
// The returned names are validated to be properly formatted presentation-format
// domain names. If the response contains invalid names, those records are filtered
// out and an error will be returned alongside the remaining results, if any.
func (r *Resolver) LookupAddr(ctx context.Context, addr string) ([]string, Result, error) {
names, result, err := r.lookupAddr(ctx, addr)
if err != nil {
return nil, result, err
}
filteredNames := make([]string, 0, len(names))
for _, name := range names {
if isDomainName(name) {
filteredNames = append(filteredNames, name)
}
}
if len(names) != len(filteredNames) {
return filteredNames, result, &DNSError{Err: errMalformedDNSRecordsDetail, Name: addr}
}
return filteredNames, result, nil
}
// LookupTLSA calls LookupTLSA on the DefaultResolver.
func LookupTLSA(port int, protocol, host string) ([]TLSA, Result, error) {
return DefaultResolver.LookupTLSA(context.Background(), port, protocol, host)
}
// LookupTLSA looks up a TLSA (TLS association) record for the port (service)
// and protocol (e.g. tcp, udp) at the host.
//
// LookupTLSA looks up DNS name "_<port>._<protocol>.host". Except when port is 0
// and protocol the empty string, then host is directly used to look up the TLSA
// record.
//
// Callers must check the Authentic field of the Result before using a TLSA
// record.
//
// Callers may want to handle DNSError with NotFound set to true (i.e. "nxdomain")
// differently from other errors. DANE support is often optional, with
// protocol-specific fallback behaviour.
//
// LookupTLSA follows CNAME records. For DANE, the secure/insecure DNSSEC
// response must be taken into account when following CNAMEs to determine the
// TLSA base domains. Callers should probably first resolve CNAMEs explicitly
// for their (in)secure status.
func (r *Resolver) LookupTLSA(ctx context.Context, port int, protocol, host string) (records []TLSA, result Result, err error) {
return r.lookupTLSA(ctx, port, protocol, host)
}
// errMalformedDNSRecordsDetail is the DNSError detail which is returned when a Resolver.Lookup...
// method receives DNS records which contain invalid DNS names. This may be returned alongside
// results which have had the malformed records filtered out.
var errMalformedDNSRecordsDetail = "DNS response contained records which contain invalid names"
// dial makes a new connection to the provided server (which must be
// an IP address) with the provided network type, using either r.Dial
// (if both r and r.Dial are non-nil) or else Dialer.DialContext.
func (r *Resolver) dial(ctx context.Context, network, server string) (net.Conn, error) {
// Calling Dial here is scary -- we have to be sure not to
// dial a name that will require a DNS lookup, or Dial will
// call back here to translate it. The DNS config parser has
// already checked that all the cfg.servers are IP
// addresses, which Dial will use without a DNS lookup.
var c net.Conn
var err error
if r != nil && r.Dial != nil {
c, err = r.Dial(ctx, network, server)
} else {
var d net.Dialer
c, err = d.DialContext(ctx, network, server)
}
if err != nil {
return nil, mapErr(err)
}
return c, nil
}
// goLookupSRV returns the SRV records for a target name, built either
// from its component service ("sip"), protocol ("tcp"), and name
// ("example.com."), or from name directly (if service and proto are
// both empty).
//
// In either case, the returned target name ("_sip._tcp.example.com.")
// is also returned on success.
//
// The records are sorted by weight.
func (r *Resolver) goLookupSRV(ctx context.Context, service, proto, name string) (target string, srvs []*net.SRV, result Result, err error) {
if service == "" && proto == "" {
target = name
} else {
target = "_" + service + "._" + proto + "." + name
}
p, server, result, err := r.lookup(ctx, target, dnsmessage.TypeSRV, nil)
if err != nil {
return "", nil, result, err
}
var cname dnsmessage.Name
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return "", nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
if h.Type != dnsmessage.TypeSRV {
if err := p.SkipAnswer(); err != nil {
return "", nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
continue
}
if cname.Length == 0 && h.Name.Length != 0 {
cname = h.Name
}
srv, err := p.SRVResource()
if err != nil {
return "", nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
srvs = append(srvs, &net.SRV{Target: srv.Target.String(), Port: srv.Port, Priority: srv.Priority, Weight: srv.Weight})
}
byPriorityWeight(srvs).sort()
return cname.String(), srvs, result, nil
}
// goLookupMX returns the MX records for name.
func (r *Resolver) goLookupMX(ctx context.Context, name string) ([]*net.MX, Result, error) {
p, server, result, err := r.lookup(ctx, name, dnsmessage.TypeMX, nil)
if err != nil {
return nil, result, err
}
var mxs []*net.MX
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
if h.Type != dnsmessage.TypeMX {
if err := p.SkipAnswer(); err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
continue
}
mx, err := p.MXResource()
if err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
mxs = append(mxs, &net.MX{Host: mx.MX.String(), Pref: mx.Pref})
}
byPref(mxs).sort()
return mxs, result, nil
}
// goLookupNS returns the NS records for name.
func (r *Resolver) goLookupNS(ctx context.Context, name string) ([]*net.NS, Result, error) {
p, server, result, err := r.lookup(ctx, name, dnsmessage.TypeNS, nil)
if err != nil {
return nil, result, err
}
var nss []*net.NS
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
if h.Type != dnsmessage.TypeNS {
if err := p.SkipAnswer(); err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
continue
}
ns, err := p.NSResource()
if err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
nss = append(nss, &net.NS{Host: ns.NS.String()})
}
return nss, result, nil
}
// goLookupTXT returns the TXT records from name.
func (r *Resolver) goLookupTXT(ctx context.Context, name string) ([]string, Result, error) {
p, server, result, err := r.lookup(ctx, name, dnsmessage.TypeTXT, nil)
if err != nil {
return nil, result, err
}
var txts []string
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
if h.Type != dnsmessage.TypeTXT {
if err := p.SkipAnswer(); err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
continue
}
txt, err := p.TXTResource()
if err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
// Multiple strings in one TXT record need to be
// concatenated without separator to be consistent
// with previous Go resolver.
n := 0
for _, s := range txt.TXT {
n += len(s)
}
txtJoin := make([]byte, 0, n)
for _, s := range txt.TXT {
txtJoin = append(txtJoin, s...)
}
if len(txts) == 0 {
txts = make([]string, 0, 1)
}
txts = append(txts, string(txtJoin))
}
return txts, result, nil
}
const typeTLSA = dnsmessage.Type(52)
// goLookupTLSA is the native Go implementation of LookupTLSA.
func (r *Resolver) goLookupTLSA(ctx context.Context, port int, protocol, host string) ([]TLSA, Result, error) {
var name string
if port == 0 && protocol == "" {
name = host
} else {
name = fmt.Sprintf("_%d._%s.%s", port, protocol, host)
}
p, server, result, err := r.lookup(ctx, name, typeTLSA, nil)
if err != nil {
return nil, result, err
}
var l []TLSA
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
if h.Type != typeTLSA {
if err := p.SkipAnswer(); err != nil {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
continue
}
r, err := p.UnknownResource()
if err != nil || len(r.Data) < 3 {
return nil, result, &DNSError{
Err: "cannot unmarshal DNS message",
Name: name,
Server: server,
}
}
record := TLSA{
TLSAUsage(r.Data[0]),
TLSASelector(r.Data[1]),
TLSAMatchType(r.Data[2]),
nil,
}
// We do not verify the contents/size of the data. We don't want to filter out
// values we don't understand. We'll leave it to the callers to see if a record is
// usable. Also because special behaviour may be required if records were found but
// all unusable.
buf := make([]byte, len(r.Data)-3)
copy(buf, r.Data[3:])
record.CertAssoc = buf
l = append(l, record)
}
return l, result, nil
}

105
vendor/github.com/mjl-/adns/lookup_unix.go generated vendored Normal file
View file

@ -0,0 +1,105 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix || wasip1
package adns
import (
"context"
"net"
"sync"
"github.com/mjl-/adns/internal/bytealg"
)
var onceReadProtocols sync.Once
// readProtocols loads contents of /etc/protocols into protocols map
// for quick access.
func readProtocols() {
file, err := open("/etc/protocols")
if err != nil {
return
}
defer file.close()
for line, ok := file.readLine(); ok; line, ok = file.readLine() {
// tcp 6 TCP # transmission control protocol
if i := bytealg.IndexByteString(line, '#'); i >= 0 {
line = line[0:i]
}
f := getFields(line)
if len(f) < 2 {
continue
}
if proto, _, ok := dtoi(f[1]); ok {
if _, ok := protocols[f[0]]; !ok {
protocols[f[0]] = proto
}
for _, alias := range f[2:] {
if _, ok := protocols[alias]; !ok {
protocols[alias] = proto
}
}
}
}
}
// lookupProtocol looks up IP protocol name in /etc/protocols and
// returns correspondent protocol number.
func lookupProtocol(_ context.Context, name string) (int, error) {
onceReadProtocols.Do(readProtocols)
return lookupProtocolMap(name)
}
func (r *Resolver) lookupHost(ctx context.Context, host string) (addrs []string, result Result, err error) {
order, conf := systemConf().hostLookupOrder(r, host)
return r.goLookupHostOrder(ctx, host, order, conf)
}
func (r *Resolver) lookupIP(ctx context.Context, network, host string) (addrs []net.IPAddr, result Result, err error) {
if r.preferGo() {
return r.goLookupIP(ctx, network, host)
}
order, conf := systemConf().hostLookupOrder(r, host)
ips, _, result, err := r.goLookupIPCNAMEOrder(ctx, network, host, order, conf)
return ips, result, err
}
func (r *Resolver) lookupPort(ctx context.Context, network, service string) (int, error) {
// Port lookup is not a DNS operation.
// Prefer the cgo resolver if possible.
return goLookupPort(network, service)
}
func (r *Resolver) lookupCNAME(ctx context.Context, name string) (string, Result, error) {
order, conf := systemConf().hostLookupOrder(r, name)
return r.goLookupCNAME(ctx, name, order, conf)
}
func (r *Resolver) lookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, Result, error) {
return r.goLookupSRV(ctx, service, proto, name)
}
func (r *Resolver) lookupMX(ctx context.Context, name string) ([]*net.MX, Result, error) {
return r.goLookupMX(ctx, name)
}
func (r *Resolver) lookupNS(ctx context.Context, name string) ([]*net.NS, Result, error) {
return r.goLookupNS(ctx, name)
}
func (r *Resolver) lookupTXT(ctx context.Context, name string) ([]string, Result, error) {
return r.goLookupTXT(ctx, name)
}
func (r *Resolver) lookupAddr(ctx context.Context, addr string) ([]string, Result, error) {
order, conf := systemConf().addrLookupOrder(r, addr)
return r.goLookupPTR(ctx, addr, order, conf)
}
func (r *Resolver) lookupTLSA(ctx context.Context, port int, protocol, host string) ([]TLSA, Result, error) {
return r.goLookupTLSA(ctx, port, protocol, host)
}

7
vendor/github.com/mjl-/adns/mac.go generated vendored Normal file
View file

@ -0,0 +1,7 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
const hexDigit = "0123456789abcdef"

110
vendor/github.com/mjl-/adns/net.go generated vendored Normal file
View file

@ -0,0 +1,110 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
"context"
"errors"
)
// Various errors contained in OpError.
var (
// For connection setup operations.
errNoSuitableAddress = errors.New("no suitable address found")
// For both read and write operations.
errCanceled = canceledError{}
)
// canceledError lets us return the same error string we have always
// returned, while still being Is context.Canceled.
type canceledError struct{}
func (canceledError) Error() string { return "operation was canceled" }
func (canceledError) Is(err error) bool { return err == context.Canceled }
// mapErr maps from the context errors to the historical internal net
// error values.
func mapErr(err error) error {
switch err {
case context.Canceled:
return errCanceled
case context.DeadlineExceeded:
return errTimeout
default:
return err
}
}
type timeout interface {
Timeout() bool
}
// Various errors contained in DNSError.
var (
errNoSuchHost = errors.New("no such host")
)
// errTimeout exists to return the historical "i/o timeout" string
// for context.DeadlineExceeded. See mapErr.
// It is also used when Dialer.Deadline is exceeded.
// error.Is(errTimeout, context.DeadlineExceeded) returns true.
//
// TODO(iant): We could consider changing this to os.ErrDeadlineExceeded
// in the future, if we make
//
// errors.Is(os.ErrDeadlineExceeded, context.DeadlineExceeded)
//
// return true.
var errTimeout error = &timeoutError{}
type timeoutError struct{}
func (e *timeoutError) Error() string { return "i/o timeout" }
func (e *timeoutError) Timeout() bool { return true }
func (e *timeoutError) Temporary() bool { return true }
func (e *timeoutError) Is(err error) bool {
return err == context.DeadlineExceeded
}
// DNSError represents a DNS lookup error.
type DNSError struct {
Underlying error // Underlying error, could be an ExtendedError.
Err string // description of the error
Name string // name looked for
Server string // server used
IsTimeout bool // if true, timed out; not all timeouts set this
IsTemporary bool // if true, error is temporary; not all errors set this
IsNotFound bool // if true, host could not be found
}
// Unwrap returns the underlying error, which could be an ExtendedError.
func (e *DNSError) Unwrap() error {
return e.Underlying
}
func (e *DNSError) Error() string {
if e == nil {
return "<nil>"
}
s := "lookup " + e.Name
if e.Server != "" {
s += " on " + e.Server
}
s += ": " + e.Err
return s
}
// Timeout reports whether the DNS lookup is known to have timed out.
// This is not always known; a DNS lookup may fail due to a timeout
// and return a DNSError for which Timeout returns false.
func (e *DNSError) Timeout() bool { return e.IsTimeout }
// Temporary reports whether the DNS error is known to be temporary.
// This is not always known; a DNS lookup may fail due to a temporary
// error and return a DNSError for which Temporary returns false.
func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary }

250
vendor/github.com/mjl-/adns/nss.go generated vendored Normal file
View file

@ -0,0 +1,250 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
import (
"errors"
"os"
"sync"
"time"
"github.com/mjl-/adns/internal/bytealg"
)
const (
nssConfigPath = "/etc/nsswitch.conf"
)
var nssConfig nsswitchConfig
type nsswitchConfig struct {
initOnce sync.Once // guards init of nsswitchConfig
// ch is used as a semaphore that only allows one lookup at a
// time to recheck nsswitch.conf
ch chan struct{} // guards lastChecked and modTime
lastChecked time.Time // last time nsswitch.conf was checked
mu sync.Mutex // protects nssConf
nssConf *nssConf
}
func getSystemNSS() *nssConf {
nssConfig.tryUpdate()
nssConfig.mu.Lock()
conf := nssConfig.nssConf
nssConfig.mu.Unlock()
return conf
}
// init initializes conf and is only called via conf.initOnce.
func (conf *nsswitchConfig) init() {
conf.nssConf = parseNSSConfFile("/etc/nsswitch.conf")
conf.lastChecked = time.Now()
conf.ch = make(chan struct{}, 1)
}
// tryUpdate tries to update conf.
func (conf *nsswitchConfig) tryUpdate() {
conf.initOnce.Do(conf.init)
// Ensure only one update at a time checks nsswitch.conf
if !conf.tryAcquireSema() {
return
}
defer conf.releaseSema()
now := time.Now()
if conf.lastChecked.After(now.Add(-5 * time.Second)) {
return
}
conf.lastChecked = now
var mtime time.Time
if fi, err := os.Stat(nssConfigPath); err == nil {
mtime = fi.ModTime()
}
if mtime.Equal(conf.nssConf.mtime) {
return
}
nssConf := parseNSSConfFile(nssConfigPath)
conf.mu.Lock()
conf.nssConf = nssConf
conf.mu.Unlock()
}
func (conf *nsswitchConfig) acquireSema() {
conf.ch <- struct{}{}
}
func (conf *nsswitchConfig) tryAcquireSema() bool {
select {
case conf.ch <- struct{}{}:
return true
default:
return false
}
}
func (conf *nsswitchConfig) releaseSema() {
<-conf.ch
}
// nssConf represents the state of the machine's /etc/nsswitch.conf file.
type nssConf struct {
mtime time.Time // time of nsswitch.conf modification
err error // any error encountered opening or parsing the file
sources map[string][]nssSource // keyed by database (e.g. "hosts")
}
type nssSource struct {
source string // e.g. "compat", "files", "mdns4_minimal"
criteria []nssCriterion
}
// standardCriteria reports all specified criteria have the default
// status actions.
func (s nssSource) standardCriteria() bool {
for i, crit := range s.criteria {
if !crit.standardStatusAction(i == len(s.criteria)-1) {
return false
}
}
return true
}
// nssCriterion is the parsed structure of one of the criteria in brackets
// after an NSS source name.
type nssCriterion struct {
negate bool // if "!" was present
status string // e.g. "success", "unavail" (lowercase)
action string // e.g. "return", "continue" (lowercase)
}
// standardStatusAction reports whether c is equivalent to not
// specifying the criterion at all. last is whether this criteria is the
// last in the list.
func (c nssCriterion) standardStatusAction(last bool) bool {
if c.negate {
return false
}
var def string
switch c.status {
case "success":
def = "return"
case "notfound", "unavail", "tryagain":
def = "continue"
default:
// Unknown status
return false
}
if last && c.action == "return" {
return true
}
return c.action == def
}
func parseNSSConfFile(file string) *nssConf {
f, err := open(file)
if err != nil {
return &nssConf{err: err}
}
defer f.close()
mtime, _, err := f.stat()
if err != nil {
return &nssConf{err: err}
}
conf := parseNSSConf(f)
conf.mtime = mtime
return conf
}
func parseNSSConf(f *file) *nssConf {
conf := new(nssConf)
for line, ok := f.readLine(); ok; line, ok = f.readLine() {
line = trimSpace(removeComment(line))
if len(line) == 0 {
continue
}
colon := bytealg.IndexByteString(line, ':')
if colon == -1 {
conf.err = errors.New("no colon on line")
return conf
}
db := trimSpace(line[:colon])
srcs := line[colon+1:]
for {
srcs = trimSpace(srcs)
if len(srcs) == 0 {
break
}
sp := bytealg.IndexByteString(srcs, ' ')
var src string
if sp == -1 {
src = srcs
srcs = "" // done
} else {
src = srcs[:sp]
srcs = trimSpace(srcs[sp+1:])
}
var criteria []nssCriterion
// See if there's a criteria block in brackets.
if len(srcs) > 0 && srcs[0] == '[' {
bclose := bytealg.IndexByteString(srcs, ']')
if bclose == -1 {
conf.err = errors.New("unclosed criterion bracket")
return conf
}
var err error
criteria, err = parseCriteria(srcs[1:bclose])
if err != nil {
conf.err = errors.New("invalid criteria: " + srcs[1:bclose])
return conf
}
srcs = srcs[bclose+1:]
}
if conf.sources == nil {
conf.sources = make(map[string][]nssSource)
}
conf.sources[db] = append(conf.sources[db], nssSource{
source: src,
criteria: criteria,
})
}
}
return conf
}
// parses "foo=bar !foo=bar"
func parseCriteria(x string) (c []nssCriterion, err error) {
err = foreachField(x, func(f string) error {
not := false
if len(f) > 0 && f[0] == '!' {
not = true
f = f[1:]
}
if len(f) < 3 {
return errors.New("criterion too short")
}
eq := bytealg.IndexByteString(f, '=')
if eq == -1 {
return errors.New("criterion lacks equal sign")
}
if hasUpperCase(f) {
lower := []byte(f)
lowerASCIIBytes(lower)
f = string(lower)
}
c = append(c, nssCriterion{
negate: not,
status: f[:eq],
action: f[eq+1:],
})
return nil
})
return
}

267
vendor/github.com/mjl-/adns/parse.go generated vendored Normal file
View file

@ -0,0 +1,267 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Simple file i/o and string manipulation, to avoid
// depending on strconv and bufio and strings.
package adns
import (
"io"
"os"
"time"
"github.com/mjl-/adns/internal/bytealg"
)
type file struct {
file *os.File
data []byte
atEOF bool
}
func (f *file) close() { f.file.Close() }
func (f *file) getLineFromData() (s string, ok bool) {
data := f.data
i := 0
for i = 0; i < len(data); i++ {
if data[i] == '\n' {
s = string(data[0:i])
ok = true
// move data
i++
n := len(data) - i
copy(data[0:], data[i:])
f.data = data[0:n]
return
}
}
if f.atEOF && len(f.data) > 0 {
// EOF, return all we have
s = string(data)
f.data = f.data[0:0]
ok = true
}
return
}
func (f *file) readLine() (s string, ok bool) {
if s, ok = f.getLineFromData(); ok {
return
}
if len(f.data) < cap(f.data) {
ln := len(f.data)
n, err := io.ReadFull(f.file, f.data[ln:cap(f.data)])
if n >= 0 {
f.data = f.data[0 : ln+n]
}
if err == io.EOF || err == io.ErrUnexpectedEOF {
f.atEOF = true
}
}
s, ok = f.getLineFromData()
return
}
func (f *file) stat() (mtime time.Time, size int64, err error) {
st, err := f.file.Stat()
if err != nil {
return time.Time{}, 0, err
}
return st.ModTime(), st.Size(), nil
}
func open(name string) (*file, error) {
fd, err := os.Open(name)
if err != nil {
return nil, err
}
return &file{fd, make([]byte, 0, 64*1024), false}, nil
}
func stat(name string) (mtime time.Time, size int64, err error) {
st, err := os.Stat(name)
if err != nil {
return time.Time{}, 0, err
}
return st.ModTime(), st.Size(), nil
}
// Count occurrences in s of any bytes in t.
func countAnyByte(s string, t string) int {
n := 0
for i := 0; i < len(s); i++ {
if bytealg.IndexByteString(t, s[i]) >= 0 {
n++
}
}
return n
}
// Split s at any bytes in t.
func splitAtBytes(s string, t string) []string {
a := make([]string, 1+countAnyByte(s, t))
n := 0
last := 0
for i := 0; i < len(s); i++ {
if bytealg.IndexByteString(t, s[i]) >= 0 {
if last < i {
a[n] = s[last:i]
n++
}
last = i + 1
}
}
if last < len(s) {
a[n] = s[last:]
n++
}
return a[0:n]
}
func getFields(s string) []string { return splitAtBytes(s, " \r\t\n") }
// Bigger than we need, not too big to worry about overflow
const big = 0xFFFFFF
// Decimal to integer.
// Returns number, characters consumed, success.
func dtoi(s string) (n int, i int, ok bool) {
n = 0
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
n = n*10 + int(s[i]-'0')
if n >= big {
return big, i, false
}
}
if i == 0 {
return 0, 0, false
}
return n, i, true
}
// Number of occurrences of b in s.
func count(s string, b byte) int {
n := 0
for i := 0; i < len(s); i++ {
if s[i] == b {
n++
}
}
return n
}
// Index of rightmost occurrence of b in s.
func last(s string, b byte) int {
i := len(s)
for i--; i >= 0; i-- {
if s[i] == b {
break
}
}
return i
}
// hasUpperCase tells whether the given string contains at least one upper-case.
func hasUpperCase(s string) bool {
for i := range s {
if 'A' <= s[i] && s[i] <= 'Z' {
return true
}
}
return false
}
// lowerASCIIBytes makes x ASCII lowercase in-place.
func lowerASCIIBytes(x []byte) {
for i, b := range x {
if 'A' <= b && b <= 'Z' {
x[i] += 'a' - 'A'
}
}
}
// lowerASCII returns the ASCII lowercase version of b.
func lowerASCII(b byte) byte {
if 'A' <= b && b <= 'Z' {
return b + ('a' - 'A')
}
return b
}
// trimSpace returns x without any leading or trailing ASCII whitespace.
func trimSpace(x string) string {
for len(x) > 0 && isSpace(x[0]) {
x = x[1:]
}
for len(x) > 0 && isSpace(x[len(x)-1]) {
x = x[:len(x)-1]
}
return x
}
// isSpace reports whether b is an ASCII space character.
func isSpace(b byte) bool {
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
}
// removeComment returns line, removing any '#' byte and any following
// bytes.
func removeComment(line string) string {
if i := bytealg.IndexByteString(line, '#'); i != -1 {
return line[:i]
}
return line
}
// foreachField runs fn on each non-empty run of non-space bytes in x.
// It returns the first non-nil error returned by fn.
func foreachField(x string, fn func(field string) error) error {
x = trimSpace(x)
for len(x) > 0 {
sp := bytealg.IndexByteString(x, ' ')
if sp == -1 {
return fn(x)
}
if field := trimSpace(x[:sp]); len(field) > 0 {
if err := fn(field); err != nil {
return err
}
}
x = trimSpace(x[sp+1:])
}
return nil
}
// stringsHasSuffix is strings.HasSuffix. It reports whether s ends in
// suffix.
func stringsHasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
// stringsHasSuffixFold reports whether s ends in suffix,
// ASCII-case-insensitively.
func stringsHasSuffixFold(s, suffix string) bool {
return len(s) >= len(suffix) && stringsEqualFold(s[len(s)-len(suffix):], suffix)
}
// stringsHasPrefix is strings.HasPrefix. It reports whether s begins with prefix.
func stringsHasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
// stringsEqualFold is strings.EqualFold, ASCII only. It reports whether s and t
// are equal, ASCII-case-insensitively.
func stringsEqualFold(s, t string) bool {
if len(s) != len(t) {
return false
}
for i := 0; i < len(s); i++ {
if lowerASCII(s[i]) != lowerASCII(t[i]) {
return false
}
}
return true
}

62
vendor/github.com/mjl-/adns/port.go generated vendored Normal file
View file

@ -0,0 +1,62 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package adns
// parsePort parses service as a decimal integer and returns the
// corresponding value as port. It is the caller's responsibility to
// parse service as a non-decimal integer when needsLookup is true.
//
// Some system resolvers will return a valid port number when given a number
// over 65536 (see https://golang.org/issues/11715). Alas, the parser
// can't bail early on numbers > 65536. Therefore reasonably large/small
// numbers are parsed in full and rejected if invalid.
func parsePort(service string) (port int, needsLookup bool) {
if service == "" {
// Lock in the legacy behavior that an empty string
// means port 0. See golang.org/issue/13610.
return 0, false
}
const (
max = uint32(1<<32 - 1)
cutoff = uint32(1 << 30)
)
neg := false
if service[0] == '+' {
service = service[1:]
} else if service[0] == '-' {
neg = true
service = service[1:]
}
var n uint32
for _, d := range service {
if '0' <= d && d <= '9' {
d -= '0'
} else {
return 0, true
}
if n >= cutoff {
n = max
break
}
n *= 10
nn := n + uint32(d)
if nn < n || nn > max {
n = max
break
}
n = nn
}
if !neg && n >= cutoff {
port = int(cutoff - 1)
} else if neg && n > cutoff {
port = int(cutoff)
} else {
port = int(n)
}
if neg {
port = -port
}
return port, false
}

58
vendor/github.com/mjl-/adns/port_unix.go generated vendored Normal file
View file

@ -0,0 +1,58 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix || (js && wasm) || wasip1
// Read system port mappings from /etc/services
package adns
import (
"sync"
"github.com/mjl-/adns/internal/bytealg"
)
var onceReadServices sync.Once
func readServices() {
file, err := open("/etc/services")
if err != nil {
return
}
defer file.close()
for line, ok := file.readLine(); ok; line, ok = file.readLine() {
// "http 80/tcp www www-http # World Wide Web HTTP"
if i := bytealg.IndexByteString(line, '#'); i >= 0 {
line = line[:i]
}
f := getFields(line)
if len(f) < 2 {
continue
}
portnet := f[1] // "80/tcp"
port, j, ok := dtoi(portnet)
if !ok || port <= 0 || j >= len(portnet) || portnet[j] != '/' {
continue
}
netw := portnet[j+1:] // "tcp"
m, ok1 := services[netw]
if !ok1 {
m = make(map[string]int)
services[netw] = m
}
for i := 0; i < len(f); i++ {
if i != 1 { // f[1] was port/net
m[f[i]] = port
}
}
}
}
// goLookupPort is the native Go implementation of LookupPort.
func goLookupPort(network, service string) (port int, err error) {
onceReadServices.Do(readServices)
return lookupPortMap(network, service)
}

115
vendor/github.com/mjl-/adns/tlsa.go generated vendored Normal file
View file

@ -0,0 +1,115 @@
package adns
import (
"fmt"
)
// TLSAUsage indicates which certificate/public key verification must be done.
type TLSAUsage uint8
const (
// PKIX/WebPKI, certificate must be valid (name, expiry, signed by CA, etc) and
// signed by the trusted-anchor (TA) in this record.
TLSAUsagePKIXTA TLSAUsage = 0
// PKIX/WebPKI, certificate must be valid (name, expiry, signed by CA, etc) and
// match the certificate in the record.
TLSAUsagePKIXEE TLSAUsage = 1
// Certificate must be signed by trusted-anchor referenced in record, with matching
// name, non-expired, etc.
TLSAUsageDANETA TLSAUsage = 2
// Certificate must match the record. No further requirements on name, expiration
// or who signed it.
TLSAUsageDANEEE TLSAUsage = 3
)
// String returns the lower-case acronym of a usage, or "(unknown)" for
// unrecognized values.
func (u TLSAUsage) String() string {
switch u {
case TLSAUsagePKIXTA:
return "pkix-ta"
case TLSAUsagePKIXEE:
return "pkix-ee"
case TLSAUsageDANETA:
return "dane-ta"
case TLSAUsageDANEEE:
return "dane-ee"
}
return "(unknown)"
}
// TLSASelecter indicates the data the "certificate association" field is based on.
type TLSASelector uint8
const (
// DER-encoded x509 certificate.
TLSASelectorCert TLSASelector = 0
// DER-encoded subject public key info (SPKI), so only the public key and its type.
TLSASelectorSPKI TLSASelector = 1
)
// String returns the lower-case acronym of a selector, or "(unknown)" for
// unrecognized values.
func (s TLSASelector) String() string {
switch s {
case TLSASelectorCert:
return "cert"
case TLSASelectorSPKI:
return "spki"
}
return "(unknown)"
}
// TLSAMatchType indicates in which form the data as indicated by the selector
// is stored in the record as certificate association.
type TLSAMatchType uint8
const (
// Full data, e.g. a full DER-encoded SPKI or even certificate.
TLSAMatchTypeFull TLSAMatchType = 0
// SHA2-256-hashed data, either SPKI or certificate.
TLSAMatchTypeSHA256 TLSAMatchType = 1
// SHA2-512-hashed data.
TLSAMatchTypeSHA512 TLSAMatchType = 2
)
// String returns the lower-case acronym of a match type, or "(unknown)" for
// unrecognized values.
func (mt TLSAMatchType) String() string {
switch mt {
case TLSAMatchTypeFull:
return "full"
case TLSAMatchTypeSHA256:
return "sha2-256"
case TLSAMatchTypeSHA512:
return "sha2-512"
}
return "(unknown)"
}
// TLSA represents a TLSA DNS record.
type TLSA struct {
Usage TLSAUsage // Which validations must be performed.
Selector TLSASelector // What needs to be validated (full certificate or only public key).
MatchType TLSAMatchType // In which form the certificate/public key is stored in CertAssoc.
CertAssoc []byte // Certificate association data.
}
// Record returns a TLSA record value for inclusion in DNS. For example:
//
// 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
//
// A full record in a zone file may look like this:
//
// _25._tcp.example.com. IN TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
//
// This record is dane-ee (3), spki (1), sha2-256 (1), and the hexadecimal data
// is the sha2-256 hash.
func (r TLSA) Record() string {
return fmt.Sprintf("%d %d %d %x", r.Usage, r.Selector, r.MatchType, r.CertAssoc)
}
// String is like Record but prints both the acronym and code for each field.
func (r TLSA) String() string {
return fmt.Sprintf("%s(%d) %s(%d) %s(%d) %x", r.Usage, r.Usage, r.Selector, r.Selector, r.MatchType, r.MatchType, r.CertAssoc)
}

27
vendor/github.com/mjl-/autocert/LICENSE generated vendored Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
vendor/github.com/mjl-/autocert/README.txt generated vendored Normal file
View file

@ -0,0 +1 @@
From golang.org/x/crypto/acme/autocert, with Manager.GetPrivateKey added.

View file

@ -92,6 +92,15 @@ func defaultHostPolicy(context.Context, string) error {
return nil
}
// KeyType represents a private key type that can be used to request a certificate
// with Manager.GetPrivateKey. More may be added in the future.
type KeyType uint8
const (
KeyRSA2048 KeyType = 0
KeyECDSAP256 KeyType = 1
)
// Manager is a stateful certificate manager built on top of acme.Client.
// It obtains and refreshes certificates automatically using "tls-alpn-01"
// or "http-01" challenge types, as well as providing them to a TLS server
@ -148,6 +157,11 @@ type Manager struct {
// Mutating the field after the first call of GetCertificate method will have no effect.
Client *acme.Client
// GetPrivateKey is called to get a private key for a host (A-labels only, no
// trailing dot) when there is no valid certificate in the cache. If GetPrivateKey
// is nil, a new key is automatically generated.
GetPrivateKey func(host string, keyType KeyType) (crypto.Signer, error)
// Email optionally specifies a contact email address.
// This is used by CAs, such as Let's Encrypt, to notify about problems
// with issued certificates.
@ -632,7 +646,23 @@ func (m *Manager) certState(ck certKey) (*certState, error) {
err error
key crypto.Signer
)
if ck.isRSA {
if m.GetPrivateKey != nil {
if ck.isRSA {
key, err = m.GetPrivateKey(ck.domain, KeyRSA2048)
if err == nil {
if _, ok := key.(*rsa.PrivateKey); !ok {
err = fmt.Errorf("got %T, expected *rsa.PrivateKey", key)
}
}
} else {
key, err = m.GetPrivateKey(ck.domain, KeyECDSAP256)
if err == nil {
if _, ok := key.(*ecdsa.PrivateKey); !ok {
err = fmt.Errorf("got %T, expected *ecdsa.PrivateKey", key)
}
}
}
} else if ck.isRSA {
key, err = rsa.GenerateKey(rand.Reader, 2048)
} else {
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)

2711
vendor/golang.org/x/net/dns/dnsmessage/message.go generated vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,6 @@
package cpu
const cacheLineSize = 32
const cacheLineSize = 64
func initOptions() {}

View file

@ -5,7 +5,7 @@
package cpu
import (
"io/ioutil"
"os"
)
const (
@ -39,7 +39,7 @@ func readHWCAP() error {
return nil
}
buf, err := ioutil.ReadFile(procAuxv)
buf, err := os.ReadFile(procAuxv)
if err != nil {
// e.g. on android /proc/self/auxv is not accessible, so silently
// ignore the error and leave Initialized = false. On some

View file

@ -1,30 +0,0 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package unsafeheader contains header declarations for the Go runtime's
// slice and string implementations.
//
// This package allows x/sys to use types equivalent to
// reflect.SliceHeader and reflect.StringHeader without introducing
// a dependency on the (relatively heavy) "reflect" package.
package unsafeheader
import (
"unsafe"
)
// Slice is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may change in a later release.
type Slice struct {
Data unsafe.Pointer
Len int
Cap int
}
// String is the runtime representation of a string.
// It cannot be used safely or portably and its representation may change in a later release.
type String struct {
Data unsafe.Pointer
Len int
}

View file

@ -7,12 +7,6 @@
package unix
import "unsafe"
func ptrace(request int, pid int, addr uintptr, data uintptr) error {
return ptrace1(request, pid, addr, data)
}
func ptracePtr(request int, pid int, addr uintptr, data unsafe.Pointer) error {
return ptrace1Ptr(request, pid, addr, data)
}

View file

@ -7,12 +7,6 @@
package unix
import "unsafe"
func ptrace(request int, pid int, addr uintptr, data uintptr) (err error) {
return ENOTSUP
}
func ptracePtr(request int, pid int, addr uintptr, data unsafe.Pointer) (err error) {
return ENOTSUP
}

View file

@ -487,8 +487,6 @@ func Fsync(fd int) error {
//sys Unlinkat(dirfd int, path string, flags int) (err error)
//sys Ustat(dev int, ubuf *Ustat_t) (err error)
//sys write(fd int, p []byte) (n int, err error)
//sys readlen(fd int, p *byte, np int) (n int, err error) = read
//sys writelen(fd int, p *byte, np int) (n int, err error) = write
//sys Dup2(oldfd int, newfd int) (err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = posix_fadvise64

View file

@ -644,189 +644,3 @@ func SysctlKinfoProcSlice(name string, args ...int) ([]KinfoProc, error) {
//sys write(fd int, p []byte) (n int, err error)
//sys mmap(addr uintptr, length uintptr, prot int, flag int, fd int, pos int64) (ret uintptr, err error)
//sys munmap(addr uintptr, length uintptr) (err error)
//sys readlen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_READ
//sys writelen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_WRITE
/*
* Unimplemented
*/
// Profil
// Sigaction
// Sigprocmask
// Getlogin
// Sigpending
// Sigaltstack
// Ioctl
// Reboot
// Execve
// Vfork
// Sbrk
// Sstk
// Ovadvise
// Mincore
// Setitimer
// Swapon
// Select
// Sigsuspend
// Readv
// Writev
// Nfssvc
// Getfh
// Quotactl
// Csops
// Waitid
// Add_profil
// Kdebug_trace
// Sigreturn
// Atsocket
// Kqueue_from_portset_np
// Kqueue_portset
// Getattrlist
// Getdirentriesattr
// Searchfs
// Delete
// Copyfile
// Watchevent
// Waitevent
// Modwatch
// Fsctl
// Initgroups
// Posix_spawn
// Nfsclnt
// Fhopen
// Minherit
// Semsys
// Msgsys
// Shmsys
// Semctl
// Semget
// Semop
// Msgctl
// Msgget
// Msgsnd
// Msgrcv
// Shm_open
// Shm_unlink
// Sem_open
// Sem_close
// Sem_unlink
// Sem_wait
// Sem_trywait
// Sem_post
// Sem_getvalue
// Sem_init
// Sem_destroy
// Open_extended
// Umask_extended
// Stat_extended
// Lstat_extended
// Fstat_extended
// Chmod_extended
// Fchmod_extended
// Access_extended
// Settid
// Gettid
// Setsgroups
// Getsgroups
// Setwgroups
// Getwgroups
// Mkfifo_extended
// Mkdir_extended
// Identitysvc
// Shared_region_check_np
// Shared_region_map_np
// __pthread_mutex_destroy
// __pthread_mutex_init
// __pthread_mutex_lock
// __pthread_mutex_trylock
// __pthread_mutex_unlock
// __pthread_cond_init
// __pthread_cond_destroy
// __pthread_cond_broadcast
// __pthread_cond_signal
// Setsid_with_pid
// __pthread_cond_timedwait
// Aio_fsync
// Aio_return
// Aio_suspend
// Aio_cancel
// Aio_error
// Aio_read
// Aio_write
// Lio_listio
// __pthread_cond_wait
// Iopolicysys
// __pthread_kill
// __pthread_sigmask
// __sigwait
// __disable_threadsignal
// __pthread_markcancel
// __pthread_canceled
// __semwait_signal
// Proc_info
// sendfile
// Stat64_extended
// Lstat64_extended
// Fstat64_extended
// __pthread_chdir
// __pthread_fchdir
// Audit
// Auditon
// Getauid
// Setauid
// Getaudit
// Setaudit
// Getaudit_addr
// Setaudit_addr
// Auditctl
// Bsdthread_create
// Bsdthread_terminate
// Stack_snapshot
// Bsdthread_register
// Workq_open
// Workq_ops
// __mac_execve
// __mac_syscall
// __mac_get_file
// __mac_set_file
// __mac_get_link
// __mac_set_link
// __mac_get_proc
// __mac_set_proc
// __mac_get_fd
// __mac_set_fd
// __mac_get_pid
// __mac_get_lcid
// __mac_get_lctx
// __mac_set_lctx
// Setlcid
// Read_nocancel
// Write_nocancel
// Open_nocancel
// Close_nocancel
// Wait4_nocancel
// Recvmsg_nocancel
// Sendmsg_nocancel
// Recvfrom_nocancel
// Accept_nocancel
// Fcntl_nocancel
// Select_nocancel
// Fsync_nocancel
// Connect_nocancel
// Sigsuspend_nocancel
// Readv_nocancel
// Writev_nocancel
// Sendto_nocancel
// Pread_nocancel
// Pwrite_nocancel
// Waitid_nocancel
// Poll_nocancel
// Msgsnd_nocancel
// Msgrcv_nocancel
// Sem_wait_nocancel
// Aio_suspend_nocancel
// __sigwait_nocancel
// __semwait_signal_nocancel
// __mac_mount
// __mac_get_mount
// __mac_getfsstat

View file

@ -47,6 +47,5 @@ func Syscall9(num, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr,
//sys getfsstat(buf unsafe.Pointer, size uintptr, flags int) (n int, err error) = SYS_GETFSSTAT64
//sys Lstat(path string, stat *Stat_t) (err error) = SYS_LSTAT64
//sys ptrace1(request int, pid int, addr uintptr, data uintptr) (err error) = SYS_ptrace
//sys ptrace1Ptr(request int, pid int, addr unsafe.Pointer, data uintptr) (err error) = SYS_ptrace
//sys Stat(path string, stat *Stat_t) (err error) = SYS_STAT64
//sys Statfs(path string, stat *Statfs_t) (err error) = SYS_STATFS64

View file

@ -47,6 +47,5 @@ func Syscall9(num, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr,
//sys getfsstat(buf unsafe.Pointer, size uintptr, flags int) (n int, err error) = SYS_GETFSSTAT
//sys Lstat(path string, stat *Stat_t) (err error)
//sys ptrace1(request int, pid int, addr uintptr, data uintptr) (err error) = SYS_ptrace
//sys ptrace1Ptr(request int, pid int, addr unsafe.Pointer, data uintptr) (err error) = SYS_ptrace
//sys Stat(path string, stat *Stat_t) (err error)
//sys Statfs(path string, stat *Statfs_t) (err error)

View file

@ -343,203 +343,5 @@ func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err e
//sys write(fd int, p []byte) (n int, err error)
//sys mmap(addr uintptr, length uintptr, prot int, flag int, fd int, pos int64) (ret uintptr, err error)
//sys munmap(addr uintptr, length uintptr) (err error)
//sys readlen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_READ
//sys writelen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_WRITE
//sys accept4(fd int, rsa *RawSockaddrAny, addrlen *_Socklen, flags int) (nfd int, err error)
//sys utimensat(dirfd int, path string, times *[2]Timespec, flags int) (err error)
/*
* Unimplemented
* TODO(jsing): Update this list for DragonFly.
*/
// Profil
// Sigaction
// Sigprocmask
// Getlogin
// Sigpending
// Sigaltstack
// Reboot
// Execve
// Vfork
// Sbrk
// Sstk
// Ovadvise
// Mincore
// Setitimer
// Swapon
// Select
// Sigsuspend
// Readv
// Writev
// Nfssvc
// Getfh
// Quotactl
// Mount
// Csops
// Waitid
// Add_profil
// Kdebug_trace
// Sigreturn
// Atsocket
// Kqueue_from_portset_np
// Kqueue_portset
// Getattrlist
// Setattrlist
// Getdirentriesattr
// Searchfs
// Delete
// Copyfile
// Watchevent
// Waitevent
// Modwatch
// Getxattr
// Fgetxattr
// Setxattr
// Fsetxattr
// Removexattr
// Fremovexattr
// Listxattr
// Flistxattr
// Fsctl
// Initgroups
// Posix_spawn
// Nfsclnt
// Fhopen
// Minherit
// Semsys
// Msgsys
// Shmsys
// Semctl
// Semget
// Semop
// Msgctl
// Msgget
// Msgsnd
// Msgrcv
// Shmat
// Shmctl
// Shmdt
// Shmget
// Shm_open
// Shm_unlink
// Sem_open
// Sem_close
// Sem_unlink
// Sem_wait
// Sem_trywait
// Sem_post
// Sem_getvalue
// Sem_init
// Sem_destroy
// Open_extended
// Umask_extended
// Stat_extended
// Lstat_extended
// Fstat_extended
// Chmod_extended
// Fchmod_extended
// Access_extended
// Settid
// Gettid
// Setsgroups
// Getsgroups
// Setwgroups
// Getwgroups
// Mkfifo_extended
// Mkdir_extended
// Identitysvc
// Shared_region_check_np
// Shared_region_map_np
// __pthread_mutex_destroy
// __pthread_mutex_init
// __pthread_mutex_lock
// __pthread_mutex_trylock
// __pthread_mutex_unlock
// __pthread_cond_init
// __pthread_cond_destroy
// __pthread_cond_broadcast
// __pthread_cond_signal
// Setsid_with_pid
// __pthread_cond_timedwait
// Aio_fsync
// Aio_return
// Aio_suspend
// Aio_cancel
// Aio_error
// Aio_read
// Aio_write
// Lio_listio
// __pthread_cond_wait
// Iopolicysys
// __pthread_kill
// __pthread_sigmask
// __sigwait
// __disable_threadsignal
// __pthread_markcancel
// __pthread_canceled
// __semwait_signal
// Proc_info
// Stat64_extended
// Lstat64_extended
// Fstat64_extended
// __pthread_chdir
// __pthread_fchdir
// Audit
// Auditon
// Getauid
// Setauid
// Getaudit
// Setaudit
// Getaudit_addr
// Setaudit_addr
// Auditctl
// Bsdthread_create
// Bsdthread_terminate
// Stack_snapshot
// Bsdthread_register
// Workq_open
// Workq_ops
// __mac_execve
// __mac_syscall
// __mac_get_file
// __mac_set_file
// __mac_get_link
// __mac_set_link
// __mac_get_proc
// __mac_set_proc
// __mac_get_fd
// __mac_set_fd
// __mac_get_pid
// __mac_get_lcid
// __mac_get_lctx
// __mac_set_lctx
// Setlcid
// Read_nocancel
// Write_nocancel
// Open_nocancel
// Close_nocancel
// Wait4_nocancel
// Recvmsg_nocancel
// Sendmsg_nocancel
// Recvfrom_nocancel
// Accept_nocancel
// Fcntl_nocancel
// Select_nocancel
// Fsync_nocancel
// Connect_nocancel
// Sigsuspend_nocancel
// Readv_nocancel
// Writev_nocancel
// Sendto_nocancel
// Pread_nocancel
// Pwrite_nocancel
// Waitid_nocancel
// Msgsnd_nocancel
// Msgrcv_nocancel
// Sem_wait_nocancel
// Aio_suspend_nocancel
// __sigwait_nocancel
// __semwait_signal_nocancel
// __mac_mount
// __mac_get_mount
// __mac_getfsstat

View file

@ -449,197 +449,5 @@ func Dup3(oldfd, newfd, flags int) error {
//sys write(fd int, p []byte) (n int, err error)
//sys mmap(addr uintptr, length uintptr, prot int, flag int, fd int, pos int64) (ret uintptr, err error)
//sys munmap(addr uintptr, length uintptr) (err error)
//sys readlen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_READ
//sys writelen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_WRITE
//sys accept4(fd int, rsa *RawSockaddrAny, addrlen *_Socklen, flags int) (nfd int, err error)
//sys utimensat(dirfd int, path string, times *[2]Timespec, flags int) (err error)
/*
* Unimplemented
*/
// Profil
// Sigaction
// Sigprocmask
// Getlogin
// Sigpending
// Sigaltstack
// Ioctl
// Reboot
// Execve
// Vfork
// Sbrk
// Sstk
// Ovadvise
// Mincore
// Setitimer
// Swapon
// Select
// Sigsuspend
// Readv
// Writev
// Nfssvc
// Getfh
// Quotactl
// Mount
// Csops
// Waitid
// Add_profil
// Kdebug_trace
// Sigreturn
// Atsocket
// Kqueue_from_portset_np
// Kqueue_portset
// Getattrlist
// Setattrlist
// Getdents
// Getdirentriesattr
// Searchfs
// Delete
// Copyfile
// Watchevent
// Waitevent
// Modwatch
// Fsctl
// Initgroups
// Posix_spawn
// Nfsclnt
// Fhopen
// Minherit
// Semsys
// Msgsys
// Shmsys
// Semctl
// Semget
// Semop
// Msgctl
// Msgget
// Msgsnd
// Msgrcv
// Shmat
// Shmctl
// Shmdt
// Shmget
// Shm_open
// Shm_unlink
// Sem_open
// Sem_close
// Sem_unlink
// Sem_wait
// Sem_trywait
// Sem_post
// Sem_getvalue
// Sem_init
// Sem_destroy
// Open_extended
// Umask_extended
// Stat_extended
// Lstat_extended
// Fstat_extended
// Chmod_extended
// Fchmod_extended
// Access_extended
// Settid
// Gettid
// Setsgroups
// Getsgroups
// Setwgroups
// Getwgroups
// Mkfifo_extended
// Mkdir_extended
// Identitysvc
// Shared_region_check_np
// Shared_region_map_np
// __pthread_mutex_destroy
// __pthread_mutex_init
// __pthread_mutex_lock
// __pthread_mutex_trylock
// __pthread_mutex_unlock
// __pthread_cond_init
// __pthread_cond_destroy
// __pthread_cond_broadcast
// __pthread_cond_signal
// Setsid_with_pid
// __pthread_cond_timedwait
// Aio_fsync
// Aio_return
// Aio_suspend
// Aio_cancel
// Aio_error
// Aio_read
// Aio_write
// Lio_listio
// __pthread_cond_wait
// Iopolicysys
// __pthread_kill
// __pthread_sigmask
// __sigwait
// __disable_threadsignal
// __pthread_markcancel
// __pthread_canceled
// __semwait_signal
// Proc_info
// Stat64_extended
// Lstat64_extended
// Fstat64_extended
// __pthread_chdir
// __pthread_fchdir
// Audit
// Auditon
// Getauid
// Setauid
// Getaudit
// Setaudit
// Getaudit_addr
// Setaudit_addr
// Auditctl
// Bsdthread_create
// Bsdthread_terminate
// Stack_snapshot
// Bsdthread_register
// Workq_open
// Workq_ops
// __mac_execve
// __mac_syscall
// __mac_get_file
// __mac_set_file
// __mac_get_link
// __mac_set_link
// __mac_get_proc
// __mac_set_proc
// __mac_get_fd
// __mac_set_fd
// __mac_get_pid
// __mac_get_lcid
// __mac_get_lctx
// __mac_set_lctx
// Setlcid
// Read_nocancel
// Write_nocancel
// Open_nocancel
// Close_nocancel
// Wait4_nocancel
// Recvmsg_nocancel
// Sendmsg_nocancel
// Recvfrom_nocancel
// Accept_nocancel
// Fcntl_nocancel
// Select_nocancel
// Fsync_nocancel
// Connect_nocancel
// Sigsuspend_nocancel
// Readv_nocancel
// Writev_nocancel
// Sendto_nocancel
// Pread_nocancel
// Pwrite_nocancel
// Waitid_nocancel
// Poll_nocancel
// Msgsnd_nocancel
// Msgrcv_nocancel
// Sem_wait_nocancel
// Aio_suspend_nocancel
// __sigwait_nocancel
// __semwait_signal_nocancel
// __mac_mount
// __mac_get_mount
// __mac_getfsstat

View file

@ -693,10 +693,10 @@ type SockaddrALG struct {
func (sa *SockaddrALG) sockaddr() (unsafe.Pointer, _Socklen, error) {
// Leave room for NUL byte terminator.
if len(sa.Type) > 13 {
if len(sa.Type) > len(sa.raw.Type)-1 {
return nil, 0, EINVAL
}
if len(sa.Name) > 63 {
if len(sa.Name) > len(sa.raw.Name)-1 {
return nil, 0, EINVAL
}
@ -704,17 +704,8 @@ func (sa *SockaddrALG) sockaddr() (unsafe.Pointer, _Socklen, error) {
sa.raw.Feat = sa.Feature
sa.raw.Mask = sa.Mask
typ, err := ByteSliceFromString(sa.Type)
if err != nil {
return nil, 0, err
}
name, err := ByteSliceFromString(sa.Name)
if err != nil {
return nil, 0, err
}
copy(sa.raw.Type[:], typ)
copy(sa.raw.Name[:], name)
copy(sa.raw.Type[:], sa.Type)
copy(sa.raw.Name[:], sa.Name)
return unsafe.Pointer(&sa.raw), SizeofSockaddrALG, nil
}
@ -1988,8 +1979,6 @@ func Signalfd(fd int, sigmask *Sigset_t, flags int) (newfd int, err error) {
//sys Unshare(flags int) (err error)
//sys write(fd int, p []byte) (n int, err error)
//sys exitThread(code int) (err error) = SYS_EXIT
//sys readlen(fd int, p *byte, np int) (n int, err error) = SYS_READ
//sys writelen(fd int, p *byte, np int) (n int, err error) = SYS_WRITE
//sys readv(fd int, iovs []Iovec) (n int, err error) = SYS_READV
//sys writev(fd int, iovs []Iovec) (n int, err error) = SYS_WRITEV
//sys preadv(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr) (n int, err error) = SYS_PREADV
@ -2493,99 +2482,3 @@ func SchedGetAttr(pid int, flags uint) (*SchedAttr, error) {
}
return attr, nil
}
/*
* Unimplemented
*/
// AfsSyscall
// ArchPrctl
// Brk
// ClockNanosleep
// ClockSettime
// Clone
// EpollCtlOld
// EpollPwait
// EpollWaitOld
// Execve
// Fork
// Futex
// GetKernelSyms
// GetMempolicy
// GetRobustList
// GetThreadArea
// Getpmsg
// IoCancel
// IoDestroy
// IoGetevents
// IoSetup
// IoSubmit
// IoprioGet
// IoprioSet
// KexecLoad
// LookupDcookie
// Mbind
// MigratePages
// Mincore
// ModifyLdt
// Mount
// MovePages
// MqGetsetattr
// MqNotify
// MqOpen
// MqTimedreceive
// MqTimedsend
// MqUnlink
// Msgctl
// Msgget
// Msgrcv
// Msgsnd
// Nfsservctl
// Personality
// Pselect6
// Ptrace
// Putpmsg
// Quotactl
// Readahead
// Readv
// RemapFilePages
// RestartSyscall
// RtSigaction
// RtSigpending
// RtSigqueueinfo
// RtSigreturn
// RtSigsuspend
// RtSigtimedwait
// SchedGetPriorityMax
// SchedGetPriorityMin
// SchedGetparam
// SchedGetscheduler
// SchedRrGetInterval
// SchedSetparam
// SchedYield
// Security
// Semctl
// Semget
// Semop
// Semtimedop
// SetMempolicy
// SetRobustList
// SetThreadArea
// SetTidAddress
// Sigaltstack
// Swapoff
// Swapon
// Sysfs
// TimerCreate
// TimerDelete
// TimerGetoverrun
// TimerGettime
// TimerSettime
// Tkill (obsolete)
// Tuxcall
// Umount2
// Uselib
// Utimensat
// Vfork
// Vhangup
// Vserver
// _Sysctl

View file

@ -356,8 +356,6 @@ func Statvfs(path string, buf *Statvfs_t) (err error) {
//sys write(fd int, p []byte) (n int, err error)
//sys mmap(addr uintptr, length uintptr, prot int, flag int, fd int, pos int64) (ret uintptr, err error)
//sys munmap(addr uintptr, length uintptr) (err error)
//sys readlen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_READ
//sys writelen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_WRITE
//sys utimensat(dirfd int, path string, times *[2]Timespec, flags int) (err error)
const (
@ -371,262 +369,3 @@ const (
func mremap(oldaddr uintptr, oldlength uintptr, newlength uintptr, flags int, newaddr uintptr) (uintptr, error) {
return mremapNetBSD(oldaddr, oldlength, newaddr, newlength, flags)
}
/*
* Unimplemented
*/
// ____semctl13
// __clone
// __fhopen40
// __fhstat40
// __fhstatvfs140
// __fstat30
// __getcwd
// __getfh30
// __getlogin
// __lstat30
// __mount50
// __msgctl13
// __msync13
// __ntp_gettime30
// __posix_chown
// __posix_fchown
// __posix_lchown
// __posix_rename
// __setlogin
// __shmctl13
// __sigaction_sigtramp
// __sigaltstack14
// __sigpending14
// __sigprocmask14
// __sigsuspend14
// __sigtimedwait
// __stat30
// __syscall
// __vfork14
// _ksem_close
// _ksem_destroy
// _ksem_getvalue
// _ksem_init
// _ksem_open
// _ksem_post
// _ksem_trywait
// _ksem_unlink
// _ksem_wait
// _lwp_continue
// _lwp_create
// _lwp_ctl
// _lwp_detach
// _lwp_exit
// _lwp_getname
// _lwp_getprivate
// _lwp_kill
// _lwp_park
// _lwp_self
// _lwp_setname
// _lwp_setprivate
// _lwp_suspend
// _lwp_unpark
// _lwp_unpark_all
// _lwp_wait
// _lwp_wakeup
// _pset_bind
// _sched_getaffinity
// _sched_getparam
// _sched_setaffinity
// _sched_setparam
// acct
// aio_cancel
// aio_error
// aio_fsync
// aio_read
// aio_return
// aio_suspend
// aio_write
// break
// clock_getres
// clock_gettime
// clock_settime
// compat_09_ogetdomainname
// compat_09_osetdomainname
// compat_09_ouname
// compat_10_omsgsys
// compat_10_osemsys
// compat_10_oshmsys
// compat_12_fstat12
// compat_12_getdirentries
// compat_12_lstat12
// compat_12_msync
// compat_12_oreboot
// compat_12_oswapon
// compat_12_stat12
// compat_13_sigaction13
// compat_13_sigaltstack13
// compat_13_sigpending13
// compat_13_sigprocmask13
// compat_13_sigreturn13
// compat_13_sigsuspend13
// compat_14___semctl
// compat_14_msgctl
// compat_14_shmctl
// compat_16___sigaction14
// compat_16___sigreturn14
// compat_20_fhstatfs
// compat_20_fstatfs
// compat_20_getfsstat
// compat_20_statfs
// compat_30___fhstat30
// compat_30___fstat13
// compat_30___lstat13
// compat_30___stat13
// compat_30_fhopen
// compat_30_fhstat
// compat_30_fhstatvfs1
// compat_30_getdents
// compat_30_getfh
// compat_30_ntp_gettime
// compat_30_socket
// compat_40_mount
// compat_43_fstat43
// compat_43_lstat43
// compat_43_oaccept
// compat_43_ocreat
// compat_43_oftruncate
// compat_43_ogetdirentries
// compat_43_ogetdtablesize
// compat_43_ogethostid
// compat_43_ogethostname
// compat_43_ogetkerninfo
// compat_43_ogetpagesize
// compat_43_ogetpeername
// compat_43_ogetrlimit
// compat_43_ogetsockname
// compat_43_okillpg
// compat_43_olseek
// compat_43_ommap
// compat_43_oquota
// compat_43_orecv
// compat_43_orecvfrom
// compat_43_orecvmsg
// compat_43_osend
// compat_43_osendmsg
// compat_43_osethostid
// compat_43_osethostname
// compat_43_osigblock
// compat_43_osigsetmask
// compat_43_osigstack
// compat_43_osigvec
// compat_43_otruncate
// compat_43_owait
// compat_43_stat43
// execve
// extattr_delete_fd
// extattr_delete_file
// extattr_delete_link
// extattr_get_fd
// extattr_get_file
// extattr_get_link
// extattr_list_fd
// extattr_list_file
// extattr_list_link
// extattr_set_fd
// extattr_set_file
// extattr_set_link
// extattrctl
// fchroot
// fdatasync
// fgetxattr
// fktrace
// flistxattr
// fork
// fremovexattr
// fsetxattr
// fstatvfs1
// fsync_range
// getcontext
// getitimer
// getvfsstat
// getxattr
// ktrace
// lchflags
// lchmod
// lfs_bmapv
// lfs_markv
// lfs_segclean
// lfs_segwait
// lgetxattr
// lio_listio
// listxattr
// llistxattr
// lremovexattr
// lseek
// lsetxattr
// lutimes
// madvise
// mincore
// minherit
// modctl
// mq_close
// mq_getattr
// mq_notify
// mq_open
// mq_receive
// mq_send
// mq_setattr
// mq_timedreceive
// mq_timedsend
// mq_unlink
// msgget
// msgrcv
// msgsnd
// nfssvc
// ntp_adjtime
// pmc_control
// pmc_get_info
// pollts
// preadv
// profil
// pselect
// pset_assign
// pset_create
// pset_destroy
// ptrace
// pwritev
// quotactl
// rasctl
// readv
// reboot
// removexattr
// sa_enable
// sa_preempt
// sa_register
// sa_setconcurrency
// sa_stacks
// sa_yield
// sbrk
// sched_yield
// semconfig
// semget
// semop
// setcontext
// setitimer
// setxattr
// shmat
// shmdt
// shmget
// sstk
// statvfs1
// swapctl
// sysarch
// syscall
// timer_create
// timer_delete
// timer_getoverrun
// timer_gettime
// timer_settime
// undelete
// utrace
// uuidgen
// vadvise
// vfork
// writev

View file

@ -326,78 +326,4 @@ func Uname(uname *Utsname) error {
//sys write(fd int, p []byte) (n int, err error)
//sys mmap(addr uintptr, length uintptr, prot int, flag int, fd int, pos int64) (ret uintptr, err error)
//sys munmap(addr uintptr, length uintptr) (err error)
//sys readlen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_READ
//sys writelen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_WRITE
//sys utimensat(dirfd int, path string, times *[2]Timespec, flags int) (err error)
/*
* Unimplemented
*/
// __getcwd
// __semctl
// __syscall
// __sysctl
// adjfreq
// break
// clock_getres
// clock_gettime
// clock_settime
// closefrom
// execve
// fhopen
// fhstat
// fhstatfs
// fork
// futimens
// getfh
// getgid
// getitimer
// getlogin
// getthrid
// ktrace
// lfs_bmapv
// lfs_markv
// lfs_segclean
// lfs_segwait
// mincore
// minherit
// mount
// mquery
// msgctl
// msgget
// msgrcv
// msgsnd
// nfssvc
// nnpfspioctl
// preadv
// profil
// pwritev
// quotactl
// readv
// reboot
// renameat
// rfork
// sched_yield
// semget
// semop
// setgroups
// setitimer
// setsockopt
// shmat
// shmctl
// shmdt
// shmget
// sigaction
// sigaltstack
// sigpending
// sigprocmask
// sigreturn
// sigsuspend
// sysarch
// syscall
// threxit
// thrsigdivert
// thrsleep
// thrwakeup
// vfork
// writev

View file

@ -698,24 +698,6 @@ func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err e
//sys setsockopt(s int, level int, name int, val unsafe.Pointer, vallen uintptr) (err error) = libsocket.setsockopt
//sys recvfrom(fd int, p []byte, flags int, from *RawSockaddrAny, fromlen *_Socklen) (n int, err error) = libsocket.recvfrom
func readlen(fd int, buf *byte, nbuf int) (n int, err error) {
r0, _, e1 := sysvicall6(uintptr(unsafe.Pointer(&procread)), 3, uintptr(fd), uintptr(unsafe.Pointer(buf)), uintptr(nbuf), 0, 0, 0)
n = int(r0)
if e1 != 0 {
err = e1
}
return
}
func writelen(fd int, buf *byte, nbuf int) (n int, err error) {
r0, _, e1 := sysvicall6(uintptr(unsafe.Pointer(&procwrite)), 3, uintptr(fd), uintptr(unsafe.Pointer(buf)), uintptr(nbuf), 0, 0, 0)
n = int(r0)
if e1 != 0 {
err = e1
}
return
}
// Event Ports
type fileObjCookie struct {

View file

@ -192,7 +192,6 @@ func (cmsg *Cmsghdr) SetLen(length int) {
//sys fcntl(fd int, cmd int, arg int) (val int, err error)
//sys read(fd int, p []byte) (n int, err error)
//sys readlen(fd int, buf *byte, nbuf int) (n int, err error) = SYS_READ
//sys write(fd int, p []byte) (n int, err error)
//sys accept(s int, rsa *RawSockaddrAny, addrlen *_Socklen) (fd int, err error) = SYS___ACCEPT_A

View file

@ -2421,6 +2421,15 @@ const (
PR_PAC_GET_ENABLED_KEYS = 0x3d
PR_PAC_RESET_KEYS = 0x36
PR_PAC_SET_ENABLED_KEYS = 0x3c
PR_RISCV_V_GET_CONTROL = 0x46
PR_RISCV_V_SET_CONTROL = 0x45
PR_RISCV_V_VSTATE_CTRL_CUR_MASK = 0x3
PR_RISCV_V_VSTATE_CTRL_DEFAULT = 0x0
PR_RISCV_V_VSTATE_CTRL_INHERIT = 0x10
PR_RISCV_V_VSTATE_CTRL_MASK = 0x1f
PR_RISCV_V_VSTATE_CTRL_NEXT_MASK = 0xc
PR_RISCV_V_VSTATE_CTRL_OFF = 0x1
PR_RISCV_V_VSTATE_CTRL_ON = 0x2
PR_SCHED_CORE = 0x3e
PR_SCHED_CORE_CREATE = 0x1
PR_SCHED_CORE_GET = 0x0

Some files were not shown because too many files have changed in this diff Show more