boulder/bdns/problem.go

160 lines
5.9 KiB
Go

package bdns
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"github.com/miekg/dns"
)
// Error wraps a DNS error with various relevant information
type Error struct {
recordType uint16
hostname string
// 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 {
var detail, additional string
if d.underlying != nil {
var netErr *net.OpError
var urlErr *url.Error
if errors.As(d.underlying, &netErr) {
if netErr.Timeout() {
detail = detailDNSTimeout
} else {
detail = detailDNSNetFailure
}
// Note: we check d.underlying here even though `Timeout()` does this because the call to `netErr.Timeout()` above only
// happens for `*net.OpError` underlying types!
} else if errors.As(d.underlying, &urlErr) && urlErr.Timeout() {
// For DOH queries, we can get back a `*url.Error` that wraps the unexported type
// `http.httpError`. Unfortunately `http.httpError` doesn't wrap any errors (like
// context.DeadlineExceeded), we can't check for that; instead we need to call Timeout().
detail = detailDNSTimeout
} else if errors.Is(d.underlying, context.DeadlineExceeded) {
detail = detailDNSTimeout
} else if errors.Is(d.underlying, context.Canceled) {
detail = detailCanceled
} else {
detail = detailServerFailure
}
} else if d.rCode != dns.RcodeSuccess {
detail = dns.RcodeToString[d.rCode]
if explanation, ok := rcodeExplanations[d.rCode]; ok {
additional = " - " + explanation
}
} else {
detail = detailServerFailure
}
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"
const detailCanceled = "query timed out (and was canceled)"
const detailDNSNetFailure = "networking error"
const detailServerFailure = "server failure at resolver"
// rcodeExplanations provide additional friendly explanatory text to be included in DNS
// error messages, for select inscrutable RCODEs.
var rcodeExplanations = map[int]string{
dns.RcodeNameError: "check that a DNS record exists for this domain",
dns.RcodeServerFailure: "the domain's nameservers may be malfunctioning",
}