diff --git a/quest/m1/auth/request-token.md b/quest/m1/auth/request-token.md index 40676e2eecf..d1d4bc40553 100644 --- a/quest/m1/auth/request-token.md +++ b/quest/m1/auth/request-token.md @@ -17,12 +17,33 @@ no longer fails the session; this quest gives it meaning. ## Plan - Decode with the SETUP option's structure and rules - (`rs/moq-net/src/ietf/token.rs`, `js/net/src/ietf/token.ts`): `USE_VALUE` yields the token, `REGISTER` is a value since we - advertise no `MAX_AUTH_TOKEN_CACHE_SIZE`, and `DELETE` or `USE_ALIAS` - closes with `PROTOCOL_VIOLATION`. Both decoder families change: the strict + (`rs/moq-net/src/ietf/token.rs`, `js/net/src/ietf/token.ts`): `USE_VALUE` yields the token. `REGISTER` closes the session with + `AUTH_TOKEN_CACHE_OVERFLOW` (0x13): we + advertise no `MAX_AUTH_TOKEN_CACHE_SIZE`, so the limit is 0, and draft-21 + §8.9 says overflowing it MUST close the session. Only SETUP falls back to + USE_VALUE (§9.1.4). `DELETE` or `USE_ALIAS` closes with + `UNKNOWN_AUTH_TOKEN_ALIAS` (0x17): a cache size of 0 "prohibits the use + of token Aliases" (§9.1.3), so no alias is ever registered. §8.9 says to + reject the message, but 0x17 exists only as a Session Termination Code + (§12.2), not a Request Error Code. Record that close as a deviation + under "moq-transport" in `doc/concept/standard.md`. A token structure that does not + decode closes with `KEY_VALUE_FORMATTING_ERROR`. Paul Gregoire's + validator checks the REGISTER and malformed-token codes (decided + 2026-10-04; aliases decided 2026-10-05). Both decoder families change: the strict `decode_params!` path, where each request reads the repeatable key into an ignored `Vec`, and draft-14's `Parameters::skip`, which consumes it unread. `js/net` keeps every instance in `Parameters` and reads none. +- 0x13 and 0x17 join the shared session registry: `SessionError` + (`rs/moq-net/src/error.rs`) and `SessionCode` (`js/net/src/error.ts`). + Lite codes below 32 carry moq-transport's meaning, so add both rows to the + Session Error Codes table in `drafts/draft-lcurley-moq-lite.md` and to + `session_codes_round_trip`, as [Malformed moq-transport + input](/quest/m2/ietf-malformed-close.md) does for its codes. No + per-version mapping. That adds two codes to moq-lite's wire registry. +- Ship the decode-and-close change as this quest's first PR. It needs + neither [Relay tokens](/quest/m1/auth/relay-refresh.md) nor the + per-request lease, so the validator's REGISTER and malformed-token rows + need not wait for the rest of the quest. - Fallback only: a request the session grant already covers is served without verifying its token. Otherwise its token becomes an `auth::Request` on the session's `auth::Handle`, the seam an AUTH stream's @@ -64,7 +85,10 @@ no longer fails the session; this quest gives it meaning. verifier; a REQUEST_UPDATE token keeps a subscription alive past the old token's expiry; an expired request token ends only that request; with no consumer a token-bearing request is refused `Unsupported`; one legacy and - one strict draft, Rust and JS. + one strict draft, Rust and JS. Regression tests for the decode: a + `REGISTER` token on a request closes with `AUTH_TOKEN_CACHE_OVERFLOW`, a + `DELETE` or `USE_ALIAS` one with `UNKNOWN_AUTH_TOKEN_ALIAS`, and a + malformed request token with `KEY_VALUE_FORMATTING_ERROR`. Public API: additive on `moq_net::auth::Request` (the request it belongs to) and on `moq_auth::Client` (the per-request lease). Wire: none new; the parameter already exists in every supported draft. diff --git a/quest/m2/README.md b/quest/m2/README.md index cb552fe1fdd..28897d96bf8 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -122,4 +122,6 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [moq-transport cluster peers](/quest/m2/ietf-cluster-peers.md) - an extended cluster draft lets moq-transport relays peer again, carrying the cluster route layer - [MSFTS convergence](/quest/m2/msfts-convergence.md) - the demultiplexed TS lane converges on MSFTS where the two still differ: program tables and the ES payload unit - [VAAPI encode and decode](/quest/m2/video-vaapi.md) - H.265 encode and decode, pre-generated bindings, and pooled resize surfaces, including the moq-dev/vaapi release that carries them +- [Malformed moq-transport input](/quest/m2/ietf-malformed-close.md) - malformed draft-18 and draft-21 control input closes the session with the draft's code, or PROTOCOL_VIOLATION where a code is a real burden and the fallback is recorded in `doc/concept/standard.md`, in moq-net and js/net +- [moq-transport request codes](/quest/m2/ietf-request-codes.md) - LARGEST_OBJECT, Range Filters (INVALID_FILTER), reserved namespaces, and RENDEZVOUS_TIMEOUT get the draft's answer, or a recorded fallback code, and the deliberate deviations are documented - [Dead worklet config](/quest/m2/worklet-dead-config.md) - delete moq-boy's unused worklet plugin and hang's unused `?worker&url` typings diff --git a/quest/m2/ietf-malformed-close.md b/quest/m2/ietf-malformed-close.md new file mode 100644 index 00000000000..202bdc7067c --- /dev/null +++ b/quest/m2/ietf-malformed-close.md @@ -0,0 +1,127 @@ +# [L] Malformed moq-transport input closes the session + +## Goal + +On draft-18 and draft-21, every malformed control input that the draft +says ends the session does end it, in both `rs/moq-net` and `js/net`. +It closes with the draft's code, or with PROTOCOL_VIOLATION where a code +is a real burden and the fallback is recorded in +`doc/concept/standard.md`. Today moq-net logs most of these and keeps +going. The cases come from Paul Gregoire's publisher validator +(github.com/mondain/moq-contribution-interop-runner), run in observed mode +against a client built on moq-net 0.17.0. Each case below gives the +runner's scenario name, where it has one: + +- An unknown control message type (`receive-unknown-message-type`). +- A message length that doesn't match its body, or a body cut short by FIN + (`d21-message-body-length-mismatch`, + `d21-request-message-truncated-at-fin`). +- A zero-length namespace field, or a namespace or full track name over + 4096 bytes. +- A Request ID with the wrong parity: INVALID_REQUEST_ID. +- A GROUP_ORDER outside 1..2. +- An understood key-value with a bad serialization: + KEY_VALUE_FORMATTING_ERROR + (`receive-understood-key-value-invalid-serialization`). +- A second GOAWAY on the control stream or on a single request stream, or + a New Session URI over 8192 bytes: PROTOCOL_VIOLATION. A first GOAWAY on + a request stream is legal (draft-18 §10.4, draft-21 §9.2) and leaves the + session up. +- A draft-18 GOAWAY cutoff Request ID with the wrong parity: + INVALID_REQUEST_ID, not a redirect. +- A server SETUP carrying AUTHORITY or PATH: INVALID_AUTHORITY or + INVALID_PATH. +- Repeated unknown or GREASE Setup Options are accepted + (`setup-unknown-grease-options-and-duplicates`, + `d21-grease-setup-options`). Only a repeated known option is a duplicate. +- A server SETUP AUTHORIZATION TOKEN that is malformed closes with + KEY_VALUE_FORMATTING_ERROR. A REGISTER that overflows our cache (size 0) + falls back to USE_VALUE, as draft-21 §9.1.4 requires. +- A SUBSCRIBE_TRACKS prefix with more than 32 fields or over 4096 bytes + closes with PROTOCOL_VIOLATION, instead of being refused per request + before it is decoded. + +## Plan + +Decided with the maintainer on 2026-10-04 and 2026-10-05: + +- **Add the missing session codes to the shared registry.** + INVALID_REQUEST_ID (0x4), INVALID_PATH (0x8) and INVALID_AUTHORITY + (0x19) become `SessionError` variants (`rs/moq-net/src/error.rs`) and + `SessionCode` entries (`js/net/src/error.ts`). Their values are the same + in drafts 18 and 21. That registry is moq-lite's too, and lite codes + below 32 carry moq-transport's meaning, so add the rows to the Session + Error Codes table in `drafts/draft-lcurley-moq-lite.md` and to + `session_codes_round_trip`. No per-version mapping. Where a code is a + real burden, the PROTOCOL_VIOLATION fallback in the Goal applies. +- **Regression tests, not a CI job for the validator.** Add one session or + codec test per case, each failing without its fix and asserting the + draft's code, or PROTOCOL_VIOLATION where the fallback is recorded in + `doc/concept/standard.md`. Include a positive case: a first GOAWAY on a + request stream keeps the session open. Run the external runner once by + hand before the PR is marked ready: observed mode, + `--publisher-no-fetch`, one scenario per run, on a track of about + 150 kbps. A full-bitrate track floods the runner's event log and aborts + the run. +- **Mirror in `js/net/src/ietf`** in the same PR. +- Request tokens are out of scope here. AUTH_TOKEN_CACHE_OVERFLOW and the + request-token decode belong to [Request + tokens](/quest/m1/auth/request-token.md). + +Where each case lives, mapped on 2026-10-04 (paths under +`rs/moq-net/src/`): + +- The SETUP/GOAWAY uni stream runs as a task spawned inline in `run_unis` + (`ietf/session.rs`, around lines 751-874). SETUP and SETUP-parameter + decode failures there already close the session; only `run_goaway`'s + error is just logged. The gated server accept, which reads SETUP early, + calls `run_goaway` a second time (`goaway_recv`, around line 375) and + does the opposite: any error, even a stream reset, ends the session. + Make both sites agree, ideally by filtering inside `run_goaway`. Close + on the errors `is_protocol_violation` (`ietf/subscriber.rs`) marks as + the peer's fault, not on every error: a reset or transport failure on + that stream must stay non-fatal, as `died_before_header` keeps it + elsewhere. That fixes the second GOAWAY, + the oversize URI, and an unknown type on that stream. On request + streams, an error from a follow-up message is only logged at debug in + `ietf/publisher.rs` (around line 500); promote decode errors there with + the same filter. +- `died_before_header` (`ietf/session.rs`) treats a body cut short at FIN as + the stream dying. A frame whose declared length passes FIN is malformed. + Responses decoded at `ietf/publisher.rs` (about lines 1858 and 1937) lack + the trailing-bytes check that the first message has. +- Namespace bounds: `ietf/namespace.rs` checks only the 32-part limit. The + track name in `ietf/subscribe.rs` is unbounded. +- Request ID parity is known only in `Control::new` (`ietf/control.rs`), so + thread it to where IDs are decoded. +- `ietf/group.rs` maps an invalid GROUP_ORDER, or 0, to Descending. +- KV serialization errors must be told apart from other decode errors. + `From<&Error> for SessionError` (`error.rs`) maps every `Decode` error to + PROTOCOL_VIOLATION, and `ietf/session.rs` and `ietf/publisher.rs` + hard-code it in a few places. +- GREASE: `ietf/parameters.rs` (around line 122) keys every unknown ID as + `Unknown(u64)` in a HashMap, so any repeat fails as `Duplicate`. In this + tree that includes the plain duplicate-unknown case the report says now + passes, so check which build the report ran. +- Client SETUP: `decode_peer_setup` (`ietf/session.rs`) ignores AUTHORITY + and PATH and never parses the server's token. Reuse `ietf/token.rs`, + which already does the server-side fallback. +- SUBSCRIBE_TRACKS is refused before its prefix is decoded + (`ietf/publisher.rs`, about lines 547 and 1609). Decode the prefix first, + then refuse. + +Test models: `an_unknown_uni_type_closes_the_session` and +`an_unknown_bidi_type_closes_the_session` in `ietf/session.rs`, which drive +`ScriptedSession`. Codec tests live in `ietf/parameters.rs` and +`ietf/subscribe.rs`. Add each new decode error to the `ietf_wire` fuzz +regressions where it fits. + +Public API: new `SessionError` variants and `SessionCode` entries. Wire: +new moq-lite session codes, with moq-transport's values; moq-transport +behaviour moves closer to the drafts. + +## Related + +- [Request tokens](/quest/m1/auth/request-token.md) - owns request-token decode and AUTH_TOKEN_CACHE_OVERFLOW +- [Request caps](/quest/m0/request-caps.md) - bounds peer lengths and counts; shares the decode paths +- [moq-transport request codes](/quest/m2/ietf-request-codes.md) - the request-level half of the same validator report diff --git a/quest/m2/ietf-request-codes.md b/quest/m2/ietf-request-codes.md new file mode 100644 index 00000000000..4792cbc0cc2 --- /dev/null +++ b/quest/m2/ietf-request-codes.md @@ -0,0 +1,67 @@ +# [M] moq-transport requests get the draft's answer + +## Goal + +On draft-18 and draft-21, `rs/moq-net` and `js/net` answer these requests +the way the drafts say, without closing the session. Each answer uses the +draft's code, or an existing code where the exact one costs more than it +is worth and the fallback is recorded in `doc/concept/standard.md`: + +- **LARGEST_OBJECT (0x09) in REQUEST_OK and REQUEST_UPDATE_OK** is + accepted. The draft makes it a MUST when objects exist. Today + `ietf/request.rs` allows only 0x08 and ACTIVE_COUNT, so a conformant + peer's reply closes the session with PROTOCOL_VIOLATION. +- **Range Filters** get REQUEST_ERROR INVALID_FILTER (0x36), not + NOT_SUPPORTED (draft-19 and later). We advertise no MAX_FILTER_RANGES, so + the default of 0 applies (draft-21 §9.1.6) and every Range Filter is over + the limit. This covers SUBSCRIBE and FETCH alike: both refuse Range + Filters today (`ietf/publisher.rs`, around lines 605 and 1292). +- **Draft-21 reserved namespaces:** draft-21 §2.4.2 and the `.session` + rules (§6.5) ask for REQUEST_ERROR DOES_NOT_EXIST, without passing the + request to the application, for: + - a request for an unrecognized `.session` track or namespace; + - a `.session` namespace with an empty track name; + - a namespace whose first field is exactly `.`. + + Only these short-circuit. Any other reserved namespace (a first field + starting with `.`) still goes to the application, as §2.4.2 requires. +- **RENDEZVOUS_TIMEOUT:** our wait is capped at 0, which draft-21 §9.20.7 + allows. If a SUBSCRIBE asks to wait and there is no publisher, it gets + TIMEOUT at once. A request that is absent or 0 still gets DOES_NOT_EXIST. + This fixes the two quic-zig `rendezvous-timeout` cells in the interop + matrix. +- **OBJECT_DELIVERY_TIMEOUT (0x02) and SUBGROUP_DELIVERY_TIMEOUT (0x06)** + on REQUEST_UPDATE are accepted and ignored, as SUBSCRIBE already does + (`ietf/subscribe.rs`). Today `ietf/request_stream.rs` refuses both as + unsupported. + +## Plan + +Decided with the maintainer on 2026-10-04 and 2026-10-05: + +- **Deliberate deviations stay.** DUPLICATE_SUBSCRIPTION and PREFIX_OVERLAP + are never sent, because moq-net deduplicates. OBJECT_DELIVERY_TIMEOUT and + SUBGROUP_DELIVERY_TIMEOUT never drop an object or reset a subgroup: they + would need wall-clock delivery deadlines, and moq-net's latency + enforcement is presentation time only (#2890). The rendezvous wait is + capped at 0. Each deviation gets a line under + "moq-transport" in `doc/concept/standard.md`, so the next validator run + has a reason to point at. +- **Exact codes when cheap.** INVALID_FILTER becomes a request code. Where + one costs more than it is worth, the fallback in the Goal applies. +- **TIMEOUT, not a timer.** The 0 cap means nothing waits, so no request is + ever parked. The reply depends only on whether the parameter asked for a + wait. `ietf/subscribe.rs` parses RENDEZVOUS_TIMEOUT and discards it + (around line 140); keep it as far as the publisher's track lookup + (`ietf/publisher.rs`, around line 617). `Error::Timeout` already maps to + TIMEOUT (`ietf/error.rs`). +- One regression test per case, failing without its fix and asserting the + draft's code, or the fallback recorded in `doc/concept/standard.md`. + Mirror in `js/net/src/ietf` in the same PR. + +Public API: none expected. Wire: none new; replies move closer to the +drafts. + +## Related + +- [Malformed moq-transport input](/quest/m2/ietf-malformed-close.md) - the session-level half of the same validator report