Repository navigation
feat(relay)!: enforce bitrate caps on QUIC sessions - #7535
Conversation
Quest-Claim: 8f1e29551735ff28
Token claims and grants carry optional publish_rate and subscribe_rate caps
(moq_auth::Rate) as the {"path","rate"} object role. The relay holds QUIC
and WebTransport sessions on the tokio and io_uring listeners to them,
taking the union's highest cap, applying re-checked caps live, and refusing
caps on every transport that cannot enforce them. Every server QUIC
connection starts with only the SETUP window, opened once admitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…56111401a74 # Conflicts: # quest/m2/rate-websocket.md # rs/moq-tokio/src/server.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the io-uring feature the gate's debug build overflowed the 1 MiB stack in stack::debug_relay_load_and_run_fit_windows_stack: main sat within 64 KiB of the limit and this branch's admission changes tipped it over. Spawning each Connection from its own function keeps the session future, built on the stack before tokio::spawn moves it, out of serve_listening's frame, which every accept stacks on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves rs/moq-relay/src/auth.rs: the dot-segment refusal moves into admit_paced, which every relay admission, paced or not, goes through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review.
|
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 07316c1cdc363906a5fa3bd8749639ccea84a0bd.
No actionable bugs found in the changed auth encoding, admission, credential-union/revalidation, and Tokio/io_uring pacing paths. No earlier published review findings to carry forward.
Direction is sound: reuse the existing QUIC limiter, apply caps before opening receive credit, and refuse capped grants where enforcement is unavailable. Keeping auth policy separate from transport pacing is simpler than introducing another payload-rate wrapper; the worker handoff is justified by io_uring's thread affinity. Strict object-role encoding preserves caps instead of silently downgrading them for legacy verifiers.
Verification: GitHub source and test inspection, including an independent Rust/JS auth-wire pass. I did not run builds, tests, interop, or throughput measurements; the PR's reported results were not independently reproduced.
(Written by OpenAI)
There was a problem hiding this comment.
Important
The upgrade and migration guides still document the previous behavior: that object roles and bitrate caps are refused. This PR re-enables them, so those entries now contradict the code.
Reviewed changes
moq-authcaps. NewRate/InvalidRate, additivepublish_rate/subscribe_rateonClaimsandGrant, and the{"path": [...], "rate": bps}wire role inwire::Role, which released verifiers refuse rather than drop the cap. Key scopes stay path-only.servecopies a token's caps into its grant.js/authmirror.WireRoleSchema,decodeRoles/nestRates/encodeRoles,encodeGrant, and caps onKey.sign;GrantSchemabecomes a pipe (its.shapeis gone).- Relay enforcement (
auth/caps.rs).Caps+Pacerhold a session's QUIC connection to the union of its live credentials' caps;Auth::admitrefuses a capped grant without a pacer,admit_pacedcarries one, and revalidation/in-band credentials pace the live connection. - Pre-auth window.
moq_net::transport::SETUP_RECEIVE_WINDOWandmoq-tokio'sRequest::with_quic_limits; every server QUIC connection holds the SETUP window untilokopens[quic] receive_window. - Tests.
bitrate_caps.rson the tokio and io_uring listeners,auth::caps/auth::refresh/auth, sharedrate.jsonvectors on both sides, and the updatedpeer_limits.
⚠️ Upgrade and migration guides still say caps are refused
This PR reverses the Unreleased "Auth roles are patterns" change: the object role {"path": [...], "rate": bps} is accepted again and Rate / InvalidRate are back. Four hand-written guides still state the opposite, and the moq-tokio guide is missing a note for the new pre-auth window. A deployer following the upgrade page would believe caps are unsupported.
Technical details
# Stale upgrade/migration prose after re-enabling caps
## Affected sites
- `doc/setup/upgrade.md:60-62` — "Auth roles are pattern arrays ... An object role, including one with a `rate` cap, is refused; use pattern arrays for `publish` and `subscribe`."
- `rs/moq-auth/MIGRATE.md:56` — "`Access`, `Rate`, and `InvalidRate` are removed; bitrate restrictions are unsupported and the whole object role is refused, including `{"path":["alice/**"],"rate":8000000}`."
- `rs/moq-relay/MIGRATE.md:125` — "Token and auth-server grant parsers refuse object roles, including bitrate caps, before relay admission; no transport currently enforces a cap."
- `js/auth/MIGRATE.md:21` — "Bitrate restrictions are unsupported and the whole object role is refused, including `{"path":["alice/**"],"rate":8000000}`."
- `rs/moq-tokio/MIGRATE.md` (`## Unreleased`) — no entry for `server::Request::with_quic_limits` or the window held until `ok`.
## Required outcome
- Each "Auth roles are patterns" entry describes the net behavior on `main`: caps are supported as
`publish_rate` / `subscribe_rate` beside the patterns, the capped object role is accepted, and the
`Access` wrapper is still gone.
- `doc/setup/upgrade.md` carries the rollout order the new wire implies: a capped role is refused by
older relays/auth servers rather than silently dropped, so upgrade auth servers and relays before
minting capped tokens.
- `rs/moq-tokio/MIGRATE.md` notes the behavior change already in its CHANGELOG: a server QUIC
connection holds `SETUP_RECEIVE_WINDOW` until `Request::ok`, so a caller that set limits with
`Request::quic().set_limits(..)` before `ok` should use `Request::with_quic_limits` instead.
## Suggested approach (optional)
- Fold the changes into the existing `## Unreleased` sections rather than adding new ones, matching the
CHANGELOG entries this PR already added under `### Added` / `### Changed`.
## Open questions for the human (optional)
- If these guides are intentionally written at release cut (per `doc/setup/upgrade.md`), say so and
close this; otherwise they contradict the CHANGELOGs in this same PR.deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
quest land watch: the merge queue removed this pull request at 07316c1 because it conflicts with The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs. (written by quest land watch) |
|
codex / Codex land fixer note:
(written by codex / Codex) |
|
quest land watch: armed auto-merge on this pull request at b9fb280, so it joins the back of the merge queue once its required checks pass, with the land fixer's fix ( (written by quest land watch) |

Finishes quest/m2/rate-quic.md: the auth and relay half, on top of the stack half from moq-dev/moq#7094. A relay session over QUIC (tokio and io_uring) is now held to its token's
publish_rateandsubscribe_rate, and a capped session over QUIC is admitted instead of refused.Summary
Rate(positive bits per second, at most 2^53 - 1) and additivepublish_rate/subscribe_rateonClaimsandGrant, as the maintainer decided on moq-dev/moq#7094. A capped role is written{"path": [...], "rate": bps}, never as legacyput/get, so released verifiers refuse it rather than drop the cap (tests/released_token.rs). Key scopes still refuse caps.moq auth servecopies a token's caps into its grant. Shared vectors inrs/moq-auth/rate.json.publish_rate/subscribe_rateon parsed claims and grants, the object role on the wire, the same vectors.rs/moq-relay/src/auth/caps.rs). APacerholds the QUIC connection to a session'sCaps: the tokio path callsset_limitson the moq-tokio connection; the io_uring path hands each change to a task on the connection's worker through akiochannel. A session without a pacer (WebSocket, iroh, HTTP/fetchand/announced,moq --listen) refuses a capped grant at connect, in band, and at revalidation.publish_ratebecomes the receive cap andsubscribe_ratethe send cap, bits to bytes rounded up.moq_net::transport::SETUP_RECEIVE_WINDOW(SETUP plus one AUTH, about 128 KiB), so a client cannot bank the full window before a cap applies. moq-tokio'sRequest::okopens[quic] receive_windowafter the limits set through the newRequest::with_quic_limits(the caps, or a peer's wider limits); the io_uring relay does the same before its SETUP reply.doc/bin/relay/auth.mdgains a Bitrate caps section;doc/lib/{rs,js}updated. The quest file is deleted and the prose that linked it now names the landed types.Impact
Rate,InvalidRate,Claims::{publish_rate, subscribe_rate, with_publish_rate, with_subscribe_rate},Grant::{publish_rate, subscribe_rate}(both structs are#[non_exhaustive]).transport::SETUP_RECEIVE_WINDOW.server::Request::with_quic_limits. Behavior: every server QUIC connection holds the SETUP window untilRequest::ok. A caller that usedRequest::quic().set_limits(..)beforeokshould usewith_quic_limitsinstead, orokreopens the configured window over it.auth::Tokengainspublish_rate/subscribe_rate;Auth::admitandLease::newrefuse a capped grant (Forbidden, "this transport cannot enforce bitrate caps"); a viewer now receives aMAX_DATAonce admitted.GrantSchemais now a zod pipe, soGrantSchema.shapeis gone; parse withGrantSchema.parse. NewencodeGrant(grant)writes a grant's caps into its roles.Decisions
The maintainer accepted all five implemented recommendations on 2026-11-01 (in the
/quest-completerun). The quest file is deleted by this PR, so this section is the paper trail.SETUP_RECEIVE_WINDOW;Request::okopens[quic] receive_window, after the limits a caller set withRequest::with_quic_limits.server::Configfield.okopens the window unless aset_limitsalready set one, tracked in hidden driver state.moq_net::transport::SETUP_RECEIVE_WINDOW, the largest SETUP plus one lite AUTH with framing (131,102 bytes), besideMIN_RECEIVE_WINDOW.pathlist and arate; a missing rate or a bare-stringpathis refused.rateand a stringpath, as the reverted moq-dev/moq#5697 did.encodeGrant(grant), and letKey.sign/KeySet.signacceptpublish_rate/subscribe_rate.Choices made without asking
Rate::from_bps/as_bps/MAXandDisplay, mirroring the reverted API; bits to bytes round up, so 1 bps is 1 B/s.Auth::admitstays public and refuses caps; the relay's QUIC paths use a crate-privateadmit_paced. The refusal isForbidden(HTTP 403, sessionUnauthorized), recorded like any other.serve_listeningspawns eachConnectionfrom its own function (spawn_connection), so the session future stays out of the accept loop's frame. Withio-uring, the gate's debug build overflowedstack::debug_relay_load_and_run_fit_windows_stack: main needs 992 to 1024 KiB, and this branch tipped it past 1 MiB. The change brings it back under, at about main's margin.admit_paced, which every relay admission goes through, paced or not.Tests
rs/moq-relay/tests/integration/bitrate_caps.rs, on the tokio listener and the io_uring workers: a capped publisher and a capped subscriber each take at least 0.7 s for 2 MiB at 8 Mbps (milliseconds uncapped); a client is blocked at the SETUP window before admission and credited once admitted.auth::caps: unions take the highest cap and an uncapped grant lifts it; bits to bytes.auth::refresh: in-band unions pace the live connection; a capped in-band token is refused without a pacer and the session keeps its grant; a revalidation into a cap ends an unpaced session.auth: capped grants are refused on every transport without a pacer and admitted with one; a revalidated cap reaches the connection.moq-auth: shared vectors, signing, scope refusal, released-verifier refusal.js/authmirrors them.peer_limitsnow expects a viewer's window to open after admission.just check origin/mainpasses on07316c1cdc363906a5fa3bd8749639ccea84a0bd(9637 Rust tests, including the io_uring ones).Follow-ups
moq auth signhas no flags for caps, so a capped token can only be minted through the library.Relay::run_inner's poll frame alone is about 431 KB. quest/m0/test-flakes-2/relay-stack-all-features.md could take this on as well: shrinking that frame fixes both the all-features overflow and this margin.wire_stats::uring_*,peer_limits::uring_*, andbitrate_caps::uring_*sit outside theio-uringnextest group, so parallel runs can exhaustRLIMIT_MEMLOCK.Review
mechanical fix by codex / Codex:
Merged base and retained both QUIC Server initializer fields; passed cargo nextest run --locked -p path+file:///work/rs/moq-auth -p path+file:///work/rs/moq-net -p path+file:///work/rs/moq-tokio -p path+file:///work/rs/moq-relay --features moq-relay/io-uring; bun run --cwd js/auth check; bun run --cwd js/auth test; bun run --cwd js/auth build; rustfmt --check --edition 2024 rs/moq-tokio/src/quic/backend.rs; git diff --cached --check.(head b9fb280)quest review), gpt-6.1-sol, medium effort, a new session for this iterate's branch.7397309693f66c62c442844109af48113120481e(the decided answers, the merge of main, and the stack fix): no findings.07316c1cdc363906a5fa3bd8749639ccea84a0bd(a second merge of main, with theauth.rsconflict resolved by hand): no findings.--deep(gpt-6-astra, medium effort), resuming that session: no findings.--deep --newsession (gpt-6-astra, medium effort), round 1 on07316c1cdc363906a5fa3bd8749639ccea84a0bd, so the security read is not anchored on the earlier rounds: no findings.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code