github.com/letsencrypt/boulder@v0.20251208.0/bdns/problem.go (about) 1 package bdns 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "net/url" 9 10 "github.com/miekg/dns" 11 ) 12 13 // Error wraps a DNS error with various relevant information 14 type Error struct { 15 recordType uint16 16 hostname string 17 // Exactly one of rCode or underlying should be set. 18 underlying error 19 rCode int 20 21 // Optional: If the resolver returned extended error information, it will be stored here. 22 // https://www.rfc-editor.org/rfc/rfc8914 23 extended *dns.EDNS0_EDE 24 } 25 26 // extendedDNSError returns non-nil if the input message contained an OPT RR 27 // with an EDE option. https://www.rfc-editor.org/rfc/rfc8914. 28 func extendedDNSError(msg *dns.Msg) *dns.EDNS0_EDE { 29 opt := msg.IsEdns0() 30 if opt != nil { 31 for _, opt := range opt.Option { 32 ede, ok := opt.(*dns.EDNS0_EDE) 33 if !ok { 34 continue 35 } 36 return ede 37 } 38 } 39 return nil 40 } 41 42 // wrapErr returns a non-nil error if err is non-nil or if resp.Rcode is not dns.RcodeSuccess. 43 // The error includes appropriate details about the DNS query that failed. 44 func wrapErr(queryType uint16, hostname string, resp *dns.Msg, err error) error { 45 if err != nil { 46 return Error{ 47 recordType: queryType, 48 hostname: hostname, 49 underlying: err, 50 extended: nil, 51 } 52 } 53 if resp.Rcode != dns.RcodeSuccess { 54 return Error{ 55 recordType: queryType, 56 hostname: hostname, 57 rCode: resp.Rcode, 58 underlying: nil, 59 extended: extendedDNSError(resp), 60 } 61 } 62 return nil 63 } 64 65 // A copy of miekg/dns's mapping of error codes to strings. We tweak it slightly so all DNSSEC-related 66 // errors say "DNSSEC" at the beginning. 67 // https://pkg.go.dev/github.com/miekg/dns#ExtendedErrorCodeToString 68 // Also note that not all of these codes can currently be emitted by Unbound. See Unbound's 69 // announcement post for EDE: https://blog.nlnetlabs.nl/extended-dns-error-support-for-unbound/ 70 var extendedErrorCodeToString = map[uint16]string{ 71 dns.ExtendedErrorCodeOther: "Other", 72 dns.ExtendedErrorCodeUnsupportedDNSKEYAlgorithm: "DNSSEC: Unsupported DNSKEY Algorithm", 73 dns.ExtendedErrorCodeUnsupportedDSDigestType: "DNSSEC: Unsupported DS Digest Type", 74 dns.ExtendedErrorCodeStaleAnswer: "Stale Answer", 75 dns.ExtendedErrorCodeForgedAnswer: "Forged Answer", 76 dns.ExtendedErrorCodeDNSSECIndeterminate: "DNSSEC: Indeterminate", 77 dns.ExtendedErrorCodeDNSBogus: "DNSSEC: Bogus", 78 dns.ExtendedErrorCodeSignatureExpired: "DNSSEC: Signature Expired", 79 dns.ExtendedErrorCodeSignatureNotYetValid: "DNSSEC: Signature Not Yet Valid", 80 dns.ExtendedErrorCodeDNSKEYMissing: "DNSSEC: DNSKEY Missing", 81 dns.ExtendedErrorCodeRRSIGsMissing: "DNSSEC: RRSIGs Missing", 82 dns.ExtendedErrorCodeNoZoneKeyBitSet: "DNSSEC: No Zone Key Bit Set", 83 dns.ExtendedErrorCodeNSECMissing: "DNSSEC: NSEC Missing", 84 dns.ExtendedErrorCodeCachedError: "Cached Error", 85 dns.ExtendedErrorCodeNotReady: "Not Ready", 86 dns.ExtendedErrorCodeBlocked: "Blocked", 87 dns.ExtendedErrorCodeCensored: "Censored", 88 dns.ExtendedErrorCodeFiltered: "Filtered", 89 dns.ExtendedErrorCodeProhibited: "Prohibited", 90 dns.ExtendedErrorCodeStaleNXDOMAINAnswer: "Stale NXDOMAIN Answer", 91 dns.ExtendedErrorCodeNotAuthoritative: "Not Authoritative", 92 dns.ExtendedErrorCodeNotSupported: "Not Supported", 93 dns.ExtendedErrorCodeNoReachableAuthority: "No Reachable Authority", 94 dns.ExtendedErrorCodeNetworkError: "Network Error between Resolver and Authority", 95 dns.ExtendedErrorCodeInvalidData: "Invalid Data", 96 } 97 98 func (d Error) Error() string { 99 var detail, additional string 100 if d.underlying != nil { 101 var netErr *net.OpError 102 var urlErr *url.Error 103 if errors.As(d.underlying, &netErr) { 104 if netErr.Timeout() { 105 detail = detailDNSTimeout 106 } else { 107 detail = detailDNSNetFailure 108 } 109 // Note: we check d.underlying here even though `Timeout()` does this because the call to `netErr.Timeout()` above only 110 // happens for `*net.OpError` underlying types! 111 } else if errors.As(d.underlying, &urlErr) && urlErr.Timeout() { 112 // For DOH queries, we can get back a `*url.Error` that wraps the unexported type 113 // `http.httpError`. Unfortunately `http.httpError` doesn't wrap any errors (like 114 // context.DeadlineExceeded), we can't check for that; instead we need to call Timeout(). 115 detail = detailDNSTimeout 116 } else if errors.Is(d.underlying, context.DeadlineExceeded) { 117 detail = detailDNSTimeout 118 } else if errors.Is(d.underlying, context.Canceled) { 119 detail = detailCanceled 120 } else { 121 detail = detailServerFailure 122 } 123 } else if d.rCode != dns.RcodeSuccess { 124 detail = dns.RcodeToString[d.rCode] 125 if explanation, ok := rcodeExplanations[d.rCode]; ok { 126 additional = " - " + explanation 127 } 128 } else { 129 detail = detailServerFailure 130 } 131 132 if d.extended == nil { 133 return fmt.Sprintf("DNS problem: %s looking up %s for %s%s", detail, 134 dns.TypeToString[d.recordType], d.hostname, additional) 135 } 136 137 summary := extendedErrorCodeToString[d.extended.InfoCode] 138 if summary == "" { 139 summary = fmt.Sprintf("Unknown Extended DNS Error code %d", d.extended.InfoCode) 140 } 141 result := fmt.Sprintf("DNS problem: looking up %s for %s: %s", 142 dns.TypeToString[d.recordType], d.hostname, summary) 143 if d.extended.ExtraText != "" { 144 result = result + ": " + d.extended.ExtraText 145 } 146 return result 147 } 148 149 const detailDNSTimeout = "query timed out" 150 const detailCanceled = "query timed out (and was canceled)" 151 const detailDNSNetFailure = "networking error" 152 const detailServerFailure = "server failure at resolver" 153 154 // rcodeExplanations provide additional friendly explanatory text to be included in DNS 155 // error messages, for select inscrutable RCODEs. 156 var rcodeExplanations = map[int]string{ 157 dns.RcodeNameError: "check that a DNS record exists for this domain", 158 dns.RcodeServerFailure: "the domain's nameservers may be malfunctioning", 159 }