From 99ad899bd9e0891a0221e8a6c3411ca8a5266a08 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 10:09:29 -0700 Subject: [PATCH 1/3] quest: origin narrowing joins auth, drop relay peer set, plan hop-list routing Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 - quest/m1/auth/README.md | 7 ++-- quest/m1/auth/narrowing.md | 58 ++++++++++++++++++++++++++ quest/m1/auth/relay-refresh.md | 4 +- quest/m1/origin-narrowing.md | 72 --------------------------------- quest/m1/relay-peer-set.md | 31 -------------- quest/m2/README.md | 1 + quest/m2/plan-routing-origin.md | 39 ++++++++++++++++++ 8 files changed, 104 insertions(+), 110 deletions(-) create mode 100644 quest/m1/auth/narrowing.md delete mode 100644 quest/m1/origin-narrowing.md delete mode 100644 quest/m1/relay-peer-set.md create mode 100644 quest/m2/plan-routing-origin.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 93cb6d34588..b4383142b3f 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -25,13 +25,11 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Rust track tail](/quest/m1/rust-track-tail.md) - a moq-net subscriber accepts groups that arrive after the subscription's end, and PublishDone carries the real stream count - [Session death error](/quest/m1/session-death-error.md) - a dying session ends its tracks with its own error in Rust and JS, never a clean end, `Dropped`, or `Cancel` - [Signal.race cleanup](/quest/m1/signal-race.md) - `Signal.race` releases its signal listeners when its result loses a race -- [Origin narrowing](/quest/m1/origin-narrowing.md) - a live origin grant narrows in place and ends the subscriptions it no longer covers, the deafen boundary #2714 asked for - [Auth expiry clock](/quest/m1/auth-expiry-clock.md) - moq-auth and the relay hold one fixed expiry deadline and honour the same skew allowance - [Binding surface](/quest/m1/binding-surface.md) - moq-ffi, libmoq, and every wrapper expose the decode delay, route source, and connection timing - [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, audio, and video namespaces built from the handle below - [Track demand](/quest/m1/track-demand.md) - Rust and JS watch a track's subscribers through `demand()` alone - [Broadcast close](/quest/m1/broadcast-close/README.md) - `close()` is the one way to end a broadcast in every language, a permanent retraction that leaves in-flight tracks alone -- [Relay peer set](/quest/m1/relay-peer-set.md) - a wire consumer tells a client hop from a peer hop, and every mesh credential can mark a peer - [CLI import clock](/quest/m1/cli-import-clock.md) - fMP4, TS, and FLV imports publish on the shared broadcast clock across restarts - [Native clock fixtures](/quest/m1/native-clock-fixtures.md) - CI drives native capture through clock edge cases and asserts the published timestamps - [CLI inspection](/quest/m1/cli-inspect/README.md) - `moq ls` lists what is live and `moq fetch` reads a group over MoQ, and a guide shows how to inspect a relay diff --git a/quest/m1/auth/README.md b/quest/m1/auth/README.md index a7de50b4974..9dd18fbcb29 100644 --- a/quest/m1/auth/README.md +++ b/quest/m1/auth/README.md @@ -39,7 +39,7 @@ Decisions settled while planning, recorded so review does not relitigate them: the union and cancels publications and subscriptions that lose authorization. Other authorized work continues on the same session. An empty union leaves the session connected with no access, so it can accept a fresh token. - [Origin narrowing](/quest/m1/origin-narrowing.md) owns the common resize + [Origin narrowing](/quest/m1/auth/narrowing.md) owns the common resize operation; relay token handling requires it rather than shipping a temporary close-on-shrink policy. - **A public grant contains publish patterns, subscribe patterns, and an @@ -96,6 +96,9 @@ existing lite-06 ALPN. each lite-06 cell's grant and that a publish outside it fails loud - [Unauthorized reset](/quest/m1/auth/unauthorized.md) - a subscription that loses access resets with a dedicated UNAUTHORIZED stream code +- [Origin narrowing](/quest/m1/auth/narrowing.md) - a live grant narrows in + place: subscriptions outside it reset, publishes outside it abort, and relay + revalidation stops closing the session - [Relay tokens](/quest/m1/auth/relay-refresh.md) - the relay verifies tokens sent in band, unions their grants, and cancels only work that loses access - [moq-transport](/quest/m1/auth/moq-transport.md) - the same exchange as a @@ -110,8 +113,6 @@ existing lite-06 ALPN. ## Related -- [Origin narrowing](/quest/m1/origin-narrowing.md) - resizes a live session - when the union shrinks, for revalidation and token expiry alike - [Pattern interest](/quest/m1/path-patterns.md) - moves AUTH's legacy wire prefixes to patterns along with ANNOUNCE_REQUEST - [Expiring media grants](/quest/m1/processor/grant-lease.md) - a worker's lease renewal is a new in-band token diff --git a/quest/m1/auth/narrowing.md b/quest/m1/auth/narrowing.md new file mode 100644 index 00000000000..0be54eceec5 --- /dev/null +++ b/quest/m1/auth/narrowing.md @@ -0,0 +1,58 @@ +# [M] A live origin grant narrows in place + +## Goal + +A `moq-net` session's grant can be replaced by a narrower pattern union +while the session is live: announce cursors retract the patterns that fell +outside, resolution refuses them, subscriptions under them reset with +`UNAUTHORIZED`, and broadcasts the session published under them abort. The +relay uses it on revalidation so a moderation decision (a deafened user's +audio path) takes effect on the session it targets, which a forked client +cannot bypass. This is the per-subscriber exclusion +[#2714](https://github.com/moq-dev/moq/issues/2714) asked for; the scope and +match half already landed in #3672 and #3746. + +A narrowing always succeeds. A changed root still closes the session. + +## Plan + +- Enforce per session, not per handout. The line's session gate for received + grants (`auth::Gate`, the lite publisher resetting subscriptions it denies, + `regrant` retracting announces no longer covered, and the IETF equivalent) + is the same mechanism pointed at the relay's own grant. Reuse it rather than + building a second one. Readers that bypass the session publisher (HTTP + `/fetch` holds its own lease and closes on it; moq-hls and WHEP enforce no + per-user grant today) are out of scope. +- #3971 built narrowing alone and was closed: it refused whenever anything + live sat in the removed part, which is exactly when a grant narrows, and it + kept the ceiling in a side map. Scopes today are copied by value into every + handle and cursor (`OriginScope` in `rs/moq-net/src/model/origin.rs`), so + narrowing needs shared state. If handles need a tree to share it, keep the + ceiling on the grant node itself (an `Arc` shared by clones and children). +- Publish side: routes and broadcasts the session published outside the + narrowed grant abort, so consumers see `Unauthorized` just as the subscribe + side does. Draining is not an option: a group may stay open as long as its + track. +- Relay revalidation uses it: a re-checked grant with the same root narrows + the session instead of closing it (`Lease::ended` reporting "grant + narrowed" in `rs/moq-relay/src/auth.rs`), on every accept path: native, + io_uring, and `moq --listen` share `connection::supervise`, while WebSocket + has its own copy of the loop. +- Prove the deafen case end to end: subscribe under a room prefix, narrow + with a grant that excludes that audio path, and assert the subscription + resets with `UNAUTHORIZED` and no further objects arrive, while a sibling + path under the same prefix keeps flowing. +- Benchmark the narrowing and per-session gate cost in + `rs/moq-net/benches/origin.rs`, swept over routes and subscribers, with a + session that never narrows as the baseline. + +Public API: additive on moq-net and on moq-relay's revalidate path. Wire: +none beyond the UNAUTHORIZED code the required quest adds. + +## Required + +- [Unauthorized reset](/quest/m1/auth/unauthorized.md) - the stream code a narrowed subscription resets with + +## Closes + +- [#2714](https://github.com/moq-dev/moq/issues/2714) - close this issue when the quest finishes diff --git a/quest/m1/auth/relay-refresh.md b/quest/m1/auth/relay-refresh.md index 36d14f7c07a..cff68ecc51a 100644 --- a/quest/m1/auth/relay-refresh.md +++ b/quest/m1/auth/relay-refresh.md @@ -34,7 +34,7 @@ expiry, an expiry that leaves the union intact ends only that token, and publish-only session never starts receiving announcements because a later token happened to carry subscribe prefixes, and swapped in whenever the set grows; keep it behind one function, and a shrinking union goes through - [Origin narrowing](/quest/m1/origin-narrowing.md) rather than a close. + [Origin narrowing](/quest/m1/auth/narrowing.md) rather than a close. - Expiry: the deadline today is the admitted lease's `closed()` inside one `tokio::select!` arm. Select on one lease per token in the set instead, and on any firing recompute the union without it: unchanged means `AUTH_ERROR { Expired }` on that token's stream and the session @@ -80,7 +80,7 @@ Additive. ## Required -- [Origin narrowing](/quest/m1/origin-narrowing.md) - the live re-scope a shrinking token union needs, so no temporary close-on-shrink policy ships +- [Origin narrowing](/quest/m1/auth/narrowing.md) - the live re-scope a shrinking token union needs, so no temporary close-on-shrink policy ships - [Pattern interest](/quest/m1/path-patterns.md) - AUTH can represent the complete grants relay revalidation returns - [Lite stream](/quest/m1/auth/lite.md) - supplies the AUTH stream and `auth::Request` this consumes diff --git a/quest/m1/origin-narrowing.md b/quest/m1/origin-narrowing.md deleted file mode 100644 index a52d5184096..00000000000 --- a/quest/m1/origin-narrowing.md +++ /dev/null @@ -1,72 +0,0 @@ -# [L] A live origin grant narrows in place - -## Goal - -A `moq-net` origin handle's grant can be replaced by a narrower pattern union -while the session is live: announce cursors retract the patterns that fell -outside, resolution refuses them, and broadcasts already handed out under -them end rather than draining. The relay uses it on revalidation so a -moderation decision (a deafened user's audio path) takes effect on the -session it targets, which a forked client cannot bypass. This is the -per-subscriber exclusion [#2714](https://github.com/moq-dev/moq/issues/2714) -asked for; the scope and match half already landed in #3672 and #3746. - -A narrowing always succeeds. Until it can end what it removes, the relay keeps -closing the session on a narrowed grant: that is simple, secure, and correct. - -## Plan - -- Narrowing and handout revocation ship together. #3971 built the narrowing - alone and was closed: it had to refuse whenever anything live sat in the - removed part (the session's own reads, another session's reads of the same - front, or its own publishes), which is exactly when a grant narrows, so it - only saved a reconnect for access nobody was using. -- `origin::Producer` and `origin::Consumer` gain one narrowing operation on - the handle, shared by every handle a session derived from it. A grant that - is not a subset of the current one is refused; widening is never a - narrowing. If handles need a tree to share it, keep the ceiling on the grant - node itself (an `Arc` shared by clones and children), not in a side map on - the origin. -- Revocation needs handouts attributed to the grant that requested them. - Decide where it is enforced first: - - **Model, per subscription.** A handout shares its state with the front - every other session reads, so ending one session's copy needs state per - handout reaching the track, subscription, and group readers. The cheapest - shape found rides the group expiry check (`GroupExpiry` in - `rs/moq-net/src/model/track.rs`), which runs only on a blocked read and - parks on the subscription's own state, so a private revoked signal wakes - parked readers for free. It still needs a registry per handout (a push per - subscribe on every relay session), a reason so readers see `Unauthorized` - rather than `Old`, and checks on ordered reads, datagrams, fetch, and peek. - The group in flight ends too: a group may stay open as long as its track, - so its boundary is no boundary at all. A transport that never learns about - the change still cannot keep reading. - - **Session.** The `moq-net` publisher watches one narrowing signal per - session and resets the subscriptions outside the grant. The cost is per - session, not per frame, but a transport reading the model directly is not - bound by it. -- Decide the publish side: whether a route or broadcast the session published - outside the narrowed grant is retracted or aborted. -- Relay revalidation uses it: a re-checked grant with the same root narrows - the session's origin handles instead of closing the session - (`Lease::ended` reporting "grant narrowed" in `rs/moq-relay/src/auth.rs`), - on every accept path (native, WebSocket, io_uring, `moq --listen`). A - changed root still closes. -- Prove the deafen case at the model layer: subscribe under a room prefix, - narrow with a grant that excludes that audio path, and assert the existing - subscription closes with `Unauthorized` and no further objects arrive, - while a sibling path under the same prefix keeps flowing. -- Benchmark the per-subscribe and routing cost in - `rs/moq-net/benches/origin.rs`, swept over routes and subscribers, with an - origin that never narrows as the baseline. - -Public API: additive on moq-net (one method) and on moq-relay's revalidate -path. Wire: none. - -## Closes - -- [#2714](https://github.com/moq-dev/moq/issues/2714) - close this issue when the quest finishes - -## Related - -- [In-band auth](/quest/m1/auth/README.md) - the token union that a narrowing later revalidates diff --git a/quest/m1/relay-peer-set.md b/quest/m1/relay-peer-set.md deleted file mode 100644 index 14a2e46b240..00000000000 --- a/quest/m1/relay-peer-set.md +++ /dev/null @@ -1,31 +0,0 @@ -# [M] A wire consumer tells a client hop from a peer hop - -## Goal - -A sidecar reading a relay over the wire knows whether a route entered on that -relay or came from one of its cluster peers, and every way a peer is admitted -can say it is one. In-process consumers already read `Route::source()` and -`origin::Consumer::local()`; a sidecar sees `[.., x, relay]` and cannot tell a -client `x` from a peer `x`. moq.pro's Python sidecar answers it today with a -hop-id bit prefix (`agent/moq_agent/matching.py`). - -## Plan - -- The relay publishes its cluster peer set in its stats broadcast: the hops of - the peers whose routes it currently holds, which is exactly what makes a - delivered route a peer's. Derive it from the origin's peer-marked routes - rather than from session bookkeeping, so the wire and in-process answers - cannot disagree. Name the frame's shape in `doc/bin/relay/config.md` (stats - section) so the sidecar and the producer agree, and land it after the - FlatBuffers stats flavor settles the track layout. -- `moq auth serve` can mark a peer (`peer: true` in the grant): an mTLS grant - for the cluster CA, and a JWT whose claims say so. Today only dials the relay - makes, LAN peers, and a custom auth server's grant mark one, so a stock mesh - counts inbound JWT and mTLS peers as clients. - -Public API: additive on moq-stats and moq-auth. Wire: the stats broadcast gains -a peer-set frame; the JWT claims may gain a field. - -## Related - -- [Route cost](/quest/m1/route-cost.md) - the other route fact JS lacks diff --git a/quest/m2/README.md b/quest/m2/README.md index f7cc1a225c8..8ef5c38b01b 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -49,6 +49,7 @@ upstream release waits in [m4](/quest/m4/README.md). - [Compressed tracks](/quest/m2/flate/README.md) - any track compresses per group from every language, not only the JSON modes - [Binary delta stats](/quest/m2/stats-delta.md) - an on-demand varint delta flavor of every stats track, if relay encode CPU still matters after the JSON fixes - [#3115](/quest/m2/3115-moqsink-the-publication-has-no-generation-so-a-flush.md) - moqsink: a flushing restart after EOS opens a new publication generation +- [Plan: routing without a hop list](/quest/m2/plan-routing-origin.md) - whether announcements can drop the hop list and stay loop-free once stitching keys on the subscribe reply - [Redundant ingest](/quest/m2/redundant-ingest.md) - decide whether two publishers sharing one epoch may splice, and who declares the incumbent dead before the keep-alive does - [Multipath spike](/quest/m2/multipath-spike.md) - whether bonded contribution over multipath QUIC is worth building, given it needs noq on both ends - [Receive timestamps](/quest/m2/quic-receive-ts.md) - per-packet arrival times in ACKs, the feedback GCC and deadlines need diff --git a/quest/m2/plan-routing-origin.md b/quest/m2/plan-routing-origin.md new file mode 100644 index 00000000000..bb6f0427931 --- /dev/null +++ b/quest/m2/plan-routing-origin.md @@ -0,0 +1,39 @@ +# [M] Plan: routing without a hop list + +## Goal + +Decide whether moq-lite announcements can drop the full hop list and still +route loop-free, then write the decision into quests. Today every +announcement carries the Hop IDs it traversed. That path does three jobs: +loop prevention (a relay discards a path containing itself), request +exclusion (never serve a request back through the peer that made it), and +route identity (the first hop decides whether a failover may stitch +seamlessly). The hop list was designed before prefix claims, and once the Spread quest in +[Wildcard advertisements](/quest/m1/wildcard/README.md) moves stitching +identity onto the subscribe reply, only the first two jobs remain. + +## Plan + +Open questions, to settle with the maintainer: + +- Loop freedom without a path. Rising cost alone ("cost + 1, never + advertise lower") counts to infinity after a withdrawal, as two relays + offer each other the stale route at ever higher cost. Remembering each + route's immediate neighbor and never echoing a route back to it (split + horizon) stops two-relay loops, but not three-relay loops. It also hides + backups: a relay never learns an alternative that passes back through the + neighbor it came from. Babel's feasibility condition (RFC 8966: a + per-origin sequence number, accepting only routes cheaper than the best + seen for it) is loop-free with only the origin on the wire. Weigh it and + any simpler scheme against what the relay cluster actually needs. +- What replaces request exclusion, and whether it still matters once + announcements are loop-free. +- What the stats and sidecar consumers lose without a path, and whether + anything besides the origin must stay on the wire. +- The version it lands in (lite-07 is `moq-lite-07-wip` today) and the + bridge to versions that still carry a hop list. + +## Related + +- [Wildcard advertisements](/quest/m1/wildcard/README.md) - its Spread quest moves stitching identity to the subscribe reply, which this builds on +- [Announce compression](/quest/m1/announce-compression.md) - reuses the hop-chain tail this would remove From 6b8d1be7a3365f66f62a65bff9219ec3d484f208 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 26 Sep 2026 07:46:47 -0700 Subject: [PATCH 2/3] quest: point narrowing links at the auth line, drop landed announce-compression link Co-Authored-By: Claude Opus 5.5 --- quest/m2/plan-routing-origin.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quest/m2/plan-routing-origin.md b/quest/m2/plan-routing-origin.md index bb6f0427931..98eaf6c09d6 100644 --- a/quest/m2/plan-routing-origin.md +++ b/quest/m2/plan-routing-origin.md @@ -31,9 +31,10 @@ Open questions, to settle with the maintainer: - What the stats and sidecar consumers lose without a path, and whether anything besides the origin must stay on the wire. - The version it lands in (lite-07 is `moq-lite-07-wip` today) and the - bridge to versions that still carry a hop list. + bridge to versions that still carry a hop list. lite-07 already compresses + each announcement's hop chain against a live base (#4196, + `rs/moq-net/src/lite/compress.rs`); dropping the hop list removes that too. ## Related - [Wildcard advertisements](/quest/m1/wildcard/README.md) - its Spread quest moves stitching identity to the subscribe reply, which this builds on -- [Announce compression](/quest/m1/announce-compression.md) - reuses the hop-chain tail this would remove From d0a921a7c069dc89bc7722fbba8a8abe0194d3e4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 26 Sep 2026 07:48:41 -0700 Subject: [PATCH 3/3] quest: state both cases of Babel's feasibility condition Co-Authored-By: Claude Opus 5.5 --- quest/m2/plan-routing-origin.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quest/m2/plan-routing-origin.md b/quest/m2/plan-routing-origin.md index 98eaf6c09d6..5a0c7f74f37 100644 --- a/quest/m2/plan-routing-origin.md +++ b/quest/m2/plan-routing-origin.md @@ -23,8 +23,9 @@ Open questions, to settle with the maintainer: horizon) stops two-relay loops, but not three-relay loops. It also hides backups: a relay never learns an alternative that passes back through the neighbor it came from. Babel's feasibility condition (RFC 8966: a - per-origin sequence number, accepting only routes cheaper than the best - seen for it) is loop-free with only the origin on the wire. Weigh it and + per-origin sequence number; a route is feasible when its sequence number is + newer, at any cost, or equal and cheaper than the best seen for it) is + loop-free with only the origin on the wire. Weigh it and any simpler scheme against what the relay cluster actually needs. - What replaces request exclusion, and whether it still matters once announcements are loop-free.