resolver: a reply marked recursion available from a root server's address stops the lookup (closes #300) #301

Merged
clawbot merged 1 commits from fix/300-intercepted-dns into next 2026-10-16 12:26:53 +02:00
Collaborator

On a network that sends all DNS to one recursive resolver, dnswatcher took that resolver's cached answers as if each nameserver had given them (#300).

  • A root server never offers recursion, so the first reply marked "recursion available" from a root server's address, a refusal included, now stops the lookup with the existing error "this network intercepts DNS queries", naming the address; no other root server is asked.
  • queryDNS now returns a refused reply with ErrRefused, so a refusal's mark can be seen. Its other caller, the per-nameserver record queries, ignores that reply as before.
  • A refusal without the mark is passed over and counted as on next; when every root server refuses, the error is unchanged.
  • A live test passes two groups of public resolvers, each from four operators, to QueryServers as the root servers and expects that error; in the second group each refuses, as 9.9.9.9 does. One answering per group suffices.
  • The README and TODO.md say dnswatcher and its tests fail on such a network, by design.

Worth knowing:

  • Every lookup starts at the root servers, so cached and uncached names fail alike.
  • Nothing asks again after such a reply, over TCP or otherwise.
  • Only root servers' replies are checked.
  • Where only some root server addresses are intercepted, a lookup that first reaches a real one still succeeds.

Hand check: with UDP, then UDP and TCP, port 53 redirected inside a throwaway container to 1.1.1.1, then 9.9.9.9, all four names failed with that error; with no redirect they resolved.

Model: opus-5-5

On a network that sends all DNS to one recursive resolver, dnswatcher took that resolver's cached answers as if each nameserver had given them (https://git.eeqj.de/sneak/dnswatcher/issues/300). - A root server never offers recursion, so the first reply marked "recursion available" from a root server's address, a refusal included, now stops the lookup with the existing error "this network intercepts DNS queries", naming the address; no other root server is asked. - `queryDNS` now returns a refused reply with `ErrRefused`, so a refusal's mark can be seen. Its other caller, the per-nameserver record queries, ignores that reply as before. - A refusal without the mark is passed over and counted as on `next`; when every root server refuses, the error is unchanged. - A live test passes two groups of public resolvers, each from four operators, to `QueryServers` as the root servers and expects that error; in the second group each refuses, as 9.9.9.9 does. One answering per group suffices. - The README and `TODO.md` say dnswatcher and its tests fail on such a network, by design. Worth knowing: - Every lookup starts at the root servers, so cached and uncached names fail alike. - Nothing asks again after such a reply, over TCP or otherwise. - Only root servers' replies are checked. - Where only some root server addresses are intercepted, a lookup that first reaches a real one still succeeds. Hand check: with UDP, then UDP and TCP, port 53 redirected inside a throwaway container to 1.1.1.1, then 9.9.9.9, all four names failed with that error; with no redirect they resolved. Model: opus-5-5
clawbot added the needs-review label 2026-10-16 09:55:32 +02:00
clawbot self-assigned this 2026-10-16 09:55:35 +02:00
Owner

no. on a network that is filtering/altering DNS packets, the tests should fail!

no. on a network that is filtering/altering DNS packets, the tests *should* fail!
clawbot added needs-rework and removed needs-review labels 2026-10-16 10:08:16 +02:00
Owner
user@heraklion:~$    dig +norec @198.41.0.4 desec.io NS
     dig +norec +tcp @198.41.0.4 desec.io NS

; <<>> DiG 9.10.6 <<>> +norec @198.41.0.4 desec.io NS
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 57481
;; flags: qr ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;desec.io.                      IN      NS

;; Query time: 35 msec
;; SERVER: 198.41.0.4#53(198.41.0.4)
;; WHEN: Fri Oct 16 01:08:15 PDT 2026
;; MSG SIZE  rcvd: 26


; <<>> DiG 9.10.6 <<>> +norec +tcp @198.41.0.4 desec.io NS
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 35524
;; flags: qr ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;desec.io.                      IN      NS

;; Query time: 21 msec
;; SERVER: 198.41.0.4#53(198.41.0.4)
;; WHEN: Fri Oct 16 01:08:15 PDT 2026
;; MSG SIZE  rcvd: 26

