Setting out to build the machine-readable schema, the first step was checking whether the anonymizer covers the fields the schema declares sensitive. It did not, and five identifying values were going out at the `balanced` level: networks[].link.addresses[].addr the device's own global IPv6 address networks[].link.routes[].gateway the ISP allocation networks[].link.dns.servers[] the configured resolver private_dns_hostname an internal hostname search_domains[] the internal domain The settings screen describes that level as pseudonymizing addresses. Root cause: classification keyed on field names, and the schema's actual names were never added to the table. Every existing test passed, because each checked a field somebody had remembered to write a case for - an unfalsifiable design for a privacy control. So beyond adding the names, classification now falls back to the *value* when the name is unknown: anything shaped like an IPv4/IPv6 address or a MAC is treated as one. Hostnames deliberately are not inferred by shape, since train.udp_updown is indistinguishable from a domain and mangling a test type would corrupt the document to protect nothing. LeakTest is the guard, and is written to fail for fields nobody thought of: it plants identifying values wherever one can occur and asserts none survive. It also pins that RFC1918 addresses stay readable, so it cannot pass by over-redacting. Route prefixes and :: needed care - 0.0.0.0/0 must stay itself or a routing table becomes unreadable for no privacy gain. Co-Authored-By: Claude Fable 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.