server: relay a test device's adb endpoint, because mDNS does not cross subnets

The beacon this replaces was a separate service wildcard-bound to
0.0.0.0:443 - it silently occupied port 443 on the reserved measurement
addresses, voiding the IPv4 interception proof for as long as it ran, and
it accepted a port report from anyone who could reach it. So this lives
where the repo's own post-mortem said it belongs: POST on the control
plane authenticated by the device credential, GET on the admin UI behind
the existing apiAdmin helper. No new listener, no new port, no wildcard.

Entries expire after 24h (ECHOLOT_ADB_ENDPOINT_RETENTION_H) on both write
and read - a LAN address is a breadcrumb for driving a test device, not
measurement data worth keeping.

Also records the BLE peer-comparison design: the case for it is that BLE
is out-of-band, which is what makes client isolation measurable at all -
silence over IP cannot distinguish an isolating AP from an absent peer,
and a peer confirming out-of-band that it was listening turns that
silence into proof.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-02 14:31:01 +02:00
co-authored by Claude Opus 5
parent ab6e278272
commit ae63bd7c7f
15 changed files with 918 additions and 14 deletions
+41
View File
@@ -143,6 +143,47 @@ curl -sk https://<host>:8443/v1/profile -H 'Authorization: Bearer <credential>'
The SPKI pin clients must verify is logged at startup (`pin-sha256`).
## Dev relay (adb endpoint)
Development scaffolding, not protocol: it appears in no capability list and in no measurement
document, and `docs/probe-protocol.md` does not describe it.
It exists because **mDNS does not cross subnets**. Android's wireless debugging advertises adbd's
port over mDNS and rotates that port every few minutes, so a developer on another subnet cannot
discover it at all. An Echolot instance running on the test LAN can — and relays it here.
```
POST /v1/devtools/adb-endpoint control plane, device credential (same Bearer as /v1/sessions)
{"host":"10.13.102.128","port":45305,"device_name":"TB330FU","note":"…"}
GET /admin/adb-endpoints admin UI, session cookie or HTTP Basic
```
Read it back from the developer's machine (the admin listener is loopback-only, so over the same
SSH tunnel as everything else):
```sh
curl -s -u admin:<password> -H 'Accept: application/json' \
http://127.0.0.1:8444/admin/adb-endpoints
# → [{"device":"…","host":"10.13.102.128","port":45305,"device_name":"TB330FU",
# "reported_at":"…","source_ip":"…","age_s":37}]
```
The same list is a card on the admin dashboard, so it is usable without curl.
The newest report per submitting device wins — a rotated port makes the previous one wrong, not
historical. The device id comes from the credential and `source_ip` from the connection, so neither
is something a body can claim. `ECHOLOT_ADB_ENDPOINT_RETENTION_H` (default 24) bounds how long an
entry lives; `0` keeps it until the device replaces it. The window exists because the value is a
LAN address that stops being true within minutes: keeping it afterwards discloses the inside of
someone's network in exchange for nothing.
**Why it lives on these two listeners and nowhere else.** Its predecessor was a separate Python
service wildcard-bound to `0.0.0.0:443`. That silently occupied port 443 on the addresses reserved
for measurement — voiding the IPv4 interception proof for as long as it ran — and it accepted a port
report from anyone who could reach it. So: no new listener, no new port, no wildcard bind, and
nothing unauthenticated. The submit side is rate-limited by the existing §2.5 actions bucket
(`ECHOLOT_RATE_ACTIONS_PER_MIN`).
## Development
```sh