diff --git a/quest/m0/README.md b/quest/m0/README.md index 1515c48658b..42faf7d455b 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -34,3 +34,4 @@ edge, which embeds moq-relay; it moved there on 2026-09-28. - [CI cost and latency](/quest/m0/ci/README.md) - cloud CI runs only in GitHub's merge queue, one tested entry at a time, expensive checks run nightly and gate `release`, caches move to R2, and runners are chosen last - [More tests under load](/quest/m0/test-flakes-2/README.md) - load-only and queue failures, one quest per flake, fixed at the cause, since each one drops a queued PR - [Agent build output can't fill the desktop's disk](/quest/m0/agent-disk-budget.md) - finished agent checkouts reclaim their mbx targets automatically, so parallel agent builds stop filling the root disk +- [GOAWAY redirects to loopback are refused in every spelling](/quest/m0/goaway-loopback.md) - Rust's `is_local` decodes `moqt://127.1`-style hosts as the dial does, and both clients' fixes reach `release` diff --git a/quest/m0/goaway-loopback.md b/quest/m0/goaway-loopback.md new file mode 100644 index 00000000000..66be522ce06 --- /dev/null +++ b/quest/m0/goaway-loopback.md @@ -0,0 +1,39 @@ +# [S] GOAWAY redirects to loopback are refused in every spelling, on main and release + +## Goal + +A GOAWAY from a non-local relay cannot steer a `redirect: "follow"` client, +and the tokens it carries, to a loopback, private, or link-local address, +whatever literal spelling the URI uses. A hostname that resolves to such +an address is out of scope: classifying DNS results would race the dial's +own lookup. Rust's `is_local` judges a non-special +scheme's host the way the native dial resolves it, as js/net's typed endpoint +does since [moq-dev/moq#8222](https://github.com/moq-dev/moq/pull/8222), and +both fixes reach the `release` line in its next patch. + +## Plan + +Planned 2026-11-08 (maintainer, approved follow-up of +[moq-dev/moq#8222](https://github.com/moq-dev/moq/pull/8222)). Milestone: +m0, since it is a hostile-input fix in shipped code plus a `release` +backport. + +- Rust on `main`: `is_local` in `rs/moq-net/src/reconnect/redirect.rs` + parses a `Host::Domain` with `str::parse::`, which refuses + shorthand and numeric IPv4 (`127.1`, `0x7f000001`, `2130706433`), so + `moqt://127.1` counts as non-local while the native dial may still reach + 127.0.0.1. Classify the host the dial will use: decode the IPv4 spellings + WHATWG's host parser accepts (as js/net reads a non-special host back + through https), and treat a trailing-dot `localhost.` as `localhost`, as + `js/net/src/connection/endpoint.ts` does. +- Test both sides from one fixture of endpoint spellings and expected + verdicts, replayed by `rs/moq-net` and `js/net`, so the two classifiers + cannot drift again. Put it beside the other shared corpora (see + [one fixture layout](/quest/m4/corpus-fixtures.md)). +- Backport to `release`: js/net's GOAWAY classification + (`js/net/src/connection/goaway.ts` `isLocal` on the opaque `moql:`/`moqt:` + host) and Rust's (`rs/moq-tokio/src/connection.rs` `is_local` there) both + have the hole. Port the smallest fix to each as PRs into `release`; the + maintainer cuts the patch. + +Public API: none. Wire: none. Behavior: such redirects are refused. diff --git a/quest/m1/audio-jitter-target/README.md b/quest/m1/audio-jitter-target/README.md index 882f7f136c7..96cfa558533 100644 --- a/quest/m1/audio-jitter-target/README.md +++ b/quest/m1/audio-jitter-target/README.md @@ -50,6 +50,7 @@ native playback against its budgets. - [Native stretch](/quest/m1/audio-jitter-target/native-stretch.md) - `moq play` copies NetEq's accelerate and preemptive expand, sharing the browser's algorithm and vectors, so its native rows reach zero steady-state underruns - [Buffered converge](/quest/m1/audio-jitter-target/buffered-converge.md) - a buffered `` time-stretches its audio back onto the shared clock after a stall or drift, so lip sync returns and no audio group is dropped - [Capped budgets](/quest/m1/audio-jitter-target/capped-budgets.md) - the AAC browser budgets are re-measured from nightlies after the TS importer groups each PES +- [Native rendition delay](/quest/m1/audio-jitter-target/native-rendition-delay.md) - `moq play` holds each subscribed rendition's catalog `delay` plus its target, as the browser does ## Closes diff --git a/quest/m1/audio-jitter-target/native-rendition-delay.md b/quest/m1/audio-jitter-target/native-rendition-delay.md new file mode 100644 index 00000000000..f7b4ae4cee3 --- /dev/null +++ b/quest/m1/audio-jitter-target/native-rendition-delay.md @@ -0,0 +1,26 @@ +# [S] moq play holds each rendition's catalog delay + +## Goal + +`moq play` sets its playout from the catalog as the browser does: +`max over subscribed renditions of (delay + target)` +(`doc/concept/audio-jitter.md`), so a rendition whose publisher states a +`delay`, such as a slow video encoder, plays in sync with audio instead of +behind it. + +## Plan + +Planned 2026-11-08 (maintainer, approved follow-up of +[moq-dev/moq#8272](https://github.com/moq-dev/moq/pull/8272), which gives +moq-mux's playout sync per-track targets and the largest-target delay). Milestone: +m1, an additive fix to shipped playback; start after #8272 merges. + +On `main`, nothing under `rs/moq-cli/src/play/` reads a rendition's +`delay`; the browser does (`js/watch/src/video/playhead.ts`, +`js/watch/src/audio/decoder.ts`). With #8272, each track's target is what it +holds through `Track::set_target`: add the rendition's catalog `delay` to it +(video holds `delay` alone, since it has no estimator), recompute when a +rendition is subscribed, dropped, or its catalog entry rises, and test that a +200 ms video `delay` delays audio to match. A fixed `--delay` stays fixed. + +Public API: none. Wire: none. diff --git a/quest/m2/README.md b/quest/m2/README.md index 15b2721d3ad..85e21e0d882 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -214,4 +214,5 @@ quests that require them. - [Cut readiness](/quest/m2/cut-gate/README.md) - the checks the cut needs that break nothing: main no slower than release, vendored fixes ported, and moq-c at parity - [Probe](/quest/m2/probe/README.md) - a session probes up to the bitrate it wants to send or receive, so a stopped simulcast rendition resumes and a viewer up-switches; browser padding is parked in m3 (held in m2 by open claims; moves to m3) - [Finish the milestone re-cut](/quest/m2/recut-held.md) - claim-held epics move to the milestone their kind and priority give them once their claims merge +- [moq-tokio's resolved config reports "off" as None](/quest/m2/tokio-resolved-options.md) - resolved timeouts and backoff accessors use `Option` while the text knobs keep `0` = off - [Nix clusters authenticate leaves with mTLS](/quest/m2/nix-cluster-mtls.md) - a clustered NixOS relay authenticates leaves with cluster-CA client certificates and `--mtls-link peer`, and the module stops minting an unbounded cluster JWT diff --git a/quest/m2/cluster-routing/README.md b/quest/m2/cluster-routing/README.md index ab7b3d446e9..b3a4bcd0bff 100644 --- a/quest/m2/cluster-routing/README.md +++ b/quest/m2/cluster-routing/README.md @@ -132,6 +132,7 @@ setup after the capabilities land. - [Outbound federation](/quest/m2/cluster-routing/federation.md) - an outbound-only local relay exchanges authorized broadcasts with the CDN and survives partition/healing - [Routes and announces](/quest/m2/cluster-routing/routes/README.md) - implement the proven route layer with Rust/JS parity and tiered-cluster acceptance +- [A relay refuses a lite-07 session with itself](/quest/m2/cluster-routing/lite07-self-dial.md) - the self-dial refusal lite-06 has, on lite-07, whose SETUP carries no Hop ID ## Related diff --git a/quest/m2/cluster-routing/lite07-self-dial.md b/quest/m2/cluster-routing/lite07-self-dial.md new file mode 100644 index 00000000000..45dfd114741 --- /dev/null +++ b/quest/m2/cluster-routing/lite07-self-dial.md @@ -0,0 +1,27 @@ +# [S] A relay refuses a lite-07 session with itself + +## Goal + +A relay whose cluster peer URL reaches itself under a spelling `PeerKey` +misses, such as an IP for its hostname, refuses that session on lite-07 as it +already does on lite-05, lite-06, and moq-transport. + +## Plan + +Planned 2026-11-08 (maintainer, approved follow-up of +[moq-dev/moq#8225](https://github.com/moq-dev/moq/pull/8225)). Milestone: +m2, since the likely fix changes lite-07-wip's SETUP, which the cut +publishes. + +`rs/moq-relay/src/connection.rs` refuses a session whose `peer_hop()` is +this relay's own Hop ID, but lite-07 SETUP carries no Hop ID +(`lite::Version::has_routes`), so the check never fires there. The route key +cannot stand in: relays may share one on purpose. + +- Decide, with the maintainer: a per-process node identity in lite-07 SETUP + (a wire change, lands while lite-07 is wip), or a local check that this + relay's own route came back over the session (no wire change; would move + this to m1). +- Test a relay dialing itself by IP on lite-07. + +Public API: none expected. Wire: possibly a lite-07-wip SETUP field. diff --git a/quest/m2/tokio-resolved-options.md b/quest/m2/tokio-resolved-options.md new file mode 100644 index 00000000000..b7fe9e8f198 --- /dev/null +++ b/quest/m2/tokio-resolved-options.md @@ -0,0 +1,33 @@ +# [S] moq-tokio's resolved config reports "off" as None + +## Goal + +moq-tokio's resolved values and accessors say "off" with `Option`, not zero, +finishing what +[moq-dev/moq#8265](https://github.com/moq-dev/moq/pull/8265) does for moq-net: +`connect::Resolved.timeout`, `connection::Backoff::{max, timeout}()`, the +GOAWAY handover, and `quic::Resolved.idle_timeout`. The text knobs (CLI, +TOML, env, FFI, C) keep `0` = off, since they need three states and absence +already means "use the default"; the conversion happens once, where they +resolve. + +## Plan + +Planned 2026-11-08 (maintainer, approved follow-up of +[moq-dev/moq#8265](https://github.com/moq-dev/moq/pull/8265), which makes +moq-net's reconnect policy take `Option` and keeps moq-tokio's config layer +on zero). Milestone: m2, since it breaks moq-tokio's public API; start after +#8265 merges. + +On `main`: `Backoff::max()` documents "zero leaves the delay uncapped" and +`timeout()` "zero meaning unlimited" (`rs/moq-tokio/src/connection.rs`); +`connect::Resolved.timeout` is a `Duration` whose `--connect-timeout` says +"zero disables this duration" (`rs/moq-tokio/src/connect.rs`); the handover +knob says the same; and `quic::Resolved.idle_timeout` is a `Duration` passed to the transport as +`Some(resolved.idle_timeout)` (`Some(0)` is QUIC's "no idle timeout"), with +zero turned into `None` only in `Resolved::keep_alive_interval` +(`rs/moq-tokio/src/quic.rs`); making the field `Option` replaces both. Audit the +other `Resolved` structs for the same pattern while there. + +Public API: moq-tokio's resolved fields and accessors become `Option`. +Migrate: compare against `None` instead of `Duration::ZERO`. Wire: none. diff --git a/quest/m4/README.md b/quest/m4/README.md index 1694d17e4d2..9c237a498b3 100644 --- a/quest/m4/README.md +++ b/quest/m4/README.md @@ -24,6 +24,10 @@ deleted. - [P2P](/quest/m4/p2p/README.md) - application-selected peer sessions use shared priorities, routing, and same-epoch handover - [Challenge proof for certificate credentials](/quest/m4/cert-challenge.md) - a relay can require a signed nonce, so a certificate credential replayed within its time window admits nothing - [Benchmark sources](/quest/m4/bench-sources.md) - every measuring nightly benchmark feeds the regression table, and none is a smoke run +- [The fan-out benchmark's session scenario builds again](/quest/m4/bench-fanout-session.md) - `just bench-fanout` measures sessions, not only the model +- [A browser round-trips media through moq-webrtc](/quest/m4/whip-whep-browser.md) - Chromium and Firefox publish over WHIP and play over WHEP nightly +- [Shared corpora live in each crate's fixtures/](/quest/m4/corpus-fixtures.md) - moq-auth's corpus moves beside moq-net's +- [An MV-HEVC stream with access unit delimiters stays one frame per access unit](/quest/m4/h265-layered-aud.md) - x265's `--aud` layered output imports, or is refused by name - [HLS and DASH browser playback](/quest/m4/hls-player-test.md) - hls.js and dash.js play, reload, switch, and survive reconfiguration nightly - [Cache fair share](/quest/m4/cache-fairness.md) - one tenant cannot crowd another out of the relay's shared group cache - [Peer grant bound](/quest/m4/peer-grant-bound.md) - a peer's AUTH_OK costs near-linear time in its bytes on every input shape, in Rust and in a browser diff --git a/quest/m4/bench-fanout-session.md b/quest/m4/bench-fanout-session.md new file mode 100644 index 00000000000..af99fa6b75b --- /dev/null +++ b/quest/m4/bench-fanout-session.md @@ -0,0 +1,24 @@ +# [XS] The fan-out benchmark's session scenario builds again + +## Goal + +`just bench-fanout` measures its `session` scenario against current `main` +and a base revision, as it does `model`, so a fan-out cost change in the +session layer shows up in its numbers. + +## Plan + +Planned 2026-11-08 (maintainer, approved follow-up of +[moq-dev/moq#8243](https://github.com/moq-dev/moq/pull/8243), whose +measurement could not run the scenario). Milestone: m4, a benchmark nobody +waits on; no Nightly job runs it. + +`bench/fanout/session/src/main.rs` imports `moq_net::Hop`, `pub(crate)` since +[moq-dev/moq#8023](https://github.com/moq-dev/moq/pull/8023), and +`origin::Config::new` now takes a `NonZeroU64`. Build the config through the +public constructor (`NonZeroU64::new(hop)`), not a new export. The harness +builds against each revision's own crates, so a base older than #8023 still +needs the old spelling: either pick the constructor by what the revision +exposes in `bench/run.sh`, or state the earliest base it measures. + +Public API: none. Wire: none. diff --git a/quest/m4/corpus-fixtures.md b/quest/m4/corpus-fixtures.md new file mode 100644 index 00000000000..fa2990abe1b --- /dev/null +++ b/quest/m4/corpus-fixtures.md @@ -0,0 +1,23 @@ +# [XS] Shared corpora live in each crate's fixtures/ + +## Goal + +Every JSON corpus a Rust crate and a JS package both replay, except the +audio jitter corpora under `doc/concept/`, lives in that crate's `fixtures/` +directory, so a contributor finds them in one place. +`rs/moq-auth/tests/corpus.json` moves to `rs/moq-auth/fixtures/`, matching +`rs/moq-net/fixtures/lite.json`, `rs/hang/fixtures/`, and +`rs/moq-msf/fixtures/`. + +## Plan + +Planned 2026-11-08 (maintainer, approved follow-up). Milestone: m4, tooling +nobody waits on. + +- Move the file and update its readers: `rs/moq-auth/src/corpus.rs` + (`include_str!` and the module doc) and `js/auth/src/corpus.test.ts`. +- `tests/` keeps only integration test targets (`released_token.rs`). +- The audio jitter corpora under `doc/concept/audio-jitter/` stay: the spec + document cites them as its own conformance data, not one crate's. + +Public API: none. Wire: none. diff --git a/quest/m4/h265-layered-aud.md b/quest/m4/h265-layered-aud.md new file mode 100644 index 00000000000..bf5cef302e8 --- /dev/null +++ b/quest/m4/h265-layered-aud.md @@ -0,0 +1,28 @@ +# [S] An MV-HEVC stream with access unit delimiters stays one frame per access unit + +## Goal + +moq-mux imports an x265 MV-HEVC stream encoded with `--aud` as one frame per +access unit, as it already does without `--aud`, or refuses it with an error +that names the cause, rather than splitting each view into its own frame. + +## Plan + +Planned 2026-11-08 (maintainer, approved follow-up of +[moq-dev/moq#8199](https://github.com/moq-dev/moq/pull/8199)). Milestone: +m4, a feature with no named consumer yet. + +x265 4.2 with `--num-views 2 --aud` writes a layer-0 AUD between the base +and enhancement views. `rs/moq-mux/src/codec/h265/split.rs` closes the open +access unit on any `NalType::AUD`, which H.265 F.7.4.2.4.4 requires for a +layer-0 AUD, so each view becomes its own frame. + +- First find out whether that stream conforms. If x265 is wrong, report it + (with the [upstream reports](/quest/m4/upstream-reports.md)) and make the + splitter refuse that specific violation with an error naming it, instead + of adding a heuristic. Annex F allows an access unit with no layer-0 + picture, so the refusal never rejects those by itself. +- If it conforms, frame by what the spec says starts an access unit and add + the x265 `--aud` fixture beside #8199's. + +Public API: none. Wire: none. diff --git a/quest/m4/qmux-02-subprotocols.md b/quest/m4/qmux-02-subprotocols.md index aeaade9ede4..5822aac740d 100644 --- a/quest/m4/qmux-02-subprotocols.md +++ b/quest/m4/qmux-02-subprotocols.md @@ -28,6 +28,14 @@ Decided 2026-11-03 (maintainer, /quest-plan of the follow-ups from need qmux-01 or later), and extend it to allow 02. - Move the version list into one place that the relay and moq-tokio share, as the comment beside `QMUX01_ONLY_ALPNS` asks. +- Added 2026-11-08 (maintainer, approved follow-up of + [moq-dev/moq#8224](https://github.com/moq-dev/moq/pull/8224)): the relay + now derives its subprotocols from `moq_net::Versions`, keeping every + moq-transport version off qmux-00 (`supported_subprotocols` in + `rs/moq-relay/src/websocket.rs`), but moq-tokio's dial side still keeps the + hand-written `QMUX01_ONLY_ALPNS = ["moqt-18", "moqt-20", "moqt-22"]` + (`rs/moq-tokio/src/websocket.rs`). Derive it from the version the same + way, so a new draft needs one edit. - Test: a JS and a Rust client each negotiate qmux-02 against the relay and a moq-tokio server. A client that offers only qmux-01 still connects. diff --git a/quest/m4/vaapi-render-tests.md b/quest/m4/vaapi-render-tests.md index 35d6fb69a41..f4aef576097 100644 --- a/quest/m4/vaapi-render-tests.md +++ b/quest/m4/vaapi-render-tests.md @@ -58,6 +58,13 @@ which covers only NVIDIA. 10-bit stream, which is refused, since moq-vaapi has no P010 output ([moq-dev/vaapi#11](https://github.com/moq-dev/vaapi/pull/11)). - Not chosen: dropping either gap. +- Added 2026-11-08 (maintainer, approved follow-up): moq-vaapi's own + tests return early too. `rs/moq-vaapi/src/encode.rs`, `decode.rs`, and + `vpp.rs` print "skipping: ..." and pass when there is no VA-API display, + no encoder or decoder for the codec, no VPP entrypoint, or no + ffmpeg/ffprobe, so a host without libva or a driver reports them green. Give + them the same `#[ignore = "requires ..."]` treatment and run them in + `just rs gpu` per render node. ## Related diff --git a/quest/m4/whip-whep-browser.md b/quest/m4/whip-whep-browser.md new file mode 100644 index 00000000000..2494b465152 --- /dev/null +++ b/quest/m4/whip-whep-browser.md @@ -0,0 +1,24 @@ +# [S] A browser round-trips media through moq-webrtc + +## Goal + +A `test/` recipe publishes from Chromium and Firefox over WHIP to a relay with +moq-webrtc and plays the broadcast back over WHEP, with fake media, and +asserts from `getStats()` that DTLS and SRTP connected and the viewer decoded +frames. It guards the vendored WebRTC stack's crypto and negotiation against +real browsers. + +## Plan + +Planned 2026-11-08 (maintainer, approved follow-up of +[moq-dev/moq#8229](https://github.com/moq-dev/moq/pull/8229), which moved the +stack onto rind assuming browser interop tests existed). None drives a +browser against moq-webrtc today: no recipe in `test/` or workflow mentions +WHIP or WHEP. Milestone: m4, a test nobody waits on. + +- Reuse the Playwright setup the interop and audio-quality harnesses use + (`test/interop/clients/js/`), with the browsers' fake capture devices. +- Run it where the other browser tests run; it is slow, so nightly, not the + merge gate. + +Public API: none. Wire: none.