chore: add minimal staging deployment (#6)
* chore(deploy): add minimal production deployment * fix(deploy): fail closed and reload renewed certs * docs(status): record staging deployment * docs(status): record public TURN verification
This commit is contained in:
1 parent
1832ca8a84
commit
db3aeafd0b
18 files changed
+339
-97
No files matched your search
+6
-2
@@ -16,7 +16,11 @@ TURN_URLS=
|
||||
TURN_SHARED_SECRET=
|
||||
TURN_CREDENTIAL_TTL_SECONDS=3600
|
||||
|
||||
# Production example (use independent untracked secrets of at least 32 bytes):
|
||||
# Minimal production example (use independent untracked secrets of at least 32 bytes):
|
||||
# ROOM_CREATION_TOKEN=replace-with-at-least-32-random-bytes
|
||||
# TURN_URLS=turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?transport=tcp,turns:turn.example.com:443?transport=tcp
|
||||
# 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 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.
|
||||
@@ -24,9 +24,11 @@ must remain on `localhost` or HTTPS because screen capture requires a secure
|
||||
context. The default has no STUN or TURN, so cross-network use still requires
|
||||
HTTPS and the production ICE configuration documented separately.
|
||||
|
||||
Production startup requires separate TURN/UDP, TURN/TCP, and
|
||||
TURN/TLS-on-TCP-443 URLs; this is a configuration preflight, not evidence that
|
||||
the external relay path works.
|
||||
Production startup requires STUN plus separate TURN/UDP and TURN/TCP URLs.
|
||||
TURN/TLS is an optional restrictive-network enhancement: use its standard TCP
|
||||
port 5349 by default, or port 443 only when the deployment has a dedicated
|
||||
public IP or a validated layer-4/SNI route. This configuration preflight is not
|
||||
evidence that any external relay path works.
|
||||
|
||||
Run the complete automated validation with:
|
||||
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
systemctl reload nginx.service
|
||||
@@ -3,9 +3,15 @@
|
||||
# Copy this file outside the repository, replace every <...> placeholder, and
|
||||
# restrict the resulting file to the coturn service account.
|
||||
|
||||
# Client-facing TURN/STUN listeners.
|
||||
# Required client-facing TURN/STUN listeners.
|
||||
listening-port=3478
|
||||
tls-listening-port=443
|
||||
|
||||
# TURN/TLS is optional. The minimal deployment keeps it disabled and needs no
|
||||
# TURN certificate. To enable TLS over TCP, remove `no-tls`, uncomment the
|
||||
# standard 5349 listener and certificate paths near the end of this file, then
|
||||
# append turns:<TURN_FQDN>:5349?transport=tcp to the application's TURN_URLS.
|
||||
no-tls
|
||||
#tls-listening-port=5349
|
||||
|
||||
# Keep the automatic address selection on a single-homed public host. For a
|
||||
# multi-homed host or a host behind 1:1 NAT, uncomment and replace these values.
|
||||
@@ -27,6 +33,9 @@ server-name=<TURN_FQDN>
|
||||
# in the application environment. Never use a static user/password or no-auth.
|
||||
use-auth-secret
|
||||
static-auth-secret=<REPLACE_WITH_RANDOM_UNTRACKED_SECRET>
|
||||
# Some packaged builds still consult SQLite after reading the shared secret.
|
||||
# Keep that optional database writable and ephemeral under RuntimeDirectory.
|
||||
userdb=/run/coturn/turndb
|
||||
stale-nonce=600
|
||||
max-allocate-lifetime=3600
|
||||
|
||||
@@ -50,14 +59,14 @@ denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
no-multicast-peers
|
||||
|
||||
# A certificate for TURN_FQDN is required by turns: URLs. Use absolute paths in
|
||||
# the deployed copy and reload/restart coturn after certificate renewal.
|
||||
cert=<ABSOLUTE_PATH_TO_FULLCHAIN_PEM>
|
||||
pkey=<ABSOLUTE_PATH_TO_PRIVATE_KEY_PEM>
|
||||
# A certificate for TURN_FQDN is required only by turns: URLs. Use absolute
|
||||
# paths in the deployed copy and reload/restart coturn after renewal.
|
||||
#cert=<ABSOLUTE_PATH_TO_FULLCHAIN_PEM>
|
||||
#pkey=<ABSOLUTE_PATH_TO_PRIVATE_KEY_PEM>
|
||||
|
||||
# Let the service supervisor apply bounded retention. Do not enable verbose or
|
||||
# log-binding in normal production operation; coturn logs can contain client IPs.
|
||||
log-file=stdout
|
||||
log-min-level=info
|
||||
new-log-timestamp
|
||||
no-cli
|
||||
pidfile=/run/coturn/turnserver.pid
|
||||
@@ -0,0 +1,52 @@
|
||||
map $http_upgrade $screener_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name share.bonfire.icu;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/letsencrypt;
|
||||
default_type text/plain;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://share.bonfire.icu$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name share.bonfire.icu;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/share.bonfire.icu/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/share.bonfire.icu/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
access_log /var/log/nginx/screener.access.log;
|
||||
error_log /var/log/nginx/screener.error.log warn;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
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 Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $screener_connection_upgrade;
|
||||
proxy_read_timeout 5h;
|
||||
proxy_send_timeout 5h;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# OPTIONAL TURN/TLS-on-443 topology only. Do not include this file for the
|
||||
# default deployment. It requires nginx's stream and ssl_preread modules.
|
||||
#
|
||||
# Before enabling it, move every HTTPS virtual host on this public address,
|
||||
# including share.bonfire.icu and any existing blog, from public 443 to the
|
||||
# loopback listener `127.0.0.1:8443 ssl`. Also enable coturn TLS on loopback
|
||||
# 5349. The public 443 listener below then routes encrypted traffic by SNI
|
||||
# without terminating TLS. A normal Cloudflare HTTP proxy cannot carry this
|
||||
# TURN route.
|
||||
map $ssl_preread_server_name $screener_tls_backend {
|
||||
turn.bonfire.icu screener_turn_tls;
|
||||
default screener_web_tls;
|
||||
}
|
||||
|
||||
upstream screener_web_tls {
|
||||
server 127.0.0.1:8443;
|
||||
}
|
||||
|
||||
upstream screener_turn_tls {
|
||||
server 127.0.0.1:5349;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443;
|
||||
listen [::]:443;
|
||||
proxy_pass $screener_tls_backend;
|
||||
ssl_preread on;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_timeout 5h;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
[Unit]
|
||||
Description=Screener coturn relay
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=turnserver
|
||||
Group=turnserver
|
||||
RuntimeDirectory=coturn
|
||||
RuntimeDirectoryMode=0750
|
||||
ExecStartPre=/usr/bin/test -r /etc/screener/turnserver.conf
|
||||
ExecStart=/usr/local/bin/turnserver -c /etc/screener/turnserver.conf
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
LimitNOFILE=65536
|
||||
UMask=0077
|
||||
|
||||
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
|
||||
@@ -0,0 +1,38 @@
|
||||
[Unit]
|
||||
Description=Screener WebRTC signaling and web application
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=screener
|
||||
Group=screener
|
||||
WorkingDirectory=/opt/screener/current
|
||||
EnvironmentFile=/etc/screener/screener.env
|
||||
ExecStart=/usr/local/bin/node dist/server/server/index.js
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
TimeoutStopSec=20s
|
||||
UMask=0077
|
||||
|
||||
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
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
- [方案设计](./方案设计.md): accepted, code-ready design for the first measurable WebRTC proof of concept.
|
||||
- [Project memory](./project-memory.md): durable decisions and unresolved questions to read at the start of future work.
|
||||
- [Current status](./status.md): bounded snapshot of the current phase, completed baseline, next step, and blockers.
|
||||
- [Deployment](./deployment.md): minimal Node, HTTPS/WSS, coturn, firewall, secret, and network-verification procedure.
|
||||
- [Deployment](./deployment.md): minimal Node, HTTPS/WSS, coturn, firewall, secret, optional TURN/TLS, and network-verification procedure.
|
||||
- [Maintenance guide](./maintenance.md): Git workflow, context hygiene, document lifecycle, research policy, and automation.
|
||||
- [P2P WebRTC research](./research/webrtc-p2p-screen-sharing.md): evidence, bandwidth model, browser constraints, reference implementations, and feasibility assessment.
|
||||
- [Agent context governance research](./research/agent-context-governance.md): official Codex, Claude Code, Hermes Agent, and GitHub practices adopted by this repository.
|
||||
|
||||
@@ -14,7 +14,7 @@ Use separate control and media planes:
|
||||
- A small HTTPS/WSS service owns identity, rooms, invitations, presence, and WebRTC signaling.
|
||||
- A viewer can join through an authenticated, expiring Web link on desktop or mobile without installing the sharing client.
|
||||
- ICE attempts a direct UDP path for every broadcaster-viewer pair, using STUN to discover candidates.
|
||||
- Only pairs that cannot connect directly use authenticated TURN, preferring UDP and falling back to TCP and TLS on port 443. TURN is required production infrastructure, not an optional enhancement.
|
||||
- Only pairs that cannot connect directly use authenticated TURN. Production requires STUN plus TURN over UDP and TCP; TURN/TLS is an optional restrictive-network enhancement, using its standard TCP port 5349 by default. Port 443 is optional and requires a dedicated public IP or a validated layer-4/SNI route when HTTPS already owns that address and port.
|
||||
- Candidate selection is independent per pair. A room may simultaneously contain direct and relayed viewers without moving working peers onto the server.
|
||||
- The broadcaster creates one peer connection per viewer. The PoC hard limit is three viewers; a later release may reconsider it only after publisher upload and encoder measurements.
|
||||
- No SFU is planned for the normal product envelope. It is only a future reconsideration point if the small-room scope changes or measured relay/upload/encoder pressure makes the chosen envelope unworkable.
|
||||
@@ -34,7 +34,8 @@ Negative:
|
||||
- Browser APIs do not guarantee that several peer connections share a single hardware encode.
|
||||
- Direct peers learn one another's network addresses; this is acceptable only for the initial trusted-friends threat model.
|
||||
- A room-level P2P-to-SFU migration adds state, keyframe, and reconnection complexity and is not part of the first prototype.
|
||||
- TURN/TLS on port 443 materially improves compatibility but cannot guarantee success through every authenticated proxy or policy-controlled network; failures still need actionable diagnostics.
|
||||
- TURN over TCP can suffer head-of-line blocking, and its `turn:` client-to-server transport is not TLS-wrapped. The WebRTC media remains protected by DTLS-SRTP independently of that TURN transport.
|
||||
- Optional TURN/TLS on port 5349 or 443 can improve compatibility but cannot guarantee success through every authenticated proxy or policy-controlled network. A normal Cloudflare HTTP proxy does not proxy TURN; using Cloudflare for TURN requires a compatible layer-4 product such as Spectrum.
|
||||
|
||||
## Rejected For The MVP
|
||||
|
||||
|
||||
+76
-32
@@ -14,9 +14,10 @@ unverified until the direct, relay, and mixed-network checks at the end pass.
|
||||
|
||||
## Topology and prerequisites
|
||||
|
||||
Use two DNS names, for example `share.example.com` and `turn.example.com`.
|
||||
The simplest deployment gives the TURN name a separate public IP so coturn can
|
||||
bind TCP 443; otherwise use an explicitly validated layer-4/SNI gateway:
|
||||
Use two DNS names, for example `share.example.com` and `turn.example.com`. They
|
||||
may resolve to the same public IP in the minimal deployment: the Web ingress
|
||||
owns TCP 443 while coturn owns UDP and TCP 3478. TURN/TLS is optional and uses
|
||||
its standard TCP port 5349 by default, which can also share that public IP.
|
||||
|
||||
```text
|
||||
browser -- HTTPS/WSS --> Caddy or nginx :443 --> Node.js :8787
|
||||
@@ -25,10 +26,11 @@ browser -- TURN only for failed ICE pairs --> coturn
|
||||
```
|
||||
|
||||
The minimum runtime is Node.js 24 LTS and coturn 4.17.2 or a newer patched
|
||||
release. Provision a valid TLS certificate for each public name, enable operating
|
||||
system time synchronization, and keep the Node application port reachable only
|
||||
from its reverse proxy. A single Node process is intentional: rooms are held in
|
||||
memory, so restarting it ends signaling and invalidates room state.
|
||||
release. Provision a valid TLS certificate for the Web name and, only when
|
||||
enabling TURN/TLS, for the TURN name. Enable operating system time
|
||||
synchronization and keep the Node application port reachable only from its
|
||||
reverse proxy. A single Node process is intentional: rooms are held in memory,
|
||||
so restarting it ends signaling and invalidates room state.
|
||||
|
||||
Build and validate the exact revision before starting it:
|
||||
|
||||
@@ -72,7 +74,7 @@ ROOM_TTL_SECONDS=14400
|
||||
MAX_ROOMS=1000
|
||||
|
||||
STUN_URLS=stun:turn.example.com:3478
|
||||
TURN_URLS=turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?transport=tcp,turns:turn.example.com:443?transport=tcp
|
||||
TURN_URLS=turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?transport=tcp
|
||||
TURN_SHARED_SECRET=<SAME_VALUE_AS_COTURN_STATIC_AUTH_SECRET>
|
||||
TURN_CREDENTIAL_TTL_SECONDS=3600
|
||||
```
|
||||
@@ -85,15 +87,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.
|
||||
|
||||
Production startup validates the configured TURN transport mix before the
|
||||
server listens. The baseline must contain separate `TURN_URLS` entries for
|
||||
`turn:` with explicit `transport=udp`, `turn:` with explicit `transport=tcp`,
|
||||
and `turns:` with explicit port 443 and `transport=tcp`. This startup check only
|
||||
validates configuration shape; it does not prove that DNS, certificates,
|
||||
firewall rules, NAT mappings, or the coturn listeners work from an external
|
||||
network. Every STUN and TURN entry is also syntax-checked; use the exact
|
||||
lowercase TURN query forms shown above because one malformed ICE URL can make a
|
||||
browser reject the whole peer-connection configuration.
|
||||
Production startup validates the configured ICE transport mix before the
|
||||
server listens. The baseline must contain STUN and separate `TURN_URLS` entries
|
||||
for `turn:` with explicit `transport=udp` and `turn:` with explicit
|
||||
`transport=tcp`. TURN/TLS is optional; when enabled, append
|
||||
`turns:turn.example.com:5349?transport=tcp`. This startup check only validates
|
||||
configuration shape; it does not prove that DNS, certificates, firewall rules,
|
||||
NAT mappings, or the coturn listeners work from an external network. Every STUN
|
||||
and TURN entry is also syntax-checked; use the exact lowercase TURN query forms
|
||||
shown above because one malformed ICE URL can make a browser reject the whole
|
||||
peer-connection configuration.
|
||||
|
||||
## HTTPS and WSS ingress
|
||||
|
||||
@@ -121,6 +124,18 @@ 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.
|
||||
|
||||
When Certbot manages the Web certificate, install the tracked
|
||||
[`reload-nginx.sh`](../deploy/certbot/reload-nginx.sh) as an executable under
|
||||
`/etc/letsencrypt/renewal-hooks/deploy/`. Run `certbot renew --dry-run` after
|
||||
the first certificate is installed. This hook only reloads the Web ingress;
|
||||
if optional TURN/TLS later uses a renewed certificate, configure and verify a
|
||||
separate coturn reload or restart action.
|
||||
|
||||
Keep the proxy's access-log retention bounded and access controlled. Viewer
|
||||
tokens are URL fragments and therefore are not sent in HTTP requests, but logs
|
||||
still contain network metadata and must not be treated as public artifacts.
|
||||
@@ -135,9 +150,19 @@ end-to-end media check.
|
||||
## coturn
|
||||
|
||||
Copy [`deploy/coturn/turnserver.conf.example`](../deploy/coturn/turnserver.conf.example)
|
||||
to an untracked service-owned location. Replace the FQDN, certificate paths, and
|
||||
secret. The `static-auth-secret` value must exactly match the application's
|
||||
to an untracked service-owned location. Replace the FQDN and secret. The
|
||||
`static-auth-secret` value must exactly match the application's
|
||||
`TURN_SHARED_SECRET`; do not configure `no-auth` or permanent browser users.
|
||||
Certificate paths are needed only if the optional TURN/TLS block is enabled.
|
||||
|
||||
When the application and coturn configs share a parent directory, both service
|
||||
accounts must be able to traverse that directory. One minimal layout is a
|
||||
`root:root` directory with mode `0751`, an application environment file owned
|
||||
by `root:screener` with mode `0640`, and a coturn config owned by
|
||||
`root:turnserver` with mode `0640`. Verify readability as the actual service
|
||||
user before starting coturn. The tracked systemd unit deliberately fails its
|
||||
pre-start check when the coturn config is missing or unreadable; coturn itself
|
||||
may otherwise warn and continue with unsafe defaults.
|
||||
|
||||
On a host with a public address directly on its interface, coturn can select the
|
||||
single listen/relay address automatically. On a multi-homed host, set
|
||||
@@ -146,10 +171,21 @@ single listen/relay address automatically. On a multi-homed host, set
|
||||
without port translation. A normal consumer NAT that changes relay port numbers
|
||||
is not a supported TURN-host topology.
|
||||
|
||||
The example binds TURN/TLS to 443 because that is the required restrictive-network
|
||||
fallback. It requires a separate public IP for `turn.example.com` or an ingress
|
||||
that explicitly supports TURN at layer 4/SNI. A normal HTTPS reverse proxy and
|
||||
coturn cannot both bind the same IP and port.
|
||||
The minimum deployment exposes TURN/UDP and TURN/TCP on 3478. `turn:` over TCP
|
||||
does not add TLS between the client and coturn, but relayed WebRTC media remains
|
||||
protected end to end by DTLS-SRTP. Optional TURN/TLS should listen directly on
|
||||
standard TCP 5349 and use `turns:turn.example.com:5349?transport=tcp`.
|
||||
|
||||
TCP 443 is an optional enhancement for networks that block uncommon ports. On
|
||||
the same public IP, coturn cannot bind it while the Web ingress already owns it.
|
||||
Use a second public IP or an explicitly validated layer-4/SNI router, such as
|
||||
the optional
|
||||
[`tls-sni-router.conf.example`](../deploy/nginx/tls-sni-router.conf.example).
|
||||
That router requires moving the site's TLS virtual host to loopback TCP 8443;
|
||||
the default nginx site template intentionally does not do this. A normal
|
||||
Cloudflare HTTP proxy does not carry TURN even on port 443. Keep the TURN DNS
|
||||
record DNS-only, or use a compatible layer-4 service such as Cloudflare
|
||||
Spectrum; the Web hostname may remain HTTP-proxied independently.
|
||||
|
||||
Open only these public listeners:
|
||||
|
||||
@@ -157,8 +193,9 @@ Open only these public listeners:
|
||||
| --- | --- | --- |
|
||||
| `share.example.com:443` | TCP | HTTPS and WSS |
|
||||
| `turn.example.com:3478` | UDP and TCP | STUN, TURN/UDP, and TURN/TCP |
|
||||
| `turn.example.com:443` | TCP | TURN/TLS on a dedicated IP or L4/SNI route |
|
||||
| `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 |
|
||||
|
||||
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
|
||||
@@ -177,8 +214,9 @@ rotate by size and time with a finite retention, such as seven days initially,
|
||||
then reduce or extend it only for a documented operational need. coturn and the
|
||||
reverse proxy necessarily see client IP addresses; restrict log access, do not
|
||||
enable `verbose` or `log-binding` continuously, and never log credentials, SDP,
|
||||
ICE candidates, or the shared secret. Reload or restart coturn after certificate
|
||||
renewal and confirm time synchronization remains healthy.
|
||||
ICE candidates, or the shared secret. When TURN/TLS is enabled, reload or
|
||||
restart coturn after certificate renewal. Confirm time synchronization remains
|
||||
healthy in every deployment.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -186,20 +224,26 @@ Run these checks from real external networks before calling the deployment usabl
|
||||
|
||||
1. Open the official [Trickle ICE sample](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/)
|
||||
and test each configured TURN URL separately with a short-lived credential.
|
||||
Each UDP, TCP, and TLS test must produce a `relay` candidate; a successful
|
||||
STUN-only `srflx` candidate does not prove TURN works.
|
||||
Each required UDP and TCP test must produce a `relay` candidate; test TLS too
|
||||
when it is configured. A successful STUN-only `srflx` candidate does not
|
||||
prove TURN works.
|
||||
2. Create a normal Screener room on two different networks. Confirm media flows
|
||||
and the selected-pair stats report a non-relay path when direct ICE succeeds.
|
||||
3. Open the host at `https://share.example.com/?relay=1`, create a new room, and
|
||||
connect a viewer. This diagnostic flag sets `iceTransportPolicy=relay`; media
|
||||
must still flow and the stats panel must report `relay`.
|
||||
4. Block UDP on a test client while leaving the TURN/TLS endpoint reachable.
|
||||
Confirm a new session succeeds through the configured `turns:` URL.
|
||||
5. Repeat with three viewers and a mixed direct/restrictive-network cohort. Record
|
||||
4. Block UDP on a test client while leaving TURN/TCP reachable. Confirm a new
|
||||
session succeeds through `turn:turn.example.com:3478?transport=tcp`.
|
||||
5. If TURN/TLS is enabled, test its `turns:` URL separately on 5349 or the
|
||||
explicitly configured 443 route. Failure of an optional endpoint must be
|
||||
diagnosed, but an intentionally omitted endpoint is not a deployment failure.
|
||||
6. Repeat with three viewers and a mixed direct/restrictive-network cohort. Record
|
||||
the selected path, RTT, bitrate, frame rate, packet loss, and coturn egress for
|
||||
each viewer. Verify a direct viewer does not start consuming relay bandwidth
|
||||
merely because another viewer needs TURN.
|
||||
|
||||
References: [coturn 4.17.2 release](https://github.com/coturn/coturn/releases/tag/4.17.2),
|
||||
[turnserver documentation](https://github.com/coturn/coturn/blob/master/README.turnserver),
|
||||
and the [official example configuration](https://github.com/coturn/coturn/blob/master/examples/etc/turnserver.conf).
|
||||
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), and
|
||||
[Cloudflare network-port guidance](https://developers.cloudflare.com/fundamentals/reference/network-ports/).
|
||||
@@ -10,7 +10,7 @@ Last updated: 2026-08-18
|
||||
- Viewers should be able to open an invite link in a desktop or mobile browser without installing a dedicated client.
|
||||
- The system should resemble a Photon-style developer experience: a central room/rendezvous service establishes sessions while realtime traffic is carried by clients whenever possible.
|
||||
- Server bandwidth cost is a primary constraint. The media path must be P2P-first, with servers used for signaling, STUN, TURN fallback, and observability. SFU is outside the normal small-room scope.
|
||||
- Avoiding TeamSpeak-like partial reachability is a primary requirement: every broadcaster-viewer pair must independently have TURN/UDP, TURN/TCP, and TURN/TLS candidates available when direct ICE cannot connect.
|
||||
- Avoiding TeamSpeak-like partial reachability is a primary requirement: every broadcaster-viewer pair must independently have TURN/UDP and TURN/TCP candidates available when direct ICE cannot connect. TURN/TLS is an optional compatibility enhancement, not a production prerequisite.
|
||||
- A web experience is preferred for convenience, but using a desktop sender is acceptable when it materially improves game capture, audio capture, or hardware encoding.
|
||||
- All project documentation, memory, code, `AGENTS.md`, and `.codex/` configuration must live in this Git repository and remain tracked for cross-device development.
|
||||
- Normal maintenance uses short-lived branches, focused commits, remote backup, pull requests, checks, merge, and branch deletion.
|
||||
@@ -22,7 +22,7 @@ Last updated: 2026-08-18
|
||||
|
||||
- Start with a Windows Chrome/Edge sharing MVP and a responsive Web viewer using one `RTCPeerConnection` per viewer. Validate current Android Chrome and iOS Safari as viewing endpoints.
|
||||
- The current PoC hard limit is one broadcaster and three viewers. Do not add a fourth viewer until publisher upload and encoder measurements justify changing the tested envelope.
|
||||
- Use HTTPS/WSS signaling, trickle ICE, STUN, and authenticated coturn candidates. Prefer direct UDP, then relay UDP, while keeping TURN/TCP and TURN/TLS on port 443 available for restrictive networks.
|
||||
- Use HTTPS/WSS signaling, trickle ICE, STUN, and authenticated coturn candidates. Prefer direct UDP, then relay UDP, with TURN/TCP as the required non-UDP fallback. Optional TURN/TLS uses standard TCP 5349 by default; TCP 443 is reserved for deployments with a dedicated public IP or validated L4/SNI routing.
|
||||
- Allow mixed connectivity in one room: direct viewers stay direct while only incompatible network pairs consume TURN bandwidth.
|
||||
- Treat 1080p60 as a best-effort quality profile, not a universal guarantee. Provide 720p60 and 720p30 fallbacks.
|
||||
- Leave codec order at the browser default in the first PoC and record the negotiated codec, encoder implementation, and power efficiency. Prefer H.264 only after target-machine measurements show that it is the hardware-efficient path; retain VP8 compatibility.
|
||||
@@ -31,10 +31,11 @@ Last updated: 2026-08-18
|
||||
|
||||
## Current Implementation
|
||||
|
||||
- The repository contains a single npm package using Node.js 24, React, TypeScript, Vite, native WebRTC, `ws`, Zod, Vitest, and a separate coturn deployment. The server defaults to an all-interface listener for LAN development and containers; the bare-metal reverse-proxy baseline explicitly binds loopback. It provides a process-only `/healthz` endpoint and requires the full TURN/UDP, TURN/TCP, and TURN/TLS-on-TCP-443 URL mix before production startup; these checks do not establish public-network reachability.
|
||||
- The repository contains a single npm package using Node.js 24, React, TypeScript, Vite, native WebRTC, `ws`, Zod, Vitest, and a separate coturn deployment. The server defaults to an all-interface listener for LAN development and containers; the bare-metal reverse-proxy baseline explicitly binds loopback. It provides a process-only `/healthz` endpoint and requires STUN plus explicit TURN/UDP and TURN/TCP URLs before production startup. TURN/TLS is accepted but optional; configuration checks do not establish public-network reachability.
|
||||
- The Web PoC implements capture-before-room creation, live source replacement without renegotiating healthy peers, expiring role tokens, one independent peer connection per viewer, stable signaling reconnect identities that also work for LAN viewers on HTTP, explicit ICE restart or peer rebuild, host generation isolation, short-lived TURN credentials, three manual quality profiles, and local WebRTC statistics.
|
||||
- Automated checks and same-machine synthetic-media Chromium recovery tests pass, including lost offer/answer, one transient offer failure, signaling-only viewer reconnect, viewer-tab replacement with old-peer cleanup, cancelled-room cleanup, and video/audio source changes on an existing connection. Real screen/game audio, public TURN/NAT behavior, mobile lifecycle handling, and latency or quality targets remain unverified.
|
||||
- The next milestone is a staging coturn deployment and the manual matrix in `docs/status.md`, not additional product surface or a native sender.
|
||||
- The Web control plane is deployed at `https://share.bonfire.icu` behind nginx with Node.js 24.19.0, and `turn.bonfire.icu` runs authenticated coturn 4.17.2 on standard UDP/TCP 3478. HTTPS, WSS, room authentication, certificate renewal, public STUN, authenticated TURN/UDP and TURN/TCP allocations, and relay-only bidirectional data paths are verified. TURN/TLS is intentionally not enabled.
|
||||
- Automated checks and same-machine synthetic-media Chromium recovery tests pass, including lost offer/answer, one transient offer failure, signaling-only viewer reconnect, viewer-tab replacement with old-peer cleanup, cancelled-room cleanup, video/audio source changes, and public relay-only DataChannel tests. Real screen/game audio, full Screener media sessions across heterogeneous networks, mobile lifecycle handling, and latency or quality targets remain unverified.
|
||||
- No infrastructure blocker remains for the current staging deployment. The next milestone is the real-device and real-media matrix in `docs/status.md`, not additional product surface or a native sender.
|
||||
|
||||
## Provisional Quality Targets
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
- STUN 让客户端发现公网映射并产生 server-reflexive candidate。它不承载媒体,也不能保证穿过所有 NAT。
|
||||
- ICE 测试 host、server-reflexive、peer-reflexive 和 relay candidates,并选择可工作的候选对。Trickle ICE 可以减少建连等待。
|
||||
- TURN 在无法直连时转发完整媒体流,是 NAT 组件中真正产生高带宽成本的部分。
|
||||
- 候选优先级应表达:direct UDP -> TURN/UDP -> TURN/TCP -> TURN/TLS 443。ICE 可能交错或并发进行候选检查,应用不应手写严格串行计时器;TCP/TLS 能穿过更严格的网络,但丢包时可能产生队头阻塞。
|
||||
- 生产最小候选集合是 STUN、TURN/UDP 和 TURN/TCP;可选再加入 TURN/TLS。整体偏好应表达 direct UDP -> TURN/UDP -> TURN/TCP -> optional TURN/TLS,但 ICE 可能交错或并发进行候选检查,应用不应手写严格串行计时器。TCP 在丢包时可能产生队头阻塞。
|
||||
|
||||
不存在适用于所有用户的权威“P2P 直连率”。CGNAT、endpoint-dependent mapping、校园/企业防火墙、移动网络、IPv6 和地区运营商都会改变结果。首版必须通过 `getStats()` 统计自己的 `host/srflx/prflx/relay` 比例,而不是引用未经验证的行业百分比。
|
||||
|
||||
@@ -63,12 +63,14 @@ ICE 是按分享者与每一名观看者的网络组合独立选路,而不是
|
||||
解决方式不是强迫所有人走服务器,也不是继续增加 STUN 地址,而是同时提供有效的 TURN 凭据和多种传输:
|
||||
|
||||
1. 将 direct UDP 设为最高优先级,成功者保持零媒体服务器路径。
|
||||
2. 同时提供有效的 TURN/UDP、TURN/TCP 与 `turns` TLS 443 candidates,由 ICE 连通性检查和优先级选择路径。
|
||||
2. 同时提供有效的 TURN/UDP 与 TURN/TCP candidates,由 ICE 连通性检查和优先级选择路径;需要额外兼容性时再加入 `turns`,默认使用标准 TCP 5349。
|
||||
3. 通过统计确认最终选中的 candidate pair,而不是从应用计时顺序推断路径。
|
||||
4. 网络切换或候选对失效时执行 ICE restart,超时后重建该 peer connection。
|
||||
5. 在 UI 和诊断中区分“直连”“服务器中继”“正在恢复”和明确失败原因。
|
||||
|
||||
这种混合房间里,一名观看者走 TURN 不会迫使其他观看者也中继。TURN/TLS 443 能显著覆盖严格网络,但仍无法穿过所有认证代理、深度检测或管理员策略,因此产品还需要连接自检和可操作的失败信息,不能承诺 100% 网络可达。
|
||||
这种混合房间里,一名观看者走 TURN 不会迫使其他观看者也中继。`turn:` over TCP 不提供客户端至 TURN 的 TLS 封装,但被中继的 WebRTC 媒体仍由端点间 DTLS-SRTP 加密。可选 TURN/TLS 使用 RFC 7065 标准 TCP 5349;改用 443 能覆盖更多只放行常见端口的网络,但需要独立公网 IP 或经过验证的 L4/SNI 路由,且仍无法穿过所有认证代理、深度检测或管理员策略。
|
||||
|
||||
普通 Cloudflare 橙云代理只代理其 HTTP/HTTPS 管线,不能因为目标端口是 443 就转发 TURN。Cloudflare 官方将任意 TCP/UDP 应用放在 Spectrum 产品边界内;不使用这类 L4 服务时,TURN DNS 记录必须直连源站。网站仍可独立使用 Cloudflare HTTP 代理。
|
||||
|
||||
来源:
|
||||
|
||||
@@ -265,7 +267,7 @@ WebRTC 标准没有承诺固定毫秒延迟。工程目标必须带网络条件
|
||||
- `ws.close()` 默认会等待关闭握手,不能用“升级成功后发送 close frame”实现公网连接硬上限。总连接和未鉴权连接容量应在 `handleUpgrade()` 前检查,超限直接返回 HTTP 503 并销毁底层 socket。
|
||||
- Node 的 `IncomingMessage.url` 是未经应用路由解析的 request-target,而 WHATWG `URL` 构造器会拒绝部分输入。upgrade handler 必须捕获解析失败并关闭 socket,不能让未鉴权输入抛到 EventEmitter 顶层。
|
||||
- coturn 使用 `use-auth-secret` 支持的 TURN REST 短期凭据:`base64(HMAC-SHA1(secret, expiry + ":" + subject))`。coturn 不提供 HTTP 凭据接口,必须由已鉴权的应用服务生成。
|
||||
- RFC 7065/5928 将 `turn` + UDP、`turn` + TCP 和 `turns` + TCP 分别映射到客户端至 TURN 的 UDP、TCP 和 TLS 传输。WebRTC 会逐条验证 ICE URL,任一坏项都可能使整个 PeerConnection 配置失败,因此启动预检先按原始 URI 语法 fail closed,再要求三类目标 URL 显式声明小写 `transport`,且 TLS URL 显式使用 TCP 443;这只能验证配置形状,不能替代公网 relay allocation 测试。
|
||||
- RFC 7065/5928 将 `turn` + UDP、`turn` + TCP 和 `turns` + TCP 分别映射到客户端至 TURN 的 UDP、TCP 和 TLS 传输,并规定 TURN/TLS 默认端口为 5349。WebRTC 会逐条验证 ICE URL,任一坏项都可能使整个 PeerConnection 配置失败,因此启动预检先按原始 URI 语法 fail closed,再要求生产基线的 TURN/UDP 和 TURN/TCP URL 显式声明小写 `transport`;`turns` 是可选项,可显式使用 5349 或部署者实际提供的端口。这只能验证配置形状,不能替代公网 relay allocation 测试。
|
||||
- Node `server.listen()` 省略 host 时可能监听未指定 IPv6 地址或 `0.0.0.0`。单机反向代理基线应显式绑定 loopback,容器或可信 LAN 才通过配置选择宽绑定;进程级 HTTP 健康检查不应同步探测 TURN 或其他外部网络。
|
||||
- 截至本次复核,coturn 应使用 4.17.2 或更新补丁版本;4.17.2 修复了此前补丁版本的 UDP TTL 回归。
|
||||
- MiroTalk BRO 当前 P2P 模式仍是 broadcaster 对每位 viewer 建独立连接;Screego 也采用独立 session 和 HMAC TURN 凭据。这验证了拓扑,但两者的静态/长时凭据与轻量恢复策略不直接照搬。
|
||||
@@ -282,6 +284,8 @@ WebRTC 标准没有承诺固定毫秒延迟。工程目标必须带网络条件
|
||||
- [coturn 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)
|
||||
- [TURN TCP/TLS allocations RFC 5928](https://www.rfc-editor.org/rfc/rfc5928.html)
|
||||
- [Cloudflare proxied network ports](https://developers.cloudflare.com/fundamentals/reference/network-ports/)
|
||||
- [Cloudflare Spectrum configuration](https://developers.cloudflare.com/spectrum/reference/configuration-options/)
|
||||
- [Node.js 24 `server.listen`](https://nodejs.org/docs/latest-v24.x/api/net.html#serverlistenport-host-backlog-callback)
|
||||
|
||||
## 实施难度与预估
|
||||
@@ -301,7 +305,7 @@ WebRTC 标准没有承诺固定毫秒延迟。工程目标必须带网络条件
|
||||
|
||||
1. 首版坚持 P2P-first,但明确只服务小房间,硬上限为三名观看者;测量完成前不开放第四名。
|
||||
2. Web 先行,目标 Windows Chrome/Edge;把 1080p60 写成 best effort,同时提供降档。
|
||||
3. 从第一天部署 coturn,并验证 direct、TURN/UDP、TURN/TCP、TURN/TLS 443 以及移动网络切换;这是避免“部分好友永远看不了”的必要条件。
|
||||
3. 从第一天部署 coturn,并验证 direct、TURN/UDP、TURN/TCP 以及移动网络切换;这是避免“部分好友永远看不了”的必要条件。TURN/TLS 按部署需要选配,默认 5349,443 只作为受限网络增强。
|
||||
4. 观看端优先做成免安装响应式 Web;分享端先 Web 验证,再按捕获/音频实测升级 Electron。
|
||||
5. 产品代码优先直接使用浏览器 WebRTC API;借鉴 MiroTalk BRO 和 Screego,不在许可证未定前直接 fork GPL/AGPL 代码。
|
||||
6. 正常人数超过产品上限时引导使用现有直播服务,不为了假设规模提前搭 SFU;若范围以后改变,首选 Broadcast Box 或 LiveKit,不从零写 SFU。
|
||||
|
||||
+14
-7
@@ -4,7 +4,7 @@ Last updated: 2026-08-18
|
||||
|
||||
## Phase
|
||||
|
||||
The first measurable WebRTC Web proof of concept is implemented. Automated checks and same-machine Chromium tests with synthetic video and audio sources pass, including live source replacement. Real display capture, game audio, public-network TURN, mobile lifecycle behavior, and performance targets remain unverified.
|
||||
The first measurable WebRTC Web proof of concept is implemented at `https://share.bonfire.icu`, including an authenticated public TURN fallback. Automated checks and same-machine Chromium tests with synthetic video and audio sources pass, including live source replacement and relay-only data paths. Real display capture, game audio, mobile lifecycle behavior, and performance targets remain unverified.
|
||||
|
||||
## Established Baseline
|
||||
|
||||
@@ -12,7 +12,7 @@ The first measurable WebRTC Web proof of concept is implemented. Automated check
|
||||
- Product scope: private game sharing for one broadcaster and a hard PoC limit of three viewers.
|
||||
- Client behavior: screen capture precedes room creation; live source changes preserve healthy peer connections; expiring fragment-token invitations open in desktop or mobile browsers; every viewer has an independent `RTCPeerConnection`.
|
||||
- Control plane: in-memory rooms, role-bound signaling, stable reconnect identities, short-lived coturn credentials, Origin and payload checks, and explicit room expiry/closure.
|
||||
- Media topology: direct ICE is preferred per viewer, with authenticated TURN/UDP, TURN/TCP, and TURN/TLS candidates available in production. Mixed direct and relay paths are supported by design.
|
||||
- Media topology: direct ICE is preferred per viewer, with authenticated TURN/UDP and TURN/TCP required in production. TURN/TLS is an optional deployment capability, using standard TCP 5349 by default; mixed direct and relay paths are supported by design.
|
||||
- Diagnostics: per-peer path, candidate types, ICE protocol, local TURN protocol, RTT, bitrate, frame rate, dimensions, loss, jitter, codec, and quality-limitation fields are read locally from WebRTC stats.
|
||||
- Large public broadcasts are out of scope and should use OBS/Twitch-class services.
|
||||
|
||||
@@ -24,22 +24,29 @@ The first measurable WebRTC Web proof of concept is implemented. Automated check
|
||||
- Cancelling a delayed room A and immediately starting room B left B live, stopped only A's capture, and invalidated A's invitation after bounded cleanup.
|
||||
- Injecting a live picker-cancellation result preserved the old stream. Replacing synthetic video, adding synthetic audio, and removing it all kept the same connected host/viewer peer objects and did not create another offer; retired capture tracks stopped after each successful change.
|
||||
- Desktop and 390 px responsive layouts were smoke-checked without horizontal overflow.
|
||||
- Production configuration fails closed when HTTPS, TURN, or sufficiently strong secrets are missing.
|
||||
- Production startup rejects malformed ICE URLs and requires explicit TURN/UDP, TURN/TCP, and TURN/TLS-on-TCP-443 entries. This checks configuration only; external relay reachability remains unverified.
|
||||
- Production configuration fails closed when HTTPS, STUN, TURN, or sufficiently strong secrets are missing.
|
||||
- Production startup rejects malformed ICE URLs and requires STUN plus explicit TURN/UDP and TURN/TCP entries. Optional TURN/TLS entries may use standard TCP 5349 or, when separately routed, TCP 443. Configuration validation remains distinct from the runtime relay checks below.
|
||||
- The application defaults to `LISTEN_HOST=0.0.0.0` for LAN development and containers, while the bare-metal reverse-proxy deployment explicitly uses loopback. It exposes a process-only `GET /healthz` liveness response.
|
||||
- Viewer session identities use `crypto.getRandomValues()` rather than the secure-context-only `crypto.randomUUID()`, so a phone can initialize the viewer over trusted LAN HTTP while the host keeps screen capture on `localhost`.
|
||||
- coturn is not installed on this workstation; its configuration has only been checked against upstream documentation and local application tests.
|
||||
- The staging host runs Debian 12, nginx, Node.js 24.19.0, and source-built coturn 4.17.2. Screener binds loopback behind nginx; authenticated STUN and TURN/UDP+TCP bind `turn.bonfire.icu:3478`. TURN/TLS remains intentionally disabled.
|
||||
- Public HTTPS, certificate validation, HTTP redirect, `/healthz`, unauthorized room rejection, authenticated room creation, WSS host authentication, and room closure were verified. Certbot's renewal dry run also passed for both the existing blog and Screener certificates; the existing blog continued returning HTTP 200.
|
||||
- Public STUN Binding succeeds over both UDP and TCP 3478. Chromium 151 obtained authenticated relay candidates through TURN/UDP and TURN/TCP, then two relay-only peer connections completed a bidirectional DataChannel ping/pong over each transport. Simultaneous guest conntrack samples observed both 3478 control traffic and relay-range UDP traffic, independently confirming the public TURN path through the cloud and host firewalls.
|
||||
- The existing TeamSpeak files, services, timers, and ports were not changed.
|
||||
|
||||
## Next Milestone
|
||||
|
||||
Deploy one staging application and coturn instance, then execute and record the manual browser/network matrix:
|
||||
Execute and record the manual browser/network matrix:
|
||||
|
||||
- real screen/window capture and available game or system audio on Windows Chrome and Edge;
|
||||
- one broadcaster with three heterogeneous viewers for 30 minutes;
|
||||
- same-LAN direct, cross-network direct, forced relay, mixed direct/relay, and UDP-blocked TURN/TLS paths;
|
||||
- same-LAN direct, cross-network direct, a forced-relay Screener media session, mixed direct/relay, and a UDP-blocked TURN/TCP path; test TURN/TLS separately only when deployed;
|
||||
- Android Chrome and iOS Safari playback, orientation, backgrounding, and network handoff;
|
||||
- actual codec implementation, encode load, publisher upload, bitrate, frame rate, first picture, and glass-to-glass latency.
|
||||
|
||||
## Current Blocker
|
||||
|
||||
- No infrastructure blocker remains for the current staging deployment. The remaining work is the real-device and real-media validation matrix above.
|
||||
|
||||
## Blocking Decisions
|
||||
|
||||
- Whole-system versus selected-game audio for the first release.
|
||||
|
||||
+7
-5
@@ -13,7 +13,7 @@
|
||||
- 分享者主动选择屏幕、窗口或标签页,并请求可用音频;
|
||||
- 创建有时限的私密房间,生成分离的分享者和观看者权限;
|
||||
- 每名观看者拥有独立的 `RTCPeerConnection`;
|
||||
- 支持 STUN、TURN/UDP、TURN/TCP 和 TURN/TLS 配置;
|
||||
- 生产基线支持 STUN、TURN/UDP 和 TURN/TCP,并允许可选 TURN/TLS 配置;
|
||||
- 展示连接状态、direct/relay、RTT、实际码率、帧率和丢包等数据;
|
||||
- 第四名观看者被明确拒绝,不静默超载;
|
||||
- 在代码、测试和真实浏览器验证完成前,不宣称达到延迟或画质目标。
|
||||
@@ -152,11 +152,13 @@ username = expiry + ":" + opaque_room_and_peer_subject
|
||||
credential = base64(HMAC-SHA1(shared_secret, username))
|
||||
```
|
||||
|
||||
`TURN_SHARED_SECRET` 只存在于应用服务和 coturn。浏览器只收到短期 username/credential、STUN/TURN URLs 和 `expiresAt`。生产环境缺少 TURN secret 或 TURN URLs 时应用启动失败;开发环境允许 STUN-only,但界面必须标记 TURN 未配置。TURN/TLS 443 需要独立公网 IP,或由明确支持该协议的 L4/SNI 入口分流,不能与普通 HTTPS 进程直接争用同一 IP:443。
|
||||
`TURN_SHARED_SECRET` 只存在于应用服务和 coturn。浏览器只收到短期 username/credential、STUN/TURN URLs 和 `expiresAt`。生产环境缺少 STUN、TURN secret、显式 TURN/UDP URL 或显式 TURN/TCP URL 时应用启动失败;开发环境允许 STUN-only,但界面必须标记 TURN 未配置。
|
||||
|
||||
TURN/TLS 是可选能力,不是生产启动门槛。启用时默认使用 RFC 7065 的标准 TCP 5349;若为了受限网络改用 443,则 TURN 主机必须有独立公网 IP,或通过明确支持 TURN 的 L4/SNI 入口分流,不能与普通 HTTPS 进程直接争用同一 IP:443。普通 Cloudflare HTTP 代理只处理 HTTP/HTTPS,不转发 TURN;需要 Cloudflare 四层代理时必须单独评估 Spectrum 等支持该协议的服务。
|
||||
|
||||
凭据过期前,客户端通过已认证 WebSocket 请求新配置,并对现有连接调用 `setConfiguration()`。已有 TURN allocation 通常不会因凭据时间点到达而立即终止,但后续 ICE restart 必须使用新凭据。
|
||||
|
||||
候选由 ICE 按优先级和连通性检查选择,不承诺 JavaScript 严格串行执行 UDP、TCP、TLS 三次尝试。界面报告最终选中的 candidate type、ICE protocol,以及本地为 relay 时的 TURN protocol,而不是推测回退步骤。
|
||||
候选由 ICE 按优先级和连通性检查选择,不承诺 JavaScript 严格串行执行 UDP、TCP 或可选 TLS 尝试。界面报告最终选中的 candidate type、ICE protocol,以及本地为 relay 时的 TURN protocol,而不是推测回退步骤。`turn:` over TCP 没有客户端至 TURN 的 TLS 封装,但被中继的 WebRTC 媒体仍由 DTLS-SRTP 保护。
|
||||
|
||||
## 可观测性
|
||||
|
||||
@@ -197,7 +199,7 @@ credential = base64(HMAC-SHA1(shared_secret, username))
|
||||
自动测试:
|
||||
|
||||
- RoomStore 的创建、token 摘要验证、过期、关闭、三人上限和第四人拒绝;
|
||||
- TURN HMAC 固定向量、TTL、未配置生产环境启动失败;
|
||||
- TURN HMAC 固定向量、TTL、生产环境缺少 STUN 或 TURN/UDP+TCP 覆盖时启动失败,并接受可选 TURN/TLS;
|
||||
- 协议非法 JSON、未知消息、错误方向、跨房间目标和 payload 限制;
|
||||
- WebSocket 集成:一名 host、三名 viewer、定向 offer/answer/candidate、连接上限、grace 内恢复、显式 restart/rebuild、离开和关闭房间;
|
||||
- 客户端策略与 stats 解析:会话替换不重连,本地 TURN protocol 与 ICE protocol 分离,远端 TURN transport 保持未知;
|
||||
@@ -207,7 +209,7 @@ credential = base64(HMAC-SHA1(shared_secret, username))
|
||||
|
||||
- 同机双标签页与同 LAN direct;
|
||||
- 真实跨网络、强制 relay、同房间 direct + relay;
|
||||
- 封 UDP 后验证 TURN/TLS 443;
|
||||
- 封 UDP 后验证必需的 TURN/TCP;若部署启用 TURN/TLS,再分别验证默认 5349 或可选 443;
|
||||
- Windows Chrome/Edge 的音轨有/无;
|
||||
- Android Chrome 与 iOS Safari 的播放手势、旋转、前后台和网络切换;
|
||||
- 一名分享者、三名观看者连续 30 分钟,记录实际上行、编码负载和首帧时间。
|
||||
|
||||
+6
-4
@@ -18,7 +18,7 @@
|
||||
- [ ] 游戏声音:Windows Chrome/Edge 目标环境可请求系统音频;没有返回音轨时必须明确提示,而不是静默无声。
|
||||
- [ ] 一对多观看:一名分享者可同时向至少三名观看者发送画面,每名观看者拥有独立的 WebRTC 连接与状态。
|
||||
- [ ] P2P 优先:ICE 必须先尝试直接 UDP 路径,服务器不得默认中转可直连的媒体。
|
||||
- [ ] NAT 兜底:每名观看者独立进行 ICE;必须提供 TURN/UDP、TURN/TCP 与 TURN/TLS 443 candidates,通过优先级偏好直连和 UDP,同时允许严格网络选择 TCP/TLS,不能因为其网络与其他观看者不同而静默失败。
|
||||
- [ ] NAT 兜底:每名观看者独立进行 ICE;生产环境必须提供 STUN、TURN/UDP 与 TURN/TCP candidates,通过优先级偏好直连和 UDP,同时允许无法使用 UDP 的网络选择 TCP,不能因为其网络与其他观看者不同而静默失败。
|
||||
- [ ] 质量档位:至少提供 1080p60、720p60、720p30 三档目标,并根据拥塞主动降档。
|
||||
- [ ] 断线恢复:临时网络变化时尝试 ICE restart 和重连,并向用户显示当前连接状态。
|
||||
- [ ] 可观测性:采集候选类型、RTT、丢包、抖动、实际码率、帧率、编码耗时和质量受限原因。
|
||||
@@ -28,7 +28,8 @@
|
||||
|
||||
- [ ] 分享端桌面封装:用 Electron 固定 Chromium 版本、改善选源/热键/系统音频体验,同时保留 Web 观看端。
|
||||
- [ ] Windows 应用音频:仅捕获所选游戏及其子进程的音频,避免把语音聊天或通知声一并分享。
|
||||
- [ ] 连接自检:在开始分享前验证信令、STUN、TURN/UDP 和 TURN/TLS 可达性,并给出不暴露凭据的诊断结果。
|
||||
- [ ] 连接自检:在开始分享前验证信令、STUN、TURN/UDP、TURN/TCP 及部署实际配置的可选 TURN/TLS 可达性,并给出不暴露凭据的诊断结果。
|
||||
- [ ] 受限网络增强:部署者可选配 TURN/TLS;默认使用标准 TCP 5349,只有独立公网 IP 或经过验证的 L4/SNI 分流可用时才占用 TCP 443。
|
||||
- [ ] 运行时编解码选择:根据端点能力和统计数据选择实际硬件编码的 H.264、VP8、VP9 或 AV1,并始终保留兼容回退。
|
||||
- [ ] 诊断页面:用户可导出不含敏感地址和凭据的连接诊断信息。
|
||||
|
||||
@@ -43,7 +44,7 @@
|
||||
- 启播:观看者请求观看后,除用户授权耗时外,3 秒内出现首帧。
|
||||
- 流畅性:选择 60 fps 档位时,不能在没有降档或提示的情况下连续 5 秒低于 50 个解码帧每秒。
|
||||
- 成本:直连媒体不经过服务器;STUN 与信令仅承担小流量;TURN 的实际流量必须可计量、限额和告警。
|
||||
- 安全:全程 HTTPS/WSS,媒体使用 WebRTC DTLS-SRTP;TURN 使用短期凭据、配额和速率限制;仓库不得包含真实密钥。
|
||||
- 安全:应用控制面全程 HTTPS/WSS,媒体端到端链路使用 WebRTC DTLS-SRTP;普通 `turn:` over TCP 不额外套 TLS,但不移除媒体层加密。TURN 使用短期凭据、配额和速率限制;仓库不得包含真实密钥。
|
||||
- 隐私:P2P 会向房间内好友暴露网络地址,首版威胁模型仅覆盖可信熟人;若要求隐藏 IP,必须切换为服务器转发。
|
||||
- 可维护性:代码、需求、调研、项目记忆、`AGENTS.md` 和 `.codex/` 均在 Git 中跟踪;开发配置不得依赖单一操作系统的绝对路径。
|
||||
- 兼容性:分享端首个目标矩阵为 Windows 10/11 的当前 Chrome/Edge;观看端还必须覆盖当前 Android Chrome 和 iOS Safari,并实测自动播放限制、横竖屏、锁屏恢复与移动网络切换。
|
||||
@@ -63,7 +64,8 @@
|
||||
|
||||
- Given 两端网络允许 UDP 打洞,When 观看者加入,Then `getStats()` 显示选中的候选对不是 `relay`,且服务器没有承载媒体流量。
|
||||
- Given 模拟对称 NAT 或封锁直连,When 观看者加入,Then 系统自动选择 TURN 并在界面标识“中继连接”,无需用户配置端口。
|
||||
- Given UDP 被封锁但 TLS 443 可达,When ICE 检查发现 direct 与 TURN/UDP candidates 不可用,Then 可用的 TURN/TLS candidate 能建立连接或返回可诊断的明确失败原因。
|
||||
- Given UDP 被封锁但 TURN/TCP 可达,When ICE 检查发现 direct 与 TURN/UDP candidates 不可用,Then TURN/TCP candidate 能建立连接或返回可诊断的明确失败原因。
|
||||
- Given 部署者启用了可选 TURN/TLS,When 单独测试其标准 5349 端口或经过验证的 443 入口,Then 对应 `turns:` candidate 能建立 relay;未启用该能力不阻塞生产验收。
|
||||
- Given 同一房间中一名观看者可以直连、另一名处于严格 NAT 或移动网络,When 两者同时观看,Then 前者保持 P2P,后者单独使用 TURN,双方都能看到同一分享且互不影响。
|
||||
|
||||
### 浏览器与移动端观看
|
||||
|
||||
@@ -188,14 +188,9 @@ function requireProductionTurnCoverage(turnUrls: readonly string[]): void {
|
||||
const hasTurnTcp = endpoints.some(
|
||||
({ scheme, transport }) => scheme === "turn" && transport === "tcp",
|
||||
);
|
||||
const hasTurnsTcp443 = endpoints.some(
|
||||
({ scheme, port, transport }) =>
|
||||
scheme === "turns" && port === 443 && transport === "tcp",
|
||||
);
|
||||
|
||||
if (!hasTurnUdp || !hasTurnTcp || !hasTurnsTcp443) {
|
||||
if (!hasTurnUdp || !hasTurnTcp) {
|
||||
throw new Error(
|
||||
"TURN_URLS must include explicit TURN/UDP, TURN/TCP, and TURN/TLS on TCP port 443 endpoints in production",
|
||||
"TURN_URLS must include explicit TURN/UDP and TURN/TCP endpoints in production",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-17
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { loadConfig } from "../src/server/config.ts";
|
||||
|
||||
const completeProductionTurnUrls =
|
||||
"turn:turn.test:3478?transport=udp,turn:turn.test:3478?transport=tcp,turns:turn.test:443?transport=tcp";
|
||||
const requiredProductionTurnUrls =
|
||||
"turn:turn.test:3478?transport=udp,turn:turn.test:3478?transport=tcp";
|
||||
|
||||
describe("server configuration", () => {
|
||||
it("allows development without TURN and defaults the origin", () => {
|
||||
@@ -51,14 +51,32 @@ describe("server configuration", () => {
|
||||
PUBLIC_BASE_URL: "https://share.test",
|
||||
ROOM_CREATION_TOKEN: "c".repeat(32),
|
||||
STUN_URLS: "stun:turn.test:3478,stuns:turn.test:5349",
|
||||
TURN_URLS: completeProductionTurnUrls,
|
||||
TURN_URLS: requiredProductionTurnUrls,
|
||||
TURN_SHARED_SECRET: "t".repeat(32),
|
||||
});
|
||||
|
||||
expect(config.turnUrls).toHaveLength(3);
|
||||
expect(config.turnUrls).toHaveLength(2);
|
||||
expect(config.turnSharedSecret).toBe("t".repeat(32));
|
||||
});
|
||||
|
||||
it.each([5349, 443])(
|
||||
"accepts optional TURN/TLS over TCP on production port %i",
|
||||
(tlsPort) => {
|
||||
const config = loadConfig({
|
||||
NODE_ENV: "production",
|
||||
PUBLIC_BASE_URL: "https://share.test",
|
||||
ROOM_CREATION_TOKEN: "c".repeat(32),
|
||||
STUN_URLS: "stun:turn.test:3478",
|
||||
TURN_URLS: `${requiredProductionTurnUrls},turns:turn.test:${tlsPort}?transport=tcp`,
|
||||
TURN_SHARED_SECRET: "t".repeat(32),
|
||||
});
|
||||
|
||||
expect(config.turnUrls.at(-1)).toBe(
|
||||
`turns:turn.test:${tlsPort}?transport=tcp`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not require transport coverage outside production", () => {
|
||||
const config = loadConfig({
|
||||
TURN_URLS: "turn:turn.test:3478",
|
||||
@@ -99,7 +117,7 @@ describe("server configuration", () => {
|
||||
PUBLIC_BASE_URL: "https://share.test",
|
||||
ROOM_CREATION_TOKEN: "c".repeat(32),
|
||||
STUN_URLS: "stun:turn.test:3478",
|
||||
TURN_URLS: `${completeProductionTurnUrls},${invalidUrl}`,
|
||||
TURN_URLS: `${requiredProductionTurnUrls},${invalidUrl}`,
|
||||
TURN_SHARED_SECRET: "t".repeat(32),
|
||||
}),
|
||||
).toThrow("TURN_URLS contains an invalid ICE URL");
|
||||
@@ -125,17 +143,7 @@ describe("server configuration", () => {
|
||||
{
|
||||
missing: "TURN/TCP",
|
||||
urls:
|
||||
"turn:turn.test:3478?transport=udp,turns:turn.test:443?transport=tcp",
|
||||
},
|
||||
{
|
||||
missing: "TURN/TLS on TCP port 443",
|
||||
urls:
|
||||
"turn:turn.test:3478?transport=udp,turn:turn.test:3478?transport=tcp,turns:turn.test:5349?transport=tcp",
|
||||
},
|
||||
{
|
||||
missing: "explicit TURN/TLS transport",
|
||||
urls:
|
||||
"turn:turn.test:3478?transport=udp,turn:turn.test:3478?transport=tcp,turns:turn.test:443",
|
||||
"turn:turn.test:3478?transport=udp,turn:turn.test:3478",
|
||||
},
|
||||
])("rejects production TURN URLs missing $missing", ({ urls }) => {
|
||||
expect(() =>
|
||||
@@ -148,7 +156,7 @@ describe("server configuration", () => {
|
||||
TURN_SHARED_SECRET: "t".repeat(32),
|
||||
}),
|
||||
).toThrow(
|
||||
"TURN_URLS must include explicit TURN/UDP, TURN/TCP, and TURN/TLS on TCP port 443 endpoints in production",
|
||||
"TURN_URLS must include explicit TURN/UDP and TURN/TCP endpoints in production",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user