resolver: try servers in a random order on each resolution (closes #138)
check / check (push) Failing after 2m8s

Every resolution walked the root servers in a fixed order, so
a.root-servers.net got every first query and its timeouts were paid on
every lookup. Each list of servers the resolver walks is now walked in a
random order from rand.Shuffle, chosen anew each time; a server that does
not reply, refuses, or gives an error reply or a referral that leads no
closer is still passed over for the next. When a referral names a zone's
nameservers without their addresses, all of them are now looked up, not
only the first that resolves, so the zone is not given up because one
randomly picked nameserver failed. No test fails if the walk stops
shuffling: which server a live query reached is not observable.

Model: opus-5-5
This commit is contained in:
2026-10-02 00:01:15 +00:00
parent c9510a986c
commit 4ff5f85320
7 changed files with 141 additions and 9 deletions
+26 -8
View File
@@ -4,7 +4,9 @@ import (
"context"
"errors"
"fmt"
"math/rand/v2"
"net"
"slices"
"sort"
"strings"
"time"
@@ -259,9 +261,25 @@ func (r *Resolver) followDelegation(
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.
// shuffled returns a copy of servers in the order shuffle puts them
// in. The resolver passes rand.Shuffle, so each time it walks a list of
// servers it starts at a random one, and no one server gets every
// first query.
func shuffled(
servers []string,
shuffle func(n int, swap func(i, j int)),
) []string {
order := slices.Clone(servers)
shuffle(len(order), func(i, j int) {
order[i], order[j] = order[j], order[i]
})
return order
}
// queryServers asks servers, the servers of zone, about name in a random
// order 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,
@@ -271,7 +289,7 @@ func (r *Resolver) queryServers(
) (*dns.Msg, error) {
var lastErr error
for _, ip := range servers {
for _, ip := range shuffled(servers, rand.Shuffle) {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
}
@@ -340,6 +358,10 @@ func nsSetFrom(resp *dns.Msg, domain string) []string {
return extractNSSet(resp.Answer)
}
// 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.
func (r *Resolver) resolveNSIPs(
ctx context.Context,
nsNames []string,
@@ -351,10 +373,6 @@ func (r *Resolver) resolveNSIPs(
if err == nil {
ips = append(ips, resolved...)
}
if len(ips) > 0 {
break
}
}
return ips