Expose Extended DNS Errors (#6906)

If the resolver provides EDE (https://www.rfc-editor.org/rfc/rfc8914),
Boulder will automatically expose it in the error message. Note that
most error messages contain the error RCODE (NXDOMAIN, SERVFAIL, etc),
when there is EDE present we omit it in the interest of brevity. In
practice it will almost always be SERVFAIL, and the extended error
information is more informative anyhow.

This will have no effect in production until we configure Unbound to
enable EDE.

Fixes #6875.

---------

Co-authored-by: Matthew McPherrin <mattm@letsencrypt.org>
This commit is contained in:
Jacob Hoffman-Andrews 2023-05-18 20:43:00 -07:00 committed by GitHub
parent 0a65e87c1b
commit 4f171604fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 29 deletions

View File

@ -9,3 +9,4 @@ strat
te
uint
vas
ede

View File

@ -415,11 +415,9 @@ func (dnsClient *impl) LookupTXT(ctx context.Context, hostname string) ([]string
var txt []string
dnsType := dns.TypeTXT
r, err := dnsClient.exchangeOne(ctx, hostname, dnsType)
if err != nil {
return nil, &Error{dnsType, hostname, err, -1}
}
if r.Rcode != dns.RcodeSuccess {
return nil, &Error{dnsType, hostname, nil, r.Rcode}
errWrap := wrapErr(dnsType, hostname, r, err)
if errWrap != nil {
return nil, errWrap
}
for _, answer := range r.Answer {
@ -453,11 +451,9 @@ func isPrivateV6(ip net.IP) bool {
func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uint16) ([]dns.RR, error) {
resp, err := dnsClient.exchangeOne(ctx, hostname, ipType)
if err != nil {
return nil, &Error{ipType, hostname, err, -1}
}
if resp.Rcode != dns.RcodeSuccess {
return nil, &Error{ipType, hostname, nil, resp.Rcode}
errWrap := wrapErr(ipType, hostname, resp, err)
if errWrap != nil {
return nil, errWrap
}
return resp.Answer, nil
}
@ -533,12 +529,9 @@ func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]net.I
func (dnsClient *impl) LookupCAA(ctx context.Context, hostname string) ([]*dns.CAA, string, error) {
dnsType := dns.TypeCAA
r, err := dnsClient.exchangeOne(ctx, hostname, dnsType)
if err != nil {
return nil, "", &Error{dnsType, hostname, err, -1}
}
if r.Rcode == dns.RcodeServerFailure {
return nil, "", &Error{dnsType, hostname, nil, r.Rcode}
errWrap := wrapErr(dnsType, hostname, r, err)
if errWrap != nil {
return nil, "", errWrap
}
var CAAs []*dns.CAA

View File

@ -394,7 +394,7 @@ func TestDNSNXDOMAIN(t *testing.T) {
test.AssertContains(t, err.Error(), "NXDOMAIN looking up AAAA for")
_, err = obj.LookupTXT(context.Background(), hostname)
expected := &Error{dns.TypeTXT, hostname, nil, dns.RcodeNameError}
expected := Error{dns.TypeTXT, hostname, nil, dns.RcodeNameError, nil}
test.AssertDeepEquals(t, err, expected)
}

View File

@ -73,7 +73,7 @@ func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]net.IP
return []net.IP{}, nil
}
if hostname == "always.timeout" {
return []net.IP{}, &Error{dns.TypeA, "always.timeout", makeTimeoutError(), -1}
return []net.IP{}, &Error{dns.TypeA, "always.timeout", makeTimeoutError(), -1, nil}
}
if hostname == "always.error" {
err := &net.OpError{
@ -86,7 +86,7 @@ func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]net.IP
m.AuthenticatedData = true
m.SetEdns0(4096, false)
logDNSError(mock.Log, "mock.server", hostname, m, nil, err)
return []net.IP{}, &Error{dns.TypeA, hostname, err, -1}
return []net.IP{}, &Error{dns.TypeA, hostname, err, -1, nil}
}
if hostname == "id.mismatch" {
err := dns.ErrId
@ -100,7 +100,7 @@ func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]net.IP
record.A = net.ParseIP("127.0.0.1")
r.Answer = append(r.Answer, record)
logDNSError(mock.Log, "mock.server", hostname, m, r, err)
return []net.IP{}, &Error{dns.TypeA, hostname, err, -1}
return []net.IP{}, &Error{dns.TypeA, hostname, err, -1, nil}
}
// dual-homed host with an IPv6 and an IPv4 address
if hostname == "ipv4.and.ipv6.localhost" {

View File

@ -15,6 +15,82 @@ type Error struct {
// Exactly one of rCode or underlying should be set.
underlying error
rCode int
// Optional: If the resolver returned extended error information, it will be stored here.
// https://www.rfc-editor.org/rfc/rfc8914
extended *dns.EDNS0_EDE
}
// extendedDNSError returns non-nil if the input message contained an OPT RR
// with an EDE option. https://www.rfc-editor.org/rfc/rfc8914.
func extendedDNSError(msg *dns.Msg) *dns.EDNS0_EDE {
opt := msg.IsEdns0()
if opt != nil {
for _, opt := range opt.Option {
ede, ok := opt.(*dns.EDNS0_EDE)
if !ok {
continue
}
return ede
}
}
return nil
}
// wrapErr returns a non-nil error if err is non-nil or if resp.Rcode is not dns.RcodeSuccess.
// The error includes appropriate details about the DNS query that failed.
func wrapErr(queryType uint16, hostname string, resp *dns.Msg, err error) error {
if err != nil {
return Error{
recordType: queryType,
hostname: hostname,
underlying: err,
extended: nil,
}
}
if resp.Rcode != dns.RcodeSuccess {
return Error{
recordType: queryType,
hostname: hostname,
rCode: resp.Rcode,
underlying: nil,
extended: extendedDNSError(resp),
}
}
return nil
}
// A copy of miekg/dns's mapping of error codes to strings. We tweak it slightly so all DNSSEC-related
// errors say "DNSSEC" at the beginning.
// https://pkg.go.dev/github.com/miekg/dns#ExtendedErrorCodeToString
// Also note that not all of these codes can currently be emitted by Unbound. See Unbound's
// announcement post for EDE: https://blog.nlnetlabs.nl/extended-dns-error-support-for-unbound/
var extendedErrorCodeToString = map[uint16]string{
dns.ExtendedErrorCodeOther: "Other",
dns.ExtendedErrorCodeUnsupportedDNSKEYAlgorithm: "DNSSEC: Unsupported DNSKEY Algorithm",
dns.ExtendedErrorCodeUnsupportedDSDigestType: "DNSSEC: Unsupported DS Digest Type",
dns.ExtendedErrorCodeStaleAnswer: "Stale Answer",
dns.ExtendedErrorCodeForgedAnswer: "Forged Answer",
dns.ExtendedErrorCodeDNSSECIndeterminate: "DNSSEC: Indeterminate",
dns.ExtendedErrorCodeDNSBogus: "DNSSEC: Bogus",
dns.ExtendedErrorCodeSignatureExpired: "DNSSEC: Signature Expired",
dns.ExtendedErrorCodeSignatureNotYetValid: "DNSSEC: Signature Not Yet Valid",
dns.ExtendedErrorCodeDNSKEYMissing: "DNSSEC: DNSKEY Missing",
dns.ExtendedErrorCodeRRSIGsMissing: "DNSSEC: RRSIGs Missing",
dns.ExtendedErrorCodeNoZoneKeyBitSet: "DNSSEC: No Zone Key Bit Set",
dns.ExtendedErrorCodeNSECMissing: "DNSSEC: NSEC Missing",
dns.ExtendedErrorCodeCachedError: "Cached Error",
dns.ExtendedErrorCodeNotReady: "Not Ready",
dns.ExtendedErrorCodeBlocked: "Blocked",
dns.ExtendedErrorCodeCensored: "Censored",
dns.ExtendedErrorCodeFiltered: "Filtered",
dns.ExtendedErrorCodeProhibited: "Prohibited",
dns.ExtendedErrorCodeStaleNXDOMAINAnswer: "Stale NXDOMAIN Answer",
dns.ExtendedErrorCodeNotAuthoritative: "Not Authoritative",
dns.ExtendedErrorCodeNotSupported: "Not Supported",
dns.ExtendedErrorCodeNoReachableAuthority: "No Reachable Authority",
dns.ExtendedErrorCodeNetworkError: "Network Error between Resolver and Authority",
dns.ExtendedErrorCodeInvalidData: "Invalid Data",
}
func (d Error) Error() string {
@ -43,8 +119,22 @@ func (d Error) Error() string {
} else {
detail = detailServerFailure
}
return fmt.Sprintf("DNS problem: %s looking up %s for %s%s", detail,
dns.TypeToString[d.recordType], d.hostname, additional)
if d.extended == nil {
return fmt.Sprintf("DNS problem: %s looking up %s for %s%s", detail,
dns.TypeToString[d.recordType], d.hostname, additional)
}
summary := extendedErrorCodeToString[d.extended.InfoCode]
if summary == "" {
summary = fmt.Sprintf("Unknown Extended DNS Error code %d", d.extended.InfoCode)
}
result := fmt.Sprintf("DNS problem: looking up %s for %s: %s",
dns.TypeToString[d.recordType], d.hostname, summary)
if d.extended.ExtraText != "" {
result = result + ": " + d.extended.ExtraText
}
return result
}
const detailDNSTimeout = "query timed out"

View File

@ -6,6 +6,7 @@ import (
"net"
"testing"
"github.com/letsencrypt/boulder/test"
"github.com/miekg/dns"
)
@ -15,25 +16,40 @@ func TestError(t *testing.T) {
expected string
}{
{
&Error{dns.TypeA, "hostname", makeTimeoutError(), -1},
&Error{dns.TypeA, "hostname", makeTimeoutError(), -1, nil},
"DNS problem: query timed out looking up A for hostname",
}, {
&Error{dns.TypeMX, "hostname", &net.OpError{Err: errors.New("some net error")}, -1},
&Error{dns.TypeMX, "hostname", &net.OpError{Err: errors.New("some net error")}, -1, nil},
"DNS problem: networking error looking up MX for hostname",
}, {
&Error{dns.TypeTXT, "hostname", nil, dns.RcodeNameError},
&Error{dns.TypeTXT, "hostname", nil, dns.RcodeNameError, nil},
"DNS problem: NXDOMAIN looking up TXT for hostname - check that a DNS record exists for this domain",
}, {
&Error{dns.TypeTXT, "hostname", context.DeadlineExceeded, -1},
&Error{dns.TypeTXT, "hostname", context.DeadlineExceeded, -1, nil},
"DNS problem: query timed out looking up TXT for hostname",
}, {
&Error{dns.TypeTXT, "hostname", context.Canceled, -1},
&Error{dns.TypeTXT, "hostname", context.Canceled, -1, nil},
"DNS problem: query timed out (and was canceled) looking up TXT for hostname",
}, {
&Error{dns.TypeCAA, "hostname", nil, dns.RcodeServerFailure},
&Error{dns.TypeCAA, "hostname", nil, dns.RcodeServerFailure, nil},
"DNS problem: SERVFAIL looking up CAA for hostname - the domain's nameservers may be malfunctioning",
}, {
&Error{dns.TypeA, "hostname", nil, dns.RcodeFormatError},
&Error{dns.TypeA, "hostname", nil, dns.RcodeServerFailure, &dns.EDNS0_EDE{InfoCode: 1, ExtraText: "oh no"}},
"DNS problem: looking up A for hostname: DNSSEC: Unsupported DNSKEY Algorithm: oh no",
}, {
&Error{dns.TypeA, "hostname", nil, dns.RcodeServerFailure, &dns.EDNS0_EDE{InfoCode: 6, ExtraText: ""}},
"DNS problem: looking up A for hostname: DNSSEC: Bogus",
}, {
&Error{dns.TypeA, "hostname", nil, dns.RcodeServerFailure, &dns.EDNS0_EDE{InfoCode: 1337, ExtraText: "mysterious"}},
"DNS problem: looking up A for hostname: Unknown Extended DNS Error code 1337: mysterious",
}, {
&Error{dns.TypeCAA, "hostname", nil, dns.RcodeServerFailure, nil},
"DNS problem: SERVFAIL looking up CAA for hostname - the domain's nameservers may be malfunctioning",
}, {
&Error{dns.TypeCAA, "hostname", nil, dns.RcodeServerFailure, nil},
"DNS problem: SERVFAIL looking up CAA for hostname - the domain's nameservers may be malfunctioning",
}, {
&Error{dns.TypeA, "hostname", nil, dns.RcodeFormatError, nil},
"DNS problem: FORMERR looking up A for hostname",
},
}
@ -43,3 +59,20 @@ func TestError(t *testing.T) {
}
}
}
func TestWrapErr(t *testing.T) {
err := wrapErr(dns.TypeA, "hostname", &dns.Msg{
MsgHdr: dns.MsgHdr{Rcode: dns.RcodeSuccess},
}, nil)
test.AssertNotError(t, err, "expected success")
err = wrapErr(dns.TypeA, "hostname", &dns.Msg{
MsgHdr: dns.MsgHdr{Rcode: dns.RcodeRefused},
}, nil)
test.AssertError(t, err, "expected error")
err = wrapErr(dns.TypeA, "hostname", &dns.Msg{
MsgHdr: dns.MsgHdr{Rcode: dns.RcodeSuccess},
}, errors.New("oh no"))
test.AssertError(t, err, "expected error")
}