resolver: pass over a server that answers SERVFAIL or refers no closer (closes #197)
check / check (push) Failing after 2m8s

When the resolver walks from the root servers towards a name, a server
that answered SERVFAIL, or referred the query back to its own zone, up
or sideways, ended the step, so finding a zone's servers gave up on the
zone though its other servers would answer. Such a reply is now passed
over for the zone's next server, as a timeout or a refusal already was.
To tell a referral that leads closer to the name from one that does
not, each walk keeps the zone of the servers it is asking. Other error
replies, such as FORMERR, are passed over too. The walk that finds a
nameserver's address shares the same server loop, so it changes too.

Model: opus-5-5
This commit was merged in pull request #201.
This commit is contained in:
2026-10-02 01:17:37 +02:00
parent 97c8138c85
commit d09822562d
5 changed files with 158 additions and 3 deletions
+53 -3
View File
@@ -207,13 +207,16 @@ func (r *Resolver) followDelegation(
domain string,
servers []string,
) ([]string, error) {
// servers are the root servers, the servers of zone ".".
zone := "."
for range maxDelegation {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
}
resp, err := r.queryServers(
ctx, servers, domain, dns.TypeNS,
ctx, servers, zone, domain, dns.TypeNS,
)
if err != nil {
return nil, err
@@ -250,14 +253,19 @@ func (r *Resolver) followDelegation(
}
servers = nextServers
zone = referralZone(resp)
}
return nil, ErrNoNameservers
}
// queryServers asks servers, the servers of zone, about name until one
// gives a usable reply. A server that times out, refuses or gives a
// reply that is not usable is passed over for the next.
func (r *Resolver) queryServers(
ctx context.Context,
servers []string,
zone string,
name string,
qtype uint16,
) (*dns.Msg, error) {
@@ -269,6 +277,12 @@ func (r *Resolver) queryServers(
}
resp, err := r.queryDNS(ctx, ip, name, qtype)
if err == nil && !usableReply(resp, zone, name) {
err = fmt.Errorf(
"query %s @%s: %w", name, ip, ErrUnusableReply,
)
}
if err == nil {
return resp, nil
}
@@ -279,6 +293,38 @@ func (r *Resolver) queryServers(
return nil, fmt.Errorf("all servers failed: %w", lastErr)
}
// usableReply reports whether resp, a reply from one of the servers of
// zone to a query about name, is usable. An error reply such as SERVFAIL
// is not. Nor is a referral, unless it refers the query to a zone below
// zone that name is in: a server that refers it back to zone, up or
// sideways does not serve zone as it should.
func usableReply(resp *dns.Msg, zone string, name string) bool {
if resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeNameError {
return false
}
child := referralZone(resp)
if resp.Authoritative || len(resp.Answer) > 0 || child == "" {
return true
}
return child != zone && dns.IsSubDomain(zone, child) &&
dns.IsSubDomain(child, name)
}
// referralZone returns the zone a referral refers the query to: the
// owner name of the NS records in resp's authority section, or "" when
// there are none.
func referralZone(resp *dns.Msg) string {
for _, rr := range resp.Ns {
if ns, ok := rr.(*dns.NS); ok {
return strings.ToLower(ns.Hdr.Name)
}
}
return ""
}
func (r *Resolver) resolveNSIPs(
ctx context.Context,
nsNames []string,
@@ -312,6 +358,7 @@ func (r *Resolver) resolveNSIterative(
domain = dns.Fqdn(domain)
servers := rootServerList()
zone := "."
for range maxDelegation {
if checkCtx(ctx) != nil {
@@ -319,7 +366,7 @@ func (r *Resolver) resolveNSIterative(
}
resp, err := r.queryServers(
ctx, servers, domain, dns.TypeNS,
ctx, servers, zone, domain, dns.TypeNS,
)
if err != nil {
return nil, err
@@ -344,6 +391,7 @@ func (r *Resolver) resolveNSIterative(
}
servers = nextServers
zone = referralZone(resp)
}
return nil, ErrNoNameservers
@@ -361,6 +409,7 @@ func (r *Resolver) resolveARecord(
hostname = dns.Fqdn(hostname)
servers := rootServerList()
zone := "."
for range maxDelegation {
if checkCtx(ctx) != nil {
@@ -368,7 +417,7 @@ func (r *Resolver) resolveARecord(
}
resp, err := r.queryServers(
ctx, servers, hostname, dns.TypeA,
ctx, servers, zone, hostname, dns.TypeA,
)
if err != nil {
return nil, fmt.Errorf(
@@ -406,6 +455,7 @@ func (r *Resolver) resolveARecord(
}
servers = nextServers
zone = referralZone(resp)
}
return nil, fmt.Errorf(