Compare commits
2
Commits
c41349fd94
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c842efdb81 | ||
|
|
b23cf43abf |
@@ -565,13 +565,13 @@ A root server never offers recursion, so a reply marked "recursion available"
|
||||
from a root server's address came from a recursive resolver on the network
|
||||
answering in its place, as on a network where a router rule, a VPN or a DNS
|
||||
filter sends all DNS to one resolver. The first such reply from a root server's
|
||||
address stops the lookup at once with the error "this network intercepts DNS
|
||||
queries", and no other root server is asked. A lookup that every root server
|
||||
refuses fails with the same error. Every lookup starts at the root servers, so
|
||||
on such a network every lookup fails with that error, whether or not that
|
||||
resolver has the name cached, and dnswatcher's tests fail there too. That is by
|
||||
design: nothing asks again another way or otherwise works around the
|
||||
interception.
|
||||
address, a refusal included, stops the lookup at once with the error "this
|
||||
network intercepts DNS queries", naming that address, and no other root server
|
||||
is asked. A lookup that every root server refuses without that mark fails with
|
||||
the same error. Every lookup starts at the root servers, so on such a network
|
||||
every lookup fails with that error, whether or not that resolver has the name
|
||||
cached, and dnswatcher's tests fail there too. That is by design: nothing asks
|
||||
again another way or otherwise works around the interception.
|
||||
|
||||
A watched name's records are stored as its nameservers return them, CNAME
|
||||
included. When they return a CNAME and no address, the chain of every CNAME
|
||||
|
||||
@@ -20,9 +20,9 @@ milestone: https://git.eeqj.de/sneak/dnswatcher/milestone/7
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-16: the first reply marked "recursion available" from a root server's
|
||||
address stops the lookup with "this network intercepts DNS queries", so on a
|
||||
network that sends DNS to a resolver dnswatcher and its tests fail, by design
|
||||
(closes #300).
|
||||
address, a refusal included, stops the lookup with "this network intercepts
|
||||
DNS queries", so on a network that sends DNS to a resolver dnswatcher and its
|
||||
tests fail, by design (closes #300).
|
||||
- 2026-10-15: a query its caller's context ends before it is sent records no
|
||||
reason about the nameserver, and a try the context's deadline cuts short is a
|
||||
timeout, so a nameserver whose only failures were timeouts is `timeout`, not a
|
||||
|
||||
@@ -54,11 +54,12 @@ var (
|
||||
)
|
||||
|
||||
// ErrIntercepted is returned when a root server's address gave a
|
||||
// reply marked recursion available, which ends the lookup at once,
|
||||
// or when every root server refused a query. Root servers do
|
||||
// neither, so those replies came from something on the network
|
||||
// answering in their place. On such a network dnswatcher fails with
|
||||
// it, by design: nothing works around the interception.
|
||||
// reply marked recursion available, a refusal included, which ends
|
||||
// the lookup at once, or when every root server refused a query.
|
||||
// Root servers do neither, so those replies came from something on
|
||||
// the network answering in their place. On such a network
|
||||
// dnswatcher fails with it, by design: nothing works around the
|
||||
// interception.
|
||||
ErrIntercepted = errors.New("this network intercepts DNS queries")
|
||||
|
||||
// ErrCNAMEDepthExceeded is returned when a CNAME chain
|
||||
|
||||
@@ -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,14 +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. When they are the root servers, the
|
||||
// first reply marked recursion available also 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.
|
||||
// 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,
|
||||
@@ -308,8 +310,10 @@ 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 err == nil && zone == "." && resp.RecursionAvailable {
|
||||
if resp != nil && zone == "." && resp.RecursionAvailable {
|
||||
return nil, fmt.Errorf(
|
||||
"query %s @%s: reply marked recursion available, "+
|
||||
"so not from a root server: %w",
|
||||
|
||||
@@ -851,59 +851,70 @@ func TestQueryServers_NotEveryRootServerRefused(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestQueryServers_RecursiveResolversAsRootServers passes public
|
||||
// recursive resolvers of four operators to QueryServers as the servers
|
||||
// of the root zone, as a network that sends every DNS query to a
|
||||
// resolver would. Their replies are marked recursion available, which
|
||||
// no root server's is: SERVFAIL, or an answer from a resolver's cache,
|
||||
// as 1.1.1.1 gives for com., which would be used if a root server gave
|
||||
// it. The first such reply ends the lookup with ErrIntercepted, naming
|
||||
// the resolver that gave it. A resolver that refuses or gives no reply
|
||||
// is passed over for the next, so one of them answering is enough.
|
||||
// recursive resolvers to QueryServers as the servers of the root zone,
|
||||
// as a network that sends every DNS query to a resolver would. Their
|
||||
// replies are marked recursion available, which no root server's is:
|
||||
// SERVFAIL, REFUSED, or an answer from a resolver's cache, as 1.1.1.1
|
||||
// gives for com., which would be used if a root server gave it. The
|
||||
// first such reply ends the lookup with ErrIntercepted, naming the
|
||||
// resolver that gave it. In the first group the replies differ; every
|
||||
// resolver in the second refuses a query that does not ask for
|
||||
// recursion, as 9.9.9.9 does, so there a refusal ends the lookup
|
||||
// instead of being passed over. Each group has resolvers of four
|
||||
// operators, and a resolver that gives no reply is passed over for the
|
||||
// next, so one of them answering is enough.
|
||||
func TestQueryServers_RecursiveResolversAsRootServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := newTestResolver(t)
|
||||
resolvers := []string{
|
||||
"1.1.1.1", "8.8.8.8", "9.9.9.9", "208.67.222.222",
|
||||
groups := [][]string{
|
||||
{"1.1.1.1", "8.8.8.8", "9.9.9.9", "208.67.222.222"},
|
||||
{"64.6.64.6", "185.222.222.222", "4.2.2.1", "94.140.14.14"},
|
||||
}
|
||||
|
||||
for _, name := range []string{"com.", "desec.io."} {
|
||||
var err error
|
||||
for _, resolvers := range groups {
|
||||
for _, name := range []string{"com.", "desec.io."} {
|
||||
var err error
|
||||
|
||||
livednstest.Retry(
|
||||
t,
|
||||
"QueryServers(public recursive resolvers as root servers, "+
|
||||
name+")",
|
||||
func(ctx context.Context) error {
|
||||
_, err = r.QueryServers(ctx, resolvers, ".", name, dns.TypeNS)
|
||||
|
||||
// When no resolver gave a reply marked recursion
|
||||
// available, each refused or gave no reply at all, so
|
||||
// the attempt is tried again.
|
||||
if err != nil &&
|
||||
!strings.Contains(err.Error(), "marked recursion available") {
|
||||
return fmt.Errorf(
|
||||
"%w: %w", livednstest.ErrNoAnswer, err,
|
||||
livednstest.Retry(
|
||||
t,
|
||||
"QueryServers("+strings.Join(resolvers, ", ")+
|
||||
" as root servers, "+name+")",
|
||||
func(ctx context.Context) error {
|
||||
_, err = r.QueryServers(
|
||||
ctx, resolvers, ".", name, dns.TypeNS,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
// When no resolver gave a reply marked recursion
|
||||
// available, none may have replied at all, so the
|
||||
// attempt is tried again.
|
||||
if err != nil && !strings.Contains(
|
||||
err.Error(), "marked recursion available",
|
||||
) {
|
||||
return fmt.Errorf(
|
||||
"%w: %w", livednstest.ErrNoAnswer, err,
|
||||
)
|
||||
}
|
||||
|
||||
require.ErrorIs(t, err, resolver.ErrIntercepted)
|
||||
|
||||
// The error names the resolver whose reply ended the lookup.
|
||||
messages := make([]string, 0, len(resolvers))
|
||||
for _, ip := range resolvers {
|
||||
messages = append(messages,
|
||||
"query "+name+" @"+ip+": reply marked recursion "+
|
||||
"available, so not from a root server: "+
|
||||
"this network intercepts DNS queries",
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
require.Contains(t, messages, err.Error())
|
||||
require.ErrorIs(t, err, resolver.ErrIntercepted)
|
||||
|
||||
// The error names the resolver whose reply ended the
|
||||
// lookup.
|
||||
messages := make([]string, 0, len(resolvers))
|
||||
for _, ip := range resolvers {
|
||||
messages = append(messages,
|
||||
"query "+name+" @"+ip+": reply marked recursion "+
|
||||
"available, so not from a root server: "+
|
||||
"this network intercepts DNS queries",
|
||||
)
|
||||
}
|
||||
|
||||
require.Contains(t, messages, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user