package checker import ( "context" "fmt" "net" "strings" "time" "github.com/miekg/dns" ) // dnsTimeout is the per-query deadline used by every helper here. const dnsTimeout = 5 * time.Second // dnsExchange sends a single query to the given server using the requested // transport ("" for UDP, "tcp"). The server address must already include a // port. RecursionDesired is forced off: this checker only talks to // authoritative servers. The measured RTT is reported by the caller // independently; this helper just exchanges the packet. func dnsExchange(ctx context.Context, proto, server string, q dns.Question, edns bool) (*dns.Msg, time.Duration, error) { client := dns.Client{Net: proto, Timeout: dnsTimeout} m := new(dns.Msg) m.Id = dns.Id() m.Question = []dns.Question{q} m.RecursionDesired = false if edns { m.SetEdns0(4096, true) } if deadline, ok := ctx.Deadline(); ok { if d := time.Until(deadline); d > 0 && d < client.Timeout { client.Timeout = d } } r, rtt, err := client.Exchange(m, server) if err != nil { return nil, rtt, err } if r == nil { return nil, rtt, fmt.Errorf("nil response from %s", server) } return r, rtt, nil } // hostPort returns "host:port", correctly bracketing IPv6 literals. func hostPort(host, port string) string { if ip := net.ParseIP(host); ip != nil && ip.To4() == nil { return "[" + host + "]:" + port } host = strings.TrimSuffix(host, ".") return host + ":" + port } // resolveHost resolves a host name to its A and AAAA addresses using the // system resolver. func resolveHost(ctx context.Context, host string) ([]string, error) { var resolver net.Resolver addrs, err := resolver.LookupHost(ctx, strings.TrimSuffix(host, ".")) if err != nil { return nil, err } return addrs, nil } // querySOA asks the given authoritative server for the SOA of zone. Returns // the SOA record (nil when absent), the AA flag from the response header, // and the observed RTT. Non-success Rcodes are reported as errors. func querySOA(ctx context.Context, proto, server, zone string) (soa *dns.SOA, aa bool, rtt time.Duration, err error) { q := dns.Question{Name: dns.Fqdn(zone), Qtype: dns.TypeSOA, Qclass: dns.ClassINET} r, rtt, err := dnsExchange(ctx, proto, server, q, false) if err != nil { return nil, false, rtt, err } if r.Rcode != dns.RcodeSuccess { return nil, r.Authoritative, rtt, fmt.Errorf("server answered %s", dns.RcodeToString[r.Rcode]) } for _, rr := range r.Answer { if t, ok := rr.(*dns.SOA); ok { return t, r.Authoritative, rtt, nil } } // Some authoritative servers place the SOA in the Authority section // (for example when queried for their own apex via a referral path). for _, rr := range r.Ns { if t, ok := rr.(*dns.SOA); ok { return t, r.Authoritative, rtt, nil } } return nil, r.Authoritative, rtt, fmt.Errorf("no SOA in answer section") } // queryNSAt asks the given authoritative server for the NS RRset of zone. func queryNSAt(ctx context.Context, server, zone string) ([]string, error) { q := dns.Question{Name: dns.Fqdn(zone), Qtype: dns.TypeNS, Qclass: dns.ClassINET} r, _, err := dnsExchange(ctx, "", server, q, false) if err != nil { return nil, err } if r.Rcode != dns.RcodeSuccess { return nil, fmt.Errorf("server answered %s", dns.RcodeToString[r.Rcode]) } var out []string for _, rr := range r.Answer { if t, ok := rr.(*dns.NS); ok { out = append(out, strings.ToLower(dns.Fqdn(t.Ns))) } } return out, nil } // probeEDNS0 checks whether the server correctly handles an EDNS0-enabled // query. A server that silently drops EDNS0 queries, returns FORMERR, or // strips the OPT record is flagged as non-compliant. // // When the UDP probe fails outright (timeout, network error), the function // retries over TCP: some middleboxes drop large UDP packets carrying the OPT // record while letting TCP/53 through, and RFC 7766 requires authoritative // servers to accept TCP fallback. A server that answers EDNS0 correctly over // TCP is still considered compliant. func probeEDNS0(ctx context.Context, server, zone string) error { q := dns.Question{Name: dns.Fqdn(zone), Qtype: dns.TypeSOA, Qclass: dns.ClassINET} r, _, err := dnsExchange(ctx, "", server, q, true) if err != nil { // UDP path failed entirely; try TCP before declaring the server // EDNS0-broken. Network errors here are reported with the original // UDP error to make debugging easier. rt, _, terr := dnsExchange(ctx, "tcp", server, q, true) if terr != nil { return fmt.Errorf("EDNS0 query failed over UDP (%v) and TCP (%w)", err, terr) } r = rt } if r.Rcode == dns.RcodeFormatError { return fmt.Errorf("server returned FORMERR on EDNS0 query") } if r.Rcode != dns.RcodeSuccess { return fmt.Errorf("server answered %s on EDNS0 query", dns.RcodeToString[r.Rcode]) } // RFC 6891 requires the OPT pseudo-RR to be echoed in the response. if r.IsEdns0() == nil { return fmt.Errorf("server stripped the EDNS0 OPT record from its response") } return nil } // parentReferral resolves the parent zone of zone via the system resolver, // then asks each of the parent's authoritative servers for the NS delegation // of zone. The first server that returns a non-empty referral wins. // // The result is a de-duplicated, lowercase, FQDN list of delegated NS names. func parentReferral(ctx context.Context, zone string) ([]string, error) { zone = dns.Fqdn(zone) labels := dns.SplitDomainName(zone) if len(labels) < 2 { return nil, fmt.Errorf("zone %q has no parent", zone) } parent := dns.Fqdn(strings.Join(labels[1:], ".")) resolver := net.Resolver{} nss, err := resolver.LookupNS(ctx, strings.TrimSuffix(parent, ".")) if err != nil { return nil, fmt.Errorf("resolving NS of parent zone %q: %w", parent, err) } var lastErr error seen := map[string]bool{} var out []string for _, ns := range nss { addrs, rerr := resolver.LookupHost(ctx, strings.TrimSuffix(ns.Host, ".")) if rerr != nil || len(addrs) == 0 { lastErr = rerr continue } for _, a := range addrs { srv := hostPort(a, "53") q := dns.Question{Name: zone, Qtype: dns.TypeNS, Qclass: dns.ClassINET} r, _, qerr := dnsExchange(ctx, "", srv, q, true) if qerr != nil { lastErr = qerr continue } if r.Rcode != dns.RcodeSuccess { lastErr = fmt.Errorf("parent %s answered %s", ns.Host, dns.RcodeToString[r.Rcode]) continue } collect := func(records []dns.RR) { for _, rr := range records { if t, ok := rr.(*dns.NS); ok { if strings.EqualFold(strings.TrimSuffix(t.Header().Name, "."), strings.TrimSuffix(zone, ".")) { name := strings.ToLower(dns.Fqdn(t.Ns)) if !seen[name] { seen[name] = true out = append(out, name) } } } } } collect(r.Answer) collect(r.Ns) if len(out) > 0 { return out, nil } } } if lastErr != nil { return nil, lastErr } return nil, fmt.Errorf("no parent server returned a delegation for %s", zone) }