Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca26499c88 |
@@ -564,14 +564,13 @@ This approach ensures:
|
|||||||
A root server never offers recursion, so a reply marked "recursion available"
|
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
|
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
|
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
|
filter sends all DNS to one resolver. Such a reply is passed over for the next
|
||||||
address stops the lookup at once with the error "this network intercepts DNS
|
root server. When every root server refuses a query or gives such a reply, the
|
||||||
queries", and no other root server is asked. A lookup that every root server
|
lookup fails with the error "this network intercepts DNS queries". Every lookup
|
||||||
refuses fails with the same error. Every lookup starts at the root servers, so
|
starts at the root servers, so on such a network every lookup fails with that
|
||||||
on such a network every lookup fails with that error, whether or not that
|
error, whether or not that resolver has the name cached, and dnswatcher's tests
|
||||||
resolver has the name cached, and dnswatcher's tests fail there too. That is by
|
fail there too. That is by design: nothing asks again another way or otherwise
|
||||||
design: nothing asks again another way or otherwise works around the
|
works around the interception.
|
||||||
interception.
|
|
||||||
|
|
||||||
A watched name's records are stored as its nameservers return them, CNAME
|
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
|
included. When they return a CNAME and no address, the chain of every CNAME
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ milestone: https://git.eeqj.de/sneak/dnswatcher/milestone/7
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-10-16: the first reply marked "recursion available" from a root server's
|
- 2026-10-16: a reply marked "recursion available" from a root server's address
|
||||||
address stops the lookup with "this network intercepts DNS queries", so on a
|
is not used; when every root server refuses or gives one, the lookup fails
|
||||||
network that sends DNS to a resolver dnswatcher and its tests fail, by design
|
with "this network intercepts DNS queries", so on such a network dnswatcher
|
||||||
(closes #300).
|
and its tests fail, by design (closes #300).
|
||||||
- 2026-10-15: a query its caller's context ends before it is sent records no
|
- 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
|
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
|
timeout, so a nameserver whose only failures were timeouts is `timeout`, not a
|
||||||
|
|||||||
@@ -53,9 +53,16 @@ var (
|
|||||||
"reply truncated and its retry over TCP failed",
|
"reply truncated and its retry over TCP failed",
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrIntercepted is returned when a root server's address gave a
|
// ErrRecursionAvailable is the reason a reply from a root server's
|
||||||
// reply marked recursion available, which ends the lookup at once,
|
// address marked recursion available is not used. Root servers offer
|
||||||
// or when every root server refused a query. Root servers do
|
// no recursion, so the reply came from something on the network
|
||||||
|
// answering in their place.
|
||||||
|
ErrRecursionAvailable = errors.New(
|
||||||
|
"reply marked recursion available, so not from a root server",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrIntercepted is returned when every root server refused a
|
||||||
|
// query or gave a reply marked recursion available. Root servers do
|
||||||
// neither, so those replies came from something on the network
|
// neither, so those replies came from something on the network
|
||||||
// answering in their place. On such a network dnswatcher fails with
|
// answering in their place. On such a network dnswatcher fails with
|
||||||
// it, by design: nothing works around the interception.
|
// it, by design: nothing works around the interception.
|
||||||
|
|||||||
@@ -284,14 +284,11 @@ func shuffled(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// queryServers asks servers, the servers of zone, about name in a random
|
// 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
|
// order until one gives a reply it uses (see replyError). A server that
|
||||||
// or gives a reply that is not usable is passed over for the next. When
|
// times out, refuses or gives a reply it does not use is passed over for
|
||||||
// every server refused, the error says so, and when they are the root
|
// the next. When every server refused, the error says so. When they are
|
||||||
// servers it is ErrIntercepted. When they are the root servers, the
|
// the root servers and each one refused or gave a reply marked recursion
|
||||||
// first reply marked recursion available also ends the lookup with
|
// available, the error is ErrIntercepted.
|
||||||
// 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.
|
|
||||||
func (r *Resolver) queryServers(
|
func (r *Resolver) queryServers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
servers []string,
|
servers []string,
|
||||||
@@ -302,6 +299,7 @@ func (r *Resolver) queryServers(
|
|||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
refused := 0
|
refused := 0
|
||||||
|
recursionAvailable := 0
|
||||||
|
|
||||||
for _, ip := range shuffled(servers, rand.Shuffle) {
|
for _, ip := range shuffled(servers, rand.Shuffle) {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
@@ -309,18 +307,8 @@ func (r *Resolver) queryServers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryDNS(ctx, ip, name, qtype)
|
resp, err := r.queryDNS(ctx, ip, name, qtype)
|
||||||
if err == nil && zone == "." && resp.RecursionAvailable {
|
if err == nil {
|
||||||
return nil, fmt.Errorf(
|
err = replyError(resp, ip, zone, name)
|
||||||
"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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -331,6 +319,10 @@ func (r *Resolver) queryServers(
|
|||||||
refused++
|
refused++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrRecursionAvailable) {
|
||||||
|
recursionAvailable++
|
||||||
|
}
|
||||||
|
|
||||||
lastErr = err
|
lastErr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +333,14 @@ func (r *Resolver) queryServers(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if refused+recursionAvailable == len(servers) && zone == "." {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"every root server refused a query for %s or gave a reply "+
|
||||||
|
"marked recursion available: %w",
|
||||||
|
name, ErrIntercepted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if refused == len(servers) {
|
if refused == len(servers) {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"every server of %s refused a query for %s: %w",
|
"every server of %s refused a query for %s: %w",
|
||||||
@@ -351,6 +351,27 @@ func (r *Resolver) queryServers(
|
|||||||
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replyError returns why resp, the reply of the server at ip, one of the
|
||||||
|
// servers of zone, to a query about name, is not used, or nil when it
|
||||||
|
// is. A reply that is not usable is not used. Nor is a reply from a root
|
||||||
|
// server marked recursion available: root servers offer no recursion,
|
||||||
|
// so it came from something on the network answering in their place.
|
||||||
|
func replyError(resp *dns.Msg, ip string, zone string, name string) error {
|
||||||
|
if zone == "." && resp.RecursionAvailable {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"query %s @%s: %w", name, ip, ErrRecursionAvailable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !usableReply(resp, zone, name) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"query %s @%s: %w", name, ip, ErrUnusableReply,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// isErrorReply reports whether msg is an error reply: one with any code
|
// isErrorReply reports whether msg is an error reply: one with any code
|
||||||
// but NOERROR and NXDOMAIN, such as SERVFAIL, NOTIMP or FORMERR. An error
|
// but NOERROR and NXDOMAIN, such as SERVFAIL, NOTIMP or FORMERR. An error
|
||||||
// reply says nothing about the name's records.
|
// reply says nothing about the name's records.
|
||||||
|
|||||||
@@ -853,12 +853,12 @@ func TestQueryServers_NotEveryRootServerRefused(t *testing.T) {
|
|||||||
// TestQueryServers_RecursiveResolversAsRootServers passes public
|
// TestQueryServers_RecursiveResolversAsRootServers passes public
|
||||||
// recursive resolvers of four operators to QueryServers as the servers
|
// recursive resolvers of four operators to QueryServers as the servers
|
||||||
// of the root zone, as a network that sends every DNS query to a
|
// of the root zone, as a network that sends every DNS query to a
|
||||||
// resolver would. Their replies are marked recursion available, which
|
// resolver would. Each refuses the query or gives a reply marked
|
||||||
// no root server's is: SERVFAIL, or an answer from a resolver's cache,
|
// recursion available, which no root server does: SERVFAIL, or an
|
||||||
// as 1.1.1.1 gives for com., which would be used if a root server gave
|
// answer from its cache, as 1.1.1.1 gives for com., which would be used
|
||||||
// it. The first such reply ends the lookup with ErrIntercepted, naming
|
// if a root server gave it. Either way the error is ErrIntercepted, and
|
||||||
// the resolver that gave it. A resolver that refuses or gives no reply
|
// since 1.1.1.1 and 8.8.8.8 refuse no such query, it says a reply was
|
||||||
// is passed over for the next, so one of them answering is enough.
|
// marked recursion available.
|
||||||
func TestQueryServers_RecursiveResolversAsRootServers(t *testing.T) {
|
func TestQueryServers_RecursiveResolversAsRootServers(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -877,11 +877,9 @@ func TestQueryServers_RecursiveResolversAsRootServers(t *testing.T) {
|
|||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
_, err = r.QueryServers(ctx, resolvers, ".", name, dns.TypeNS)
|
_, err = r.QueryServers(ctx, resolvers, ".", name, dns.TypeNS)
|
||||||
|
|
||||||
// When no resolver gave a reply marked recursion
|
// When the error is another, one resolver may have given
|
||||||
// available, each refused or gave no reply at all, so
|
// no reply at all, so the attempt is tried again.
|
||||||
// the attempt is tried again.
|
if err != nil && !errors.Is(err, resolver.ErrIntercepted) {
|
||||||
if err != nil &&
|
|
||||||
!strings.Contains(err.Error(), "marked recursion available") {
|
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"%w: %w", livednstest.ErrNoAnswer, err,
|
"%w: %w", livednstest.ErrNoAnswer, err,
|
||||||
)
|
)
|
||||||
@@ -892,18 +890,12 @@ func TestQueryServers_RecursiveResolversAsRootServers(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
require.ErrorIs(t, err, resolver.ErrIntercepted)
|
require.ErrorIs(t, err, resolver.ErrIntercepted)
|
||||||
|
require.EqualError(
|
||||||
// The error names the resolver whose reply ended the lookup.
|
t, err,
|
||||||
messages := make([]string, 0, len(resolvers))
|
"every root server refused a query for "+name+" or gave a "+
|
||||||
for _, ip := range resolvers {
|
"reply marked recursion available: "+
|
||||||
messages = append(messages,
|
"this network intercepts DNS queries",
|
||||||
"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