resolver: a reply marked recursion available from a root server's address stops the lookup (closes #300)
check / check (push) Waiting to run

dnswatcher never asks for recursion and a root server never offers it,
so a reply marked recursion available from a root server's address
came from a resolver on the network answering in its place. The first
such reply, a refusal included, now stops the lookup at once with
"this network intercepts DNS queries", naming the address, whether or
not that resolver had the name cached. queryDNS returns a refused reply
along with ErrRefused so the mark can be seen; a refusal without the
mark is handled as before. On such a network dnswatcher and its tests
fail with that error, by design, as the owner ruled on the issue.

Model: opus-5-5
This commit was merged in pull request #301.
This commit is contained in:
2026-10-16 12:26:52 +02:00
parent adc8131191
commit b23cf43abf
5 changed files with 113 additions and 9 deletions
+22 -6
View File
@@ -115,7 +115,8 @@ func (r *Resolver) retryTCP(
}
// queryDNS sends a DNS query to a specific server IP, never asking it
// for recursion. A reply of REFUSED is returned as ErrRefused.
// for recursion. A reply of REFUSED is returned together with
// ErrRefused, so that the caller can still read the reply's flags.
func (r *Resolver) queryDNS(
ctx context.Context,
serverIP string,
@@ -139,7 +140,7 @@ func (r *Resolver) queryDNS(
}
if resp.Rcode == dns.RcodeRefused {
return nil, fmt.Errorf(
return resp, fmt.Errorf(
"query %s @%s: %w", name, serverIP, ErrRefused,
)
}
@@ -284,10 +285,15 @@ func shuffled(
}
// 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. When
// every server refused, the error says so, and when they are the root
// servers it is ErrIntercepted.
// order until one gives a usable reply. When they are the root servers,
// the first reply marked recursion available, a refusal included, ends
// the lookup with ErrIntercepted, naming the address it came from, and
// no other root server is asked: root servers offer no recursion, so
// that reply came from something on the network answering in their
// place. Otherwise a server that times out, refuses or gives a reply
// that is not usable is passed over for the next. When every server
// refused, the error says so, and when they are the root servers it is
// ErrIntercepted.
func (r *Resolver) queryServers(
ctx context.Context,
servers []string,
@@ -304,7 +310,17 @@ func (r *Resolver) queryServers(
return nil, ErrContextCanceled
}
// queryDNS returns a refusal together with its reply, so a
// refusal's mark is checked here too.
resp, err := r.queryDNS(ctx, ip, name, qtype)
if resp != nil && zone == "." && resp.RecursionAvailable {
return nil, fmt.Errorf(
"query %s @%s: reply marked recursion available, "+
"so not from a root server: %w",
name, ip, ErrIntercepted,
)
}
if err == nil && !usableReply(resp, zone, name) {
err = fmt.Errorf(
"query %s @%s: %w", name, ip, ErrUnusableReply,