v6.no_icmp_reply infers trouble from silence, which is ambiguous by construction: a firewall dropping echo requests looks the same as a network that cannot carry IPv6 at all. Two much stronger signals were already sitting unread in the link snapshot, and a test device on a Netbird tunnel surfaced both at once. v6.route_without_address — a ::/0 route with no global address. The router advertises itself as an IPv6 gateway while SLAAC produces nothing usable. Hosts believe IPv6 is available and pay a connection timeout on every dual-stack destination before falling back, which is felt as general slowness with no packet loss to explain it. v6.no_default_route — the mirror: a global address with nothing to route it. A VPN installing host routes to specific destinations produces this deliberately and it works, so a VPN transport reports it as INFO rather than as a fault; without one it means the network handed out an address it does not carry traffic for. Both are read from the routing table, so neither is inferred from silence, and both are reported per interface — "IPv6 is broken" is useless advice when wifi is the broken one and cellular is fine. Classification lives in core-measurement rather than the ViewModel so it can be tested without a device; the fixtures are a real dumpsys table (wifi advertising a route it cannot source from, working cellular, a tunnel with two host routes) because the risk here is not bad boolean logic but imagining shapes real networks do not produce. 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.