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

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 is contained in:
2026-10-01 22:37:02 +00:00
parent d2f154b2cf
commit dc1f0a8376
5 changed files with 155 additions and 3 deletions
+88
View File
@@ -41,6 +41,94 @@ func TestCollectIPs_FailedIsNoAnswer(t *testing.T) {
assert.Empty(t, ips)
}
// nsRecord builds an NS record that names a server of zone.
func nsRecord(zone string) dns.RR {
return &dns.NS{
Hdr: dns.RR_Header{
Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET,
},
Ns: "ns1.example.net.",
}
}
// referralTo builds a reply that refers the query to the servers of
// zone.
func referralTo(zone string) *dns.Msg {
msg := new(dns.Msg)
msg.Ns = []dns.RR{nsRecord(zone)}
return msg
}
// TestUsableReply checks which replies from one of a zone's servers are
// used. A reply that is not usable moves the query on to the zone's
// next server.
func TestUsableReply(t *testing.T) {
t.Parallel()
servfail := new(dns.Msg)
servfail.Rcode = dns.RcodeServerFailure
answer := new(dns.Msg)
answer.Authoritative = true
answer.Answer = []dns.RR{nsRecord("example.com.")}
nxdomain := new(dns.Msg)
nxdomain.Authoritative = true
nxdomain.Rcode = dns.RcodeNameError
tests := []struct {
name string
resp *dns.Msg
zone string
query string
want bool
}{
{
name: "SERVFAIL", resp: servfail,
zone: "example.com.", query: "example.com.", want: false,
},
{
name: "answer", resp: answer,
zone: "example.com.", query: "example.com.", want: true,
},
{
name: "NXDOMAIN", resp: nxdomain,
zone: "com.", query: "example.com.", want: true,
},
{
name: "root refers to com", resp: referralTo("com."),
zone: ".", query: "example.com.", want: true,
},
{
name: "com refers to example.com", resp: referralTo("example.com."),
zone: "com.", query: "www.example.com.", want: true,
},
{
name: "referral back to the zone", resp: referralTo("example.com."),
zone: "example.com.", query: "example.com.", want: false,
},
{
name: "referral up to the root", resp: referralTo("."),
zone: "example.com.", query: "example.com.", want: false,
},
{
name: "referral sideways", resp: referralTo("net."),
zone: ".", query: "example.com.", want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.want,
resolver.UsableReply(tt.resp, tt.zone, tt.query),
)
})
}
}
func TestExtractRecordValue_LetterCase(t *testing.T) {
t.Parallel()