feat: stream private office screens
This commit is contained in:
@@ -32,6 +32,9 @@ tera.example.com {
|
||||
file_server
|
||||
header {
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'"
|
||||
# Screen sharing remains a per-action browser prompt. Webcam capture stays
|
||||
# disabled until that separate feature is deliberately deployed.
|
||||
Permissions-Policy "display-capture=(self), camera=(), microphone=(), geolocation=(), payment=(), usb=()"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# Certbot deploy hook: install only the renewed turn certificate, then ask the
|
||||
# distro unit to reload or restart. A restart can interrupt active allocations;
|
||||
# schedule renewal windows and verify the unit's ExecReload before production.
|
||||
set -eu
|
||||
|
||||
: "${RENEWED_LINEAGE:?certbot did not provide RENEWED_LINEAGE}"
|
||||
openssl x509 -in "$RENEWED_LINEAGE/fullchain.pem" -noout \
|
||||
-checkhost turn.lumbridgecorp.com >/dev/null 2>&1 || exit 0
|
||||
getent group turnserver >/dev/null || {
|
||||
echo "coturn certificate hook: turnserver group does not exist" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
install -d -o root -g turnserver -m 0750 /etc/coturn/certs
|
||||
install -o root -g turnserver -m 0644 "$RENEWED_LINEAGE/fullchain.pem" /etc/coturn/certs/turn.fullchain.pem
|
||||
install -o root -g turnserver -m 0640 "$RENEWED_LINEAGE/privkey.pem" /etc/coturn/certs/turn.privkey.pem
|
||||
systemctl try-reload-or-restart coturn.service
|
||||
@@ -0,0 +1,7 @@
|
||||
# Install as /etc/systemd/system/coturn.service.d/tera.conf after confirming the
|
||||
# distro package calls its unit coturn.service and runs as user/group turnserver.
|
||||
[Service]
|
||||
ExecStartPre=/usr/local/libexec/tera-coturn-preflight
|
||||
LimitNOFILE=65536
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
# Fail a coturn start before an example placeholder becomes a public credential.
|
||||
set -eu
|
||||
|
||||
config=${TURN_CONFIG:-/etc/turnserver.conf}
|
||||
|
||||
test -r "$config" || { echo "coturn preflight: cannot read $config" >&2; exit 1; }
|
||||
if grep -q 'REQUIRED_' "$config"; then
|
||||
echo "coturn preflight: unresolved REQUIRED_ placeholder in $config" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
secret=$(sed -n 's/^static-auth-secret=//p' "$config")
|
||||
case "$secret" in
|
||||
*[!0-9A-Fa-f]*|'')
|
||||
echo "coturn preflight: static-auth-secret must be hexadecimal" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
test "${#secret}" -ge 64 || {
|
||||
echo "coturn preflight: static-auth-secret must contain at least 64 hex characters" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for path in /etc/coturn/certs/turn.fullchain.pem /etc/coturn/certs/turn.privkey.pem; do
|
||||
test -r "$path" || { echo "coturn preflight: cannot read $path" >&2; exit 1; }
|
||||
done
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,12 @@
|
||||
# HTTP-01 only. Review, place beside the real Caddyfile, and import it there.
|
||||
# Caddy does not proxy TURN; coturn owns 3478/5349 and its UDP relay range.
|
||||
http://turn.lumbridgecorp.com {
|
||||
bind REQUIRED_PRIVATE_IPV4
|
||||
|
||||
handle /.well-known/acme-challenge/* {
|
||||
root * /var/www/turn-acme
|
||||
file_server
|
||||
}
|
||||
|
||||
respond 404
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# Tera coturn template. Copy to /etc/turnserver.conf only after replacing every
|
||||
# REQUIRED_* value and completing the operator review in README.md.
|
||||
|
||||
listening-port=3478
|
||||
tls-listening-port=5349
|
||||
|
||||
# Oracle assigns the public address through one-to-one NAT. Coturn must bind the
|
||||
# VNIC address while advertising the port-preserving public/private mapping.
|
||||
listening-ip=REQUIRED_PRIVATE_IPV4
|
||||
relay-ip=REQUIRED_PRIVATE_IPV4
|
||||
external-ip=REQUIRED_PUBLIC_IPV4/REQUIRED_PRIVATE_IPV4
|
||||
|
||||
realm=turn.lumbridgecorp.com
|
||||
server-name=turn.lumbridgecorp.com
|
||||
fingerprint
|
||||
|
||||
# Tera's authenticated ICE endpoint and coturn hold the same random secret.
|
||||
# Do not enable both lt-cred-mech and use-auth-secret; the latter is coturn's
|
||||
# time-limited TURN REST authentication mode.
|
||||
use-auth-secret
|
||||
static-auth-secret=REQUIRED_RANDOM_64_HEX_SECRET
|
||||
stale-nonce
|
||||
|
||||
cert=/etc/coturn/certs/turn.fullchain.pem
|
||||
pkey=/etc/coturn/certs/turn.privkey.pem
|
||||
|
||||
# Dedicated to Tera. Do not overlap the retired LiveKit 50000-50200 range.
|
||||
min-port=52000
|
||||
max-port=53023
|
||||
|
||||
# One credential may briefly own several allocations during ICE restart. The
|
||||
# total stays below the 1,024-port relay range; tune only from observed usage.
|
||||
user-quota=4
|
||||
total-quota=900
|
||||
max-bps=2000000
|
||||
bps-capacity=500000000
|
||||
|
||||
# Browser media uses UDP relay endpoints even when its connection to coturn is
|
||||
# TCP/TLS. Disabling RFC 6062 TCP peer relays reduces proxy-abuse surface.
|
||||
no-tcp-relay
|
||||
no-dtls
|
||||
no-multicast-peers
|
||||
no-cli
|
||||
no-software-attribute
|
||||
|
||||
# Never turn the public relay into a route to local, cloud metadata, Docker,
|
||||
# Tailscale/CGNAT, documentation, multicast, or reserved networks. Coturn uses
|
||||
# inclusive address ranges here, not CIDR notation.
|
||||
denied-peer-ip=0.0.0.0-0.255.255.255
|
||||
denied-peer-ip=10.0.0.0-10.255.255.255
|
||||
denied-peer-ip=100.64.0.0-100.127.255.255
|
||||
denied-peer-ip=127.0.0.0-127.255.255.255
|
||||
denied-peer-ip=169.254.0.0-169.254.255.255
|
||||
denied-peer-ip=172.16.0.0-172.31.255.255
|
||||
denied-peer-ip=192.0.0.0-192.0.0.255
|
||||
denied-peer-ip=192.0.2.0-192.0.2.255
|
||||
denied-peer-ip=192.168.0.0-192.168.255.255
|
||||
denied-peer-ip=198.18.0.0-198.19.255.255
|
||||
denied-peer-ip=198.51.100.0-198.51.100.255
|
||||
denied-peer-ip=203.0.113.0-203.0.113.255
|
||||
denied-peer-ip=224.0.0.0-255.255.255.255
|
||||
denied-peer-ip=::1-::1
|
||||
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
denied-peer-ip=ff00::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
|
||||
# Binding-request logging is intentionally left disabled. Keep logs in the
|
||||
# journal/syslog and never add credentials or full SDP to application logs.
|
||||
syslog
|
||||
@@ -42,6 +42,9 @@ services:
|
||||
TERA_AUTH_ENTRY_URL: "${TERA_AUTH_ENTRY_URL:-}"
|
||||
TERA_AUTH_REVALIDATE_URL: "${TERA_AUTH_REVALIDATE_URL:-}"
|
||||
TERA_AUTH_JWT_SECRET: "${TERA_AUTH_JWT_SECRET:-}"
|
||||
TERA_ICE_URLS: "${TERA_ICE_URLS:-}"
|
||||
TERA_TURN_SHARED_SECRET: "${TERA_TURN_SHARED_SECRET:-}"
|
||||
TERA_TURN_CREDENTIAL_TTL: "${TERA_TURN_CREDENTIAL_TTL:-}"
|
||||
# Offices and marker snapshots are files. Mount them read-only where you
|
||||
# keep them; the container writes nothing, ever.
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user