# 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 | 52000–53023 | 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: ```bash 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 50000–50200; 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: ```bash 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: ```bash 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: ```ini TERA_TURN_SHARED_SECRET= 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: ```bash 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: `:` - 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: ```bash 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. ```bash 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: ```bash dig +short A turn.lumbridgecorp.com openssl s_client -connect turn.lumbridgecorp.com:5349 \ -servername turn.lumbridgecorp.com