Skip to main content

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}