diff --git a/quest/m1/README.md b/quest/m1/README.md index 32ebdb63cc5..2ccf0b934dc 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -24,12 +24,10 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Track tail interop](/quest/m1/track-tail-interop.md) - a Rust publisher ending a track with a group in flight is read to its end by the JS subscriber, and the reverse, in `just test interop` - [Interop flakes](/quest/m1/interop-flakes.md) - the interop harness passes with other runs sharing the machine - [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 - [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi, libmoq, and every wrapper configure and observe audio playout delay - [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 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 - [Session close](/quest/m1/session-close.md) - a graceful session end withdraws announces and waits one second for the ack - [JS caught up](/quest/m1/js-announce-caught-up.md) - @moq/net's announce consumer says when the initial set has landed, like Rust diff --git a/quest/m1/auth/README.md b/quest/m1/auth/README.md index 7042f61feb6..6ec02ba9d11 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 - [Request tokens](/quest/m1/auth/request-token.md) - an `AUTHORIZATION @@ -113,8 +116,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/auth/request-token.md b/quest/m1/auth/request-token.md index c39486256af..c6125f82950 100644 --- a/quest/m1/auth/request-token.md +++ b/quest/m1/auth/request-token.md @@ -40,7 +40,7 @@ on the unknown key, and the legacy drafts silently ignore it. until it lapses. When a request's grant expires or is revoked, that request alone ends with `EXPIRED_AUTH_TOKEN` or `UNAUTHORIZED`. A session grant that shrinks cancels the requests it covered, as for any request, through - [Origin narrowing](/quest/m1/origin-narrowing.md). + [Origin narrowing](/quest/m1/auth/narrowing.md). - Relay: each such request attaches its own lease through the `Client::attach` path [Relay tokens](/quest/m1/auth/relay-refresh.md) builds, once per request, with no sharing across requests carrying the 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 d80049439fe..80fa09dd1b0 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -53,6 +53,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..5a0c7f74f37 --- /dev/null +++ b/quest/m2/plan-routing-origin.md @@ -0,0 +1,41 @@ +# [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; 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. +- 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. 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 diff --git a/rs/moq-relay/src/auth.rs b/rs/moq-relay/src/auth.rs index a3eb5dda48c..57a5316b233 100644 --- a/rs/moq-relay/src/auth.rs +++ b/rs/moq-relay/src/auth.rs @@ -273,7 +273,7 @@ impl Lease { /// revoked, or was re-checked into one that no longer covers the token. /// /// A changed root or a narrower grant ends it: origin handles cannot yet narrow - /// a live scope in place (tracked by `quest/m1/origin-narrowing.md`). A flipped + /// a live scope in place (tracked by `quest/m1/auth/narrowing.md`). A flipped /// `peer` ends it too, since the routes it already announced would be /// misreported as entering here or from a peer. A changed tier keeps the /// session and moves its [stats](Self::with_stats) to the new tier.