12 KiB
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:
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:
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.
- Create
/var/www/turn-acmeroot-owned and readable by Caddy. - Review
turn-acme.Caddyfile.example, replace its private IP, import it, runcaddy validate, and reload Caddy. - Verify a test file under
/.well-known/acme-challenge/is reachable over public port 80. - Use Certbot's webroot mode for
turn.lumbridgecorp.com. - Install
certbot-deploy-hookunder/etc/letsencrypt/renewal-hooks/deploy/mode0755. - Run the hook once with
RENEWED_LINEAGEset 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.