veza/infra/coturn/turnserver.conf
senke b8eed72f96 feat(webrtc): coturn ICE config endpoint + frontend wiring + ops template (v1.0.9 item 1.2)
Closes FUNCTIONAL_AUDIT.md §4 #1: WebRTC 1:1 calls had working
signaling but no NAT traversal, so calls between two peers behind
symmetric NAT (corporate firewalls, mobile carrier CGNAT, Incus
container default networking) failed silently after the SDP exchange.

Backend:
  - GET /api/v1/config/webrtc (public) returns {iceServers: [...]}
    built from WEBRTC_STUN_URLS / WEBRTC_TURN_URLS / *_USERNAME /
    *_CREDENTIAL env vars. Half-config (URLs without creds, or vice
    versa) deliberately omits the TURN block — a half-configured TURN
    surfaces auth errors at call time instead of falling back cleanly
    to STUN-only.
  - 4 handler tests cover the matrix.

Frontend:
  - services/api/webrtcConfig.ts caches the config for the page
    lifetime and falls back to the historical hardcoded Google STUN
    if the fetch fails.
  - useWebRTC fetches at mount, hands iceServers synchronously to
    every RTCPeerConnection, exposes a {hasTurn, loaded} hint.
  - CallButton tooltip warns up-front when TURN isn't configured
    instead of letting calls time out silently.

Ops:
  - infra/coturn/turnserver.conf — annotated template with the SSRF-
    safe denied-peer-ip ranges, prometheus exporter, TLS for TURNS,
    static lt-cred-mech (REST-secret rotation deferred to v1.1).
  - infra/coturn/README.md — Incus deploy walkthrough, smoke test
    via turnutils_uclient, capacity rules of thumb.
  - docs/ENV_VARIABLES.md gains a 13bis. WebRTC ICE servers section.

Coturn deployment itself is a separate ops action — this commit lands
the plumbing so the deploy can light up the path with zero code
changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:38:42 +02:00

100 lines
3.8 KiB
Text

# coturn configuration template — Veza v1.0.9 item 1.2
#
# Deploy onto an Incus container reachable from the public internet on
# UDP/3478 (TURN) and UDP/49152-65535 (relay range). The container
# **must** be on the host network or have UDP NAT forwarded for those
# ranges — TURN traffic does NOT survive Docker/Incus default NAT.
#
# Substitute the ${UPPERCASE} tokens via your secrets manager before
# rendering this file (Ansible `ansible.builtin.template`, sops, age,
# etc.). Never commit the rendered version to git.
#
# Smoke test post-deploy:
# $ turnutils_uclient -u veza -w "${WEBRTC_TURN_CREDENTIAL}" \
# -p 3478 turn.veza.fr
# (succeeds = relay works; failure = check listening-ip / external-ip)
# --- Listening setup -------------------------------------------------
# Listen on every interface so coturn picks up the public IP regardless
# of how the container's network stack is wired.
listening-port=3478
tls-listening-port=5349
# Public reachable IP. REQUIRED behind NAT — without it, coturn
# advertises the container's RFC1918 address in candidates and clients
# can't reach the relay.
external-ip=${WEBRTC_TURN_PUBLIC_IP}
# UDP relay port range. 16k ports is the common-sense default; raise if
# you expect more than ~8k concurrent TURN sessions (each session uses
# 1-2 ports).
min-port=49152
max-port=65535
# Realm — used in the TURN handshake. Must match what the frontend
# sends (it doesn't, today: the frontend sends only urls/username/cred).
# Set to your domain so logs are unambiguous.
realm=${WEBRTC_TURN_REALM}
# --- Authentication --------------------------------------------------
# Static long-term credentials — simple, works, and matches what
# `internal/handlers/webrtc_config_handler.go` returns to the SPA.
#
# v1.0.9 keeps this static. v1.1 should switch to the time-limited REST
# shared-secret scheme (RFC draft-uberti-behave-turn-rest), which lets
# the backend mint per-session credentials and rotate without restarting
# coturn. Documented in ORIGIN_SECURITY_FRAMEWORK.md (deferred section).
lt-cred-mech
user=${WEBRTC_TURN_USERNAME}:${WEBRTC_TURN_CREDENTIAL}
# --- TLS for TURNS ---------------------------------------------------
# Cert chain. Either Let's Encrypt (cert-manager / certbot cron) or a
# rotated wildcard mounted into the container at these paths.
cert=/etc/coturn/cert.pem
pkey=/etc/coturn/key.pem
# Modern TLS only. coturn defaults are permissive; tighten here so the
# whole chain matches the rest of the platform.
no-tlsv1
no-tlsv1_1
# --- Hardening ------------------------------------------------------
# Disable the multiplexed TLS-on-TCP-3478 listener — TURN-over-TCP is
# slow and we already expose port 5349 for TURNS-over-TLS. Keeps the
# attack surface predictable.
no-multicast-peers
no-cli
no-loopback-peers
# Avoid relaying to RFC1918 / loopback / link-local ranges. Without
# this, a malicious peer could use Veza's TURN as an SSRF springboard.
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
# --- Observability ---------------------------------------------------
# Prometheus exporter on a non-public port. Scrape from the same
# Prometheus instance that already targets backend-api.
prometheus
# Verbose enough to diagnose, quiet enough not to flood the disk on
# peak traffic. Bump to `verbose` temporarily during incidents.
log-file=/var/log/coturn/turnserver.log
no-stdout-log
syslog