1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134 | # The relay
sueta v2 needs exactly one piece of always-on infrastructure: a **libp2p
circuit relay** browsers can reach over websockets. It does three things:
1. accepts websocket connections from browsers,
2. grants circuit-relay-v2 reservations so browsers can hand each other the
SDP for direct WebRTC connections,
3. participates in one gossipsub topic (`_peer-discovery._p2p._pubsub`) so
members of a room can find each other's ephemeral PeerIds.
That's all. Room traffic — presence, messages, log replication, call
signaling — flows member⇄member over direct connections once each pair's
WebRTC upgrade completes. Until then (or for pairs whose upgrade can't
complete — hard NAT with TURN down) their traffic transits the relay as
opaque circuit frames, bounded by the circuit-relay defaults (2 min /
128 KiB per relayed connection). The relay holds no state, and every frame it
carries is noise-encrypted with the payloads inside additionally sealed
end-to-end (PROTOCOL.md §8).
Plus TURN: WebRTC (both the libp2p transport upgrades and call media) wants a
STUN/TURN server for NAT traversal, with ephemeral credentials served over
HTTPS (coturn REST scheme, PROTOCOL.md §4).
## Production (managed)
chat.ardegazu.ro uses ird's managed p2p service: the app's hostname serves the
libp2p relay on `wss://…:443`, the TURN credential endpoint, and a
`/.well-known/ap2p` descriptor advertising both:
```json
{ "relay": { "peer_id": "12D3Koo…", "multiaddrs": ["/dns4/…/tcp/443/tls/ws/p2p/12D3Koo…"] },
"endpoints": { "turn_credentials": "https://…/turn-credentials" } }
```
The client bakes the current values as defaults and re-reads the descriptor at
boot (2 s budget, falls back to baked values offline) — so the operator can
rotate the relay key or move TURN without a client release. Note: the relay's
origin allowlist covers the libp2p websocket endpoint too; dev origins
`localhost:5173`/`4173` are allowed for this app.
## Offline mailbox (optional, v16)
The managed node also offers a per-room **store-and-forward mailbox**
(PROTOCOL.md §9): members deposit their sealed entry/image/identity blocks so
peers who are never online at the same time still converge. It is OFF by
default and feature-detected — enabling it on the ird app
(`mailbox_enabled: true`) makes the descriptor grow a `mailbox` block
```json
{ "mailbox": { "endpoint": "https://…/mailbox/v1",
"mint_endpoint": "https://…/mailbox-credentials",
"ttl_hours": 168, "max_mb": 512, "max_message_kb": 64 } }
```
and clients pick the feature up on their next boot; disabling it drops the
node's stored blobs. Limits are the `mailbox_*` knobs (retention TTL, per-room
ring-buffer size, per-deposit cap — this app runs 168 h / 512 MB / 64 KB).
Browsers mint their own bearer tokens at `mint_endpoint` (origin-checked
against the same allowlist). Billing: MiB-hours stored + relayed bytes. The
node holds only ciphertext; what it can and cannot see is PROTOCOL.md §8/§9.
## Dev
```sh
cd deploy/relay && npm install && npm run dev # ws://127.0.0.1:9090
```
`--dev` derives a deterministic key (PeerId
`12D3KooWJU33yQvEdNF1AXcm5RWyLLyDKxyKkayRvDpJcJtYqhYt`), which the client's
dev build bakes as its default relay — no config needed. The e2e suites spawn
their own relay instance; stop yours before running them.
TURN in dev: the client fetches credentials from the production endpoint
(dev origins are allowed), and degrades to host candidates if unreachable —
loopback connections work without TURN anyway.
## Self-hosting
Any machine that can hold a websocket open works:
```sh
cd deploy/relay
npm install
node relay.mjs --port 9090 --key relay.key # key file persists the PeerId
```
Put it behind any TLS reverse proxy (caddy: `reverse_proxy / localhost:9090`)
so browsers can dial `wss://relay.example.org/`.
Abuse limits: the relay enforces a per-IP inbound connection cap
(`--max-per-ip`, default 16) on top of the global pools (128 reservations /
256 connections) — without it one host minting free PeerIds could lock every
legitimate browser out of joins. Add per-IP connection/rate limits at the
reverse proxy too; that's the right layer for bans and slowdowns.
Then build the client against it:
```sh
VITE_RELAY_MULTIADDR="/dns4/relay.example.org/tcp/443/tls/ws/p2p/<peerId printed at start>" \
VITE_TURN_CREDS_URL="https://relay.example.org/turn-credentials" \
npm run build
```
For TURN, run coturn with `use-auth-secret` (see `deploy/turnserver.conf` for
a hardened config — **edit `realm=` to your own domain**, credentials are
minted against it) and any tiny endpoint that mints REST-scheme credentials:
`username = unix_now + 3600`, `credential = base64(HMAC-SHA1(secret, username))`,
served as the JSON shape in PROTOCOL.md §4. TURN is optional-but-recommended:
without it, peers behind symmetric NATs can't upgrade to direct connections.
TURN quota sizing: every pc that gathers relay candidates allocates against
ONE cached username per page — member connection upgrades plus call media
pcs. Set `user-quota` ≥ 2× your expected room size (the shipped config uses
32); too low and relay-forced calls starve with `486 TURN allocate error`.
⚠ TURN hairpin: the box's own public IP must stay relayable (relayed pairs
target each other's allocation on this box), which means anyone with valid
ephemeral credentials can reach the box's OWN ports with traffic that appears
to come from the box itself — bypassing source-IP firewall allowlists (e.g.
"SSH only from my admin IP"). Firewall hairpin traffic to every port outside
the relay range (49152–65535); details in `deploy/turnserver.conf`.
Nothing about the relay is sueta-specific — any libp2p circuit relay v2 node
with gossipsub on the discovery topic serves any number of apps built on this
stack, and learns nothing about any of them.
## Notes
- The v1 Go signaling server (`server/`) and its installer are gone — v2
clients don't speak that protocol. The last v1 state is tagged
`v1-ws-final` if you need it.
- Relay outages don't interrupt established rooms or calls; only new joins
need it. Clients redial with backoff and re-announce on wake.
|