user@heraklion:~$
``` user@heraklion:~$ dig +norec @198.41.0.4 desec.io NS dig +norec +tcp @198.41.0.4 desec.io NS ; <<>> DiG 9.10.6 <<>> +norec @198.41.0.4 desec.io NS ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 57481 ;; flags: qr ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;desec.io. IN NS ;; Query time: 35 msec ;; SERVER: 198.41.0.4#53(198.41.0.4) ;; WHEN: Fri Oct 16 01:08:15 PDT 2026 ;; MSG SIZE rcvd: 26 ; <<>> DiG 9.10.6 <<>> +norec +tcp @198.41.0.4 desec.io NS ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 35524 ;; flags: qr ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;desec.io. IN NS ;; Query time: 21 msec ;; SERVER: 198.41.0.4#53(198.41.0.4) ;; WHEN: Fri Oct 16 01:08:15 PDT 2026 ;; MSG SIZE rcvd: 26 user@heraklion:~$ ```
Owner
fsn1app1# dig +norec @198.41.0.4 desec.io NS

; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> +norec @198.41.0.4 desec.io NS
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20570
;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 9

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;desec.io.                      IN      NS

;; AUTHORITY SECTION:
io.                     172800  IN      NS      c0.nic.io.
io.                     172800  IN      NS      a2.nic.io.
io.                     172800  IN      NS      a0.nic.io.
io.                     172800  IN      NS      b0.nic.io.

;; ADDITIONAL SECTION:
c0.nic.io.              172800  IN      A       65.22.162.17
c0.nic.io.              172800  IN      AAAA    2a01:8840:a0::17
a2.nic.io.              172800  IN      A       65.22.163.17
a2.nic.io.              172800  IN      AAAA    2a01:8840:a1::17
a0.nic.io.              172800  IN      A       65.22.160.17
a0.nic.io.              172800  IN      AAAA    2a01:8840:9e::17
b0.nic.io.              172800  IN      A       65.22.161.17
b0.nic.io.              172800  IN      AAAA    2a01:8840:9f::17

;; Query time: 11 msec
;; SERVER: 198.41.0.4#53(198.41.0.4) (UDP)
;; WHEN: Fri Oct 16 10:11:25 CEST 2026
;; MSG SIZE  rcvd: 285

fsn1app1# dig +norec +tcp @198.41.0.4 desec.io NS

; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> +norec +tcp @198.41.0.4 desec.io NS
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54071
;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 9

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;desec.io.                      IN      NS

;; AUTHORITY SECTION:
io.                     172800  IN      NS      c0.nic.io.
io.                     172800  IN      NS      a2.nic.io.
io.                     172800  IN      NS      a0.nic.io.
io.                     172800  IN      NS      b0.nic.io.

;; ADDITIONAL SECTION:
c0.nic.io.              172800  IN      A       65.22.162.17
c0.nic.io.              172800  IN      AAAA    2a01:8840:a0::17
a2.nic.io.              172800  IN      A       65.22.163.17
a2.nic.io.              172800  IN      AAAA    2a01:8840:a1::17
a0.nic.io.              172800  IN      A       65.22.160.17
a0.nic.io.              172800  IN      AAAA    2a01:8840:9e::17
b0.nic.io.              172800  IN      A       65.22.161.17
b0.nic.io.              172800  IN      AAAA    2a01:8840:9f::17

;; Query time: 11 msec
;; SERVER: 198.41.0.4#53(198.41.0.4) (TCP)
;; WHEN: Fri Oct 16 10:11:30 CEST 2026
;; MSG SIZE  rcvd: 285

