mumble_server_runtime_protocol/
framing.rs

1//! TCP control-channel framing for the Mumble protocol.
2//!
3//! Every TCP message (after TLS termination) is length-prefixed with a fixed
4//! six-byte header:
5//!
6//! ```text
7//! [ type: u16 big-endian ][ length: u32 big-endian ][ payload: `length` bytes ]
8//! ```
9//!
10//! REF: references/vendored/protocol/Connection.cpp : Connection::socketRead
11//!      (reads 6 header bytes, then `iPacketLength` payload bytes; incremental).
12//! REF: references/vendored/protocol/Connection.cpp : `if (iPacketLength > 0x7fffff)`
13//!      (a larger declared length is a "huge packet" and the peer is dropped).
14//! REF: references/vendored/protocol/MumbleProtocol.h : TCPMessageType (type codes 0..=26).
15//!
16//! This module is deliberately dumb about semantics: it extracts the raw type
17//! code and the payload bytes. Mapping the code to a known message type, and
18//! rejecting unknown codes, is [`TcpMessageType::try_from`]'s job, kept separate
19//! so a partial or unknown frame is never confused with a malformed one.
20
21use thiserror::Error;
22
23/// Fixed size of a TCP message header: a 2-byte type plus a 4-byte length.
24/// REF: Connection.cpp : `unsigned char a_ucBuffer[6]`.
25pub const HEADER_LEN: usize = 6;
26
27/// Largest payload length the framer accepts. A declared length above this is
28/// treated as hostile; the caller must drop the connection (fail closed, R6).
29/// REF: Connection.cpp : `if (iPacketLength > 0x7fffff) { ... "huge packet" ... }`.
30pub const MAX_PAYLOAD_LEN: u32 = 0x7f_ffff;
31
32/// Errors produced while framing the TCP control channel.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
34pub enum FramingError {
35    /// The header declared a payload longer than [`MAX_PAYLOAD_LEN`]. Carries the
36    /// declared length so the caller can log which connection misbehaved.
37    #[error("declared payload length {declared} exceeds maximum {max}")]
38    PayloadTooLarge { declared: u32, max: u32 },
39
40    /// The type code does not correspond to any known Mumble TCP message.
41    #[error("unknown TCP message type code {0}")]
42    UnknownMessageType(u16),
43}
44
45/// One complete TCP frame borrowed from an input buffer.
46///
47/// [`Frame::message_type`] is the raw wire code; use [`TcpMessageType::try_from`]
48/// to resolve it to a known message. [`Frame::total_len`] is the number of bytes
49/// the caller should consume from the front of the buffer before parsing again.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Frame<'a> {
52    pub message_type: u16,
53    pub payload: &'a [u8],
54}
55
56impl Frame<'_> {
57    /// Total bytes this frame occupies on the wire (header plus payload). Never
58    /// overflows: the payload length was bounded by [`MAX_PAYLOAD_LEN`] at parse.
59    pub fn total_len(&self) -> usize {
60        HEADER_LEN + self.payload.len()
61    }
62}
63
64/// Try to parse a single frame from the front of `buf`.
65///
66/// - `Ok(Some(frame))` — a complete frame is present; consume `frame.total_len()`
67///   bytes and call again for the next one.
68/// - `Ok(None)` — the buffer holds only a partial frame; append more bytes and
69///   retry. This is how incremental parsing over arbitrarily split TCP chunks
70///   works: the same partial buffer is re-offered once it has grown.
71/// - `Err(_)` — a hard protocol violation; the connection must be dropped.
72pub fn parse_frame(buf: &[u8]) -> Result<Option<Frame<'_>>, FramingError> {
73    let header = match buf.get(..HEADER_LEN) {
74        Some(header) => header,
75        None => return Ok(None),
76    };
77
78    // `header` is exactly HEADER_LEN bytes, so these fixed indices are in bounds.
79    let message_type = u16::from_be_bytes([header[0], header[1]]);
80    let declared = u32::from_be_bytes([header[2], header[3], header[4], header[5]]);
81
82    if declared > MAX_PAYLOAD_LEN {
83        return Err(FramingError::PayloadTooLarge {
84            declared,
85            max: MAX_PAYLOAD_LEN,
86        });
87    }
88
89    // `declared <= MAX_PAYLOAD_LEN` (well within usize), so both conversions and
90    // the addition below cannot overflow, but we still check rather than assume.
91    let payload_len = usize::try_from(declared).map_err(|_| FramingError::PayloadTooLarge {
92        declared,
93        max: MAX_PAYLOAD_LEN,
94    })?;
95    let end = HEADER_LEN
96        .checked_add(payload_len)
97        .ok_or(FramingError::PayloadTooLarge {
98            declared,
99            max: MAX_PAYLOAD_LEN,
100        })?;
101
102    match buf.get(HEADER_LEN..end) {
103        Some(payload) => Ok(Some(Frame {
104            message_type,
105            payload,
106        })),
107        None => Ok(None),
108    }
109}
110
111/// Encode a frame (header plus payload) onto `out`. The inverse of
112/// [`parse_frame`]; `parse_frame(write_frame(t, p)) == (t, p)`.
113///
114/// Refuses payloads longer than [`MAX_PAYLOAD_LEN`] rather than emitting a frame
115/// the wire would reject.
116pub fn write_frame(
117    message_type: u16,
118    payload: &[u8],
119    out: &mut Vec<u8>,
120) -> Result<(), FramingError> {
121    let declared = u32::try_from(payload.len()).map_err(|_| FramingError::PayloadTooLarge {
122        declared: MAX_PAYLOAD_LEN.saturating_add(1),
123        max: MAX_PAYLOAD_LEN,
124    })?;
125    if declared > MAX_PAYLOAD_LEN {
126        return Err(FramingError::PayloadTooLarge {
127            declared,
128            max: MAX_PAYLOAD_LEN,
129        });
130    }
131    out.extend_from_slice(&message_type.to_be_bytes());
132    out.extend_from_slice(&declared.to_be_bytes());
133    out.extend_from_slice(payload);
134    Ok(())
135}
136
137/// The TCP message type register.
138///
139/// REF: references/vendored/protocol/MumbleProtocol.h : `MUMBLE_ALL_TCP_MESSAGES`
140///      X-macro (`Version = 0` .. `PluginDataTransmission = 26`). Variant names
141///      are idiomatic Rust; the numeric codes are the wire truth.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143#[repr(u16)]
144pub enum TcpMessageType {
145    Version = 0,
146    /// Special case: the payload is raw UDP audio bytes, not a protobuf message.
147    /// REF: ServerHandler.cpp : `memcpy(uc + 6, data, len)` under `UDPTunnel`.
148    UdpTunnel = 1,
149    Authenticate = 2,
150    Ping = 3,
151    Reject = 4,
152    ServerSync = 5,
153    ChannelRemove = 6,
154    ChannelState = 7,
155    UserRemove = 8,
156    UserState = 9,
157    BanList = 10,
158    TextMessage = 11,
159    PermissionDenied = 12,
160    Acl = 13,
161    QueryUsers = 14,
162    CryptSetup = 15,
163    ContextActionModify = 16,
164    ContextAction = 17,
165    UserList = 18,
166    VoiceTarget = 19,
167    PermissionQuery = 20,
168    CodecVersion = 21,
169    UserStats = 22,
170    RequestBlob = 23,
171    ServerConfig = 24,
172    SuggestConfig = 25,
173    PluginDataTransmission = 26,
174}
175
176impl TryFrom<u16> for TcpMessageType {
177    type Error = FramingError;
178
179    fn try_from(value: u16) -> Result<Self, Self::Error> {
180        Ok(match value {
181            0 => Self::Version,
182            1 => Self::UdpTunnel,
183            2 => Self::Authenticate,
184            3 => Self::Ping,
185            4 => Self::Reject,
186            5 => Self::ServerSync,
187            6 => Self::ChannelRemove,
188            7 => Self::ChannelState,
189            8 => Self::UserRemove,
190            9 => Self::UserState,
191            10 => Self::BanList,
192            11 => Self::TextMessage,
193            12 => Self::PermissionDenied,
194            13 => Self::Acl,
195            14 => Self::QueryUsers,
196            15 => Self::CryptSetup,
197            16 => Self::ContextActionModify,
198            17 => Self::ContextAction,
199            18 => Self::UserList,
200            19 => Self::VoiceTarget,
201            20 => Self::PermissionQuery,
202            21 => Self::CodecVersion,
203            22 => Self::UserStats,
204            23 => Self::RequestBlob,
205            24 => Self::ServerConfig,
206            25 => Self::SuggestConfig,
207            26 => Self::PluginDataTransmission,
208            other => return Err(FramingError::UnknownMessageType(other)),
209        })
210    }
211}
212
213impl From<TcpMessageType> for u16 {
214    fn from(value: TcpMessageType) -> Self {
215        value as u16
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    // `expect_used` is workspace-warn (allowed but monitored) and CI runs with
222    // `-D warnings`, which would promote it to an error. Tests legitimately use
223    // `.expect(msg)` to assert Results; production code above stays strict.
224    #![allow(clippy::expect_used)]
225
226    use super::*;
227
228    #[test]
229    fn empty_buffer_needs_more() {
230        assert_eq!(parse_frame(&[]).expect("no error"), None);
231    }
232
233    #[test]
234    fn partial_header_needs_more() {
235        // Five bytes: one short of a header.
236        assert_eq!(parse_frame(&[0, 1, 0, 0, 0]).expect("no error"), None);
237    }
238
239    #[test]
240    fn partial_payload_needs_more() {
241        // Header declares 4 payload bytes but only 2 are present.
242        let buf = [0x00, 0x09, 0x00, 0x00, 0x00, 0x04, 0xAA, 0xBB];
243        assert_eq!(parse_frame(&buf).expect("no error"), None);
244    }
245
246    #[test]
247    fn parses_complete_frame() {
248        // type = 9 (UserState), length = 3, payload = [1,2,3].
249        let buf = [0x00, 0x09, 0x00, 0x00, 0x00, 0x03, 1, 2, 3];
250        let frame = parse_frame(&buf).expect("no error").expect("a frame");
251        assert_eq!(frame.message_type, 9);
252        assert_eq!(frame.payload, &[1, 2, 3]);
253        assert_eq!(frame.total_len(), 9);
254    }
255
256    #[test]
257    fn parses_empty_payload_frame() {
258        // A zero-length payload is legal (e.g. an empty Ping body).
259        let buf = [0x00, 0x03, 0x00, 0x00, 0x00, 0x00];
260        let frame = parse_frame(&buf).expect("no error").expect("a frame");
261        assert_eq!(frame.message_type, 3);
262        assert_eq!(frame.payload, &[] as &[u8]);
263        assert_eq!(frame.total_len(), HEADER_LEN);
264    }
265
266    #[test]
267    fn ignores_trailing_bytes_of_next_frame() {
268        // One full frame followed by the start of another; only the first parses.
269        let mut buf = Vec::new();
270        write_frame(7, &[0xDE, 0xAD], &mut buf).expect("write");
271        buf.extend_from_slice(&[0x00, 0x05]); // partial next header
272        let frame = parse_frame(&buf).expect("no error").expect("a frame");
273        assert_eq!(frame.message_type, 7);
274        assert_eq!(frame.payload, &[0xDE, 0xAD]);
275        assert_eq!(frame.total_len(), 8);
276    }
277
278    #[test]
279    fn rejects_oversized_payload() {
280        // Declared length MAX + 1.
281        let declared = MAX_PAYLOAD_LEN + 1;
282        let mut buf = vec![0x00, 0x0B];
283        buf.extend_from_slice(&declared.to_be_bytes());
284        assert_eq!(
285            parse_frame(&buf),
286            Err(FramingError::PayloadTooLarge {
287                declared,
288                max: MAX_PAYLOAD_LEN,
289            })
290        );
291    }
292
293    #[test]
294    fn accepts_max_length_header() {
295        // A header declaring exactly MAX is not itself an error; it just needs
296        // that many payload bytes. With none present, the framer asks for more.
297        let mut buf = vec![0x00, 0x0B];
298        buf.extend_from_slice(&MAX_PAYLOAD_LEN.to_be_bytes());
299        assert_eq!(parse_frame(&buf).expect("not too large"), None);
300    }
301
302    #[test]
303    fn incremental_growth_yields_frame_once_complete() {
304        let full = {
305            let mut buf = Vec::new();
306            write_frame(2, &[1, 2, 3, 4, 5], &mut buf).expect("write");
307            buf
308        };
309        // Feeding one byte at a time returns None until the last byte arrives.
310        for len in 0..full.len() {
311            assert_eq!(
312                parse_frame(&full[..len]).expect("no error"),
313                None,
314                "len {len}"
315            );
316        }
317        let frame = parse_frame(&full).expect("no error").expect("a frame");
318        assert_eq!(frame.message_type, 2);
319        assert_eq!(frame.payload, &[1, 2, 3, 4, 5]);
320    }
321
322    #[test]
323    fn drains_multiple_frames_in_sequence() {
324        let mut buf = Vec::new();
325        write_frame(0, b"ver", &mut buf).expect("write");
326        write_frame(3, b"", &mut buf).expect("write");
327        write_frame(9, b"user", &mut buf).expect("write");
328
329        let mut cursor = &buf[..];
330        let mut seen = Vec::new();
331        while let Some(frame) = parse_frame(cursor).expect("no error") {
332            seen.push((frame.message_type, frame.payload.to_vec()));
333            cursor = &cursor[frame.total_len()..];
334        }
335        assert_eq!(
336            seen,
337            vec![
338                (0u16, b"ver".to_vec()),
339                (3, b"".to_vec()),
340                (9, b"user".to_vec()),
341            ]
342        );
343        assert!(cursor.is_empty());
344    }
345
346    #[test]
347    fn roundtrip_write_then_parse() {
348        for message_type in 0u16..=26 {
349            let payload: Vec<u8> = (0..message_type as usize).map(|i| i as u8).collect();
350            let mut buf = Vec::new();
351            write_frame(message_type, &payload, &mut buf).expect("write");
352            let frame = parse_frame(&buf).expect("no error").expect("a frame");
353            assert_eq!(frame.message_type, message_type);
354            assert_eq!(frame.payload, &payload[..]);
355        }
356    }
357
358    #[test]
359    fn all_known_type_codes_resolve() {
360        for code in 0u16..=26 {
361            let resolved = TcpMessageType::try_from(code).expect("known code");
362            assert_eq!(u16::from(resolved), code);
363        }
364    }
365
366    #[test]
367    fn unknown_type_code_is_rejected() {
368        assert_eq!(
369            TcpMessageType::try_from(27),
370            Err(FramingError::UnknownMessageType(27))
371        );
372        assert_eq!(
373            TcpMessageType::try_from(u16::MAX),
374            Err(FramingError::UnknownMessageType(u16::MAX))
375        );
376    }
377
378    #[test]
379    fn udp_tunnel_is_type_one() {
380        // The audio-tunnel special case must stay at code 1.
381        // REF: MumbleProtocol.h : PROCESS_MUMBLE_TCP_MESSAGE(UDPTunnel, 1).
382        assert_eq!(u16::from(TcpMessageType::UdpTunnel), 1);
383        assert_eq!(
384            TcpMessageType::try_from(1).expect("known"),
385            TcpMessageType::UdpTunnel
386        );
387    }
388}