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>
Echolot app
The production Android client (spec). Native Kotlin + Jetpack Compose. Multi-module; built bottom-up from a verifiable protocol spine.
Modules
| Module | Type | Status |
|---|---|---|
core-protocol |
pure Kotlin/JVM | done — client half of probe-protocol.md, verified live against the server |
core-measurement |
pure Kotlin/JVM | planned — measurement-schema.md types |
core-probe |
Android lib | planned — app-tier probes, ported from echolot-prober |
core-shizuku |
Android lib | planned — dual-path executor (UserService + newProcess fallback) |
app |
Android app | planned — Compose UI |
core-protocol is deliberately Android-free so it builds and unit-tests on any JDK (no Android
SDK) and can run integration tests against a live server.
core-protocol
Implements the control plane (SPKI-pinned enrollment/profile/sessions via HttpsURLConnection —
Android-API-1 compatible, hostname verification off because trust is the pin), the HKDF-SHA256
session-key schedule, and the binary ELT1 UDP data plane (HMAC gate, ECHO + observation block,
MTU probe) — byte-compatible with the Go server.
./gradlew :core-protocol:test # unit tests (crypto vectors, wire round-trip)
scripts/test-fmr.sh # live end-to-end test against the deployed server
test-fmr.sh mints an enrollment token over SSH, enrolls via the public control plane, computes
the SPKI pin from the served cert, and runs LiveServerTest — proving the client speaks the wire
protocol to the real server (enroll → profile → session → echo+observation → MTU → observations).
The live test self-skips when ECHOLOT_LIVE_* env vars are absent, so unit runs and CI stay green
offline.