diff --git a/.env.example b/.env.example index 88ce17e..0d90799 100644 --- a/.env.example +++ b/.env.example @@ -40,14 +40,15 @@ LIVEKIT_API_SECRET= # Separate multiple URLs with commas. STUN_URLS= -# Optional independent STUN observations used only when a Host enables the -# default-off NAT traversal experiment. Maximum two comma-separated URLs. -NAT_PREDICTION_STUN_URLS= +# Optional bounded NAT traversal support. When enabled, Hosts see a per-share +# switch that defaults on, and Browser P2P derives self-hosted UDP 3479/3480 +# from the first STUN_URLS endpoint on UDP 3478. +NAT_PREDICTION_ENABLED=false # Minimal production example (use independent untracked secrets): # SITE_ACCESS_PASSWORD=replace-with-an-independent-access-key # STUN_URLS=stun:stun.example.com:3478 -# NAT_PREDICTION_STUN_URLS=stun:stun-observer.example.com:3478 +# NAT_PREDICTION_ENABLED=true # Automatic SFU fallback (requires PEER_ASSISTED_MEDIA=true and does not # replace STUN above): # LIVEKIT_URL=wss://share.example.com diff --git a/docs/README.md b/docs/README.md index b80889c..d7cc571 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,8 +45,8 @@ ordinary implementation and UI detail. owned media adaptation. - [ADR-0008](./adr/0008-window-scoped-audio-capture.md): Browser/native window audio boundary. -- [ADR-0009](./adr/0009-connection-local-nat-prediction-experiment.md): opt-in - connection-local NAT candidate experiment. +- [ADR-0009](./adr/0009-optional-nat-prediction.md): optional connection-local + NAT prediction. ## Evidence diff --git a/docs/adr/0009-connection-local-nat-prediction-experiment.md b/docs/adr/0009-connection-local-nat-prediction-experiment.md deleted file mode 100644 index b1b6583..0000000 --- a/docs/adr/0009-connection-local-nat-prediction-experiment.md +++ /dev/null @@ -1,83 +0,0 @@ -# ADR-0009: Connection-Local NAT Candidate Experiment - -- Status: accepted as an opt-in Browser experiment; default off -- Date: 2026-09-01 - -## Context - -The self-hosted deployment has two auxiliary STUN-only listeners for controlled -NAT measurements. A disposable namespace lab showed that a sequential, -endpoint-dependent mapping can sometimes be reached when the remote peer -receives a small set of adjacent server-reflexive candidates. The result is -network- and generation-specific; it is not a reliable NAT type or participant -capability. - -The existing route-policy and ICE candidate owners are sufficient. The current -strict protocol carries the room policy, bounded independent observation URLs, -and anonymous selected-path provenance, but adds no NAT label, route-controller -state, or media path. - -## Decision - -1. Host Advanced settings exposes `NAT traversal experiment`, disabled by - default and locked while sharing. It is part of the current share's room - policy and is broadcast to every authenticated Viewer; it is not persisted - beyond that share generation. -2. When enabled, every Browser P2P connection adds ports 3479 and 3480 for the - same STUN authority already supplied by the server. This covers Host direct, - Viewer upstream, and Viewer relay connections. SFU PeerConnections remain - unchanged. A deployment may also provide at most two independent `stun:` - observation URLs. They are added only inside the enabled experiment and are - never part of ordinary room ICE. -3. For one ICE gathering generation, the adapter observes only UDP `srflx` - candidates whose Browser-reported STUN URL belongs to the self-hosted - 3478/3479/3480 survey set. Independent observations remain ordinary ICE - candidates and cannot enter the arithmetic sequence. If three or more - distinct survey candidates for one media section and public address form an - arithmetic port sequence, the adapter appends at most eight bounded - candidates outward from both sequence endpoints: four above the high endpoint - and four below the low endpoint. Every ordinary candidate keeps normal - Trickle ICE timing; no ordinary candidate is delayed, rejected, or replaced. - A Browser that does not expose candidate source URLs keeps ordinary ICE but - does not mix independent observations into prediction. -4. Candidate observations remain in memory for that connection only. No NAT - label, raw address, port, score, hard candidate skip, periodic probe, or - route-controller branch is introduced. Sanitized diagnostics record only - whether a signaled candidate was ordinary, predicted, or end-of-candidates, - and whether the selected remote foundation was ordinary, predicted, or - unknown. - -## Consequences - -Positive: - -- operators can test the measured hole-punching mechanism without changing the - default media path; -- normal ICE remains immediately available, so an unavailable auxiliary STUN - listener or unsuccessful prediction cannot delay the existing direct or SFU - fallback; and -- the experiment is bounded to one connection generation and one small - candidate set; and -- selected-foundation attribution can validate actual field benefit without - uploading candidate endpoints. - -Negative: - -- the Browser may still reject synthetic candidates or the NAT may not have a - matching mapping; -- every independent STUN operator observes connection metadata and supplies no - availability promise unless its own service contract says otherwise; and -- two endpoint-dependent NATs remain sensitive to candidate scheduling and - intervening mappings, so room-wide coverage is not a success guarantee. - -The controlled lab result (14/15 sequential-to-restricted runs with a bounded -prediction set) is mechanism evidence, not a production success guarantee. -Target-network prevalence, candidate direction, and long-run resource impact -remain acceptance work. - -## Primary Sources - -- [RFC 8445: Interactive Connectivity Establishment](https://www.rfc-editor.org/rfc/rfc8445) -- [RFC 4787: UDP NAT behavioral requirements](https://www.rfc-editor.org/rfc/rfc4787) -- [RFC 5780: NAT Behavior Discovery Using STUN](https://www.rfc-editor.org/rfc/rfc5780) -- [W3C WebRTC candidate model](https://www.w3.org/TR/webrtc/) diff --git a/docs/adr/0009-optional-nat-prediction.md b/docs/adr/0009-optional-nat-prediction.md new file mode 100644 index 0000000..61d4915 --- /dev/null +++ b/docs/adr/0009-optional-nat-prediction.md @@ -0,0 +1,86 @@ +# ADR-0009: Optional Connection-Local NAT Prediction + +- Status: accepted as an optional deployment capability +- Date: 2026-09-02 + +## Context + +The self-hosted deployment has two auxiliary STUN-only listeners for controlled +NAT measurements. A disposable namespace lab showed that a sequential, +endpoint-dependent mapping can sometimes be reached when the remote peer +receives a small set of adjacent server-reflexive candidates. The result is +network- and generation-specific; it is not a reliable NAT type or participant +capability. + +The existing deployment configuration, route policy, and ICE candidate owners +are sufficient. The mechanism needs no NAT label, route-controller state, new +media path, or third-party STUN dependency. + +## Decision + +1. `NAT_PREDICTION_ENABLED` defaults to `false`. A disabled deployment does not + expose the Host control and the server rejects NAT prediction in the room + policy. An enabled deployment exposes `NAT traversal`; the pre-share switch + defaults on, remains Host-controllable, locks while sharing, and is not + persisted beyond that share generation. +2. An enabled deployment derives ports 3479 and 3480 from the first ordinary + STUN authority on UDP 3478. When the share switch is on, every Browser P2P + connection adds those same-host endpoints. This covers Host direct, Viewer + upstream, and Viewer relay connections. SFU PeerConnections remain + unchanged. No independent or third-party STUN endpoint is added. +3. For one ICE gathering generation, the adapter observes only UDP `srflx` + candidates whose Browser-reported STUN URL belongs to the self-hosted + 3478/3479/3480 survey set. If three or more distinct survey candidates for + one media section and public address form an arithmetic port sequence, the + adapter appends at most eight bounded candidates outward from both sequence + endpoints: four above the high endpoint and four below the low endpoint. + Every ordinary candidate keeps normal Trickle ICE timing; no ordinary + candidate is delayed, rejected, or replaced. A Browser that does not expose + candidate source URLs keeps ordinary ICE without prediction. Rejection of a + predicted remote candidate discards only that optional candidate; ordinary + candidate errors retain their normal connection error semantics. +4. Candidate observations remain in memory for that connection only. No NAT + label, raw address, port, score, hard candidate skip, periodic probe, or + route-controller branch is introduced. Sanitized diagnostics record only + whether a signaled candidate was ordinary, predicted, or end-of-candidates, + and whether the selected remote foundation was ordinary, predicted, or + unknown. + +## Consequences + +Positive: + +- deployments that operate the required STUN listeners can offer the bounded + mechanism while smaller deployments retain stock ICE; +- normal ICE remains immediately available, so an unavailable auxiliary STUN + listener or unsuccessful prediction cannot delay direct or SFU fallback; +- prediction is bounded to one connection generation and one small candidate + set; and +- selected-foundation attribution can validate actual field benefit without + uploading candidate endpoints. + +Negative: + +- the Browser may still reject synthetic candidates or the NAT may not have a + matching mapping; +- enabled P2P connections perform two extra self-hosted STUN transactions and + may add connectivity checks for up to eight adjacent UDP ports; and +- two endpoint-dependent NATs remain sensitive to candidate scheduling and + intervening mappings, so room-wide coverage is not a success guarantee. + +Predicted connectivity checks originate from participant Browsers and target +the other participant's bounded candidate window. The Screener server only +receives Binding requests on its fixed 3478/3479/3480 listeners; enabling this +capability does not make the cloud host scan external ports. + +The controlled lab result (14/15 sequential-to-restricted runs with a bounded +prediction set) is mechanism evidence, not a production success guarantee. +Target-network prevalence, candidate direction, and long-run resource impact +remain acceptance work. + +## Primary Sources + +- [RFC 8445: Interactive Connectivity Establishment](https://www.rfc-editor.org/rfc/rfc8445) +- [RFC 4787: UDP NAT behavioral requirements](https://www.rfc-editor.org/rfc/rfc4787) +- [RFC 5780: NAT Behavior Discovery Using STUN](https://www.rfc-editor.org/rfc/rfc5780) +- [W3C WebRTC candidate model](https://www.w3.org/TR/webrtc/) diff --git a/docs/operations/self-hosting.md b/docs/operations/self-hosting.md index 30c9a14..7811398 100644 --- a/docs/operations/self-hosting.md +++ b/docs/operations/self-hosting.md @@ -73,10 +73,10 @@ Coturn runs `stun-only`, `no-tcp`, and `no-tls`. It must answer STUN binding and reject TURN allocation. Avoid verbose STUN/ICE logging because infrastructure necessarily observes client IP addresses. -## Optional NAT Prediction Experiment +## Optional NAT Prediction -Rooms can use the default-off NAT prediction experiment only while both -auxiliary STUN-only listeners are reachable. Add the following to the +Set `NAT_PREDICTION_ENABLED=true` only while both auxiliary STUN-only listeners +are reachable. Add the following to the service-owned coturn configuration, using the local address already bound by the main listener: @@ -91,21 +91,14 @@ the complete ruleset with `nft -c -f /etc/nftables.conf`, then restart only `coturn.service`. Verify `ss -lunp` shows all three UDP listeners and that an ordinary STUN Binding succeeds on each. Do not add these URLs to production `STUN_URLS`; enabled Browser connections derive them from the ordinary STUN -authority. Keep both listeners and firewall rules for as long as the room -experiment is offered. Disable the experiment before removing both +authority. Keep both listeners and firewall rules for as long as the capability +is enabled. Set `NAT_PREDICTION_ENABLED=false` before removing both `aux-server` lines and both firewall rules, validate again, and restart coturn. These same-IP auxiliary ports measure destination-port mapping behavior. They are not a full RFC 5780 alternate-address deployment; that requires a second public IPv4. -An operator may configure at most two independent observation URLs through -`NAT_PREDICTION_STUN_URLS`. They are contacted only by Browser P2P connections -in an opted-in room, remain ordinary ICE candidates, and require no local -listener or firewall change. Verify them with a real Browser from the target -network before use. DNS resolution alone is not a STUN availability check, and -an unaffiliated public endpoint has no implied SLA. - ## Operational Verification - `/healthz` returns `{"status":"ok"}` locally and through the public origin. diff --git a/docs/product/routing-transport.md b/docs/product/routing-transport.md index bc5b888..8eb34c2 100644 --- a/docs/product/routing-transport.md +++ b/docs/product/routing-transport.md @@ -32,13 +32,14 @@ share generation: - native-edge topology convergence is enabled by default. The Host may disable it before sharing; when disabled, quality evidence stays diagnostic and availability routing is unchanged; -- the default-off NAT traversal experiment augments direct ICE candidates on - every Browser P2P edge in that share. It is not a participant capability, - route score, or SFU preference. A deployment may add a bounded independent - STUN observation list only inside that experiment. Candidate generation and - selected-pair diagnostics retain only `ordinary | predicted | unknown` - provenance. [ADR-0009](../adr/0009-connection-local-nat-prediction-experiment.md) - owns the bounded behavior and evidence boundary. +- NAT traversal is absent unless the deployment enables its self-hosted + auxiliary STUN capability. When available, its Host switch defaults on and + augments every Browser P2P edge in that share; the Host may disable it before + sharing. It is not a participant capability, route score, or SFU preference. + Candidate generation and selected-pair diagnostics retain only + `ordinary | predicted | unknown` provenance. + [ADR-0009](../adr/0009-optional-nat-prediction.md) owns the bounded behavior + and evidence boundary. These are route-policy gates, not new routing algorithms. Changing any policy requires stopping the current share and starting another generation. diff --git a/docs/project-memory.md b/docs/project-memory.md index fd22dd2..e749333 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -1,6 +1,6 @@ # Project Memory -Last updated: 2026-09-01 +Last updated: 2026-09-02 Screener is private, low-latency game screen sharing for one Host and up to 20 authenticated friends. The current product surface is Web Host, Web Viewer, and @@ -30,11 +30,11 @@ implementation and routine UI detail. lease. Production enables optional SQLite stable room authority; live participants, routes, and media remain process-only. - Media is automatic and distributed. Ordinary peers are STUN-only and prefer - direct/peer UDP. Host may opt the current share into a bounded, room-wide, - connection-local NAT candidate experiment; a deployment may add two - independent observation STUN URLs only inside that experiment, and anonymous - provenance distinguishes ordinary from predicted selected paths. The default - path remains stock ICE. The only application fallback is one bounded LiveKit SFU/UDP + direct/peer UDP. A deployment may enable same-host auxiliary STUN for bounded, + room-wide, connection-local NAT prediction; its Host switch defaults on when + available, and anonymous provenance distinguishes ordinary from predicted + selected paths. The configuration default remains stock ICE. The only + application fallback is one bounded LiveKit SFU/UDP publication; Screener configures no TURN or TCP media route. A pre-share peer-only policy can exclude that SFU suffix for one share generation. - Every endpoint shares one steady outbound-copy cap, default `2` and limited to diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 875fd4e..8e83b2d 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -20,7 +20,7 @@ service secret store or an untracked access-restricted environment file. | `PEER_ASSISTED_MEDIA` | Enables the all-room bounded Peer/SFU controller. | | `ENDPOINT_MEDIA_COPY_CAPACITY` | Shared endpoint steady-copy cap `1..3`, default `2`. | | `STUN_URLS` | Comma-separated `stun:` URLs; at least one is required in production. | -| `NAT_PREDICTION_STUN_URLS` | Optional comma-separated independent `stun:` observations, maximum `2`; used only by rooms that enable the default-off NAT experiment. | +| `NAT_PREDICTION_ENABLED` | Optional bounded NAT prediction capability, default `false`; requires an ordinary `STUN_URLS` endpoint on UDP 3478 plus reachable same-host UDP 3479/3480 listeners. | Automatic SFU fallback is enabled only when `PEER_ASSISTED_MEDIA=true` and all four values below are present: @@ -51,7 +51,7 @@ Peer IDs, SDP, ICE candidates, tokens or media credentials. | ---: | --- | --- | | TCP 80/443 | public | HTTP redirect and HTTPS/WSS reverse proxy | | UDP 3478 | public | STUN-only coturn | -| UDP 3479/3480 | public while the optional NAT experiment is offered | auxiliary STUN-only coturn listeners | +| UDP 3479/3480 | public when NAT prediction is enabled | auxiliary STUN-only coturn listeners | | UDP 7882 | public when SFU enabled | LiveKit WebRTC media | | TCP 8787 | private | Screener application | | TCP 7880 | private when SFU enabled | LiveKit signaling/control | @@ -60,20 +60,16 @@ TCP 3478, TCP/TLS 5349, TURN relay ranges, LiveKit media TCP, and other media ports remain closed. HTTPS/WSS transport is independent of the UDP-only media contract. -The default-off NAT experiment derives `stun::3479` and `:3480` -from the ordinary STUN authority for enabled Browser P2P connections. It does -not change `STUN_URLS` or add a media route, and it needs both cloud -security-group rules and the host's `/etc/nftables.conf` rule. Coturn must bind -the auxiliary listeners to the host's local address with `aux-server`; opening -a cloud port without a listener has no effect. No new DNS record is required. -Keep both listeners available while the experiment is offered and remove them -only after disabling it. Same-IP ports expose destination-port allocation -behavior; a full RFC 5780 alternate-address test requires a second public IPv4. -Optional `NAT_PREDICTION_STUN_URLS` add independent ordinary candidates without -changing `STUN_URLS`. Their operators receive client address, port, time, and -protocol metadata when the Host enables the experiment. They need no inbound -server port or DNS change, but should be omitted when that dependency is not -accepted. +When `NAT_PREDICTION_ENABLED=true`, the server derives +`stun::3479` and `:3480` from the first ordinary STUN authority +on UDP 3478. The Host sees a pre-share switch that defaults on and may disable +it. The capability adds no media route or third-party service. It needs both +cloud security-group rules and the host's `/etc/nftables.conf` rule; coturn must +bind both auxiliary listeners with `aux-server`. Opening a cloud port without a +listener has no effect, and no new DNS record is required. Disable the +capability before removing either listener or firewall rule. Same-IP ports +expose destination-port allocation behavior; a full RFC 5780 alternate-address +test requires a second public IPv4. ## Bounds diff --git a/docs/research/nat-traversal.md b/docs/research/nat-traversal.md index 8d6f202..9d049b5 100644 --- a/docs/research/nat-traversal.md +++ b/docs/research/nat-traversal.md @@ -1,6 +1,6 @@ # Browser NAT Traversal -Last reviewed: 2026-09-01 +Last reviewed: 2026-09-02 This document owns evidence for improving direct Browser ICE without adding a new relay or a custom transport. The current product contract remains standard @@ -56,20 +56,20 @@ supports an opt-in mechanism, not a default or a participant-wide classifier. ## Current Decision -The flagship STUN configuration remains unchanged. The self-hosted endpoint is -still the ordinary discovery service; ports 3479 and 3480 are optional -STUN-only listeners for an explicitly enabled room experiment. +The ordinary self-hosted endpoint remains the discovery service. NAT prediction +is absent by default; a deployment may enable same-host STUN-only listeners on +3479 and 3480. Enabled deployments expose a per-share Host switch that defaults +on and remains locked while sharing. -The accepted experiment is deliberately connection-local and additive. It +The accepted adapter is deliberately connection-local and additive. It derives the two auxiliary ports from the existing STUN authority, waits for a clear three-point arithmetic `srflx` shape in one ICE generation, and appends a small two-sided candidate window from the observed port-sequence endpoint. Ordinary candidates trickle immediately, so unavailable auxiliary listeners cannot hold back stock ICE. There is no NAT label, hard candidate skip, -route-controller input, or SFU preference. The Host switch is default off, -locks while sharing, and applies to Host, Viewer upstream, and Viewer relay P2P -connections. Its exact scope is recorded in -[ADR-0009](../adr/0009-connection-local-nat-prediction-experiment.md). +route-controller input, or SFU preference. The switch applies to Host, Viewer +upstream, and Viewer relay P2P connections. Its exact scope is recorded in +[ADR-0009](../adr/0009-optional-nat-prediction.md). ### Independent Observation And Attribution Preflight @@ -77,9 +77,9 @@ On 2026-09-01, Chrome 151 on the flagship Windows network gathered one `srflx` candidate independently from the self-hosted endpoint, Xiaomi, Bilibili, and Cloudflare within a five-second bound. `stun.qq.com:3478` produced none in the same ordered run. Candidate events and local candidate stats preserved the exact -STUN `url`, so independent destinations can remain ordinary candidates while -only self-hosted 3478/3479/3480 observations feed prediction. These are measured -reachability results, not third-party availability commitments. +STUN `url`, but the destinations supplied no selected-path benefit. They remain +research evidence only; the product has no independent or third-party STUN +configuration. A second real-Chrome loopback rewrote only one remote candidate foundation to `sp1`, established the data channel, and read `sp1` from the selected remote @@ -88,6 +88,21 @@ unknown` selection without retaining or uploading addresses, ports, foundations, or STUN URLs. Field acceptance still requires target-network outcomes; this preflight proves the attribution mechanism, not prediction prevalence. +The first flagship field runs emitted six predicted candidates in one 8545 +generation and selected none. Later 8545 and 5643 generations did not form the +required arithmetic survey shape, so no prediction was emitted. This is +negative prevalence evidence, not a regression: ordinary ICE and SFU fallback +remained independent. The flagship deployment enables the bounded capability +to continue attributable field observation without claiming a demonstrated +reachability gain. + +After an SFU commit, the current route controller gives each deferred Peer +parent one full direct-convergence attempt and then consumes that continuation. +Further generations come from a new participant session, publication, or share, +not a periodic retry loop. This bounds current field exposure. A fixed repeat +budget remains unjustified until attributable runs show that fresh generations +repeatedly recover a usable arithmetic shape. + The Browser-only alternatives do not yet have an accepted implementation: - WebRTC ICE already exchanges observed candidates and performs coordinated @@ -120,11 +135,11 @@ but cannot produce the proposed `eim | edm-sequential | edm-random` participant fact. They cannot safely remove a Peer candidate or alter route ordering and deadlines. -Port prediction remains an experiment rather than a default path. The -controlled namespace matrix supports the bounded mechanism, but field -prevalence, direction, and resource impact are not established. The experiment -therefore stays opt-in and additive until target-network evidence justifies -enabling it by default. +Port prediction remains additive rather than an assumed path. The controlled +namespace matrix supports the bounded mechanism, but field prevalence, +direction, and resource impact are not established. Configuration therefore +stays off by default even though the flagship deployment explicitly enables its +field rollout. ## Rejected Product Inference diff --git a/docs/status.md b/docs/status.md index d45011b..bd18359 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,6 +1,6 @@ # Current Status -Last updated: 2026-09-01 +Last updated: 2026-09-02 This is the current execution index. Git history owns completed timelines; [verification status](./verification-status.md) owns evidence boundaries. @@ -23,11 +23,12 @@ This is the current execution index. Git history owns completed timelines; - Site access uses a stateless 24-hour rolling idle cookie. An active site-authorized page renews it hourly through the existing status request; Viewer-grant admission remains independent and cannot create or renew it. -- Public media listeners remain STUN-only UDP 3478 and LiveKit UDP 7882. The - optional NAT experiment also uses auxiliary STUN-only UDP 3479 and 3480; - ordinary `STUN_URLS` and media routes remain unchanged. Web ingress - is TCP 80/443; Node 8787 and LiveKit control/signaling 7880 are private. TURN, - ICE/TCP, media TCP, TLS relay, port 5349, and relay ranges are disabled. +- Public media listeners remain STUN-only UDP 3478 and LiveKit UDP 7882. + Production enables optional NAT prediction with self-hosted STUN-only UDP + 3479 and 3480; ordinary `STUN_URLS` and media routes remain unchanged. Web + ingress is TCP 80/443; Node 8787 and LiveKit control/signaling 7880 are + private. TURN, ICE/TCP, media TCP, TLS relay, port 5349, and relay ranges are + disabled. - Browser video uses the content-independent H.264 sender gate with VP8 fallback, plus the locked pre-share `VP8 | Auto | H264` Host selector and resolved codec display. Video retains `contentHint = "motion"`; audio uses @@ -75,12 +76,13 @@ This is the current execution index. Git history owns completed timelines; sender windows retain the bounded Peer operation; failed exact candidates stay consumed until the sender or candidate opportunity changes. The pre-share gate defaults on and locks while sharing. -- Host Advanced settings include a default-off, per-share NAT traversal - experiment. Every Browser P2P role uses the same bounded, connection-local - candidate adapter. At most two deployment-supplied independent STUN - observations are contacted only inside opted-in rooms; candidate and selected - path logs retain anonymous `ordinary | predicted | unknown` provenance. - Ordinary candidates and SFU fallback remain unchanged. +- Source keeps NAT prediction deployment-gated and disabled by default. When a + deployment enables it, Host Advanced settings shows a per-share switch that + defaults on. Every Browser P2P role uses the same bounded, connection-local + adapter with only the deployment's self-hosted 3478/3479/3480 STUN endpoints; + candidate and selected-path logs retain anonymous + `ordinary | predicted | unknown` provenance. Ordinary candidates and SFU + fallback remain unchanged. - Every direct, Browser-relay, and Host SFU video sender owns one clone of its source track; the original remains presentation and source authority only. Replacement, rollback, unpublish, physical LiveKit sender recreation, and diff --git a/docs/todo.md b/docs/todo.md index 0ce3da1..080095a 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,6 @@ # Current TODO Ledger -Last reviewed: 2026-09-01 +Last reviewed: 2026-09-02 Only **Now** is executable. Observations, old branches, experiments, and parked topics are not implementation authority. @@ -81,14 +81,17 @@ until their stated boundary is met. `MediaStream` output contract. Reopen only for a registered custom receiver acting as an ordinary Viewer after a named Browser and physical receiver pass the [platform-output gate](./research/platform-output.md). -14. **NAT inference and predictive candidates.** The default-off room-wide, - connection-local experiment is accepted under - [ADR-0009](./adr/0009-connection-local-nat-prediction-experiment.md). Keep - multi-STUN observations and injected candidates additive and diagnostic; - do not make them a participant-wide NAT label, hard candidate skip, route - score, or SFU preference. Candidate generation and selected-path provenance - are now observable without endpoint data. Validate target-network success, - candidate direction, and bounded resource impact before enabling it by default. +14. **NAT inference and predictive candidates.** The deployment-gated, + connection-local capability is accepted under + [ADR-0009](./adr/0009-optional-nat-prediction.md). Production enables its + self-hosted field rollout; the general configuration default stays off. + Keep injected candidates additive and diagnostic; do not make them a + participant-wide NAT label, hard candidate skip, route score, or SFU + preference. Validate selected predicted paths and bounded resource impact + before recommending it as a default for other deployments. Direct + convergence currently consumes each deferred Peer parent once; add no + cross-generation retry budget until field evidence proves that fresh ICE + generations expose a repeatable prediction opportunity. 15. **Unresolved route-state ownership claims.** Reopen lower-revision reauthentication, active SFU failure during an unrelated prepare, and multi-child relay-evidence ownership only from an exact current-wire diff --git a/scripts/access-privacy-gate.ts b/scripts/access-privacy-gate.ts index b124bb6..bbbe43d 100644 --- a/scripts/access-privacy-gate.ts +++ b/scripts/access-privacy-gate.ts @@ -99,7 +99,7 @@ async function main(): Promise { publicBaseUrl: new URL(baseUrl), allowedOrigins: new Set([baseUrl]), siteAccessPassword: sitePassword, roomLeaseMs: 86_400_000, maxViewersPerRoom: 2, endpointMediaCopyCapacity: 2, - peerAssistedMedia: false, stunUrls: [], natPredictionStunUrls: [], + peerAssistedMedia: false, stunUrls: [], natPredictionEnabled: false, }; server = await createScreenerServer({ config, staticDirectory: resolve(root, "dist/client") }); server.httpServer.prependListener("request", (request, response) => { diff --git a/src/client/App.tsx b/src/client/App.tsx index 0786b6d..38f28d8 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -8,9 +8,11 @@ import { import { ApiError, authenticateSiteAccess, + getRuntimeCapabilities, getSiteAccess, type SiteAccessStatus, } from "./lib/api"; +import type { RuntimeCapabilities } from "../shared/protocol"; import { parseAppRoute, readViewerRoute } from "./lib/session"; import { AppHeader } from "./components/living/Header"; import { BrandLoader } from "./components/living/BrandMark"; @@ -157,13 +159,24 @@ function SiteAccessGate({ }) { const { t, vis } = useCopy(); const [access, setAccess] = useState({ kind: "checking" }); + const [capabilities, setCapabilities] = + useState(null); const [password, setPassword] = useState(""); const [submitting, setSubmitting] = useState(false); useEffect(() => { let active = true; - void getSiteAccess().then( - (status) => active && setAccess(stateFromStatus(status)), + void Promise.all([ + getSiteAccess(), + surface === "host" + ? getRuntimeCapabilities() + : Promise.resolve({ natPrediction: false }), + ]).then( + ([status, nextCapabilities]) => { + if (!active) return; + setCapabilities(nextCapabilities); + setAccess(stateFromStatus(status)); + }, (error: unknown) => active && setAccess({ kind: "unavailable", message: readableError(error, t) }), @@ -206,7 +219,14 @@ function SiteAccessGate({ async function retry(): Promise { setAccess({ kind: "checking" }); try { - setAccess(stateFromStatus(await getSiteAccess())); + const [status, nextCapabilities] = await Promise.all([ + getSiteAccess(), + surface === "host" + ? getRuntimeCapabilities() + : Promise.resolve({ natPrediction: false }), + ]); + setCapabilities(nextCapabilities); + setAccess(stateFromStatus(status)); } catch (error) { setAccess({ kind: "unavailable", message: readableError(error, t) }); } @@ -248,6 +268,7 @@ function SiteAccessGate({ } return ( setAccess({ kind: "required", error: t("gate.expired") }) } diff --git a/src/client/components/living/hints/set4.tsx b/src/client/components/living/hints/set4.tsx index 0fa55d5..685e0e8 100644 --- a/src/client/components/living/hints/set4.tsx +++ b/src/client/components/living/hints/set4.tsx @@ -493,8 +493,8 @@ ${rmBlock( ); /* hint-nat-prediction: [one direct path] -> [a small bounded fan of - alternate direct paths]. The dots stay lightweight so the scene describes - extra ICE chances without suggesting a media relay. */ + alternate direct paths]. The scene describes extra direct chances without + suggesting a media relay. */ const HintNatPrediction: HintScene = ({ theme }) => ( <>