resolver: pass over a server that answers SERVFAIL or refers no closer (closes #197)
check / check (push) Failing after 30s
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:
@@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-01: a zone's server that answers SERVFAIL or a referral leading no
|
||||||
|
closer is passed over for the next, as one that times out is (closes #197).
|
||||||
- 2026-10-01: `ResolveIPAddresses` returns an error, not no addresses, when no
|
- 2026-10-01: `ResolveIPAddresses` returns an error, not no addresses, when no
|
||||||
nameserver of the name's zone answered (closes #190).
|
nameserver of the name's zone answered (closes #190).
|
||||||
- 2026-10-01: `make fmt` and `make fmt-check` cover Markdown with prettier, run
|
- 2026-10-01: `make fmt` and `make fmt-check` cover Markdown with prettier, run
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ var (
|
|||||||
// so whether the name has addresses is unknown.
|
// so whether the name has addresses is unknown.
|
||||||
ErrNoNameserverAnswered = errors.New("no nameserver answered")
|
ErrNoNameserverAnswered = errors.New("no nameserver answered")
|
||||||
|
|
||||||
|
// ErrUnusableReply is returned when a server replied with an
|
||||||
|
// error such as SERVFAIL, or with a referral that leads no
|
||||||
|
// closer to the name asked about.
|
||||||
|
ErrUnusableReply = errors.New(
|
||||||
|
"reply is an error or a referral that leads no closer",
|
||||||
|
)
|
||||||
|
|
||||||
// ErrCNAMEDepthExceeded is returned when a CNAME chain
|
// ErrCNAMEDepthExceeded is returned when a CNAME chain
|
||||||
// exceeds MaxCNAMEDepth.
|
// exceeds MaxCNAMEDepth.
|
||||||
ErrCNAMEDepthExceeded = errors.New(
|
ErrCNAMEDepthExceeded = errors.New(
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ func ExtractRecordValue(rr dns.RR) string {
|
|||||||
return extractRecordValue(rr)
|
return extractRecordValue(rr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UsableReply exports usableReply for testing.
|
||||||
|
func UsableReply(resp *dns.Msg, zone string, name string) bool {
|
||||||
|
return usableReply(resp, zone, name)
|
||||||
|
}
|
||||||
|
|
||||||
// CollectIPs exports collectIPs for testing.
|
// CollectIPs exports collectIPs for testing.
|
||||||
func CollectIPs(
|
func CollectIPs(
|
||||||
results map[string]*NameserverResponse,
|
results map[string]*NameserverResponse,
|
||||||
|
|||||||
@@ -207,13 +207,16 @@ func (r *Resolver) followDelegation(
|
|||||||
domain string,
|
domain string,
|
||||||
servers []string,
|
servers []string,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
|
// servers are the root servers, the servers of zone ".".
|
||||||
|
zone := "."
|
||||||
|
|
||||||
for range maxDelegation {
|
for range maxDelegation {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
return nil, ErrContextCanceled
|
return nil, ErrContextCanceled
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryServers(
|
resp, err := r.queryServers(
|
||||||
ctx, servers, domain, dns.TypeNS,
|
ctx, servers, zone, domain, dns.TypeNS,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -250,14 +253,19 @@ func (r *Resolver) followDelegation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
servers = nextServers
|
servers = nextServers
|
||||||
|
zone = referralZone(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrNoNameservers
|
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(
|
func (r *Resolver) queryServers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
servers []string,
|
servers []string,
|
||||||
|
zone string,
|
||||||
name string,
|
name string,
|
||||||
qtype uint16,
|
qtype uint16,
|
||||||
) (*dns.Msg, error) {
|
) (*dns.Msg, error) {
|
||||||
@@ -269,6 +277,12 @@ func (r *Resolver) queryServers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryDNS(ctx, ip, name, qtype)
|
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 {
|
if err == nil {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -279,6 +293,38 @@ func (r *Resolver) queryServers(
|
|||||||
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
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(
|
func (r *Resolver) resolveNSIPs(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
nsNames []string,
|
nsNames []string,
|
||||||
@@ -312,6 +358,7 @@ func (r *Resolver) resolveNSIterative(
|
|||||||
|
|
||||||
domain = dns.Fqdn(domain)
|
domain = dns.Fqdn(domain)
|
||||||
servers := rootServerList()
|
servers := rootServerList()
|
||||||
|
zone := "."
|
||||||
|
|
||||||
for range maxDelegation {
|
for range maxDelegation {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
@@ -319,7 +366,7 @@ func (r *Resolver) resolveNSIterative(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryServers(
|
resp, err := r.queryServers(
|
||||||
ctx, servers, domain, dns.TypeNS,
|
ctx, servers, zone, domain, dns.TypeNS,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -344,6 +391,7 @@ func (r *Resolver) resolveNSIterative(
|
|||||||
}
|
}
|
||||||
|
|
||||||
servers = nextServers
|
servers = nextServers
|
||||||
|
zone = referralZone(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrNoNameservers
|
return nil, ErrNoNameservers
|
||||||
@@ -361,6 +409,7 @@ func (r *Resolver) resolveARecord(
|
|||||||
|
|
||||||
hostname = dns.Fqdn(hostname)
|
hostname = dns.Fqdn(hostname)
|
||||||
servers := rootServerList()
|
servers := rootServerList()
|
||||||
|
zone := "."
|
||||||
|
|
||||||
for range maxDelegation {
|
for range maxDelegation {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
@@ -368,7 +417,7 @@ func (r *Resolver) resolveARecord(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryServers(
|
resp, err := r.queryServers(
|
||||||
ctx, servers, hostname, dns.TypeA,
|
ctx, servers, zone, hostname, dns.TypeA,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
@@ -406,6 +455,7 @@ func (r *Resolver) resolveARecord(
|
|||||||
}
|
}
|
||||||
|
|
||||||
servers = nextServers
|
servers = nextServers
|
||||||
|
zone = referralZone(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
|
|||||||
@@ -41,6 +41,94 @@ func TestCollectIPs_FailedIsNoAnswer(t *testing.T) {
|
|||||||
assert.Empty(t, ips)
|
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) {
|
func TestExtractRecordValue_LetterCase(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user