1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/deploy/coturn/README.md
T

12 KiB
Raw Blame History

TURN for office screen sharing

This directory is an operator-reviewed template, not an installer. Nothing here changes DNS, Caddy, systemd, UFW or Oracle Cloud. Those are public routing changes and require explicit operator approval on the deployment host.

Tera's browser peers normally connect directly. Coturn is the fallback for symmetric NATs and restrictive networks; it relays encrypted WebRTC packets and never receives application signaling, screen URLs or recordings. The signaling service remains the separate loopback API described in server/README.md.

Mandatory public-enablement gate: the repository endpoint now requires an active, unexpired screen-signaling grant for the exact office/screen binding and authenticates it to the same signed-in subject. Do not infer that the live deployment has this build or that coturn is ready: prove the deployed cases in §4, then approve DNS/firewall/certificate/quota changes before exposing TURN listeners or setting TERA_ICE_URLS / TERA_TURN_SHARED_SECRET.

The production host audited on 2026-08-11 has private VNIC 10.0.0.2, public IPv4 170.9.14.61, and an existing turn.lumbridgecorp.com A record. Reconfirm all three immediately before deployment. Do not copy those addresses into a different installation merely because they appear here.

1. Review the network boundary

The intended public listeners are:

Transport Port Purpose
UDP 3478 primary STUN/TURN listener
TCP 3478 TURN fallback
TCP/TLS 5349 TURN/TLS fallback
UDP 5200053023 dedicated relay allocations

Open the same ingress in both UFW and the instance's OCI security list or NSG. The OCI console is authoritative; a host firewall rule does not prove the cloud edge permits traffic. Relay UDP must accept arbitrary Internet peers, not only the browser that created an allocation. Outbound traffic remains allowed.

Example commands to review, not paste blindly:

dig +short A turn.lumbridgecorp.com
ip -br address
curl -4 https://api.ipify.org
sudo ufw status verbose

# Only after the operator approves these public ingress changes:
sudo ufw allow 3478/udp comment 'Tera TURN UDP'
sudo ufw allow 3478/tcp comment 'Tera TURN TCP'
sudo ufw allow 5349/tcp comment 'Tera TURN TLS'
sudo ufw allow 52000:53023/udp comment 'Tera TURN relay'

Add equivalent stateful OCI ingress rules. Do not open UDP 5349: this template disables DTLS. Do not reuse UDP 5000050200; stopped LiveKit deployments on the audited host reserve that range and could otherwise collide if restarted.

The public/private NAT must preserve relay port numbers. This is why the config uses external-ip=PUBLIC/PRIVATE; a relay candidate advertising 10.0.0.2 or a different public port is a failed deployment.

2. Install and render the config

Prefer Ubuntu's native coturn package and unit. Docker adds no isolation benefit to a service that needs a large host UDP range, and bridged port translation is an extra failure mode. Inspect the package before enabling it:

apt-cache policy coturn
systemctl cat coturn.service
getent passwd turnserver
getent group turnserver

Some distro packages also ship /etc/default/coturn with an explicit enable switch. Inspect it and the unit's conditions; set TURNSERVER_ENABLED=1 only as part of the reviewed enablement, never by assuming a successful systemctl command means the daemon actually bound its sockets.

Generate one 256-bit hexadecimal secret without putting its value in shell history:

umask 077
TURN_SECRET="$(openssl rand -hex 32)"
test "${#TURN_SECRET}" -eq 64

Render turnserver.conf.example to a root-owned /etc/turnserver.conf, replacing the two IP placeholders and the secret placeholder. The installed file should be root:turnserver mode 0640. Put the same raw secret in the Tera API's root-owned environment file:

TERA_TURN_SHARED_SECRET=<same 64 hex characters>
TERA_ICE_URLS=stun:turn.lumbridgecorp.com:3478,turn:turn.lumbridgecorp.com:3478?transport=udp,turn:turn.lumbridgecorp.com:3478?transport=tcp,turns:turn.lumbridgecorp.com:5349?transport=tcp
TERA_TURN_CREDENTIAL_TTL=300

The audited service reads /etc/tera-api.env; the repository unit reads /etc/tera/tera.env. Use the path its installed unit actually declares. Never commit either rendered file, print the secret, put it in a URL, or pass it as a process argument. Clear the shell variable after both files are installed:

unset TURN_SECRET

The repository POST /api/v1/media/ice contract carries an active screen- signaling credential and exact binding in its POST body. The server validates its token hash, subject, role, binding, lease and revocation state, then rate- limits issuance independently by subject and trusted client IP. Before enabling the environment above, prove that this exact behavior is running on the deployed API; a source-tree test is not evidence that the host was upgraded. The presenter or viewer must join signaling before requesting ICE configuration. GET/query- string credentials remain forbidden.

Coturn removes the expiry prefix from a REST username before applying user-quota. The suffix therefore must be a stable, opaque, session-participant accounting key across repeat ICE issuance for that participant. Do not use the authentication subject, email, profile id or another identity in this suffix, and do not generate a fresh random suffix for every request: the former leaks identity into TURN state/logs, while the latter gives every issuance a fresh user-quota. A keyed digest of the opaque signaling session and participant ids is an appropriate suffix. Application-side subject/IP issuance and concurrent session limits remain mandatory; coturn cannot infer a Lumbridge member.