fsn1app1#
``` fsn1app1# dig +norec @198.41.0.4 desec.io NS ; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> +norec @198.41.0.4 desec.io NS ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20570 ;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 9 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;desec.io. IN NS ;; AUTHORITY SECTION: io. 172800 IN NS c0.nic.io. io. 172800 IN NS a2.nic.io. io. 172800 IN NS a0.nic.io. io. 172800 IN NS b0.nic.io. ;; ADDITIONAL SECTION: c0.nic.io. 172800 IN A 65.22.162.17 c0.nic.io. 172800 IN AAAA 2a01:8840:a0::17 a2.nic.io. 172800 IN A 65.22.163.17 a2.nic.io. 172800 IN AAAA 2a01:8840:a1::17 a0.nic.io. 172800 IN A 65.22.160.17 a0.nic.io. 172800 IN AAAA 2a01:8840:9e::17 b0.nic.io. 172800 IN A 65.22.161.17 b0.nic.io. 172800 IN AAAA 2a01:8840:9f::17 ;; Query time: 11 msec ;; SERVER: 198.41.0.4#53(198.41.0.4) (UDP) ;; WHEN: Fri Oct 16 10:11:25 CEST 2026 ;; MSG SIZE rcvd: 285 fsn1app1# dig +norec +tcp @198.41.0.4 desec.io NS ; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> +norec +tcp @198.41.0.4 desec.io NS ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54071 ;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 9 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;desec.io. IN NS ;; AUTHORITY SECTION: io. 172800 IN NS c0.nic.io. io. 172800 IN NS a2.nic.io. io. 172800 IN NS a0.nic.io. io. 172800 IN NS b0.nic.io. ;; ADDITIONAL SECTION: c0.nic.io. 172800 IN A 65.22.162.17 c0.nic.io. 172800 IN AAAA 2a01:8840:a0::17 a2.nic.io. 172800 IN A 65.22.163.17 a2.nic.io. 172800 IN AAAA 2a01:8840:a1::17 a0.nic.io. 172800 IN A 65.22.160.17 a0.nic.io. 172800 IN AAAA 2a01:8840:9e::17 b0.nic.io. 172800 IN A 65.22.161.17 b0.nic.io. 172800 IN AAAA 2a01:8840:9f::17 ;; Query time: 11 msec ;; SERVER: 198.41.0.4#53(198.41.0.4) (TCP) ;; WHEN: Fri Oct 16 10:11:30 CEST 2026 ;; MSG SIZE rcvd: 285 fsn1app1# ```
clawbot force-pushed fix/300-intercepted-dns from d1024b2b20 to ca26499c88 2026-10-16 10:23:09 +02:00 Compare
clawbot changed title from resolver: ask again over TCP when a reply offers recursion (closes #300) to resolver: a root server's reply marked recursion available means interception (closes #300) 2026-10-16 10:28:06 +02:00
clawbot added needs-review and removed needs-rework labels 2026-10-16 10:28:08 +02:00
Author
Collaborator

Review: pass.

Model: opus-5-5

Review: pass. Model: opus-5-5
Author
Collaborator

Rework, one point. The ruling (#300 (comment)) is that dnswatcher stops with the interception error and fails on such a network every time. As it stands, a root server's reply marked "recursion available" is passed over and the next root server is asked. On a network that intercepts only some root server addresses, the lookup therefore still succeeds. The new test also needs all four public resolvers to answer within one attempt.

Acceptable: the first reply marked "recursion available" from a root server's address ends the lookup at once with "this network intercepts DNS queries". The other root servers are not asked, and nothing is counted. The case where every root server refuses stays as on next. The README, TODO.md, the code comments, the commit message and the PR body are updated to match.

Model: opus-5-5

Rework, one point. The ruling (https://git.eeqj.de/sneak/dnswatcher/issues/300#issuecomment-144959) is that dnswatcher stops with the interception error and fails on such a network every time. As it stands, a root server's reply marked "recursion available" is passed over and the next root server is asked. On a network that intercepts only some root server addresses, the lookup therefore still succeeds. The new test also needs all four public resolvers to answer within one attempt. Acceptable: the first reply marked "recursion available" from a root server's address ends the lookup at once with "this network intercepts DNS queries". The other root servers are not asked, and nothing is counted. The case where every root server refuses stays as on `next`. The README, `TODO.md`, the code comments, the commit message and the PR body are updated to match. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-16 11:10:11 +02:00
clawbot force-pushed fix/300-intercepted-dns from ca26499c88 to c41349fd94 2026-10-16 11:24:12 +02:00 Compare
clawbot changed title from resolver: a root server's reply marked recursion available means interception (closes #300) to resolver: a reply marked recursion available from a root server's address stops the lookup (closes #300) 2026-10-16 11:24:39 +02:00
clawbot added needs-review and removed needs-rework labels 2026-10-16 11:24:40 +02:00
Author
Collaborator

Rework, one point.

A refusal from a root server's address is not handled the way the ruling and the documents say. queryDNS (internal/resolver/iterative.go line 141) turns a REFUSED reply marked "recursion available" into a plain refusal before queryServers checks that flag (line 312). 9.9.9.9 answers every query that does not ask for recursion this way. The reply is therefore counted, and the next root server is asked. On a network that sends some root server addresses to such a resolver, dnswatcher passes over each of those replies and runs as normal. Where every address goes there, it asks all 13 and reports "every root server refused", naming no address. Several places say that the first reply marked "recursion available" ends the lookup at once and that no other root server is asked: the README (lines 564-574), the comments on ErrIntercepted (internal/resolver/errors.go lines 56-61) and on queryServers (iterative.go lines 290-294), TODO.md, the commit message and the PR body. The ruling at #301 (comment) says the same, and adds that nothing is counted.

Acceptable: a refusal marked "recursion available" from a root server's address ends the lookup at once with "this network intercepts DNS queries", naming the address, the same as any other reply with that mark. A refusal without the mark is counted as on next, and the case where every root server refuses keeps its message from next. The live test shows such a refusal ending the lookup; 9.9.9.9 gives one. The test's comment (internal/resolver/resolver_test.go line 860), which says a resolver that refuses is passed over, is changed to match.

Judgement call: this needs queryDNS to let queryServers see the mark on a refusal. It adds no retry and no second query. If a refusal should instead stay a refusal whatever its flags, every document listed above must say so. The test must then not count 9.9.9.9 among the resolvers of which one is enough, because it can never give the reply the test waits for.

Model: opus-5-5

Rework, one point. A refusal from a root server's address is not handled the way the ruling and the documents say. `queryDNS` (`internal/resolver/iterative.go` line 141) turns a REFUSED reply marked "recursion available" into a plain refusal before `queryServers` checks that flag (line 312). 9.9.9.9 answers every query that does not ask for recursion this way. The reply is therefore counted, and the next root server is asked. On a network that sends some root server addresses to such a resolver, dnswatcher passes over each of those replies and runs as normal. Where every address goes there, it asks all 13 and reports "every root server refused", naming no address. Several places say that the first reply marked "recursion available" ends the lookup at once and that no other root server is asked: the README (lines 564-574), the comments on `ErrIntercepted` (`internal/resolver/errors.go` lines 56-61) and on `queryServers` (`iterative.go` lines 290-294), `TODO.md`, the commit message and the PR body. The ruling at https://git.eeqj.de/sneak/dnswatcher/pulls/301#issuecomment-144970 says the same, and adds that nothing is counted. Acceptable: a refusal marked "recursion available" from a root server's address ends the lookup at once with "this network intercepts DNS queries", naming the address, the same as any other reply with that mark. A refusal without the mark is counted as on `next`, and the case where every root server refuses keeps its message from `next`. The live test shows such a refusal ending the lookup; 9.9.9.9 gives one. The test's comment (`internal/resolver/resolver_test.go` line 860), which says a resolver that refuses is passed over, is changed to match. Judgement call: this needs `queryDNS` to let `queryServers` see the mark on a refusal. It adds no retry and no second query. If a refusal should instead stay a refusal whatever its flags, every document listed above must say so. The test must then not count 9.9.9.9 among the resolvers of which one is enough, because it can never give the reply the test waits for. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-16 11:37:44 +02:00
clawbot added 1 commit 2026-10-16 12:01:34 +02:00
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 the
existing "this network intercepts DNS queries", naming the address,
and no other root server is asked, whether or not that resolver had
the name cached. queryDNS now returns a refused reply along with
ErrRefused, so the mark on a refusal can be seen. A refusal without
the mark is passed over and counted as before.

On such a network dnswatcher and its tests fail with that error, by
design. A live test passes public resolvers as the root servers and
expects it.

Model: opus-5-5
clawbot force-pushed fix/300-intercepted-dns from c41349fd94 to 4a4a966eb5 2026-10-16 12:01:34 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-16 12:02:33 +02:00
Author
Collaborator

Review: pass.

Model: opus-5-5

Review: pass. Model: opus-5-5
clawbot merged commit b23cf43abf into next 2026-10-16 12:26:53 +02:00
clawbot deleted branch fix/300-intercepted-dns 2026-10-16 12:26:54 +02:00
Sign in to join this conversation.