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  }