diff --git a/docs/build-status.md b/docs/build-status.md index 5e53372..4252433 100644 --- a/docs/build-status.md +++ b/docs/build-status.md @@ -1284,4 +1284,65 @@ signature against the key in the source it just built. `RELEASE_SIGNING_KEY` (Settings → Actions → Secrets) with the contents of `~/.echolot/release-signing-key` from the dev PC. Ordering is safe: the currently deployed v0.3.x updater does not verify, so it will happily install the first signed release; every -release after that is verified. +release after that is verified. **Done 2026-08-02** — the secret is in place. (The "v0.3.x" +above should read "the currently deployed release": deployments had moved on to v0.9.x by the +time signing landed; the point — the deployed updater predates verification and will accept the +first signed release — is unchanged.) + +## Prober fold: traceroute.udp4 and the mDNS inventory go production (2026-08-02, app 0.2.2) + +The two highest-value validated capabilities moved from the prober into `core-probe`: + +- **`traceroute.udp4`** (`TracerouteProbe`): UDP traceroute reading ICMP time-exceeded off the + socket error queue via `Os.recvmsg(MSG_ERRQUEUE)` through the reflection facade — no root, no + raw socket, no JNI, ~250 ms for six hops. Emits the schema's `TracerouteEvidence` (rtt in ns). + `OsAbi` came with it, including the measured fact that `Os.getsockoptInt` exists on neither + known device, so PMTU must always be read from the errqueue (`ee_info`), never + `getsockopt(IP_MTU)`. The load-bearing line survived the port: EAGAIN out of the reflected + `recvmsg` means "queue empty", not failure. +- **`local.mdns_inventory`** (`MdnsInventoryProbe`): MulticastLock + NSD discovery, the service + inventory that doubles as the VLAN-leakage detector. Both hardware lessons kept: the + `_services._dns-sd._udp.` meta-query returns 0 beside live services on both devices (so the + concrete types are the measurement and the meta-query result is itself evidence), and the + listen window is 10 s because 4 s missed services. + +Still to fold, in order: the Shizuku dump *parsers* (the raw `link.ip_monitor` captures already +hold two divergent vendor formats that could feed `link.ra_source` and `sec.arp_watch`); +`multinetwork.request_and_bind` (extend ConstraintDetector to *request* transports rather than +only probing present ones); `peer.ble_advertise` (needs three new permissions and a peer mode to +exist first). + +## Server v0.9.2: trains, real TTL/DSCP/ECN, rate limits (2026-08-02) + +The spec-vs-implementation gap audit closed its top items; protocol_version 1.0.0 → 1.0.1 +(additive — below 1.0.0 the minor is the breaking axis, and nothing here breaks an old client): + +- **Upstream trains** (§3.2, types 0x03/0x04/0x05): per-train bounded columnar buffer (8192 + rows, head kept on overflow with `Truncated` set — mirrors the schema's `evidence_truncated` + honesty), TRAIN_REPORT split across ≤1200-byte datagrams, grant-free with the §3.4 argument + spelled out (a 17-byte report row answers a ≥36-byte HMAC-valid packet). Unknown train id + gets a zero-row report: "nothing arrived" is an answer. Also surfaced as `udp.trains` in the + observations API. +- **Real TTL/DSCP/ECN observation** (§3.3): the read loop is `ReadMsgUDPAddrPort` with + IP_RECVTTL/IP_RECVTOS/IPV6_RECVHOPLIMIT/IPV6_RECVTCLASS cmsgs on Linux; `0xFF` stays the + "not observed" sentinel elsewhere. This unblocks `sec.dscp_ecn_survival` both directions, + paired with the new `dscp` parameter on `downtrain` (validated 0–63, refused not clamped, + `dscp_applied` in the response). +- **Rate limiting** (§2.5, was entirely absent): token buckets keyed per credential AND per + source IP; 429 + Retry-After on session/action creation (`/v1/profile` stays ungated), silent + drop on the data plane — charged after the HMAC gate so a spoofed flood cannot drain a + victim's budget, before the replay window so a dropped seq stays usable. UDP ceilings default + above the largest legitimate run (a 200 Mbps throughput test), because a rate limit that + clips a real measurement produces a confidently wrong number. +- **`action_id` in every granted packet** (§5/§9): payload bytes [8:16] across all granted + types, so overlapping actions are attributable. Verified the deployed Kotlin client parses + only ECHO_RESP and MTU_ACK payloads, so the reshuffle strands nobody. +- **Canary log retention**: the stated 24 h privacy default is now enforced + (`ECHOLOT_DNS_LOG_RETENTION_H`), where before the log was time-unbounded. +- **`POST /admin/enroll-tokens`** now answers the spec's JSON shape under content negotiation; + the README's curl works as documented. +- Spec §2.3 registry gained `downtrain` and `tcp-echo`, which the server had been advertising + as strings a conformant client must ignore. + +Client-side counterparts still to build: sending 0x03 trains + parsing 0x05 reports +(`train.udp_updown`), and passing `dscp` on downtrain actions.