watcher tests: fewer queries per domain check, longer live attempts (closes #214)
check / check (push) Successful in 1m32s
check / check (push) Successful in 1m32s
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
This commit is contained in:
@@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
# Completed Steps
|
# 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
|
- 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).
|
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
|
- 2026-10-02: a name listed more than once in `DNSWATCHER_TARGETS`, in any
|
||||||
|
|||||||
@@ -36,11 +36,14 @@ const (
|
|||||||
// before the test fails.
|
// before the test fails.
|
||||||
attempts = 3
|
attempts = 3
|
||||||
|
|
||||||
// AttemptTimeout bounds one attempt. Worst case for an operation
|
// AttemptTimeout bounds one attempt. It must fit the longest
|
||||||
// is attempts * AttemptTimeout plus the backoff — about 26
|
// operation, a watcher check, which sends over a hundred queries one
|
||||||
// seconds, well inside the 90-second `go test -timeout` backstop
|
// after another and on a slow build host takes several times as long
|
||||||
// even when several operations exhaust their attempts.
|
// as the few seconds it takes on a fast one. Worst case for an
|
||||||
AttemptTimeout = 8 * time.Second
|
// 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
|
// backoffBase is the delay after the first failed attempt; it is
|
||||||
// multiplied by backoffFactor each time.
|
// multiplied by backoffFactor each time.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (r *Resolver) QueryEachNS(
|
|||||||
nameservers []string,
|
nameservers []string,
|
||||||
hostname string,
|
hostname string,
|
||||||
) (map[string]*NameserverResponse, error) {
|
) (map[string]*NameserverResponse, error) {
|
||||||
return r.queryEachNS(ctx, nameservers, hostname)
|
return r.queryEachNS(ctx, nameservers, hostname, recordTypes())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveNSIPs exports resolveNSIPs for testing.
|
// ResolveNSIPs exports resolveNSIPs for testing.
|
||||||
|
|||||||
@@ -533,12 +533,39 @@ func (r *Resolver) FindAuthoritativeNameservers(
|
|||||||
return nil, ErrNoNameservers
|
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
|
// QueryNameserver queries a specific nameserver for all record
|
||||||
// types and builds a NameserverResponse.
|
// types and builds a NameserverResponse.
|
||||||
func (r *Resolver) QueryNameserver(
|
func (r *Resolver) QueryNameserver(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
nsHostname string,
|
nsHostname string,
|
||||||
hostname 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) {
|
) (*NameserverResponse, error) {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
return nil, ErrContextCanceled
|
return nil, ErrContextCanceled
|
||||||
@@ -551,7 +578,7 @@ func (r *Resolver) QueryNameserver(
|
|||||||
|
|
||||||
hostname = dns.Fqdn(hostname)
|
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,
|
// QueryNameserverIP queries a nameserver by its IP address directly,
|
||||||
@@ -568,14 +595,15 @@ func (r *Resolver) QueryNameserverIP(
|
|||||||
|
|
||||||
hostname = dns.Fqdn(hostname)
|
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,
|
ctx context.Context,
|
||||||
nsHostname string,
|
nsHostname string,
|
||||||
nsIP string,
|
nsIP string,
|
||||||
hostname string,
|
hostname string,
|
||||||
|
qtypes []uint16,
|
||||||
) (*NameserverResponse, error) {
|
) (*NameserverResponse, error) {
|
||||||
resp := &NameserverResponse{
|
resp := &NameserverResponse{
|
||||||
Nameserver: nsHostname,
|
Nameserver: nsHostname,
|
||||||
@@ -583,12 +611,6 @@ func (r *Resolver) queryAllTypes(
|
|||||||
Status: StatusOK,
|
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)
|
state := r.queryEachType(ctx, nsIP, hostname, qtypes, resp)
|
||||||
classifyResponse(resp, state)
|
classifyResponse(resp, state)
|
||||||
|
|
||||||
@@ -779,18 +801,19 @@ func (r *Resolver) QueryAllNameservers(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.queryEachNS(ctx, nameservers, hostname)
|
return r.queryEachNS(ctx, nameservers, hostname, recordTypes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) queryEachNS(
|
func (r *Resolver) queryEachNS(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
nameservers []string,
|
nameservers []string,
|
||||||
hostname string,
|
hostname string,
|
||||||
|
qtypes []uint16,
|
||||||
) (map[string]*NameserverResponse, error) {
|
) (map[string]*NameserverResponse, error) {
|
||||||
results := make(map[string]*NameserverResponse)
|
results := make(map[string]*NameserverResponse)
|
||||||
|
|
||||||
for _, ns := range nameservers {
|
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
|
// A query the context cut short says nothing about the
|
||||||
// nameserver, so it must not be returned as its failure.
|
// 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
|
// ResolveIPAddresses resolves a hostname to all IPv4 and IPv6
|
||||||
// addresses, following CNAME chains up to MaxCNAMEDepth. When no
|
// addresses, following CNAME chains up to MaxCNAMEDepth. It asks each
|
||||||
// nameserver of the name's zone answered, it returns an error rather
|
// nameserver of the name's zone for its A, AAAA and CNAME records only.
|
||||||
// than no addresses.
|
// When no nameserver of the name's zone answered, it returns an error
|
||||||
|
// rather than no addresses.
|
||||||
func (r *Resolver) ResolveIPAddresses(
|
func (r *Resolver) ResolveIPAddresses(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
hostname string,
|
hostname string,
|
||||||
@@ -858,7 +882,12 @@ func (r *Resolver) resolveIPWithCNAME(
|
|||||||
return nil, ErrCNAMEDepthExceeded
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user