The template's max-bps and bps-capacity units are bytes per second, with input and output accounted separately. In particular, bps-capacity=500000000 is a 500 MB/s (roughly 4 Gbit/s) ceiling, not a recommended launch budget. Before public enablement, the operator must lower total-quota and bps-capacity to the measured instance/network ceiling and an explicitly accepted relay-egress budget. Start below that budget and raise only from observed concurrent screen bitrate and allocation data. The 1,024-port relay range is a hard capacity boundary, not evidence that 900 simultaneous relays are operationally or financially safe.

An accepted response uses:

  • username: <expiry-unix-seconds>:<stable-opaque-participant-accounting-key>
  • credential: Base64 HMAC-SHA1 of that username using TERA_TURN_SHARED_SECRET
  • response: Cache-Control: private, no-store

The shared secret never leaves the server. Expiry prevents a credential from starting a newly authenticated TURN session. It does not terminate an allocation that already exists, and coturn may continue accepting authenticated refreshes within that established session. stale-nonce rotates coturn's protocol nonce (the browser handles the 438 challenge); it is not credential revocation. Stop and revoke must still close browser peer connections, while coturn's total allocation and bandwidth limits bound residual abuse.

Install tera-coturn-preflight as /usr/local/libexec/tera-coturn-preflight mode 0755, and install the reviewed systemd drop-in only after confirming the distro unit name. The preflight deliberately refuses unresolved placeholders, short/non-hex secrets, and missing certificates.

3. Issue and renew the TLS certificate

Do not point coturn into Caddy's private certificate store. It is owned by the Caddy account, and Caddy renewal does not provide coturn a reliable reload hook.

  1. Create /var/www/turn-acme root-owned and readable by Caddy.
  2. Review turn-acme.Caddyfile.example, replace its private IP, import it, run caddy validate, and reload Caddy.
  3. Verify a test file under /.well-known/acme-challenge/ is reachable over public port 80.
  4. Use Certbot's webroot mode for turn.lumbridgecorp.com.
  5. Install certbot-deploy-hook under /etc/letsencrypt/renewal-hooks/deploy/ mode 0755.
  6. Run the hook once with RENEWED_LINEAGE set to the issued lineage, then verify ownership and certificate names without printing the private key.

Example issuance, after review:

sudo certbot certonly --webroot -w /var/www/turn-acme \
  -d turn.lumbridgecorp.com
sudo env RENEWED_LINEAGE=/etc/letsencrypt/live/turn.lumbridgecorp.com \
  /etc/letsencrypt/renewal-hooks/deploy/certbot-deploy-hook
sudo openssl x509 -in /etc/coturn/certs/turn.fullchain.pem \
  -noout -subject -issuer -dates -ext subjectAltName

try-reload-or-restart may restart coturn when the package unit has no reload action, interrupting allocations. Confirm the installed unit's behavior and schedule renewal accordingly. Test renewal with certbot renew --dry-run before calling the certificate path production-ready.

4. Validate before enabling

First prove the mandatory application gate: a signed-in member with no active signaling grant, and a caller using a stopped, revoked, expired, wrong-subject or wrong-binding grant, must all receive no TURN credential. Only then configure the shared secret/ICE URLs and approve public firewall changes. Keep the TURN ports closed and both Tera TURN environment values unset if any case fails.

Run the repository preflight against a staged rendered config, then use coturn's installed version/config inspection facilities. Do not start the daemon merely to discover an unresolved placeholder on a public interface.

sudo TURN_CONFIG=/etc/turnserver.conf \
  /usr/local/libexec/tera-coturn-preflight
turnserver --version
sudo systemctl daemon-reload
sudo systemctl start coturn.service       # operator-approved change
sudo systemctl --no-pager --full status coturn.service
sudo journalctl -u coturn.service --since -10m --no-pager
sudo ss -lntup | grep -E ':3478|:5349'

From a machine outside OCI:

dig +short A turn.lumbridgecorp.com
openssl s_client -connect turn.lumbridgecorp.com:5349 \
  -servername turn.lumbridgecorp.com </dev/null
turnutils_stunclient -p 3478 turn.lumbridgecorp.com

Obtain a temporary credential through the authenticated ICE endpoint rather than reading the shared secret. Exercise it with turnutils_uclient and with a browser RTCPeerConnection configured with iceTransportPolicy: "relay". Verify through getStats() or browser WebRTC diagnostics that:

  • the selected candidate type is relay;
  • the relay address is the current public IP, never the private VNIC address;
  • two peers on different networks can exchange a screen track;
  • blocking UDP forces TURN/TCP 3478, then TURN/TLS 5349;
  • expired TURN credentials cannot start a new authenticated allocation;
  • repeated ICE issuance for one signaling participant retains one opaque TURN accounting suffix and the fifth concurrent allocation is refused by user-quota=4, while a different participant remains independent;
  • signaling revocation closes the application's peer connections, but an already-issued stateless TURN credential remains usable until its short expiry (and an existing authenticated allocation may remain refreshable until the client disconnects);
  • presenter stop and viewer leave close their browser peer connections;
  • no CSP or Permissions Policy violation appears on either Tera entry host.

Finally test certificate renewal/reload and monitor allocation count, port exhaustion, authentication failures and relay bandwidth. TURN is an egress and abuse boundary; quota increases require the same operator review as firewall changes. TLS on 5349 will not cross networks that allow only destination 443. Supporting turns:443 requires a separate public IP or carefully tested L4/SNI multiplexing and is deliberately outside this minimal deployment.

Upstream references