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: 16-bit BE on drafts 11-22, a varint in
146 // the draft's own encoding on drafts 07-10 and 23.
147 let (payload_len, total) = if self.draft.uses_fixed_length_framing() {
148 // Drafts 11-22: 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 // Drafts 07-10 and 23: 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 the 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 the framing of drafts 11-22,
292 /// which draft-19 is one of. 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 /// Breaking out of the loop on a decode error, having already advanced
333 /// past the frame, would drop everything buffered behind the bad frame
334 /// with it — so one malformed message could cost an arbitrary number of
335 /// good ones, and the stricter the codec about draft-19's MUSTs, the more
336 /// of the stream a single refusal would take with it. That is backwards
337 /// for a proxy whose purpose is to report the traffic it sees.
338 #[cfg(feature = "draft19")]
339 #[test]
340 fn a_refused_frame_does_not_cost_the_frames_behind_it() {
341 let mut parser = ControlStreamParser::new(DraftVersion::Draft19);
342
343 let mut wire = Vec::new();
344 wire.extend_from_slice(GOOD_FRAME);
345 wire.extend_from_slice(BAD_FRAME);
346 wire.extend_from_slice(GOOD_FRAME);
347 wire.extend_from_slice(GOOD_FRAME);
348
349 // The refused frame in its own position, not merely absent: a
350 // caller that forwards this sequence writes it in this order, and a
351 // refusal collected to one side would be written after the two good
352 // frames that followed it on the wire.
353 assert_eq!(
354 outcomes(parser.feed(&wire)),
355 vec![Ok(0), Err(0x03), Ok(0), Ok(0)],
356 "a bad frame must not swallow the good frames behind it, and must keep its place \
357 among them"
358 );
359 }
360
361 /// A refused frame comes back, with its bytes and its type.
362 ///
363 /// Stepping over one in silence would lose two things. An observer could
364 /// not tell a control message this proxy failed to read from one the peer
365 /// never sent — opposite conclusions, and for a tool whose product is the
366 /// account of the traffic, the wrong one to default to. And on the
367 /// mutating control pipe, where this parser *is* the forwarding path, the
368 /// frame would be deleted from the session: the peer would receive a
369 /// stream with a message missing from the middle of it, and neither
370 /// endpoint would have anything to attribute that to.
371 ///
372 /// Capturing mode is what the second half needs, so both modes are
373 /// checked here — the observation-only parser must not start copying
374 /// bytes it has no forwarding use for.
375 #[cfg(feature = "draft19")]
376 #[test]
377 fn a_refused_frame_comes_back_with_its_bytes_and_its_type() {
378 let mut watching = ControlStreamParser::new(DraftVersion::Draft19);
379 match watching.feed(UNKNOWN_TYPE_FRAME) {
380 ParseResult::Framed(items) => match &items[..] {
381 [ParsedItem::Refused(r)] => {
382 assert_eq!(r.type_id, 0x3A, "the type the frame declared");
383 assert!(
384 r.raw_bytes.is_none(),
385 "the observation-only parser copies no frame it is not asked to forward"
386 );
387 }
388 other => panic!("expected one refused frame, got {other:?}"),
389 },
390 ParseResult::NeedMore => panic!(
391 "a whole frame arrived and the decoder refused it; waiting for more bytes would \
392 hold a frame nothing will ever complete"
393 ),
394 }
395
396 let mut forwarding = ControlStreamParser::new_capturing(DraftVersion::Draft19);
397 match forwarding.feed(UNKNOWN_TYPE_FRAME) {
398 ParseResult::Framed(items) => match &items[..] {
399 [ParsedItem::Refused(r)] => assert_eq!(
400 r.raw_bytes.as_deref(),
401 Some(UNKNOWN_TYPE_FRAME),
402 "the capturing parser owns the forwarding path, so it must hand back every \
403 byte it consumed"
404 ),
405 other => panic!("expected one refused frame, got {other:?}"),
406 },
407 ParseResult::NeedMore => panic!("a whole frame arrived and the decoder refused it"),
408 }
409 }
410
411 /// Two refusals of different types, each in its own place.
412 /// The interesting half is that the second is not folded into the first. A
413 /// caller reports the loss once per direction and counts every occurrence,
414 /// and it can only do both if the sequence distinguishes them; a parser
415 /// that recorded *something was refused* would make the count and the
416 /// report the same number.
417 #[cfg(feature = "draft19")]
418 #[test]
419 fn refusals_are_distinguished_from_each_other() {
420 let mut parser = ControlStreamParser::new(DraftVersion::Draft19);
421
422 let mut wire = Vec::new();
423 wire.extend_from_slice(UNKNOWN_TYPE_FRAME);
424 wire.extend_from_slice(GOOD_FRAME);
425 wire.extend_from_slice(OTHER_UNKNOWN_TYPE_FRAME);
426
427 assert_eq!(outcomes(parser.feed(&wire)), vec![Err(0x3A), Ok(0), Err(0x3B)]);
428 }
429
430 /// The buffer is left in a state the next feed can use.
431 ///
432 /// Skipping a frame has to consume exactly its declared length: consume too
433 /// little and the parser resynchronises on payload bytes, too much and it
434 /// eats the frame behind it. Feeding the tail separately is what checks the
435 /// boundary, since a wrong skip leaves the good frame unparseable.
436 #[cfg(feature = "draft19")]
437 #[test]
438 fn a_refused_frame_leaves_the_parser_aligned_for_the_next_feed() {
439 let mut parser = ControlStreamParser::new(DraftVersion::Draft19);
440
441 // The bad frame alone: nothing decodes, and the parser says which
442 // frame it was rather than asking for bytes that would not help.
443 assert_eq!(outcomes(parser.feed(BAD_FRAME)), vec![Err(0x03)]);
444
445 // The next frame arrives on its own and must be found intact, which is
446 // only true if the skip landed on the frame boundary.
447 // `Ok` and not merely one item: a skip that landed a byte early
448 // or late still frames *something* out of the bytes that follow,
449 // and a length assertion alone would take that for the message.
450 match parser.feed(GOOD_FRAME) {
451 ParseResult::Framed(items) => assert_eq!(
452 outcomes(ParseResult::Framed(items)),
453 vec![Ok(0)],
454 "the frame after a refused one was lost or misread; the skip missed the boundary"
455 ),
456 ParseResult::NeedMore => {
457 panic!("the parser resynchronised in the wrong place after skipping a bad frame")
458 }
459 }
460 }
461}