resolver: never resend a refused query asking for recursion (closes #206)
check / check (push) Failing after 2m28s
check / check (push) Failing after 2m28s
queryDNS resent a query that a server refused, this time asking for recursion, so on a network that intercepts DNS the answers could come from a recursive resolver without anyone knowing. A refusal is now only a refusal, and the server is passed over for the next. When every server of a zone refuses, the error says so. When every root server refuses, the error is ErrIntercepted: root servers refuse no query, so something on the network is answering in their place. FindAuthoritativeNameservers stops at that error instead of trying each parent name, so the watcher's log line says it. A live test asks Quad9, which refuses a query not asking for recursion, so that the resend cannot come back unnoticed. Model: opus-5-5
This commit is contained in:
@@ -107,9 +107,8 @@ func (r *Resolver) retryTCP(
|
||||
return resp
|
||||
}
|
||||
|
||||
// queryDNS sends a DNS query to a specific server IP.
|
||||
// Tries non-recursive first, falls back to recursive on
|
||||
// REFUSED (handles DNS interception environments).
|
||||
// queryDNS sends a DNS query to a specific server IP, never asking it
|
||||
// for recursion. A reply of REFUSED is returned as ErrRefused.
|
||||
func (r *Resolver) queryDNS(
|
||||
ctx context.Context,
|
||||
serverIP string,
|
||||
@@ -133,25 +132,12 @@ func (r *Resolver) queryDNS(
|
||||
}
|
||||
|
||||
if resp.Rcode == dns.RcodeRefused {
|
||||
msg.RecursionDesired = true
|
||||
|
||||
resp, err = r.tryExchange(ctx, msg, addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"query %s @%s: %w", name, serverIP, err,
|
||||
)
|
||||
}
|
||||
|
||||
if resp.Rcode == dns.RcodeRefused {
|
||||
return nil, fmt.Errorf(
|
||||
"query %s @%s: %w", name, serverIP, ErrRefused,
|
||||
)
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"query %s @%s: %w", name, serverIP, ErrRefused,
|
||||
)
|
||||
}
|
||||
|
||||
resp = r.retryTCP(ctx, msg, addr, resp)
|
||||
|
||||
return resp, nil
|
||||
return r.retryTCP(ctx, msg, addr, resp), nil
|
||||
}
|
||||
|
||||
func extractNSSet(rrs []dns.RR) []string {
|
||||
@@ -279,7 +265,9 @@ 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.
|
||||
// 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,
|
||||
@@ -289,6 +277,8 @@ func (r *Resolver) queryServers(
|
||||
) (*dns.Msg, error) {
|
||||
var lastErr error
|
||||
|
||||
refused := 0
|
||||
|
||||
for _, ip := range shuffled(servers, rand.Shuffle) {
|
||||
if checkCtx(ctx) != nil {
|
||||
return nil, ErrContextCanceled
|
||||
@@ -305,9 +295,27 @@ func (r *Resolver) queryServers(
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrRefused) {
|
||||
refused++
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
if refused == len(servers) && zone == "." {
|
||||
return nil, fmt.Errorf(
|
||||
"every root server refused a query for %s: %w",
|
||||
name, ErrIntercepted,
|
||||
)
|
||||
}
|
||||
|
||||
if refused == len(servers) {
|
||||
return nil, fmt.Errorf(
|
||||
"every server of %s refused a query for %s: %w",
|
||||
zone, name, ErrRefused,
|
||||
)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
||||
}
|
||||
|
||||
@@ -528,6 +536,11 @@ func (r *Resolver) FindAuthoritativeNameservers(
|
||||
|
||||
return nsNames, nil
|
||||
}
|
||||
|
||||
// The root servers would refuse every parent name too.
|
||||
if errors.Is(err, ErrIntercepted) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrNoNameservers
|
||||
|
||||
Reference in New Issue
Block a user