app: dns.canary probe — client half of the canary measurement, verified live

Resolves the server's canary zone through the platform resolver and
compares against the spec-frozen ground truth (probe-protocol §6.1):
reference records detect answers rewritten in flight, and a per-run nonce
name (uncacheable) proves the query reached the authoritative server.
Findings: dns.answer_rewritten (high), dns.authoritative_unreachable
(medium).

Verified on the OnePlus against the deployed fmr zone: 4/4 reference
records matched exactly, nonce name answered 192.0.2.21 with
reached_authoritative=true. First full client<->server measurement loop
on real hardware; report archived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 09:02:43 +02:00
co-authored by Claude Opus 5
parent cf5cd2dc68
commit 59ba1c16bc
23 changed files with 2170 additions and 0 deletions
@@ -12,6 +12,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import app.echo_lot.measurement.*
import app.echo_lot.probe.CaptivePortalProbe
import app.echo_lot.probe.DnsCanaryProbe
import app.echo_lot.probe.IcmpProbe
import app.echo_lot.probe.LinkSnapshotProbe
import app.echo_lot.probe.NetworkInventory
@@ -71,6 +72,9 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
IcmpProbe(entries, v6 = false),
IcmpProbe(entries, v6 = true),
CaptivePortalProbe(entries),
// Canary zone served by the Echolot probe server (probe-protocol §6.1). Hardcoded to
// the reference deployment until profiles/enrollment land in the UI.
DnsCanaryProbe(canaryZone = "c.echo-lot.app", sessionPrefix = "adhoc"),
)
val tests = ArrayList<Test>()
@@ -157,6 +161,30 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
)
}
}
if (t.type == TestType.DNS_CANARY) {
val ev = t.evidence?.toString() ?: ""
if (ev.contains("MISMATCH")) {
out.add(
Finding(
id = ids.uuid(), code = "dns.answer_rewritten", category = Category.DNS,
severity = Severity.HIGH, confidence = Confidence.HIGH,
title = "DNS answers are being rewritten",
description = "A canary reference record returned different RDATA than the spec-defined ground truth — something on the path is rewriting DNS answers (interception, filtering, or a middlebox).",
evidenceRefs = listOf(EvidenceRef(t.id)),
)
)
} else if (ev.contains("\"reached_authoritative\":false")) {
out.add(
Finding(
id = ids.uuid(), code = "dns.authoritative_unreachable", category = Category.DNS,
severity = Severity.MEDIUM, confidence = Confidence.MEDIUM,
title = "Canary queries don't reach the authoritative server",
description = "A per-run nonce name (which cannot be cached) was not answered by the canary server — the resolver is intercepting or failing to reach it.",
evidenceRefs = listOf(EvidenceRef(t.id)),
)
)
}
}
if (t.type == TestType.ICMP_PING6 && t.status == TestStatus.FAILED) {
out.add(
Finding(