118 lines
5.1 KiB
Markdown
118 lines
5.1 KiB
Markdown
|
|
# coturn — Veza TURN/STUN relay (v1.0.9 item 1.2)
|
||
|
|
|
||
|
|
Deployed alongside Veza to fix the **NAT traversal hole-punching gap**
|
||
|
|
identified in `FUNCTIONAL_AUDIT.md` §4 #1: WebRTC 1:1 calls signaling
|
||
|
|
works (chat WebSocket relays the SDP offer/answer/ICE candidates) but
|
||
|
|
the actual media stream fails between two peers behind symmetric NAT
|
||
|
|
(corporate firewalls, mobile carrier CGNAT, Incus container default
|
||
|
|
networking). coturn provides the relay that lets the media flow
|
||
|
|
through a public IP when peer-to-peer hole-punching fails.
|
||
|
|
|
||
|
|
## Topology
|
||
|
|
|
||
|
|
```
|
||
|
|
Caller (browser) Veza backend coturn (Incus)
|
||
|
|
│ │ │
|
||
|
|
│── GET /api/v1/config/webrtc ──▶ │ HTTPS
|
||
|
|
│◀── { iceServers: [...] } ── │
|
||
|
|
│ │
|
||
|
|
│── WS POST /chat ─▶ [SDP offer] ─▶ Callee │ WSS
|
||
|
|
│ │
|
||
|
|
│── ICE candidate (UDP probe) ─▶ Callee │ fail
|
||
|
|
│ │
|
||
|
|
│── ICE candidate (TURN relay) ─────────────▶─────────┤ UDP 3478
|
||
|
|
│◀──────────────── relay ────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
```
|
||
|
|
|
||
|
|
The backend never proxies media itself. coturn is the only component
|
||
|
|
that handles the relay path; backend-api just hands the client a list
|
||
|
|
of candidate ICE servers it can try.
|
||
|
|
|
||
|
|
## Deploy on Incus
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 1. Create the container with host-network UDP forwarding for the
|
||
|
|
# listener port and the relay range.
|
||
|
|
incus launch images:debian/12 turn-veza
|
||
|
|
incus config device add turn-veza turn-udp proxy \
|
||
|
|
listen=udp:0.0.0.0:3478 connect=udp:127.0.0.1:3478
|
||
|
|
# Range proxy is more involved; the cleanest is to put the container
|
||
|
|
# directly on the host network:
|
||
|
|
# incus config set turn-veza security.privileged true
|
||
|
|
# incus config device add turn-veza host-network nic nictype=macvlan parent=eno1
|
||
|
|
# Or use a dedicated public IP and skip macvlan.
|
||
|
|
|
||
|
|
# 2. Install coturn.
|
||
|
|
incus exec turn-veza -- apt-get update
|
||
|
|
incus exec turn-veza -- apt-get install -y coturn
|
||
|
|
|
||
|
|
# 3. Render this directory's turnserver.conf with secrets — Ansible
|
||
|
|
# template OR sops-decrypt OR raw envsubst:
|
||
|
|
#
|
||
|
|
# WEBRTC_TURN_PUBLIC_IP=<public_ip> \
|
||
|
|
# WEBRTC_TURN_REALM=turn.veza.fr \
|
||
|
|
# WEBRTC_TURN_USERNAME=<from_vault> \
|
||
|
|
# WEBRTC_TURN_CREDENTIAL=<from_vault> \
|
||
|
|
# envsubst < turnserver.conf | incus file push - turn-veza/etc/turnserver.conf
|
||
|
|
|
||
|
|
# 4. Drop in the TLS cert+key (Let's Encrypt or a rotated wildcard).
|
||
|
|
incus file push fullchain.pem turn-veza/etc/coturn/cert.pem
|
||
|
|
incus file push privkey.pem turn-veza/etc/coturn/key.pem
|
||
|
|
|
||
|
|
# 5. Enable + start.
|
||
|
|
incus exec turn-veza -- systemctl enable coturn
|
||
|
|
incus exec turn-veza -- systemctl start coturn
|
||
|
|
```
|
||
|
|
|
||
|
|
## Configure Veza backend
|
||
|
|
|
||
|
|
Set these env vars on the backend container so the SPA gets the right
|
||
|
|
ICE servers from `GET /api/v1/config/webrtc`:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
WEBRTC_STUN_URLS=stun:turn.veza.fr:3478 # comma-separated
|
||
|
|
WEBRTC_TURN_URLS=turn:turn.veza.fr:3478,turns:turn.veza.fr:5349
|
||
|
|
WEBRTC_TURN_USERNAME=<same as turnserver.conf>
|
||
|
|
WEBRTC_TURN_CREDENTIAL=<same as turnserver.conf>
|
||
|
|
```
|
||
|
|
|
||
|
|
If any of the TURN vars is empty, the handler returns STUN-only and the
|
||
|
|
SPA's `useWebRTC().nat.hasTurn` reports false — the CallButton tooltip
|
||
|
|
warns the user up-front instead of letting calls time out silently.
|
||
|
|
|
||
|
|
## Smoke test
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# From any machine outside the Incus host network:
|
||
|
|
turnutils_uclient \
|
||
|
|
-u <WEBRTC_TURN_USERNAME> \
|
||
|
|
-w <WEBRTC_TURN_CREDENTIAL> \
|
||
|
|
-p 3478 turn.veza.fr
|
||
|
|
|
||
|
|
# Should succeed within ~1s. Failure modes:
|
||
|
|
# "Cannot get a TURN allocation" — listening-ip/port wrong, or NAT not forwarded
|
||
|
|
# "401 Unauthorized" — username/credential mismatch with config
|
||
|
|
# "BAD-REQUEST" — realm mismatch
|
||
|
|
```
|
||
|
|
|
||
|
|
For an end-to-end test from the SPA: open the browser devtools, start a
|
||
|
|
call, watch `chrome://webrtc-internals` for `iceConnectionState=connected`
|
||
|
|
and the candidate pair selected — should be `relay`/`relay` when on
|
||
|
|
symmetric-NAT networks.
|
||
|
|
|
||
|
|
## Operational notes
|
||
|
|
|
||
|
|
- Static credentials are rotated by changing `user=` in `turnserver.conf`
|
||
|
|
and reloading coturn (`systemctl reload coturn`). The backend env
|
||
|
|
vars must be updated to match in the same change window — the SPA
|
||
|
|
caches the config for the page lifetime, so rotation is invisible to
|
||
|
|
in-flight users.
|
||
|
|
- v1.1 will switch to RFC-draft REST shared-secret credentials so the
|
||
|
|
backend can mint per-user, per-call ephemeral credentials without
|
||
|
|
reloading coturn. See `ORIGIN_SECURITY_FRAMEWORK.md` (deferred).
|
||
|
|
- Capacity rule of thumb: each TURN relay session uses ~50 KB/s for
|
||
|
|
audio. A 4-vCPU coturn handles ~1000 concurrent audio sessions before
|
||
|
|
CPU saturation. Scale horizontally with a second container behind a
|
||
|
|
DNS round-robin if needed; coturn is stateless across instances.
|