package checker import ( "context" "crypto/rand" "encoding/hex" "fmt" "net" "strings" "sync" "time" "github.com/miekg/dns" ) const dnsTimeout = 5 * time.Second // dnsExchange sends a single query against a host:port server. // rd controls the RD bit (set false when querying an authoritative server), // dnssec controls the DO bit so the server returns RRSIG / NSEC[3] records. func dnsExchange(ctx context.Context, proto, server string, q dns.Question, rd, dnssec bool) (*dns.Msg, error) { client := dns.Client{Net: proto, Timeout: dnsTimeout} m := new(dns.Msg) m.Id = dns.Id() m.Question = []dns.Question{q} m.RecursionDesired = rd m.SetEdns0(4096, dnssec) if deadline, ok := ctx.Deadline(); ok { if d := time.Until(deadline); d > 0 && d < client.Timeout { client.Timeout = d } } r, _, err := client.Exchange(m, server) if err != nil { return nil, err } if r == nil { return nil, fmt.Errorf("nil response from %s", server) } return r, nil } func recursiveExchange(ctx context.Context, server string, q dns.Question, dnssec bool) (*dns.Msg, error) { return dnsExchange(ctx, "", server, q, true, dnssec) } func systemResolver() string { cfg, err := dns.ClientConfigFromFile("/etc/resolv.conf") if err != nil || len(cfg.Servers) == 0 { return net.JoinHostPort("1.1.1.1", "53") } return net.JoinHostPort(cfg.Servers[0], cfg.Port) } func hostPort(host, port string) string { return net.JoinHostPort(strings.TrimSuffix(host, "."), port) } func lowerFQDN(name string) string { return strings.ToLower(dns.Fqdn(name)) } // resolveAuthNS returns "host:port" addresses for every authoritative NS of // zone, asking the bootstrap resolver. The list is deduplicated and sorted // only by NS host order so the per-server section of the report is stable. // Per-host lookup failures are returned as nsErrors so the caller can surface // them without aborting the whole collection. func resolveAuthNS(ctx context.Context, zone, resolver string) (hosts []string, addrs []string, nsErrors []string, err error) { q := dns.Question{Name: dns.Fqdn(zone), Qtype: dns.TypeNS, Qclass: dns.ClassINET} r, err := recursiveExchange(ctx, resolver, q, false) if err != nil { return nil, nil, nil, fmt.Errorf("NS lookup for %s: %w", zone, err) } if r.Rcode != dns.RcodeSuccess { return nil, nil, nil, fmt.Errorf("NS lookup for %s: rcode %s", zone, dns.RcodeToString[r.Rcode]) } for _, rr := range r.Answer { if ns, ok := rr.(*dns.NS); ok { hosts = append(hosts, strings.ToLower(strings.TrimSuffix(ns.Ns, "."))) } } if len(hosts) == 0 { return nil, nil, nil, fmt.Errorf("no NS records for %s", zone) } results := make([][]string, len(hosts)) errs := make([]string, len(hosts)) var wg sync.WaitGroup wg.Add(len(hosts)) for i, host := range hosts { go func() { defer wg.Done() a, err := net.DefaultResolver.LookupHost(ctx, host) if err != nil { errs[i] = fmt.Sprintf("address lookup for %s: %v", host, err) return } out := make([]string, 0, len(a)) for _, ip := range a { out = append(out, hostPort(ip, "53")) } results[i] = out }() } wg.Wait() seen := map[string]struct{}{} for _, batch := range results { for _, a := range batch { if _, ok := seen[a]; ok { continue } seen[a] = struct{}{} addrs = append(addrs, a) } } for _, e := range errs { if e != "" { nsErrors = append(nsErrors, e) } } return hosts, addrs, nsErrors, nil } // hasParentDS asks the bootstrap resolver whether the parent zone publishes // a DS for zone. Failures are reported as "false, nil" because absence-of- // evidence is the practical fallback when the network is glitchy. func hasParentDS(ctx context.Context, zone, resolver string) bool { q := dns.Question{Name: dns.Fqdn(zone), Qtype: dns.TypeDS, Qclass: dns.ClassINET} r, err := recursiveExchange(ctx, resolver, q, true) if err != nil || r == nil || r.Rcode != dns.RcodeSuccess { return false } for _, rr := range r.Answer { if _, ok := rr.(*dns.DS); ok { return true } } return false } // randomLabel returns a 32-hex-char label used as the leftmost component of // the NXDOMAIN probe name. 32 hex chars = 128 bits of entropy: collision // with an existing wildcard or zone name is statistically impossible. func randomLabel() string { var b [16]byte _, _ = rand.Read(b[:]) return hex.EncodeToString(b[:]) }