Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1024b2b20 |
@@ -561,16 +561,16 @@ This approach ensures:
|
|||||||
- Ability to detect split-horizon or inconsistent responses across authoritative
|
- Ability to detect split-horizon or inconsistent responses across authoritative
|
||||||
servers.
|
servers.
|
||||||
|
|
||||||
A root server never offers recursion, so a reply marked "recursion available"
|
dnswatcher never asks a server for recursion, so a reply marked "recursion
|
||||||
from a root server's address came from a recursive resolver on the network
|
available" did not come from the server it was sent to. It came from a recursive
|
||||||
answering in its place, as on a network where a router rule, a VPN or a DNS
|
resolver on the network answering in that server's place, as on a network where
|
||||||
filter sends all DNS to one resolver. Such a reply is passed over for the next
|
a router rule, a VPN or a DNS filter sends all DNS to one resolver. Such a query
|
||||||
root server. When every root server refuses a query or gives such a reply, the
|
is sent again over TCP, which many of those networks leave alone, and the reply
|
||||||
lookup fails with the error "this network intercepts DNS queries". Every lookup
|
over TCP is used; when that retry fails, the reply over UDP is kept. A root
|
||||||
starts at the root servers, so on such a network every lookup fails with that
|
server never offers recursion, so a reply from a root server's address that is
|
||||||
error, whether or not that resolver has the name cached, and dnswatcher's tests
|
still marked so after that is passed over. When every root server refuses a
|
||||||
fail there too. That is by design: nothing asks again another way or otherwise
|
query or gives such a reply, the lookup fails with the error "this network
|
||||||
works around the interception.
|
intercepts DNS queries".
|
||||||
|
|
||||||
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: a reply marked "recursion available" from a root server's address
|
- 2026-10-16: a reply over UDP marked "recursion available" is asked again over
|
||||||
is not used; when every root server refuses or gives one, the lookup fails
|
TCP; such a reply from a root server's address is not used, and when every
|
||||||
with "this network intercepts DNS queries", so on such a network dnswatcher
|
root server refuses or gives one, the error is "this network intercepts DNS
|
||||||
and its tests fail, by design (closes #300).
|
queries" (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,9 @@ var (
|
|||||||
"reply truncated and its retry over TCP failed",
|
"reply truncated and its retry over TCP failed",
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrRecursionAvailable is the reason a reply from a root server's
|
// ErrRecursionAvailable is the reason a reply from a root server
|
||||||
// address marked recursion available is not used. Root servers offer
|
// marked recursion available is not used. Root servers offer no
|
||||||
// no recursion, so the reply came from something on the network
|
// recursion, so the reply came from something on the network
|
||||||
// answering in their place.
|
// answering in their place.
|
||||||
ErrRecursionAvailable = errors.New(
|
ErrRecursionAvailable = errors.New(
|
||||||
"reply marked recursion available, so not from a root server",
|
"reply marked recursion available, so not from a root server",
|
||||||
@@ -64,8 +64,7 @@ var (
|
|||||||
// ErrIntercepted is returned when every root server refused a
|
// ErrIntercepted is returned when every root server refused a
|
||||||
// query or gave a reply marked recursion available. Root servers do
|
// 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.
|
||||||
// it, by design: nothing works around the interception.
|
|
||||||
ErrIntercepted = errors.New("this network intercepts DNS queries")
|
ErrIntercepted = errors.New("this network intercepts DNS queries")
|
||||||
|
|
||||||
// ErrCNAMEDepthExceeded is returned when a CNAME chain
|
// ErrCNAMEDepthExceeded is returned when a CNAME chain
|
||||||
|
|||||||
@@ -94,15 +94,18 @@ func (r *Resolver) tryExchange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// retryTCP returns the reply to msg over TCP when resp, its reply over
|
// retryTCP returns the reply to msg over TCP when resp, its reply over
|
||||||
// UDP, is truncated. When that fails it returns resp, still truncated,
|
// UDP, is truncated or marked recursion available. msg does not ask for
|
||||||
// which holds only the records that fit.
|
// recursion, so a reply marked so came from a recursive resolver on the
|
||||||
|
// network answering in the server's place, and many networks that send
|
||||||
|
// DNS to one do so only for UDP. When the retry fails it returns resp;
|
||||||
|
// a truncated one holds only the records that fit.
|
||||||
func (r *Resolver) retryTCP(
|
func (r *Resolver) retryTCP(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
msg *dns.Msg,
|
msg *dns.Msg,
|
||||||
addr string,
|
addr string,
|
||||||
resp *dns.Msg,
|
resp *dns.Msg,
|
||||||
) *dns.Msg {
|
) *dns.Msg {
|
||||||
if !resp.Truncated {
|
if !resp.Truncated && !resp.RecursionAvailable {
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +118,9 @@ func (r *Resolver) retryTCP(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// queryDNS sends a DNS query to a specific server IP, never asking it
|
// 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. When the reply over UDP is truncated or marked
|
||||||
|
// recursion available, the query is sent again over TCP (see retryTCP).
|
||||||
|
// A reply of REFUSED is returned as ErrRefused.
|
||||||
func (r *Resolver) queryDNS(
|
func (r *Resolver) queryDNS(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
serverIP string,
|
serverIP string,
|
||||||
@@ -138,13 +143,15 @@ func (r *Resolver) queryDNS(
|
|||||||
return nil, fmt.Errorf("query %s @%s: %w", name, serverIP, err)
|
return nil, fmt.Errorf("query %s @%s: %w", name, serverIP, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp = r.retryTCP(ctx, msg, addr, resp)
|
||||||
|
|
||||||
if resp.Rcode == dns.RcodeRefused {
|
if resp.Rcode == dns.RcodeRefused {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"query %s @%s: %w", name, serverIP, ErrRefused,
|
"query %s @%s: %w", name, serverIP, ErrRefused,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.retryTCP(ctx, msg, addr, resp), nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractNSSet(rrs []dns.RR) []string {
|
func extractNSSet(rrs []dns.RR) []string {
|
||||||
|
|||||||
@@ -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. Each refuses the query or gives a reply marked
|
// resolver would. Over UDP and over TCP alike, they mark every reply
|
||||||
// recursion available, which no root server does: SERVFAIL, or an
|
// recursion available, which no root server does. Each refuses the
|
||||||
// answer from its cache, as 1.1.1.1 gives for com., which would be used
|
// query, replies SERVFAIL, or answers from its cache, as 1.1.1.1 does
|
||||||
// if a root server gave it. Either way the error is ErrIntercepted, and
|
// for com.: a reply that would be used if a root server gave it. Either
|
||||||
// since 1.1.1.1 and 8.8.8.8 refuse no such query, it says a reply was
|
// way the error is ErrIntercepted, and since 1.1.1.1 and 8.8.8.8 refuse
|
||||||
// marked recursion available.
|
// no such query, it says a reply was marked recursion available.
|
||||||
func TestQueryServers_RecursiveResolversAsRootServers(t *testing.T) {
|
func TestQueryServers_RecursiveResolversAsRootServers(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user