From 4e8150272ba6831540d8a13e3109d95e23c7e9df Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Sep 2026 09:53:27 -0700 Subject: [PATCH 1/3] quest: watch refusal, auth outage clock, and relay auth client CA Follow-ups from an audit of recently merged PRs (#4230, #4244, #4291) and #4364's API shape decision. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 3 ++ quest/m1/auth-outage-clock.md | 62 ++++++++++++++++++++++++++++++++ quest/m1/relay-auth-client-ca.md | 33 +++++++++++++++++ quest/m1/watch-refusal.md | 37 +++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 quest/m1/auth-outage-clock.md create mode 100644 quest/m1/relay-auth-client-ca.md create mode 100644 quest/m1/watch-refusal.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 50e3d328e9..1f3ef4d4f2 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -36,6 +36,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Raw stream codes](/quest/m1/raw-stream-codes.md) - raw QUIC stream resets and stops carry the application's code, not an HTTP/3-mapped one - [JS caught up](/quest/m1/js-announce-caught-up.md) - @moq/net's announce consumer says when the initial set has landed, like Rust - [Live in apps](/quest/m1/announce-live-apps.md) - the demo and `@moq/room` show "no broadcasts" from the `live` marker, which waits for the first session on page load +- [Watch refusal](/quest/m1/watch-refusal.md) - `` shows an origin refusal as an error instead of sitting offline - [Bindings caught up](/quest/m1/announce-live-bindings.md) - moq-ffi, libmoq, and every wrapper yield the same flat announce event, `Live` included - [Optional max age](/quest/m1/ietf-max-age.md) - max age is optional, set only by the publisher, and crosses moq-transport as MAX_CACHE_DURATION - [IETF announce count](/quest/m1/ietf-announce-count.md) - an opt-in moq-transport extension carries the replay count, so IETF announce consumers go live without a timer @@ -50,6 +51,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [moq play decode schedule](/quest/m1/play-decode-schedule.md) - `moq play` video keeps valid pictures across rewinds, reordering deeper than 100 ms, and decoder batches larger than three - [moqsrc stop](/quest/m1/moqsrc-stop.md) - moqsrc's stop blocks until its session ends, without deadlocking on a blocked pad push - [More tests under load](/quest/m1/test-flakes-2.md) - the second round of load-only failures, fixed at the cause +- [Auth outage clock](/quest/m1/auth-outage-clock.md) - the relay and moq-auth outage tests run on a paused clock again and assert both bounds of `expires` - [UnknownSession log flood](/quest/m1/unknown-session-logs.md) - streams reset before their WebTransport header stop being reported as UnknownSession at WARN - [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main - [Wire compatibility](/quest/m1/wire-compat.md) - a nightly run tests this checkout against the last published release for tokens, session wire, and catalog/container @@ -64,6 +66,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Video surface](/quest/m1/video-surface.md) - on dev, moq-ffi's `native` becomes `surface`, refused on platforms with no surface - [HLS discontinuity sequence](/quest/m1/hls-discontinuity-sequence.md) - on dev, `Segment::discontinuity` is the absolute sequence, so every cursor agrees - [Strict Redirect::resolve](/quest/m1/redirect-resolve.md) - on dev, `Redirect::resolve` can no longer quietly turn a refused redirect into a redial +- [Auth client CA](/quest/m1/relay-auth-client-ca.md) - on dev, `auth::Config::validate` and `init` take the client-CA flag, so no caller can skip the check - [RTMP TLS only](/quest/m1/rtmp-tls-only.md) - an RTMP listener configured for TLS can refuse plaintext instead of sniffing and serving it - [HLS linger](/quest/m1/hls-linger.md) - `moq_hls::Server` serves an ended broadcast for its playlist window plus grace, so the moq.pro edge drops its own pool - [Gateway live clock](/quest/m1/gateway-live-clock.md) - moq-srt, moq-rtmp, and HLS import publish on the broadcast clock, so encoder reconnects don't restart timestamps diff --git a/quest/m1/auth-outage-clock.md b/quest/m1/auth-outage-clock.md new file mode 100644 index 0000000000..d02f5bd134 --- /dev/null +++ b/quest/m1/auth-outage-clock.md @@ -0,0 +1,62 @@ +# [M] Auth outage tests on a paused clock + +## Goal + +The moq-relay and moq-auth outage tests run on tokio's paused clock again and +assert both bounds: a session (or grant) survives an auth outage until its +`expires`, and closes at `expires`, not later. No wall-clock sleeps, no +widened timeouts, and no dependence on how fast the OS delivers loopback. + +## Plan + +- The tests: `an_outage_keeps_the_session_until_expires` in + `rs/moq-relay/tests/auth_lifetime.rs` + ([#4244](https://github.com/moq-dev/moq/pull/4244)) and + `an_outage_keeps_the_grant_until_expires` in `rs/moq-auth/src/client.rs` + ([#4291](https://github.com/moq-dev/moq/pull/4291)). Both moved to the real + clock because a paused clock auto-advances while the runtime waits on a + real socket, so a virtual timer fired before macOS delivered loopback. That + swapped one violation of "unit tests mock time" for another, and #4244 + dropped the upper bound. Read both PR descriptions: they list what was + tried and why it failed (restoring a listener probe, pausing after setup, + waiting on the log). +- The race is real sockets under virtual time, so fix it by taking the + sockets out of these tests. Look at what the codebase already offers + before building anything: `rs/moq-net/tests/support/mock.rs` (an in-memory + session pair), `moq_relay::auth::Auth::embedded` with its `Admissions` + (decides leases in-process), and the lease driver in moq-auth, which could + be exercised against an in-process answer source instead of HTTP. If the + relay's `Connection` or moq-auth's `Client` cannot take such a transport, + prefer the small seam that lets them over a test-only shim. +- Decide where each assertion belongs. The outage semantics (a 503 keeps the + grant until `expires`) are moq-auth's; the relay test may only need to show + that a lease reaching `expires` closes the session as `Expired` and reports + `end`. Don't keep two tests proving the same thing. +- Measure against tokio's clock, not `SystemTime`: the grant still carries a + wall-clock `expires`, so pin how it maps onto the paused clock. +- Other paused-clock tests that touch real sockets share the hazard. #4291's + audit named `a_grant_within_clock_skew_stays_live` (moq-auth) and + `fixed_addresses_keep_tls_name_and_request_host` (moq-tokio websocket); + moq-auth's `clock_server` helper exists only to keep axum on the paused + clock. Move those onto the same seam if it is cheap. +- Prove it: loop the tests with every core loaded, on macOS if available, + and mutate the deadline both ways (close early, close late) to see each + bound fail. + +Public API: none unless a transport seam is needed; report it if so. Wire: +none. + +## Related + +- [More tests under load](/quest/m1/test-flakes-2.md) - the same rule + applied to other load-only failures +- [moq-shaper virtual time](/quest/m1/shaper-virtual-time.md) - the same + paused-clock-versus-real-socket fight in moq-shaper +- [#4280](https://github.com/moq-dev/moq/pull/4280) - moq-archive and + moq-hls tests poll with real-clock sleeps, on the archive track-timeline + line +- [#4281](https://github.com/moq-dev/moq/pull/4281) - OBS `WaitFor` polling, + on the C++ line +- [Nightly 2026-09-26](https://github.com/moq-dev/moq/actions/runs/36240326747/job/108399481809) - + the macOS relay tarball job failed this test with "publisher connect + timeout", before #4244 landed diff --git a/quest/m1/relay-auth-client-ca.md b/quest/m1/relay-auth-client-ca.md new file mode 100644 index 0000000000..5c5478b748 --- /dev/null +++ b/quest/m1/relay-auth-client-ca.md @@ -0,0 +1,33 @@ +# [S] moq-relay auth validate takes the client-CA flag + +## Goal + +On dev, no caller of `moq_relay::auth::Config` can start with `--auth-public` +rules alongside a listener TLS client CA by forgetting a check. +`Config::validate` takes the client-CA flag, `init` requires it too, and +`validate_client_ca` is gone. The `moq` CLI fails loud on an invalid auth +config instead of quietly refusing every session. + +## Plan + +- [#4364](https://github.com/moq-dev/moq/pull/4364) adds an additive + `Config::validate_client_ca(&self, client_ca: bool)` that `Relay::load` and + the CLI must each remember to call; the CLI missing the original check is + the bug it fixes. Fold it into `validate(&self, client_ca: bool)` so every + caller has to answer, and have `init` take the same answer so a caller that + skips `validate` still cannot start. The exact shape (a bool, or something + `init` already receives such as the listener TLS config) is open; prefer + whatever makes the wrong call unrepresentable. +- `spawn_server` in `rs/moq-cli/src/main.rs` maps any `auth.validate()` error + to `Auth::refuse`. Return the error instead, so a bad config stops startup. +- Update every caller, the tests #4364 added in both `moq-cli` and + `moq-relay`, and `doc/bin/relay/auth.md` or `doc/lib/rs` wherever they name + the methods. + +Public API: breaks `moq_relay::auth::Config::validate` and `init`, removes +`validate_client_ca`, so this targets `dev`. Wire: none. + +## Required + +- #4364 merged to `main` +- `dev` has merged `main` after #4364 lands diff --git a/quest/m1/watch-refusal.md b/quest/m1/watch-refusal.md new file mode 100644 index 0000000000..344bfafdfb --- /dev/null +++ b/quest/m1/watch-refusal.md @@ -0,0 +1,37 @@ +# [S] Watch shows a refusal + +## Goal + +When the origin refuses the broadcast `` asks for, the player shows +that refusal as an error instead of sitting offline as if nothing was +published yet. Refusal stays terminal, as +[#4230](https://github.com/moq-dev/moq/pull/4230) made it in `@moq/net` to +match Rust moq-net: the player never re-asks a handler that already said no. + +## Plan + +- The gap is Codex's P1 on #4230 + ([r4109909244](https://github.com/moq-dev/moq/pull/4230#discussion_r4109909244)): + `js/watch/src/broadcast.ts` only watches `request.active`, so after a + `dynamic()` handler refuses, the request closes with an error that nobody + reads and `active` stays `undefined` forever. +- Observe `Requesting.closed` (and decide whether `unroutable` matters too) + and carry the error into the broadcast's state. Whether that is a new + `"error"` status, a separate error signal, or both is open; mirror how the + element already surfaces other terminal states, such as the unsupported + indicator. Keep the error's message so the UI can say why. +- What clears the error is part of the design: a change of `name`, origin, + or `enabled` naturally makes a fresh request, and that should be the only + way back. No retry loop. +- Cover both the announced and unannounced paths in `#runBroadcast`. +- Show it in the UI, and update `demo/web` if it consumes the status. Add a + test in `js/watch` where a `dynamic()` handler refuses and the broadcast + reports the error. +- Update `doc/` wherever the watch status values are documented. + +Public API: likely additive (a new status value or error signal on the watch +broadcast and element). Wire: none. + +## Related + +- [#4230](https://github.com/moq-dev/moq/pull/4230) - made JS refusals terminal From 3b0a768d240f8717f1a721a3abe24b381575851b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Sep 2026 11:37:59 -0700 Subject: [PATCH 2/3] quest: address review on watch refusal, auth outage clock, client CA Co-Authored-By: Claude Opus 5.5 --- quest/m1/auth-outage-clock.md | 5 +++-- quest/m1/relay-auth-client-ca.md | 7 ++++--- quest/m1/watch-refusal.md | 17 +++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/quest/m1/auth-outage-clock.md b/quest/m1/auth-outage-clock.md index d02f5bd134..43c923a8b3 100644 --- a/quest/m1/auth-outage-clock.md +++ b/quest/m1/auth-outage-clock.md @@ -34,8 +34,9 @@ widened timeouts, and no dependence on how fast the OS delivers loopback. `end`. Don't keep two tests proving the same thing. - Measure against tokio's clock, not `SystemTime`: the grant still carries a wall-clock `expires`, so pin how it maps onto the paused clock. -- Other paused-clock tests that touch real sockets share the hazard. #4291's - audit named `a_grant_within_clock_skew_stays_live` (moq-auth) and +- Other paused-clock tests touch real sockets and would share the hazard + once a timeout lands on their path. #4291's audit named + `a_grant_within_clock_skew_stays_live` (moq-auth) and `fixed_addresses_keep_tls_name_and_request_host` (moq-tokio websocket); moq-auth's `clock_server` helper exists only to keep axum on the paused clock. Move those onto the same seam if it is cheap. diff --git a/quest/m1/relay-auth-client-ca.md b/quest/m1/relay-auth-client-ca.md index 5c5478b748..49b56cb468 100644 --- a/quest/m1/relay-auth-client-ca.md +++ b/quest/m1/relay-auth-client-ca.md @@ -15,9 +15,10 @@ config instead of quietly refusing every session. the CLI must each remember to call; the CLI missing the original check is the bug it fixes. Fold it into `validate(&self, client_ca: bool)` so every caller has to answer, and have `init` take the same answer so a caller that - skips `validate` still cannot start. The exact shape (a bool, or something - `init` already receives such as the listener TLS config) is open; prefer - whatever makes the wrong call unrepresentable. + skips `validate` still cannot start. `init` today only receives the + outbound auth TLS, so the listener's client-CA answer is a new input; its + shape (a bool, or the listener TLS config) is open. Prefer whatever makes + the wrong call unrepresentable. - `spawn_server` in `rs/moq-cli/src/main.rs` maps any `auth.validate()` error to `Auth::refuse`. Return the error instead, so a bad config stops startup. - Update every caller, the tests #4364 added in both `moq-cli` and diff --git a/quest/m1/watch-refusal.md b/quest/m1/watch-refusal.md index 344bfafdfb..4412cf6113 100644 --- a/quest/m1/watch-refusal.md +++ b/quest/m1/watch-refusal.md @@ -15,14 +15,15 @@ match Rust moq-net: the player never re-asks a handler that already said no. `js/watch/src/broadcast.ts` only watches `request.active`, so after a `dynamic()` handler refuses, the request closes with an error that nobody reads and `active` stays `undefined` forever. -- Observe `Requesting.closed` (and decide whether `unroutable` matters too) - and carry the error into the broadcast's state. Whether that is a new - `"error"` status, a separate error signal, or both is open; mirror how the - element already surfaces other terminal states, such as the unsupported - indicator. Keep the error's message so the UI can say why. -- What clears the error is part of the design: a change of `name`, origin, - or `enabled` naturally makes a fresh request, and that should be the only - way back. No retry loop. +- Observe `Requesting.closed` and carry the error into the broadcast's + state. Whether that is a new `"error"` status, a separate error signal, or + both is open; mirror how the element already surfaces other terminal + states, such as the unsupported indicator. Keep the error's message so the + UI can say why. `unroutable` is also true for a path nothing serves yet, so + it cannot tell a refusal from offline. +- What clears the error is part of the design: a fresh request (a new + `name` or origin, or re-enabling) should be the only way back. No retry + loop. - Cover both the announced and unannounced paths in `#runBroadcast`. - Show it in the UI, and update `demo/web` if it consumes the status. Add a test in `js/watch` where a `dynamic()` handler refuses and the broadcast From f88701d76d86b686373ec95fd3708b2dfae3f79b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Sep 2026 11:50:56 -0700 Subject: [PATCH 3/3] quest: keep the LAN-only empty-auth fallback in relay-auth-client-ca Co-Authored-By: Claude Opus 5.5 --- quest/m1/relay-auth-client-ca.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quest/m1/relay-auth-client-ca.md b/quest/m1/relay-auth-client-ca.md index 49b56cb468..5393c341df 100644 --- a/quest/m1/relay-auth-client-ca.md +++ b/quest/m1/relay-auth-client-ca.md @@ -20,7 +20,10 @@ config instead of quietly refusing every session. shape (a bool, or the listener TLS config) is open. Prefer whatever makes the wrong call unrepresentable. - `spawn_server` in `rs/moq-cli/src/main.rs` maps any `auth.validate()` error - to `Auth::refuse`. Return the error instead, so a bad config stops startup. + to `Auth::refuse`. That fallback is only right for a LAN-only mesh with no + auth configured, which `MoqSide::validate` permits and whose peers admit + through the cluster. Make that case explicit and let any other error stop + startup. - Update every caller, the tests #4364 added in both `moq-cli` and `moq-relay`, and `doc/bin/relay/auth.md` or `doc/lib/rs` wherever they name the methods.