fmr binds two IPv4 addresses. connFor picked whichever socket of the right family came first in the bind list, so a downtrain for a session established on .150 went out from .151 — and every packet was dropped by the client's NAT, which has no mapping for that pair. tcpdump on the server showed all 50 leaving; the client saw none. Read as "100% downstream loss", which is the worst kind of wrong: a confident measurement of something that never happened. Sessions now record which of our own bound addresses received their traffic, and granted sends (and delayed echo) go back out through that socket. The fallback to a family match is kept for the case where nothing has been received yet, and the test pins both paths — a single-homed lab can never reproduce this. Also: the client-side halves of the same work — anonymizer (core-privacy), local run archive with retention (core-archive), upload client, and the app's settings and history screens. 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.