Files
python-tdd/infra/coturn.conf.j2
Disco DeDisco c9a61e5614 coturn: optional dual-stack TURN via guarded coturn_public_ip6
Set coturn_public_ip6 in inventory to advertise IPv6 relay candidates (2nd external-ip) AND emit matching v6 denied-peer-ip ranges (::1, fe80::/10, fc00::/7) for SSRF parity with the v4 lockdown. Unset → byte-identical pure-IPv4 config as before, so it's zero-risk opt-in. Droplet now has IPv6 on; this makes the conf dual-stack-ready.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:10:28 -04:00

65 lines
2.9 KiB
Django/Jinja

# coturn (TURN/STUN) config for the EarthmanRPG WebRTC mesh voice feature —
# Phase C of the my-sea invite/voice sprint. Rendered by coturn-playbook.yaml
# onto a DEDICATED droplet (PySwiss-style split), NOT the app droplet.
#
# The app's /api/voice/turn-credentials/ endpoint signs ephemeral credentials
# with HMAC-SHA1(<expiry>:<user_id>, secret); `use-auth-secret` +
# `static-auth-secret` here must use the SAME secret (COTURN_SHARED_SECRET in
# the app env).
listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
use-auth-secret
static-auth-secret={{ coturn_secret }}
realm={{ coturn_realm }}
# ── THE #1 FOOTGUN ──────────────────────────────────────────────────────────
# Without external-ip, coturn hands out its PRIVATE address as the relay
# candidate and every relayed connection silently fails. On a DigitalOcean
# droplet with a single public IP set it to that IP; if the droplet also has a
# private/anchor IP, use PUBLIC/PRIVATE so coturn maps between them.
external-ip={{ coturn_public_ip }}{% if coturn_private_ip is defined and coturn_private_ip %}/{{ coturn_private_ip }}{% endif %}
{% if coturn_public_ip6 is defined and coturn_public_ip6 %}
# Dual-stack: advertise IPv6 relay candidates too. coturn auto-binds all
# interfaces (incl. v6) since no listening-ip is pinned; this maps the public
# v6 explicitly. Set coturn_public_ip6 in inventory to enable — leave it unset
# for a pure-IPv4 server (the v6 peer-lockdown below is gated on the same var).
external-ip={{ coturn_public_ip6 }}
{% endif %}
# Relay port range — open this exact UDP range in the firewall (playbook does).
min-port=49152
max-port=65535
# ── TLS (turns: on 5349) — prod hardening ──────────────────────────────────
{% if coturn_tls_cert is defined and coturn_tls_cert %}
cert={{ coturn_tls_cert }}
pkey={{ coturn_tls_key }}
{% endif %}
no-tlsv1
no-tlsv1_1
# ── Lockdown: relay only, no SSRF via the TURN server ───────────────────────
no-multicast-peers
no-cli
no-software-attribute
# Block relaying to private ranges so the box can't be used to probe internals.
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
{% if coturn_public_ip6 is defined and coturn_public_ip6 %}
# IPv6 lockdown parity (only emitted when serving v6): loopback, link-local
# (fe80::/10), and unique-local (fc00::/7). coturn takes start-end ranges, not
# CIDR. Keeps a dual-stack relay from being pointed at internal v6 addresses.
denied-peer-ip=::1
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
{% endif %}
log-file=/var/log/turnserver/turn.log
simple-log