160 lines
5.9 KiB
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",
|
|
}
|