Files
tether/rendezvous
Patrick Ecord eefd58cf5b rendezvous: optional bearer-token auth (TETHER_RENDEZVOUS_TOKEN)
When TETHER_RENDEZVOUS_TOKEN is set, register/peers require
`Authorization: Bearer <token>` (healthz stays open); empty token = open, so the
first cross-CGNAT test needs no token but production can gate. IrohTransport
sends the token from the same env var, so agent/CLI clients pick it up
automatically (iOS needs a token field — small Swift follow-up).

Verified: 401 without/with wrong token, 200 with the right one, healthz open; and
the full engine flow (text + 200KB photo) still syncs end-to-end through a
token-gated rendezvous. Default ship build untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 01:31:37 -05:00
..

tether-rendezvous

The one piece of server tether still needs once it's on iroh: a bootstrap rendezvous. To join the gossip topic a device needs to reach ≥1 peer already in it; this hands it one. After that, everything is peer-to-peer.

It is deliberately tiny and content-blind:

  • maps room → {EndpointId: last_seen} in memory, TTL 90s
  • room is already sha256(account)[..4], so the account is never exposed
  • it only ever sees iroh public keys + timestamps — never clipboard content (that's E2E between peers)

This is "supernode #1, the boring half." The other half is a self-hosted iroh-relay (for holepunch-assist / relay fallback). Neither replaces the old Go server's role in content — there is none anymore.

API

POST /rendezvous/register  {"room":"<hex>","id":"<endpoint-id>"}
     → {"peers":["<other-id>", ...]}      # current members minus the caller
GET  /rendezvous/peers?room=<hex>          → {"peers":[...]}
GET  /healthz                              → "ok"

Clients register on a short interval (well under the 90s TTL) so the member list reflects who's actually online.

Run

cargo run --release            # binds 0.0.0.0:8765
PORT=9000 cargo run --release

Deploy (homelab)

It needs to be publicly reachable for cross-network (cellular/CGNAT) devices to bootstrap — same requirement as the iroh-relay. On the homelab:

  1. Run it on a CT/VM (systemd unit below).

  2. Front it with Caddy for TLS, e.g. rendezvous.pecord.io:

    rendezvous.pecord.io {
        reverse_proxy 127.0.0.1:8765
    }
    

    (Plain HTTP reverse-proxy is fine — unlike the iroh-relay, there's no UDP/QUIC path here, just small JSON over HTTPS.)

  3. Point the tether clients' server field at https://rendezvous.pecord.io.

systemd unit

[Unit]
Description=tether rendezvous
After=network-online.target

[Service]
ExecStart=/opt/tether/tether-rendezvous
Environment=PORT=8765
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target

Auth

Set TETHER_RENDEZVOUS_TOKEN to require Authorization: Bearer <token> on register/peers (healthz stays open). Clients send it via the same env var (TETHER_RENDEZVOUS_TOKEN), so the agent/CLI pick it up automatically; the iOS app needs a field for it (small Swift follow-up). Empty token = open — fine for a LAN/test run, but set one before exposing it publicly. Without a token, anyone who knows a room (a hash) can register/enumerate — metadata only, content stays E2E, but still worth gating.

Remaining hardening (follow-up): per-IP rate-limiting (the MAX_PER_ROOM cap is only a flood blunt); behind Caddy use X-Forwarded-For for the real client IP.