Stop three DNS checks reporting success they cannot verify - #31
Merged
Conversation
Three tools report success unconditionally on any host running a macOS DNS proxy network extension. check_internet_connection reports "Connected" with the WAN unplugged; the root server and resolver checks report every server reachable when none is. Measured with negative controls throughout: UDP and TCP port 53 answer for destinations that cannot possibly answer, including reserved space at 240.0.0.1, while ports 443 and 853 and ICMP all behave correctly. Nothing listens on 53 and routing is ordinary, so this is a NEDNSProxyProvider capturing flows system-wide. Two bypasses were tried and both are recorded as dead ends so they are not re-proposed. Two tools can be genuinely repaired: the internet check needs no DNS at all and moves to 443, and eight of ten resolvers answer over DoT. The root servers cannot be repaired -- they serve port 53 only and offer no uniform encrypted path -- so that tool must report that it could not run rather than passing, with ICMP offered as an explicitly weaker signal. Note two things the probing corrected rather than confirmed. G-root and L-root do not answer ICMP at all, so non-response must never be reported as down. And a first ICMP probe counted any received packet as a reply, so a router's destination-unreachable for 240.0.0.1 read as a 2310 ms success -- the negative control caught it, which is the argument for having one on every signal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check_internet_connection reported "Connected" with the WAN unplugged,
and the root server and resolver checks reported every server reachable
when none had been contacted. A macOS DNS proxy network extension
captures DNS flows system-wide, so a query to any address on port 53 --
including reserved, unroutable space -- is answered locally.
check_internet_connection is fully repaired. Nothing about it needed
DNS; it needed one TCP handshake, and the port was incidental. It now
uses 443, which is not intercepted, and tries three hosts before
declaring an outage. Verified both ways: Connected in 0.0 s against real
hosts, Disconnected in 9.0 s against addresses that cannot answer, which
the old implementation could not produce at all.
check_dns_resolvers is repaired for eight of ten. DoT on port 853 is not
captured and reaches the real resolver; measured, all eight answer with
a passing negative control. Comodo offers no DoT, so on an intercepted
host it cannot be checked and is now reported as such.
check_dns_root_servers cannot be repaired. The roots serve port 53 only
and offer no uniform encrypted path -- B accepts 853 with a certificate
that does not verify, F accepts 443, A and K time out, M refuses -- so
it now reports that the check could not be run, and why. ICMP is offered
as an explicitly weaker signal, labelled as such.
Two things the probing corrected rather than confirmed. G-root and
L-root drop ICMP even when healthy, so a non-reply is reported as "did
not answer a ping" and never as down. And a first ICMP probe counted any
received packet as a reply, so a router's destination-unreachable read
as a 2310 ms success; only an echo reply carrying our own payload counts
now, and a test pins each rejected packet type.
The root query also changes from 'example.com A' to '.' NS. A root
server is not recursive and returns a referral with an empty answer
section, which Resolver.resolve() raises NoAnswer on -- so the old check
may have been reporting every root unreachable on any host without a
proxy masking it. That is recorded as a prediction, not a measurement:
it cannot be confirmed from an intercepted host. '.' NS is the correct
question regardless.
Helpers stay out of the tool registry via a module-local
get_module_tools returning {}, which the registry honours before it
falls back to scanning, rather than four more names in the central
exclusion set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
check_internet_connectionreported "Connected" with the WAN unplugged.check_dns_root_serversandcheck_dns_resolversreported every serverreachable without having contacted any of them.
Design and full measurements:
docs/dns_interception_srd.md.Cause
A macOS DNS proxy network extension (a
NEDNSProxyProvider— Tailscale's here,but any corporate VPN client or captive portal does the same) captures DNS flows
system-wide. A query addressed to any IP on port 53 is answered locally.
Every row below has a negative control: an address that must not be able to
answer.
240.0.0.1is reserved and unroutable; the rest are RFC 5737documentation ranges.
IP_BOUND_IFen0ENXIOENXIONothing is listening on port 53 and routing is ordinary, so this is not a port
binding or a route and no socket option reaches it. Both attempted bypasses are
recorded in the SRD as dead ends so they are not re-proposed.
Fixes
check_internet_connection— fully repaired. Nothing about it needed DNS; itneeded one TCP handshake, and the port was incidental. Now 443, trying three
hosts before declaring an outage. Verified both ways:
Connectedin 0.0 sagainst real hosts,
Disconnectedin 9.0 s against addresses that cannotanswer — an outcome the old implementation could not produce at all.
check_dns_resolvers— repaired for 8 of 10. DoT on 853 is not captured andreaches the real resolver; all eight answered, with the control timing out.
Comodo offers no DoT, so on an intercepted host it cannot be checked and is
reported as such.
check_dns_root_servers— cannot be repaired. The roots serve port 53 onlyand offer no uniform encrypted path (B accepts 853 with an unverifiable
certificate, F accepts 443, A and K time out, M refuses). It now reports that
the check could not be run, and why. ICMP is offered as an explicitly weaker
signal.
A third result state
REACHABLE/UNREACHABLE/UNKNOWN.UNKNOWNis the point: a server nobodycould reach a verdict on is not one that answered, and summary lines never fold
it into either of the others.
Two things the probing corrected rather than confirmed
Reporting 11/13 as "two are down" would swap a false positive for a false
negative, so a non-reply is "did not answer a ping", never "down".
so a router's destination-unreachable for
240.0.0.1read as a 2310 mssuccess. Only an echo reply carrying our own payload counts now, and a test
pins each rejected packet type. The negative control is what caught it.
Also
The root query changes from
example.com Ato.NS. A root server is notrecursive and returns a referral with an empty answer section, which
Resolver.resolve()raisesNoAnsweron — so the old check may have beenreporting every root unreachable on any host without a proxy masking it. That is
recorded as a prediction, not a measurement: it cannot be confirmed from an
intercepted host.
.NS is the correct question regardless. See SRD §5.7.Helpers stay out of the tool registry via a module-local
get_module_toolsreturning
{}, which the registry honours before falling back to scanning,rather than four more names in the central exclusion set.
Testing
tests/test_dns_interception.py, offline. Detection in all three outcomes(intercepted, clean, could-not-probe), ICMP reply validation against four
rejected packet shapes, and the invariant that a report containing unknowns
never claims an all-clear.
python instability.py run-testspasses.🤖 Generated with Claude Code