2023-01-30 16:27:06 +03:00
|
|
|
// Package dns helps parse internationalized domain names (IDNA), canonicalize
|
|
|
|
// names and provides a strict and metrics-keeping logging DNS resolver.
|
|
|
|
package dns
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2024-11-24 15:30:29 +03:00
|
|
|
"net"
|
2023-01-30 16:27:06 +03:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"golang.org/x/net/idna"
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
|
|
|
|
"github.com/mjl-/adns"
|
2023-01-30 16:27:06 +03:00
|
|
|
)
|
|
|
|
|
2023-12-05 23:13:57 +03:00
|
|
|
// Pedantic enables stricter parsing.
|
|
|
|
var Pedantic bool
|
|
|
|
|
2023-10-25 14:01:11 +03:00
|
|
|
var (
|
|
|
|
errTrailingDot = errors.New("dns name has trailing dot")
|
|
|
|
errUnderscore = errors.New("domain name with underscore")
|
|
|
|
errIDNA = errors.New("idna")
|
|
|
|
)
|
2023-01-30 16:27:06 +03:00
|
|
|
|
|
|
|
// Domain is a domain name, with one or more labels, with at least an ASCII
|
|
|
|
// representation, and for IDNA non-ASCII domains a unicode representation.
|
2023-12-12 17:47:26 +03:00
|
|
|
// The ASCII string must be used for DNS lookups. The strings do not have a
|
|
|
|
// trailing dot. When using with StrictResolver, add the trailing dot.
|
2023-01-30 16:27:06 +03:00
|
|
|
type Domain struct {
|
|
|
|
// A non-unicode domain, e.g. with A-labels (xn--...) or NR-LDH (non-reserved
|
2023-12-12 17:47:26 +03:00
|
|
|
// letters/digits/hyphens) labels. Always in lower case. No trailing dot.
|
2023-01-30 16:27:06 +03:00
|
|
|
ASCII string
|
|
|
|
|
2024-03-08 23:08:40 +03:00
|
|
|
// Name as U-labels, in Unicode NFC. Empty if this is an ASCII-only domain. No
|
|
|
|
// trailing dot.
|
2023-01-30 16:27:06 +03:00
|
|
|
Unicode string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the unicode name if set, otherwise the ASCII name.
|
|
|
|
func (d Domain) Name() string {
|
|
|
|
if d.Unicode != "" {
|
|
|
|
return d.Unicode
|
|
|
|
}
|
|
|
|
return d.ASCII
|
|
|
|
}
|
|
|
|
|
|
|
|
// XName is like Name, but only returns a unicode name when utf8 is true.
|
|
|
|
func (d Domain) XName(utf8 bool) string {
|
|
|
|
if utf8 && d.Unicode != "" {
|
|
|
|
return d.Unicode
|
|
|
|
}
|
|
|
|
return d.ASCII
|
|
|
|
}
|
|
|
|
|
|
|
|
// ASCIIExtra returns the ASCII version of the domain name if smtputf8 is true and
|
|
|
|
// this is a unicode domain name. Otherwise it returns an empty string.
|
|
|
|
//
|
|
|
|
// This function is used to add the punycode name in a comment to SMTP message
|
|
|
|
// headers, e.g. Received and Authentication-Results.
|
|
|
|
func (d Domain) ASCIIExtra(smtputf8 bool) string {
|
|
|
|
if smtputf8 && d.Unicode != "" {
|
|
|
|
return d.ASCII
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// Strings returns a human-readable string.
|
|
|
|
// For IDNA names, the string contains both the unicode and ASCII name.
|
|
|
|
func (d Domain) String() string {
|
2023-03-09 22:18:34 +03:00
|
|
|
return d.LogString()
|
|
|
|
}
|
|
|
|
|
|
|
|
// LogString returns a domain for logging.
|
2023-12-12 17:47:26 +03:00
|
|
|
// For IDNA names, the string is the slash-separated Unicode and ASCII name.
|
|
|
|
// For ASCII-only domain names, just the ASCII string is returned.
|
2023-03-09 22:18:34 +03:00
|
|
|
func (d Domain) LogString() string {
|
2023-01-30 16:27:06 +03:00
|
|
|
if d.Unicode == "" {
|
|
|
|
return d.ASCII
|
|
|
|
}
|
|
|
|
return d.Unicode + "/" + d.ASCII
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsZero returns if this is an empty Domain.
|
|
|
|
func (d Domain) IsZero() bool {
|
|
|
|
return d == Domain{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseDomain parses a domain name that can consist of ASCII-only labels or U
|
|
|
|
// labels (unicode).
|
|
|
|
// Names are IDN-canonicalized and lower-cased.
|
|
|
|
// Characters in unicode can be replaced by equivalents. E.g. "Ⓡ" to "r". This
|
2024-03-08 23:08:40 +03:00
|
|
|
// means you should only compare parsed domain names, never unparsed strings
|
|
|
|
// directly.
|
2023-01-30 16:27:06 +03:00
|
|
|
func ParseDomain(s string) (Domain, error) {
|
|
|
|
if strings.HasSuffix(s, ".") {
|
|
|
|
return Domain{}, errTrailingDot
|
|
|
|
}
|
2023-10-25 14:01:11 +03:00
|
|
|
|
2023-01-30 16:27:06 +03:00
|
|
|
ascii, err := idna.Lookup.ToASCII(s)
|
|
|
|
if err != nil {
|
2023-10-25 14:01:11 +03:00
|
|
|
return Domain{}, fmt.Errorf("%w: to ascii: %v", errIDNA, err)
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
unicode, err := idna.Lookup.ToUnicode(s)
|
|
|
|
if err != nil {
|
2023-10-25 14:01:11 +03:00
|
|
|
return Domain{}, fmt.Errorf("%w: to unicode: %w", errIDNA, err)
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
// todo: should we cause errors for unicode domains that were not in
|
|
|
|
// canonical form? we are now accepting all kinds of obscure spellings
|
|
|
|
// for even a basic ASCII domain name.
|
|
|
|
// Also see https://daniel.haxx.se/blog/2022/12/14/idn-is-crazy/
|
|
|
|
if ascii == unicode {
|
|
|
|
return Domain{ascii, ""}, nil
|
|
|
|
}
|
|
|
|
return Domain{ascii, unicode}, nil
|
|
|
|
}
|
|
|
|
|
2023-10-25 14:01:11 +03:00
|
|
|
// ParseDomainLax parses a domain like ParseDomain, but allows labels with
|
|
|
|
// underscores if the entire domain name is ASCII-only non-IDNA and Pedantic mode
|
|
|
|
// is not enabled. Used for interoperability, e.g. domains may specify MX
|
|
|
|
// targets with underscores.
|
|
|
|
func ParseDomainLax(s string) (Domain, error) {
|
2023-12-05 23:13:57 +03:00
|
|
|
if Pedantic || !strings.Contains(s, "_") {
|
2023-10-25 14:01:11 +03:00
|
|
|
return ParseDomain(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there is any non-ASCII, this is certainly not an A-label-only domain.
|
|
|
|
s = strings.ToLower(s)
|
|
|
|
for _, c := range s {
|
|
|
|
if c >= 0x80 {
|
|
|
|
return Domain{}, fmt.Errorf("%w: underscore and non-ascii not allowed", errUnderscore)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try parsing with underscores replaced with allowed ASCII character.
|
|
|
|
// If that's not valid, the version with underscore isn't either.
|
|
|
|
repl := strings.ReplaceAll(s, "_", "a")
|
|
|
|
d, err := ParseDomain(repl)
|
|
|
|
if err != nil {
|
|
|
|
return Domain{}, fmt.Errorf("%w: %v", errUnderscore, err)
|
|
|
|
}
|
|
|
|
// If we found an IDNA domain, we're not going to allow it.
|
|
|
|
if d.Unicode != "" {
|
|
|
|
return Domain{}, fmt.Errorf("%w: idna domain with underscores not allowed", errUnderscore)
|
|
|
|
}
|
|
|
|
// Just to be safe, ensure no unexpected conversions happened.
|
|
|
|
if d.ASCII != repl {
|
|
|
|
return Domain{}, fmt.Errorf("%w: underscores and non-canonical names not allowed", errUnderscore)
|
|
|
|
}
|
|
|
|
return Domain{ASCII: s}, nil
|
|
|
|
}
|
|
|
|
|
2024-11-24 15:30:29 +03:00
|
|
|
// IsNotFound returns whether an error is an adns.DNSError or net.DNSError with
|
|
|
|
// IsNotFound set.
|
|
|
|
//
|
2023-01-30 16:27:06 +03:00
|
|
|
// IsNotFound means the requested type does not exist for the given domain (a
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
// nodata or nxdomain response). It doesn't not necessarily mean no other types for
|
|
|
|
// that name exist.
|
2023-01-30 16:27:06 +03:00
|
|
|
//
|
|
|
|
// 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.
|
2023-12-12 17:47:26 +03:00
|
|
|
// The adns resolver (just like the Go resolver) returns an IsNotFound error for
|
|
|
|
// both cases, there is no need to explicitly check for zero entries.
|
2023-01-30 16:27:06 +03:00
|
|
|
func IsNotFound(err error) bool {
|
2024-11-24 15:30:29 +03:00
|
|
|
var adnsErr *adns.DNSError
|
|
|
|
var dnsErr *net.DNSError
|
|
|
|
return err != nil && (errors.As(err, &adnsErr) && adnsErr.IsNotFound || errors.As(err, &dnsErr) && dnsErr.IsNotFound)
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|