diff --git a/.codespell.ignore.txt b/.codespell.ignore.txt index d839d1b98..c8d33b2ef 100644 --- a/.codespell.ignore.txt +++ b/.codespell.ignore.txt @@ -9,3 +9,4 @@ strat te uint vas +ede diff --git a/bdns/dns.go b/bdns/dns.go index 7b0f61f1d..7134928a7 100644 --- a/bdns/dns.go +++ b/bdns/dns.go @@ -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 diff --git a/bdns/dns_test.go b/bdns/dns_test.go index cff7527ef..7d849d510 100644 --- a/bdns/dns_test.go +++ b/bdns/dns_test.go @@ -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) } diff --git a/bdns/mocks.go b/bdns/mocks.go index 546356890..eb4eeb470 100644 --- a/bdns/mocks.go +++ b/bdns/mocks.go @@ -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" { diff --git a/bdns/problem.go b/bdns/problem.go index ac9726812..7e22fbedf 100644 --- a/bdns/problem.go +++ b/bdns/problem.go @@ -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" diff --git a/bdns/problem_test.go b/bdns/problem_test.go index 97ff0a644..f20f5bdb3 100644 --- a/bdns/problem_test.go +++ b/bdns/problem_test.go @@ -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") +}