Files
echolot/server/README.md
T
mrambossekandClaude Opus 5 8a80026d49 server: Go skeleton — control plane, UDP data plane, Docker + systemd modes
Pure stdlib. Implements the spec's core: enrollment (single-use tokens),
profile (SPKI pin, only real capabilities advertised), sessions with the
§2.4 HKDF-SHA256 key schedule; UDP data plane with the 32-byte ELT1
header, 4-byte HMAC gate, 1024-wide anti-replay window, ECHO_RESP with
observation block, TIMESYNC, and the §3.4 anti-amplification cap. Wire
format has tests (roundtrip + silent-drop cases); enroll→profile→session
smoke-tested live.

Modes: container (autodetect /.dockerenv|/run/.containerenv|cgroup, or
--docker/ECHOLOT_DOCKER=1; config via ECHOLOT_* env; distroless image;
network_mode host required — Docker NAT would falsify observed sources)
and native (--install-systemd/--uninstall-systemd with a hardened unit,
opt-in --self-update from Gitea releases; refused in containers).

CI: tests on any server/ push; server-v* tags build+push the image to the
Gitea registry and attach linux amd64/arm64 binaries + SHA256SUMS to a
release — the artifact self-update consumes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:09:08 +02:00

2.6 KiB

echolot-server

The probe server (spec). Pure Go, stdlib only, GPL-3.0-or-later.

Skeleton status: control plane (enroll / profile / sessions with the spec's HKDF key schedule), UDP data plane (ECHO with observation block, TIMESYNC, HMAC gate, anti-replay, anti-amplification — wire format covered by tests). Not yet: TCP/TLS echo, STUN, canary DNS, actions, observations API, admin UI beyond token minting.

Run in Docker (config via env)

docker compose up -d          # see compose.yaml — network_mode: host is required

Host networking is not negotiable: behind Docker NAT the server would observe the proxy's source addresses and TTLs instead of the client's — falsifying exactly what it measures. Container mode is autodetected (/.dockerenv etc.); --docker / ECHOLOT_DOCKER=1 forces it. In this mode systemd install and self-update are refused — update by pulling a new image tag.

Run native (systemd)

go build -o /usr/local/bin/echolot-server ./cmd/echolot-server
sudo /usr/local/bin/echolot-server --install-systemd     # writes unit, enables, starts
sudo /usr/local/bin/echolot-server --uninstall-systemd

Config precedence: flags > ECHOLOT_* env > defaults. Every flag has an env twin (--udp-listenECHOLOT_UDP_LISTEN).

Self-update (opt-in, native only)

echolot-server --self-update \
  --self-update-api https://git.example.net/api/v1/repos/owner/repo

Fetches the newest server-v* release asset for this OS/arch and atomically replaces the binary; systemd's Restart= brings up the new version. Run it from a systemd timer for unattended updates. TODO before enabling anywhere untrusted: signature verification of the downloaded asset.

First contact

# 1. mint an enrollment token (admin listener is loopback-only)
curl -s -X POST 'http://127.0.0.1:8444/admin/enroll-tokens?note=phone'
# 2. device enrolls with it (normally via the echolot:// QR code)
curl -sk -X POST https://<host>:8443/v1/enroll -H 'Authorization: Bearer <token>'
# 3. device fetches its profile
curl -sk https://<host>:8443/v1/profile -H 'Authorization: Bearer <credential>'

The SPKI pin clients must verify is logged at startup (pin-sha256).

Development

go test ./...   # includes wire-format tests for the UDP data plane
go vet ./...

CI (.gitea/workflows/build-server.yml): tests on every push touching server/; tagging server-v1.2.3 builds + pushes the container image to the Gitea registry and attaches static linux amd64/arm64 binaries (+ SHA256SUMS) to a release — the same artifacts --self-update consumes.