From 30f8f05fd5fa1ecf71b7f646d9bcb1fbc2e27be4 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 02:07:00 +0000 Subject: [PATCH] watcher tests: fewer queries per domain check, longer live attempts (closes #214) ResolveIPAddresses, which a domain check runs for each nameserver, asked every nameserver of the name's zone for all eight record types and read only A, AAAA and CNAME. It now asks for those three, so a domain check of example.com sends about a third fewer queries. The per-attempt deadline in livednstest goes from 8 to 18 seconds. It was sized for one lookup, while a watcher check sends over a hundred queries in a row and on the CI runner ran past 8 seconds. Three attempts still end under the 60-second cap. A nameserver that answers none of the three queries now counts as not answering even if it would have answered another type; the watcher keeps the previous addresses either way. Model: opus-5-5 --- TODO.md | 2 + internal/livednstest/livednstest.go | 13 ++++--- internal/resolver/export_test.go | 2 +- internal/resolver/iterative.go | 59 +++++++++++++++++++++-------- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/TODO.md b/TODO.md index 49e335e..14d0ac9 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149 # Completed Steps +- 2026-10-02: a nameserver's addresses are looked up asking only for A, AAAA and + CNAME records, and a live test attempt may take 18s, not 8s (closes #214). - 2026-10-02: the resolver tries root servers, and every other server list it walks, in a random order each time, not always from the top (closes #138). - 2026-10-02: a name listed more than once in `DNSWATCHER_TARGETS`, in any diff --git a/internal/livednstest/livednstest.go b/internal/livednstest/livednstest.go index 338f3aa..2d576f6 100644 --- a/internal/livednstest/livednstest.go +++ b/internal/livednstest/livednstest.go @@ -36,11 +36,14 @@ const ( // before the test fails. attempts = 3 - // AttemptTimeout bounds one attempt. Worst case for an operation - // is attempts * AttemptTimeout plus the backoff — about 26 - // seconds, well inside the 90-second `go test -timeout` backstop - // even when several operations exhaust their attempts. - AttemptTimeout = 8 * time.Second + // AttemptTimeout bounds one attempt. It must fit the longest + // operation, a watcher check, which sends over a hundred queries one + // after another and on a slow build host takes several times as long + // as the few seconds it takes on a fast one. Worst case for an + // operation is attempts * AttemptTimeout plus the backoff — about 56 + // seconds, under the suite's 60-second cap and inside the 90-second + // `go test -timeout` backstop. + AttemptTimeout = 18 * time.Second // backoffBase is the delay after the first failed attempt; it is // multiplied by backoffFactor each time. diff --git a/internal/resolver/export_test.go b/internal/resolver/export_test.go index 868174c..8840dd9 100644 --- a/internal/resolver/export_test.go +++ b/internal/resolver/export_test.go @@ -34,7 +34,7 @@ func (r *Resolver) QueryEachNS( nameservers []string, hostname string, ) (map[string]*NameserverResponse, error) { - return r.queryEachNS(ctx, nameservers, hostname) + return r.queryEachNS(ctx, nameservers, hostname, recordTypes()) } // ResolveNSIPs exports resolveNSIPs for testing. diff --git a/internal/resolver/iterative.go b/internal/resolver/iterative.go index 0c526e5..629e043 100644 --- a/internal/resolver/iterative.go +++ b/internal/resolver/iterative.go @@ -533,12 +533,39 @@ func (r *Resolver) FindAuthoritativeNameservers( return nil, ErrNoNameservers } +// recordTypes returns the record types a nameserver is asked for when a +// name is checked. +func recordTypes() []uint16 { + return []uint16{ + dns.TypeA, dns.TypeAAAA, dns.TypeCNAME, + dns.TypeMX, dns.TypeTXT, dns.TypeSRV, + dns.TypeCAA, dns.TypeNS, + } +} + +// addressTypes returns the record types ResolveIPAddresses asks for, +// the only ones it reads. +func addressTypes() []uint16 { + return []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeCNAME} +} + // QueryNameserver queries a specific nameserver for all record // types and builds a NameserverResponse. func (r *Resolver) QueryNameserver( ctx context.Context, nsHostname string, hostname string, +) (*NameserverResponse, error) { + return r.queryNameserver(ctx, nsHostname, hostname, recordTypes()) +} + +// queryNameserver queries a specific nameserver for the record types +// in qtypes and builds a NameserverResponse. +func (r *Resolver) queryNameserver( + ctx context.Context, + nsHostname string, + hostname string, + qtypes []uint16, ) (*NameserverResponse, error) { if checkCtx(ctx) != nil { return nil, ErrContextCanceled @@ -551,7 +578,7 @@ func (r *Resolver) QueryNameserver( hostname = dns.Fqdn(hostname) - return r.queryAllTypes(ctx, nsHostname, nsIPs[0], hostname) + return r.queryTypes(ctx, nsHostname, nsIPs[0], hostname, qtypes) } // QueryNameserverIP queries a nameserver by its IP address directly, @@ -568,14 +595,15 @@ func (r *Resolver) QueryNameserverIP( hostname = dns.Fqdn(hostname) - return r.queryAllTypes(ctx, nsHostname, nsIP, hostname) + return r.queryTypes(ctx, nsHostname, nsIP, hostname, recordTypes()) } -func (r *Resolver) queryAllTypes( +func (r *Resolver) queryTypes( ctx context.Context, nsHostname string, nsIP string, hostname string, + qtypes []uint16, ) (*NameserverResponse, error) { resp := &NameserverResponse{ Nameserver: nsHostname, @@ -583,12 +611,6 @@ func (r *Resolver) queryAllTypes( Status: StatusOK, } - qtypes := []uint16{ - dns.TypeA, dns.TypeAAAA, dns.TypeCNAME, - dns.TypeMX, dns.TypeTXT, dns.TypeSRV, - dns.TypeCAA, dns.TypeNS, - } - state := r.queryEachType(ctx, nsIP, hostname, qtypes, resp) classifyResponse(resp, state) @@ -779,18 +801,19 @@ func (r *Resolver) QueryAllNameservers( return nil, err } - return r.queryEachNS(ctx, nameservers, hostname) + return r.queryEachNS(ctx, nameservers, hostname, recordTypes()) } func (r *Resolver) queryEachNS( ctx context.Context, nameservers []string, hostname string, + qtypes []uint16, ) (map[string]*NameserverResponse, error) { results := make(map[string]*NameserverResponse) for _, ns := range nameservers { - resp, err := r.QueryNameserver(ctx, ns, hostname) + resp, err := r.queryNameserver(ctx, ns, hostname, qtypes) // A query the context cut short says nothing about the // nameserver, so it must not be returned as its failure. @@ -835,9 +858,10 @@ func (r *Resolver) LookupAllRecords( } // ResolveIPAddresses resolves a hostname to all IPv4 and IPv6 -// addresses, following CNAME chains up to MaxCNAMEDepth. When no -// nameserver of the name's zone answered, it returns an error rather -// than no addresses. +// addresses, following CNAME chains up to MaxCNAMEDepth. It asks each +// nameserver of the name's zone for its A, AAAA and CNAME records only. +// When no nameserver of the name's zone answered, it returns an error +// rather than no addresses. func (r *Resolver) ResolveIPAddresses( ctx context.Context, hostname string, @@ -858,7 +882,12 @@ func (r *Resolver) resolveIPWithCNAME( return nil, ErrCNAMEDepthExceeded } - results, err := r.QueryAllNameservers(ctx, hostname) + nameservers, err := r.FindAuthoritativeNameservers(ctx, hostname) + if err != nil { + return nil, err + } + + results, err := r.queryEachNS(ctx, nameservers, hostname, addressTypes()) if err != nil { return nil, err }