moqtap_proxy/parser/control.rs
1//! Inline control stream parser.
2//!
3//! Buffers raw bytes from the forwarding loop and decodes complete MoQT
4//! control messages without modifying the forwarded data.
5//!
6//! Everything the parser framed comes back, decoded or not. A frame whose
7//! declared length is intact but whose body the decoder refuses is a
8//! [`ParsedItem::Refused`] carrying its bytes, not a gap: on the mutating
9//! control pipe this parser *is* the forwarding path, so a refusal it kept
10//! to itself would delete a control message from the wire and desynchronize
11//! the peer's view of the session.
12
13use bytes::{Buf, Bytes, BytesMut};
14
15use moqtap_codec::dispatch::AnyControlMessage;
16use moqtap_codec::version::DraftVersion;
17
18/// A successfully parsed control frame.
19///
20/// `raw_bytes` is populated only when the parser was constructed with
21/// [`ControlStreamParser::new_capturing`]; the default observation-only
22/// parser leaves it as `None` to avoid copying bytes that already flow
23/// through the forwarding path.
24#[derive(Debug, Clone)]
25pub struct ParsedFrame {
26 /// The decoded control message.
27 pub message: AnyControlMessage,
28 /// The original wire bytes of this frame — only set when the parser
29 /// is in capturing mode (used by hook-driven mutation).
30 pub raw_bytes: Option<Bytes>,
31}
32
33/// A frame the parser located but the decoder refused.
34///
35/// The frame header decoded — that is what said where this frame ends and
36/// the next begins — and nothing inside it did, so the Message Type is the
37/// whole of what it can still say about itself.
38#[derive(Debug, Clone)]
39pub struct RefusedFrame {
40 /// The Message Type varint the frame declared.
41 pub type_id: u64,
42 /// The original wire bytes of this frame — set on the same terms as
43 /// [`ParsedFrame::raw_bytes`], and for the same caller.
44 ///
45 /// A caller that owns the forwarding path **must still write these**.
46 /// The proxy could not read the message; the peer it was addressed to
47 /// may well be able to, and a message dropped in transit is a fault
48 /// the two endpoints have no way to attribute.
49 pub raw_bytes: Option<Bytes>,
50}
51
52/// One thing the parser framed, in wire order.
53///
54/// Order is the reason this is one sequence rather than two collections.
55/// A chunk holding a good frame, a refused one and a second good one has
56/// to be forwarded in that order; a caller handed the good frames and the
57/// refused bytes separately would write them in whichever order it chose,
58/// which for a control stream is a reordering the peer decodes as a
59/// different session.
60#[derive(Debug, Clone)]
61pub enum ParsedItem {
62 /// A frame the decoder read.
63 Frame(ParsedFrame),
64 /// A frame the decoder refused.
65 Refused(RefusedFrame),
66}
67
68/// Result of feeding bytes to the control stream parser.
69#[derive(Debug)]
70pub enum ParseResult {
71 /// One or more complete frames were located. Not every one of them
72 /// decoded — see [`ParsedItem`].
73 Framed(Vec<ParsedItem>),
74 /// Need more data — bytes are buffered internally.
75 NeedMore,
76}
77
78/// Stateful inline parser for a MoQT control stream.
79///
80/// Accepts raw byte chunks (as they arrive from `RecvStream::read`),
81/// buffers them, and emits complete `ParsedFrame`s. In the default
82/// (non-capturing) mode the parser does not clone the frame bytes; in
83/// capturing mode it does, so a hook can rewrite the frame before the
84/// proxy forwards it.
85pub struct ControlStreamParser {
86 buf: BytesMut,
87 draft: DraftVersion,
88 capture_raw: bool,
89}
90
91impl ControlStreamParser {
92 /// Create a new observation-only parser.
93 ///
94 /// `ParsedFrame::raw_bytes` will be `None`; use
95 /// [`Self::new_capturing`] when a hook needs to mutate frames.
96 pub fn new(draft: DraftVersion) -> Self {
97 Self { buf: BytesMut::with_capacity(4096), draft, capture_raw: false }
98 }
99
100 /// Create a new parser that captures the raw wire bytes of each frame.
101 ///
102 /// Use this variant only when a hook may rewrite frames; the extra
103 /// `Bytes::copy_from_slice` per frame is unnecessary for pure
104 /// pass-through forwarding.
105 pub fn new_capturing(draft: DraftVersion) -> Self {
106 Self { buf: BytesMut::with_capacity(4096), draft, capture_raw: true }
107 }
108
109 /// Feed raw bytes into the parser.
110 ///
111 /// Returns [`ParseResult::Framed`] if one or more complete frames were
112 /// located, or [`ParseResult::NeedMore`] if more data is needed.
113 /// Partial frames are buffered internally.
114 ///
115 /// A located frame the decoder refuses is [`ParsedItem::Refused`] and
116 /// not an omission, so a chunk carrying nothing but a refused frame is
117 /// `Framed`, never `NeedMore`: nothing more is coming that would make
118 /// that frame readable, and a caller told to wait would hold bytes it
119 /// is supposed to be forwarding.
120 pub fn feed(&mut self, data: &[u8]) -> ParseResult {
121 self.buf.extend_from_slice(data);
122 let mut items = Vec::new();
123
124 loop {
125 // Need at least 1 byte to determine type varint length
126 if self.buf.is_empty() {
127 break;
128 }
129
130 // Read type_id varint length from first byte
131 let type_len = self.draft.varint_len(self.buf[0]);
132 if self.buf.len() < type_len {
133 break;
134 }
135
136 // Peek at type_id (don't advance buf yet). Kept rather than
137 // discarded: it is the only thing a refused frame can still say
138 // about itself, since by definition nothing inside it decoded.
139 let mut cursor = &self.buf[..type_len];
140 let type_id = match self.draft.decode_varint(&mut cursor) {
141 Ok(v) => v.into_inner(),
142 Err(_) => break,
143 };
144
145 // Read payload length. Draft-11+ uses 16-bit BE; earlier drafts
146 // use a QUIC varint.
147 let (payload_len, total) = if self.draft.uses_fixed_length_framing() {
148 // Draft-11+: type_id(vi) + length(u16 BE) + payload
149 if self.buf.len() < type_len + 2 {
150 break;
151 }
152 let hi = self.buf[type_len] as usize;
153 let lo = self.buf[type_len + 1] as usize;
154 let payload_len = (hi << 8) | lo;
155 (payload_len, type_len + 2 + payload_len)
156 } else {
157 // Draft-07..10: type_id(vi) + length(vi) + payload
158 if self.buf.len() <= type_len {
159 break;
160 }
161 let payload_len_varint_len = self.draft.varint_len(self.buf[type_len]);
162 if self.buf.len() < type_len + payload_len_varint_len {
163 break;
164 }
165 let mut cursor = &self.buf[type_len..type_len + payload_len_varint_len];
166 let payload_len = match self.draft.decode_varint(&mut cursor) {
167 Ok(v) => v.into_inner() as usize,
168 Err(_) => break,
169 };
170 (payload_len, type_len + payload_len_varint_len + payload_len)
171 };
172 let _ = payload_len; // used via total
173
174 // Check if we have the full frame
175 if self.buf.len() < total {
176 break;
177 }
178
179 // Only clone the wire bytes when a hook might rewrite them;
180 // the observation-only path forwards the original buffer.
181 let raw_bytes = if self.capture_raw {
182 Some(Bytes::copy_from_slice(&self.buf[..total]))
183 } else {
184 None
185 };
186
187 // Decode from a clone (so we don't corrupt the buffer on error)
188 let mut decode_buf = &self.buf[..total];
189 match AnyControlMessage::decode(self.draft, &mut decode_buf) {
190 Ok(message) => {
191 self.buf.advance(total);
192 items.push(ParsedItem::Frame(ParsedFrame { message, raw_bytes }));
193 }
194 Err(_) => {
195 // Skip this frame and keep going. The declared length told
196 // us where the next frame starts, so one frame the decoder
197 // refuses says nothing about the frames behind it.
198 //
199 // Stopping here instead would discard every frame already
200 // buffered after this one, which is the opposite of what a
201 // proxy that exists to observe traffic should do with a
202 // malformed message: the stricter the decoder gets, the
203 // more of the stream a single refusal would take with it.
204 //
205 // `advance` is what rules out the infinite loop, not the
206 // exit: `total` is at least the type varint plus the length
207 // field, so it is always positive and the buffer always
208 // shrinks.
209 self.buf.advance(total);
210
211 // Handed back rather than swallowed, in the position it
212 // held. A skip that said nothing cost two things: an
213 // observer could not tell a message this proxy failed to
214 // read from one the peer never sent, and on the mutating
215 // pipe — where this parser is the forwarding path — the
216 // frame left the session entirely.
217 items.push(ParsedItem::Refused(RefusedFrame { type_id, raw_bytes }));
218 continue;
219 }
220 }
221 }
222
223 if items.is_empty() {
224 ParseResult::NeedMore
225 } else {
226 ParseResult::Framed(items)
227 }
228 }
229
230 /// Returns the draft version this parser is configured for.
231 pub fn draft(&self) -> DraftVersion {
232 self.draft
233 }
234}
235
236impl Default for ControlStreamParser {
237 fn default() -> Self {
238 Self::new(DraftVersion::Draft14)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 /// The type field is measured from its first byte before the rest has
247 /// arrived, so the parser has to know which encoding the draft uses:
248 /// RFC 9000's two-bit prefix through draft-16, MoQT's leading-1s count
249 /// from draft-17.
250 #[test]
251 fn varint_len_follows_the_draft() {
252 let old = DraftVersion::Draft14;
253 assert_eq!(old.varint_len(0x00), 1);
254 assert_eq!(old.varint_len(0x3F), 1);
255 assert_eq!(old.varint_len(0x40), 2);
256 assert_eq!(old.varint_len(0x80), 4);
257 assert_eq!(old.varint_len(0xC0), 8);
258
259 let new = DraftVersion::Draft19;
260 assert_eq!(new.varint_len(0x00), 1);
261 assert_eq!(new.varint_len(0x7F), 1);
262 // SETUP's type id, 0x2F00, is `af00`: two bytes, not four.
263 assert_eq!(new.varint_len(0xAF), 2);
264 assert_eq!(new.varint_len(0xC0), 3);
265 assert_eq!(new.varint_len(0xFF), 9);
266 }
267
268 // ── the draft-19 fixtures ───────────────────────────────────────
269 //
270 // Every constant and test below is draft-19 wire bytes, and each is
271 // gated on that draft rather than on the module. A build without it
272 // has no decoder for these frames, so `GOOD_FRAME` is refused like
273 // everything else and each assertion below would measure the feature
274 // set instead of the parser. The end-to-end coverage that survives a
275 // reduced build is `tests/control_undecodable.rs`, which drives
276 // whichever draft the build compiled.
277
278 /// A SUBSCRIBE whose declared Message Length overruns its body by two
279 /// bytes. Draft-19 Section 10 answers the mismatch with a session close, so
280 /// the codec refuses it — which makes it the shortest frame that reaches
281 /// this parser's decode-failure path.
282 #[cfg(feature = "draft19")]
283 const BAD_FRAME: &[u8] =
284 &[0x03, 0x00, 0x09, 0x00, 0x01, 0x01, 0x61, 0x01, 0x62, 0x00, 0xff, 0xff];
285
286 /// A frame carrying a Message Type no draft assigns, with an honest
287 /// zero length.
288 ///
289 /// `0x3A` is unassigned on all thirteen drafts and is below `0x40`, so
290 /// it is a single byte under RFC 9000's encoding and under MoQT's
291 /// alike; the two-byte big-endian length is draft-11-and-later framing,
292 /// which is what draft-19 uses. Nothing about it is malformed — the
293 /// parser can say exactly where it ends — and the decoder still has
294 /// nowhere to send it.
295 #[cfg(feature = "draft19")]
296 const UNKNOWN_TYPE_FRAME: &[u8] = &[0x3A, 0x00, 0x00];
297
298 /// The same, one type along, so "the first refusal" is a claim about
299 /// which one rather than about the only one.
300 #[cfg(feature = "draft19")]
301 const OTHER_UNKNOWN_TYPE_FRAME: &[u8] = &[0x3B, 0x00, 0x00];
302
303 /// Everything one feed framed, as `Ok(type_id)` for a decoded frame and
304 /// `Err(type_id)` for a refused one — in wire order.
305 ///
306 /// A shape assertions can compare in one piece. Reading counts off two
307 /// filtered collections would say how many of each arrived and nothing
308 /// about the order they arrived in, and the order is the half a
309 /// forwarding caller depends on.
310 #[cfg(feature = "draft19")]
311 fn outcomes(result: ParseResult) -> Vec<Result<u64, u64>> {
312 match result {
313 ParseResult::Framed(items) => items
314 .into_iter()
315 .map(|item| match item {
316 ParsedItem::Frame(_) => Ok(0),
317 ParsedItem::Refused(r) => Err(r.type_id),
318 })
319 .collect(),
320 ParseResult::NeedMore => Vec::new(),
321 }
322 }
323
324 /// A well-formed SUBSCRIBE: same shape, honest length, no parameters.
325 ///
326 /// Namespace `["a"]`, track name `"b"`, request id 0.
327 #[cfg(feature = "draft19")]
328 const GOOD_FRAME: &[u8] = &[0x03, 0x00, 0x07, 0x00, 0x01, 0x01, 0x61, 0x01, 0x62, 0x00];
329
330 /// A frame the decoder refuses costs that frame and no more.
331 ///
332 /// The parser used to `break` out of the loop on a decode error, having
333 /// already advanced past the frame. Everything buffered behind the bad
334 /// frame was dropped on the floor with it — so one malformed message could
335 /// cost an arbitrary number of good ones, and the stricter the codec became
336 /// about draft-19's MUSTs, the more of the stream a single refusal took
337 /// with it. That is backwards for a proxy whose purpose is to report the
338 /// traffic it sees.
339 ///
340 /// *Ablation (measured):* restore `continue` to `break`.
341 ///
342 /// ```text
343 /// ---- parser::control::tests::a_refused_frame_does_not_cost_the_frames_behind_it stdout ----
344 /// assertion `left == right` failed: a bad frame must not swallow the good
345 /// frames behind it, and must keep its place among them
346 /// left: [Ok(0), Err(3)]
347 /// right: [Ok(0), Err(3), Ok(0), Ok(0)]
348 /// ```
349 ///
350 /// The frame ahead of the bad one survives and the refusal is still
351 /// reported; both frames behind it are gone.
352 #[cfg(feature = "draft19")]
353 #[test]
354 fn a_refused_frame_does_not_cost_the_frames_behind_it() {
355 let mut parser = ControlStreamParser::new(DraftVersion::Draft19);
356
357 let mut wire = Vec::new();
358 wire.extend_from_slice(GOOD_FRAME);
359 wire.extend_from_slice(BAD_FRAME);
360 wire.extend_from_slice(GOOD_FRAME);
361 wire.extend_from_slice(GOOD_FRAME);
362
363 // The refused frame in its own position, not merely absent: a
364 // caller that forwards this sequence writes it in this order, and a
365 // refusal collected to one side would be written after the two good
366 // frames that followed it on the wire.
367 assert_eq!(
368 outcomes(parser.feed(&wire)),
369 vec![Ok(0), Err(0x03), Ok(0), Ok(0)],
370 "a bad frame must not swallow the good frames behind it, and must keep its place \
371 among them"
372 );
373 }
374
375 /// A refused frame comes back, with its bytes and its type.
376 ///
377 /// The parser used to step over one in silence. Two things were lost with
378 /// it. An observer could not tell a control message this proxy failed to
379 /// read from one the peer never sent — opposite conclusions, and for a
380 /// tool whose product is the account of the traffic, the wrong one to
381 /// default to. And on the mutating control pipe, where this parser *is*
382 /// the forwarding path, the frame was deleted from the session: the peer
383 /// received a stream with a message missing from the middle of it, and
384 /// neither endpoint had anything to attribute that to.
385 ///
386 /// Capturing mode is what the second half needs, so both modes are
387 /// checked here — the observation-only parser must not start copying
388 /// bytes it has no forwarding use for.
389 ///
390 /// *Ablation (measured):* drop the `items.push(ParsedItem::Refused(..))`
391 /// and step over the frame in silence, as the parser used to.
392 ///
393 /// ```text
394 /// ---- parser::control::tests::a_refused_frame_comes_back_with_its_bytes_and_its_type stdout ----
395 /// a whole frame arrived and the decoder refused it; waiting for more bytes
396 /// would hold a frame nothing will ever complete
397 /// ```
398 ///
399 /// `NeedMore` for a frame that is entirely present, which is the shape
400 /// of the original defect: nothing is coming that would make it
401 /// readable, and a caller told to wait holds bytes it is meant to be
402 /// forwarding. Three of this module's four refusal tests redden under
403 /// that cut, each in its own place.
404 #[cfg(feature = "draft19")]
405 #[test]
406 fn a_refused_frame_comes_back_with_its_bytes_and_its_type() {
407 let mut watching = ControlStreamParser::new(DraftVersion::Draft19);
408 match watching.feed(UNKNOWN_TYPE_FRAME) {
409 ParseResult::Framed(items) => match &items[..] {
410 [ParsedItem::Refused(r)] => {
411 assert_eq!(r.type_id, 0x3A, "the type the frame declared");
412 assert!(
413 r.raw_bytes.is_none(),
414 "the observation-only parser copies no frame it is not asked to forward"
415 );
416 }
417 other => panic!("expected one refused frame, got {other:?}"),
418 },
419 ParseResult::NeedMore => panic!(
420 "a whole frame arrived and the decoder refused it; waiting for more bytes would \
421 hold a frame nothing will ever complete"
422 ),
423 }
424
425 let mut forwarding = ControlStreamParser::new_capturing(DraftVersion::Draft19);
426 match forwarding.feed(UNKNOWN_TYPE_FRAME) {
427 ParseResult::Framed(items) => match &items[..] {
428 [ParsedItem::Refused(r)] => assert_eq!(
429 r.raw_bytes.as_deref(),
430 Some(UNKNOWN_TYPE_FRAME),
431 "the capturing parser owns the forwarding path, so it must hand back every \
432 byte it consumed"
433 ),
434 other => panic!("expected one refused frame, got {other:?}"),
435 },
436 ParseResult::NeedMore => panic!("a whole frame arrived and the decoder refused it"),
437 }
438 }
439
440 /// Two refusals of different types, each in its own place.
441 /// The interesting half is that the second is not folded into the first. A
442 /// caller reports the loss once per direction and counts every occurrence,
443 /// and it can only do both if the sequence distinguishes them; a parser
444 /// that recorded *something was refused* would make the count and the
445 /// report the same number.
446 ///
447 /// *Ablation (measured):* step over a refused frame in silence.
448 ///
449 /// ```text
450 /// ---- parser::control::tests::refusals_are_distinguished_from_each_other stdout ----
451 /// assertion `left == right` failed
452 /// left: [Ok(0)]
453 /// right: [Err(58), Ok(0), Err(59)]
454 /// ```
455 #[cfg(feature = "draft19")]
456 #[test]
457 fn refusals_are_distinguished_from_each_other() {
458 let mut parser = ControlStreamParser::new(DraftVersion::Draft19);
459
460 let mut wire = Vec::new();
461 wire.extend_from_slice(UNKNOWN_TYPE_FRAME);
462 wire.extend_from_slice(GOOD_FRAME);
463 wire.extend_from_slice(OTHER_UNKNOWN_TYPE_FRAME);
464
465 assert_eq!(outcomes(parser.feed(&wire)), vec![Err(0x3A), Ok(0), Err(0x3B)]);
466 }
467
468 /// The buffer is left in a state the next feed can use.
469 ///
470 /// Skipping a frame has to consume exactly its declared length: consume too
471 /// little and the parser resynchronises on payload bytes, too much and it
472 /// eats the frame behind it. Feeding the tail separately is what checks the
473 /// boundary, since a wrong skip leaves the good frame unparseable.
474 #[cfg(feature = "draft19")]
475 #[test]
476 fn a_refused_frame_leaves_the_parser_aligned_for_the_next_feed() {
477 let mut parser = ControlStreamParser::new(DraftVersion::Draft19);
478
479 // The bad frame alone: nothing decodes, and the parser says which
480 // frame it was rather than asking for bytes that would not help.
481 assert_eq!(outcomes(parser.feed(BAD_FRAME)), vec![Err(0x03)]);
482
483 // The next frame arrives on its own and must be found intact, which is
484 // only true if the skip landed on the frame boundary.
485 // `Ok` and not merely one item: a skip that landed a byte early
486 // or late still frames *something* out of the bytes that follow,
487 // and a length assertion alone would take that for the message.
488 match parser.feed(GOOD_FRAME) {
489 ParseResult::Framed(items) => assert_eq!(
490 outcomes(ParseResult::Framed(items)),
491 vec![Ok(0)],
492 "the frame after a refused one was lost or misread; the skip missed the boundary"
493 ),
494 ParseResult::NeedMore => {
495 panic!("the parser resynchronised in the wrong place after skipping a bad frame")
496 }
497 }
498 }
499}