resolver: ask a referral's nameservers that come without addresses (closes #221)
check / check (push) Canceled after 0s

Looking up a nameserver's own address followed only the addresses a
referral gave, so a nameserver whose zone is delegated without them,
such as a.ntpns.org of pool.ntp.org, never resolved. The walk to a
name's nameservers looked addresses up only when a referral gave none,
so when it gave some it asked only those. Both now ask the nameservers
whose addresses the referral gives first and, if none of them gives a
usable reply, look up and ask the others; with no addresses given, all
are looked up, as before. Looking up all of them at once sent too many
queries to the root servers. maxLookupDepth stops lookups two deep, so
delegations that point at each other still end.

Model: opus-5-5
This commit is contained in:
2026-10-02 06:14:36 +00:00
parent a18803ff28
commit 031e595616
5 changed files with 199 additions and 37 deletions
+91 -34
View File
@@ -19,6 +19,14 @@ const (
maxRetries = 2
maxDelegation = 20
timeoutMultiplier = 2
// maxLookupDepth is how many lookups of nameserver addresses may be
// under way one inside another. Looking up a nameserver's address
// can meet a referral that names nameservers without their
// addresses, which are then looked up in turn; without a limit,
// delegations that point at each other would never end. Each level
// multiplies the queries sent.
maxLookupDepth = 2
)
// ErrRefused is returned when a DNS server refuses a query.
@@ -198,13 +206,15 @@ func (r *Resolver) followDelegation(
// servers are the root servers, the servers of zone ".".
zone := "."
var withoutAddresses []string
for range maxDelegation {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
}
resp, err := r.queryServers(
ctx, servers, zone, domain, dns.TypeNS,
resp, err := r.queryNameservers(
ctx, servers, withoutAddresses, zone, domain, dns.TypeNS, 0,
)
if err != nil {
return nil, err
@@ -229,18 +239,7 @@ func (r *Resolver) followDelegation(
return r.resolveNSIterative(ctx, domain)
}
glue := extractGlue(resp.Extra)
nextServers := glueIPs(authNS, glue)
if len(nextServers) == 0 {
nextServers = r.resolveNSIPs(ctx, authNS)
}
if len(nextServers) == 0 {
return nil, ErrNoNameservers
}
servers = nextServers
servers, withoutAddresses = referralNameservers(resp)
zone = referralZone(resp)
}
@@ -366,18 +365,80 @@ func nsSetFrom(resp *dns.Msg, domain string) []string {
return extractNSSet(resp.Answer)
}
// referralNameservers returns the IPv4 addresses that resp, a referral,
// gives for the nameservers it names, and the names of the nameservers
// it gives no address for.
func referralNameservers(resp *dns.Msg) ([]string, []string) {
glue := extractGlue(resp.Extra)
var given, withoutAddresses []string
for _, ns := range extractNSSet(resp.Ns) {
ips := glueIPs([]string{ns}, glue)
if len(ips) == 0 {
withoutAddresses = append(withoutAddresses, ns)
}
given = append(given, ips...)
}
return given, withoutAddresses
}
// queryNameservers asks the servers of zone about name as queryServers
// does: first those at given, the addresses a referral gave, and only
// when none of them gives a usable reply, the nameservers named
// withoutAddresses, once their addresses are looked up. depth is how
// many lookups of a nameserver's address are under way, 0 in the walk
// to a domain's nameservers; at maxLookupDepth, no address is looked
// up.
func (r *Resolver) queryNameservers(
ctx context.Context,
given []string,
withoutAddresses []string,
zone string,
name string,
qtype uint16,
depth int,
) (*dns.Msg, error) {
err := fmt.Errorf(
"no address for any nameserver of %s: %w", zone, ErrNoNameservers,
)
if len(given) > 0 {
var resp *dns.Msg
resp, err = r.queryServers(ctx, given, zone, name, qtype)
if err == nil {
return resp, nil
}
}
if len(withoutAddresses) == 0 || depth >= maxLookupDepth {
return nil, err
}
lookedUp := r.resolveNSIPs(ctx, withoutAddresses, depth+1)
if len(lookedUp) == 0 {
return nil, err
}
return r.queryServers(ctx, lookedUp, zone, name, qtype)
}
// resolveNSIPs returns the addresses of every nameserver in nsNames
// whose name resolves, for a referral that carries none. The walk can
// then go on to the zone's other nameservers when one gives no usable
// reply.
// whose name resolves, each looked up at depth (see resolveARecord).
// The walk can then go on to the zone's other nameservers when one
// gives no usable reply.
func (r *Resolver) resolveNSIPs(
ctx context.Context,
nsNames []string,
depth int,
) []string {
var ips []string
for _, ns := range nsNames {
resolved, err := r.resolveARecord(ctx, ns)
resolved, err := r.resolveARecord(ctx, ns, depth)
if err == nil {
ips = append(ips, resolved...)
}
@@ -438,11 +499,14 @@ func (r *Resolver) resolveNSIterative(
return nil, ErrNoNameservers
}
// resolveARecord resolves a hostname to IPv4 addresses using
// iterative resolution through the delegation chain.
// resolveARecord resolves a hostname, a nameserver's name, to IPv4
// addresses using iterative resolution through the delegation chain.
// depth is how many lookups of a nameserver's address are under way,
// this one included: 1 for a lookup that no other lookup started.
func (r *Resolver) resolveARecord(
ctx context.Context,
hostname string,
depth int,
) ([]string, error) {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
@@ -452,13 +516,16 @@ func (r *Resolver) resolveARecord(
servers := rootServerList()
zone := "."
var withoutAddresses []string
for range maxDelegation {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
}
resp, err := r.queryServers(
ctx, servers, zone, hostname, dns.TypeA,
resp, err := r.queryNameservers(
ctx, servers, withoutAddresses, zone, hostname, dns.TypeA,
depth,
)
if err != nil {
return nil, fmt.Errorf(
@@ -485,17 +552,7 @@ func (r *Resolver) resolveARecord(
break
}
glue := extractGlue(resp.Extra)
nextServers := glueIPs(authNS, glue)
if len(nextServers) == 0 {
// Resolve NS IPs iteratively — but guard
// against infinite recursion by using only
// already-resolved servers.
break
}
servers = nextServers
servers, withoutAddresses = referralNameservers(resp)
zone = referralZone(resp)
}
@@ -584,7 +641,7 @@ func (r *Resolver) queryNameserver(
return nil, ErrContextCanceled
}
nsIPs, err := r.resolveARecord(ctx, nsHostname)
nsIPs, err := r.resolveARecord(ctx, nsHostname, 1)
if err != nil {
return nil, fmt.Errorf("resolving NS %s: %w", nsHostname, err)
}