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:
parent
0a65e87c1b
commit
4f171604fe
|
@ -9,3 +9,4 @@ strat
|
|||
te
|
||||
uint
|
||||
vas
|
||||
ede
|
||||
|
|
25
bdns/dns.go
25
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue