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 }