feat: stream private office screens
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
# 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:** do not expose the TURN listeners or set
|
||||
> `TERA_ICE_URLS` / `TERA_TURN_SHARED_SECRET` in production until the deployed
|
||||
> ICE endpoint requires an active, unexpired screen-signaling grant for the exact
|
||||
> office/screen binding and authenticates that grant to the same signed-in
|
||||
> subject. The current endpoint checks only global member authentication, which
|
||||
> would let any signed-in member mint a general-purpose Internet relay credential.
|
||||
> It is intentionally not approved for broad public TURN enablement.
|
||||
|
||||
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=<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:
|
||||
|
||||
```bash
|
||||
unset TURN_SECRET
|
||||
```
|
||||
|
||||
The current authenticated `POST /api/v1/media/ice` endpoint validates only the
|
||||
signed-in caller and returns a five-minute username/password generated with
|
||||
coturn's REST scheme. That is insufficient authorization for a public relay.
|
||||
Before enabling the environment above, the deployed request must also carry an
|
||||
active screen-signaling credential and exact binding in its POST body. The server
|
||||
must validate its token hash, subject, role, binding, lease and revocation state,
|
||||
then rate-limit issuance by both subject and trusted client IP. The presenter or
|
||||
viewer must join signaling before requesting ICE configuration. GET/query-string
|
||||
credentials remain forbidden.
|
||||
|
||||
An accepted response uses:
|
||||
|
||||
- username: `<expiry-unix-seconds>:<random-opaque-nonce>`
|
||||
- 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 new allocations and
|
||||
refreshes; it cannot instantly terminate an allocation that already exists.
|
||||
|
||||
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 </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 create or refresh allocations;
|
||||
- signaling revocation closes the application's peer connections, but an already-issued
|
||||
stateless TURN credential remains usable until its short expiry (and an existing
|
||||
allocation until coturn's configured allocation lifetime);
|
||||
- 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
|
||||
|
||||
- [coturn server options and REST authentication](https://github.com/coturn/coturn/blob/master/README.turnserver)
|
||||
- [coturn container/networking notes](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md)
|
||||
- [Let's Encrypt port 80 guidance](https://letsencrypt.org/docs/allow-port-80/)
|
||||
- [Certbot webroot and deploy-hook documentation](https://eff-certbot.readthedocs.io/en/stable/using.html)
|
||||
Reference in New Issue
Block a user