app: tell a broken device resolver apart from a broken network

Diagnosing a tablet that claimed "no internet" took twenty adb commands
to establish something the app should have said in one run: ping to
1.1.1.1 worked, the configured DNS server answered a raw UDP query in
65 bytes, and Android still could not resolve a hostname. The network was
fine; netd had wedged.

Those two failures look identical to a user and want opposite responses —
"look at your router" against "toggle your wifi" — so dns.resolver asks
the network's own servers directly and compares the answer against what
the platform returns for the same name. The query is hand-rolled over a
plain DatagramSocket on purpose: anything routed through a resolver API
would inherit the very fault being looked for.

dns.system_resolver_broken fires only on the pairing that is otherwise
unattributable: server answered, platform did not. Per network, because a
phone can have wedged wifi and working cellular at once.

Also records Android's own verdict per network — validated, captive
portal, partial connectivity — which the app reproduced with its own HTTP
probes but never stored. It is free, it is what the user sees in the
status bar, and its disagreement with our measurements is exactly what
identified the tablet. NET_CAPABILITY_PARTIAL_CONNECTIVITY is @SystemApi
so the constant is inlined with its rationale, in the manner of OsAbi.kt,
and read defensively enough to report unknown rather than false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-02 09:44:49 +02:00
co-authored by Claude Opus 5
parent 40e76c52ca
commit d65dbbc75a
7 changed files with 264 additions and 1 deletions
@@ -397,6 +397,9 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
// app happened to be developed against. With no server configured they get blank
// strings and report themselves skipped, which is the honest outcome — the
// alternative measures someone else's infrastructure and calls it your network.
// Before the canary: "can this device resolve at all" has to be answered before
// "are the answers being tampered with" means anything.
app.echo_lot.probe.DnsResolverProbe(entries),
DnsCanaryProbe(canaryZone = settings.canaryZone, sessionPrefix = "adhoc"),
StunProbe(serverHost = settings.serverHost()),
)
@@ -656,6 +659,39 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
)
}
}
if (t.type == TestType.DNS_RESOLVER && t.status == TestStatus.OK) {
// One finding per network: on a phone the wifi resolver can be wedged while
// cellular is fine, and "DNS is broken" would be wrong about half the device.
val ev = t.evidence
if (ev != null) {
for ((_, v) in ev) {
val o = v as? kotlinx.serialization.json.JsonObject ?: continue
fun str(k: String) =
(o[k] as? kotlinx.serialization.json.JsonPrimitive)?.content
if (str("verdict") != "server answers, device resolver does not") continue
val ref = str("network_ref")
val where = networks.firstOrNull { it.id == ref }?.iface
?.takeIf { it.isNotBlank() } ?: "this network"
out.add(
Finding(
id = ids.uuid(),
code = FindingRegistry.DNS_SYSTEM_RESOLVER_BROKEN.code,
category = FindingRegistry.DNS_SYSTEM_RESOLVER_BROKEN.category,
severity = FindingRegistry.DNS_SYSTEM_RESOLVER_BROKEN.severity,
confidence = Confidence.HIGH,
title = "This device cannot resolve names, but the DNS server is fine ($where)",
description = "A DNS query sent straight from this device was " +
"answered by ${str("servers") ?: "the configured server"}, yet " +
"asking Android to resolve the same name fails. The network is " +
"working; this device's resolver is not. Turning wifi off and on " +
"again, or rejoining the network, usually clears it. If it " +
"returns after a restart, look at the network instead.",
evidenceRefs = listOf(EvidenceRef(t.id)),
)
)
}
}
}
if (t.type == TestType.ICMP_PING6) {
// A network with no IPv6 at all is NORMAL — most networks are still IPv4-only, and
// that is not a defect. What IS a defect is IPv6 the network claims to provide (a