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: 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}