diff --git a/.env.example b/.env.example index 5bd64de..31ffaf3 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,15 @@ MAX_VIEWERS_PER_ROOM=8 # Experimental standard-WebRTC relay chains. Keep false outside measured tests. PEER_ASSISTED_MEDIA=false +# Optional automatic SFU fallback capacity. Set PEER_ASSISTED_MEDIA=true and +# supply all three values together; leaving them empty keeps LiveKit completely +# dormant. Production requires wss and an independent API secret of 32+ bytes. +LIVEKIT_URL= +LIVEKIT_API_KEY= +LIVEKIT_API_SECRET= +# Effective only when the complete LiveKit tuple is configured. Valid range: 1-2. +MAX_SFU_ROOTS_PER_ROOM=2 + # ICE servers. Separate multiple URLs with commas. STUN_URLS= TURN_URLS= @@ -31,6 +40,12 @@ TURN_CREDENTIAL_TTL_SECONDS=3600 # STUN_URLS=stun:turn.example.com:3478 # TURN_URLS=turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?transport=tcp # TURN_SHARED_SECRET=replace-with-at-least-32-random-bytes +# Optional automatic SFU fallback (requires PEER_ASSISTED_MEDIA=true and does +# not replace STUN/TURN above): +# LIVEKIT_URL=wss://share.example.com +# LIVEKIT_API_KEY=replace-with-a-livekit-api-key +# LIVEKIT_API_SECRET=replace-with-at-least-32-independent-random-bytes +# MAX_SFU_ROOTS_PER_ROOM=2 # Optional TURN/TLS (standard port; append to TURN_URLS when configured): # turns:turn.example.com:5349?transport=tcp # Port 443 is an optional restrictive-network enhancement, not the default. diff --git a/deploy/livekit/livekit.yaml.example b/deploy/livekit/livekit.yaml.example new file mode 100644 index 0000000..fb3ddb5 --- /dev/null +++ b/deploy/livekit/livekit.yaml.example @@ -0,0 +1,27 @@ +# Minimal single-node LiveKit capacity for Screener's optional automatic SFU +# fallback. Copy this file to an untracked service-owned location such as +# /etc/livekit/livekit.yaml. Store only the generated key mapping in +# /etc/livekit/keys.yaml, owned by the livekit service user with mode 0600. + +port: 7880 +key_file: /etc/livekit/keys.yaml + +rtc: + # These media ports must be reached directly by clients. Port 7880 is only + # for the same-host reverse proxy and must be denied by every public firewall. + tcp_port: 7881 + udp_port: 7882 + use_external_ip: true + +room: + auto_create: true + # Screener enforces its configured admission limit before issuing a token. + # This server-side ceiling covers the repository's maximum 16 viewers + host. + max_participants: 17 + +logging: + level: info + pion_level: error + +# Redis, embedded TURN, recording, ingress, egress, and webhooks are omitted on +# purpose. This first fallback deployment is one process on one node. diff --git a/deploy/nginx/share.bonfire.icu.conf.example b/deploy/nginx/share.bonfire.icu.conf.example index 543c76b..8e27726 100644 --- a/deploy/nginx/share.bonfire.icu.conf.example +++ b/deploy/nginx/share.bonfire.icu.conf.example @@ -37,6 +37,47 @@ server { add_header X-Frame-Options "DENY" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + # Optional LiveKit signaling for automatic SFU fallback. Set LIVEKIT_URL to + # wss://share.bonfire.icu. Legacy clients use the exact /rtc endpoint. + location = /rtc { + # The WebSocket query contains a short-lived LiveKit join token. Do not + # persist the request target in either access or error logs. + access_log off; + error_log /dev/null crit; + proxy_pass http://127.0.0.1:7880; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $screener_connection_upgrade; + proxy_read_timeout 5h; + proxy_send_timeout 5h; + proxy_buffering off; + } + + # Current clients use versioned paths below /rtc/. + location ^~ /rtc/ { + # The WebSocket query contains a short-lived LiveKit join token. Do not + # persist the request target in either access or error logs. + access_log off; + error_log /dev/null crit; + proxy_pass http://127.0.0.1:7880; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $screener_connection_upgrade; + proxy_read_timeout 5h; + proxy_send_timeout 5h; + proxy_buffering off; + } + location / { proxy_pass http://127.0.0.1:8787; proxy_http_version 1.1; diff --git a/deploy/systemd/livekit.service.example b/deploy/systemd/livekit.service.example new file mode 100644 index 0000000..825ca4a --- /dev/null +++ b/deploy/systemd/livekit.service.example @@ -0,0 +1,39 @@ +[Unit] +Description=LiveKit single-node SFU fallback for Screener +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=livekit +Group=livekit +ExecStartPre=/usr/bin/test -r /etc/livekit/livekit.yaml +ExecStartPre=/usr/bin/test -r /etc/livekit/keys.yaml +ExecStart=/usr/local/bin/livekit-server --config /etc/livekit/livekit.yaml +Restart=on-failure +RestartSec=3s +TimeoutStopSec=20s +UMask=0077 +LimitNOFILE=65535 + +NoNewPrivileges=true +PrivateDevices=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true +ProtectControlGroups=true +ProtectClock=true +ProtectHostname=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true +LockPersonality=true +CapabilityBoundingSet= +SystemCallArchitectures=native + +[Install] +WantedBy=multi-user.target diff --git a/docs/deployment.md b/docs/deployment.md index f5444d3..37635e8 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -8,6 +8,11 @@ Caddy or nginx; coturn is the separate STUN/TURN service. Normal media remains browser-to-browser. Only an ICE pair that cannot connect directly consumes TURN bandwidth. +A deployment may additionally provide one single-node LiveKit process as the +automatic final media fallback. This capacity is dormant unless the complete +`LIVEKIT_URL`/key/secret tuple is configured. It does not replace the P2P path, +the peer-assisted experiment, or the required application STUN/TURN settings. + The public staging infrastructure has already passed the relay checks recorded in `docs/status.md`, but every new deployment must run the direct, relay, and mixed-network procedures below. Configuration validation alone is not evidence @@ -52,6 +57,12 @@ explicitly sets `LISTEN_HOST=127.0.0.1` because only the same-host reverse proxy should connect. A container can instead use `0.0.0.0` and enforce the intended boundary with port publishing rules or a host firewall. +The optional same-host LiveKit layout needs no additional public hostname: +`LIVEKIT_URL=wss://share.example.com` and nginx forwards only the exact `/rtc` +endpoint and paths below `/rtc/` to LiveKit. Its HTTP/WebSocket listener on TCP +7880 is private to nginx, while WebRTC media reaches LiveKit directly on TCP +7881 or UDP 7882. + ## Production application environment Keep the real values in the process secret store or an untracked, access-restricted @@ -83,6 +94,16 @@ TURN_SHARED_SECRET= TURN_CREDENTIAL_TTL_SECONDS=3600 ``` +To make automatic SFU fallback capacity available, add the complete tuple: + +```dotenv +PEER_ASSISTED_MEDIA=true +LIVEKIT_URL=wss://share.example.com +LIVEKIT_API_KEY= +LIVEKIT_API_SECRET= +MAX_SFU_ROOTS_PER_ROOM=2 +``` + `ALLOWED_ORIGINS` must list exact `http` or `https` origins, never `*`. `ACCESS_PASSWORD` is optional: omit it or leave it empty for a public site. A non-empty value must contain 1 through 128 visible ASCII characters (`0x21` @@ -101,6 +122,16 @@ browsers receive HMAC-SHA1-derived, time-limited credentials after room authentication. Keep host clocks synchronized because the credential username contains its Unix expiry time. +The three `LIVEKIT_*` values must either all be absent or all be present, and a +complete tuple requires `PEER_ASSISTED_MEDIA=true`. An empty tuple keeps the +optional SDK and server path dormant. `LIVEKIT_URL` must be a plain `ws:` or +`wss:` origin with no `/rtc` suffix; production requires `wss:`. +`LIVEKIT_API_SECRET` must contain at least 32 bytes and must not reuse +`ACCESS_PASSWORD` or `TURN_SHARED_SECRET`. `MAX_SFU_ROOTS_PER_ROOM` defaults to +2 and accepts only 1 or 2; it is ignored when LiveKit is not configured. These +credentials authorize short-lived LiveKit room tokens and do not provide E2EE: +the LiveKit operator can access ordinary SFU media. + When `ACCESS_PASSWORD` is configured, both host and viewer routes first show the same login gate. Only `POST /api/session` accepts the password in an `Authorization: Bearer` header, then returns a 12-hour stateless HMAC-SHA256 @@ -192,7 +223,11 @@ location / { The tracked [`share.bonfire.icu.conf.example`](../deploy/nginx/share.bonfire.icu.conf.example) follows this simple model and listens directly on public TCP 443. It does not -depend on the optional SNI router. +depend on the optional SNI router. Its optional exact `/rtc` and `/rtc/` prefix +locations preserve the LiveKit request URI and disable both access logging and +request-line error logging. Legacy `/rtc?token=...` and current versioned +requests carry a short-lived LiveKit JWT in the query string, so neither request +target may fall through to the site's ordinary logs. When Certbot manages the Web certificate, install the tracked [`reload-nginx.sh`](../deploy/certbot/reload-nginx.sh) as an executable under @@ -213,6 +248,39 @@ TURN reachability, or any external dependency, so use it only to decide whether the Node process can accept HTTP requests. It is not a deployment-readiness or end-to-end media check. +## Optional single-node LiveKit fallback + +Use the tracked +[`livekit.yaml.example`](../deploy/livekit/livekit.yaml.example) and +[`livekit.service.example`](../deploy/systemd/livekit.service.example) as the +minimal single-node baseline. Install a reviewed, pinned LiveKit Server release +from its official release assets, generate an independent key pair with +`livekit-server generate-keys`, and place only the YAML key mapping in +`/etc/livekit/keys.yaml`. Keep that file owned by the `livekit` service account +with mode `0600`; put the same values in Screener's untracked process secrets. + +The example deliberately omits Redis and every recording, ingress, egress, +webhook, and embedded-TURN service. Do not add Redis for this one-node workload. +The service journal is the diagnostic log; keep its retention finite and access +restricted. Never enable debug/Pion packet logging continuously or persist JWTs, +SDP, ICE candidates, API secrets, or full `/rtc` and `/rtc/*` request targets. + +Configure both the host firewall and the provider firewall independently: + +| Destination | Protocol | Exposure | Purpose | +| --- | --- | --- | --- | +| `127.0.0.1:7880` | TCP | Private | nginx to LiveKit signaling/API | +| public host `:7881` | TCP | Public | WebRTC ICE/TCP fallback | +| public host `:7882` | UDP | Public | WebRTC ICE/UDP mux | + +LiveKit may listen beyond loopback while it discovers and advertises public +media addresses, so the public-firewall denial of TCP 7880 is mandatory; nginx +must be its only ingress. TCP 7881 and UDP 7882 are direct media listeners and +must not be placed behind the HTTP reverse proxy. If the host is behind NAT, +forward them without port translation and verify the advertised candidate from +an external network. LiveKit ICE/TCP is not equivalent to authenticated TURN; +strict-network SFU reachability remains unproven until separately tested. + ## coturn Copy [`deploy/coturn/turnserver.conf.example`](../deploy/coturn/turnserver.conf.example) @@ -262,6 +330,8 @@ Open only these public listeners: | `turn.example.com:49152-49251` | UDP, bidirectional | Relayed media endpoints | | `turn.example.com:5349` | TCP, optional | Standard TURN/TLS | | `turn.example.com:443` | TCP, optional | TURN/TLS on a dedicated IP or validated L4/SNI route | +| host public address `:7881` | TCP, optional SFU | LiveKit WebRTC ICE/TCP | +| host public address `:7882` | UDP, optional SFU | LiveKit WebRTC ICE/UDP mux | The 100-port relay range is an initial small-room limit, not a universal sizing rule. Monitor 508/allocation failures and concurrent allocations before widening @@ -318,5 +388,7 @@ References: [coturn 4.17.2 release](https://github.com/coturn/coturn/releases/ta the [official example configuration](https://github.com/coturn/coturn/blob/master/examples/etc/turnserver.conf), [TURN URI scheme RFC 7065](https://www.rfc-editor.org/rfc/rfc7065.html), [Cloudflare network-port guidance](https://developers.cloudflare.com/fundamentals/reference/network-ports/), -[RFC6265bis](https://datatracker.ietf.org/doc/draft-ietf-httpbis-rfc6265bis/), and -[Node.js Crypto](https://nodejs.org/api/crypto.html#cryptocreatehmacalgorithm-key-options). +[RFC6265bis](https://datatracker.ietf.org/doc/draft-ietf-httpbis-rfc6265bis/), +[Node.js Crypto](https://nodejs.org/api/crypto.html#cryptocreatehmacalgorithm-key-options), +[LiveKit firewall guidance](https://docs.livekit.io/deploy/admin/firewall/), and +the [official LiveKit configuration sample](https://github.com/livekit/livekit/blob/master/config-sample.yaml).