mumble_server_runtime_protocol/
control.rs

1//! Typed decoding of TCP control-channel messages.
2//!
3//! [`decode_control`] maps a framed message (type code plus payload) to a typed
4//! [`ControlMessage`]. Every type prost-decodes its payload except one:
5//!
6//! REF: references/vendored/Mumble.proto : `message UDPTunnel` — "Not used".
7//! REF: references/vendored/protocol/Connection.cpp / ServerHandler.cpp : a TCP
8//!      frame of type `UDPTunnel` (1) carries a raw UDP audio packet at offset 6,
9//!      NOT an encoded `UDPTunnel` protobuf message. [`ControlMessage::UdpTunnel`]
10//!      therefore holds the raw bytes verbatim. `udp_tunnel_*` tests below fail if
11//!      this special case is ever "fixed" into a protobuf decode.
12
13use prost::Message;
14use thiserror::Error;
15
16use crate::framing::{Frame, FramingError, TcpMessageType, write_frame};
17use crate::messages::tcp;
18
19/// A decoded TCP control-channel message.
20#[derive(Debug, Clone, PartialEq)]
21pub enum ControlMessage {
22    Version(tcp::Version),
23    /// Raw UDP audio bytes tunneled over TCP — not the (unused) `UDPTunnel`
24    /// protobuf message. See the module REF notes.
25    UdpTunnel(Vec<u8>),
26    Authenticate(tcp::Authenticate),
27    Ping(tcp::Ping),
28    Reject(tcp::Reject),
29    ServerSync(tcp::ServerSync),
30    ChannelRemove(tcp::ChannelRemove),
31    ChannelState(tcp::ChannelState),
32    UserRemove(tcp::UserRemove),
33    UserState(tcp::UserState),
34    BanList(tcp::BanList),
35    TextMessage(tcp::TextMessage),
36    PermissionDenied(tcp::PermissionDenied),
37    Acl(tcp::Acl),
38    QueryUsers(tcp::QueryUsers),
39    CryptSetup(tcp::CryptSetup),
40    ContextActionModify(tcp::ContextActionModify),
41    ContextAction(tcp::ContextAction),
42    UserList(tcp::UserList),
43    VoiceTarget(tcp::VoiceTarget),
44    PermissionQuery(tcp::PermissionQuery),
45    CodecVersion(tcp::CodecVersion),
46    UserStats(tcp::UserStats),
47    RequestBlob(tcp::RequestBlob),
48    ServerConfig(tcp::ServerConfig),
49    SuggestConfig(tcp::SuggestConfig),
50    PluginDataTransmission(tcp::PluginDataTransmission),
51}
52
53/// Errors from decoding a control-channel message.
54#[derive(Debug, Error)]
55pub enum DecodeError {
56    /// The frame's type code is not a known Mumble TCP message.
57    #[error(transparent)]
58    Framing(#[from] FramingError),
59
60    /// The payload was not a valid protobuf encoding of its declared message.
61    #[error("protobuf decode failed for {message_type:?} ({payload_len} bytes): {source}")]
62    Protobuf {
63        message_type: TcpMessageType,
64        payload_len: usize,
65        source: prost::DecodeError,
66    },
67}
68
69/// Decode a control-channel message from a raw type code and payload.
70///
71/// Fails closed: an unknown type code or a malformed protobuf payload is an
72/// error, never a silently accepted message (R6).
73pub fn decode_control(message_type: u16, payload: &[u8]) -> Result<ControlMessage, DecodeError> {
74    let message_type = TcpMessageType::try_from(message_type)?;
75    Ok(match message_type {
76        // The load-bearing special case: raw audio, not a protobuf message.
77        TcpMessageType::UdpTunnel => ControlMessage::UdpTunnel(payload.to_vec()),
78
79        TcpMessageType::Version => ControlMessage::Version(decode_pb(message_type, payload)?),
80        TcpMessageType::Authenticate => {
81            ControlMessage::Authenticate(decode_pb(message_type, payload)?)
82        }
83        TcpMessageType::Ping => ControlMessage::Ping(decode_pb(message_type, payload)?),
84        TcpMessageType::Reject => ControlMessage::Reject(decode_pb(message_type, payload)?),
85        TcpMessageType::ServerSync => ControlMessage::ServerSync(decode_pb(message_type, payload)?),
86        TcpMessageType::ChannelRemove => {
87            ControlMessage::ChannelRemove(decode_pb(message_type, payload)?)
88        }
89        TcpMessageType::ChannelState => {
90            ControlMessage::ChannelState(decode_pb(message_type, payload)?)
91        }
92        TcpMessageType::UserRemove => ControlMessage::UserRemove(decode_pb(message_type, payload)?),
93        TcpMessageType::UserState => ControlMessage::UserState(decode_pb(message_type, payload)?),
94        TcpMessageType::BanList => ControlMessage::BanList(decode_pb(message_type, payload)?),
95        TcpMessageType::TextMessage => {
96            ControlMessage::TextMessage(decode_pb(message_type, payload)?)
97        }
98        TcpMessageType::PermissionDenied => {
99            ControlMessage::PermissionDenied(decode_pb(message_type, payload)?)
100        }
101        TcpMessageType::Acl => ControlMessage::Acl(decode_pb(message_type, payload)?),
102        TcpMessageType::QueryUsers => ControlMessage::QueryUsers(decode_pb(message_type, payload)?),
103        TcpMessageType::CryptSetup => ControlMessage::CryptSetup(decode_pb(message_type, payload)?),
104        TcpMessageType::ContextActionModify => {
105            ControlMessage::ContextActionModify(decode_pb(message_type, payload)?)
106        }
107        TcpMessageType::ContextAction => {
108            ControlMessage::ContextAction(decode_pb(message_type, payload)?)
109        }
110        TcpMessageType::UserList => ControlMessage::UserList(decode_pb(message_type, payload)?),
111        TcpMessageType::VoiceTarget => {
112            ControlMessage::VoiceTarget(decode_pb(message_type, payload)?)
113        }
114        TcpMessageType::PermissionQuery => {
115            ControlMessage::PermissionQuery(decode_pb(message_type, payload)?)
116        }
117        TcpMessageType::CodecVersion => {
118            ControlMessage::CodecVersion(decode_pb(message_type, payload)?)
119        }
120        TcpMessageType::UserStats => ControlMessage::UserStats(decode_pb(message_type, payload)?),
121        TcpMessageType::RequestBlob => {
122            ControlMessage::RequestBlob(decode_pb(message_type, payload)?)
123        }
124        TcpMessageType::ServerConfig => {
125            ControlMessage::ServerConfig(decode_pb(message_type, payload)?)
126        }
127        TcpMessageType::SuggestConfig => {
128            ControlMessage::SuggestConfig(decode_pb(message_type, payload)?)
129        }
130        TcpMessageType::PluginDataTransmission => {
131            ControlMessage::PluginDataTransmission(decode_pb(message_type, payload)?)
132        }
133    })
134}
135
136/// Decode a control-channel message directly from a parsed [`Frame`].
137pub fn decode_frame(frame: &Frame<'_>) -> Result<ControlMessage, DecodeError> {
138    decode_control(frame.message_type, frame.payload)
139}
140
141/// Encode a control message to its wire type code and payload — the inverse of
142/// [`decode_control`], so `decode_control(encode_control(m)) == m`.
143///
144/// The `UdpTunnel` special case is preserved in reverse: its bytes are the raw
145/// tunneled audio, emitted verbatim, never wrapped in the (unused) `UDPTunnel`
146/// protobuf message.
147pub fn encode_control(message: &ControlMessage) -> (u16, Vec<u8>) {
148    match message {
149        // The load-bearing special case: raw audio, emitted as-is.
150        ControlMessage::UdpTunnel(bytes) => (u16::from(TcpMessageType::UdpTunnel), bytes.clone()),
151
152        ControlMessage::Version(m) => encode_pb(TcpMessageType::Version, m),
153        ControlMessage::Authenticate(m) => encode_pb(TcpMessageType::Authenticate, m),
154        ControlMessage::Ping(m) => encode_pb(TcpMessageType::Ping, m),
155        ControlMessage::Reject(m) => encode_pb(TcpMessageType::Reject, m),
156        ControlMessage::ServerSync(m) => encode_pb(TcpMessageType::ServerSync, m),
157        ControlMessage::ChannelRemove(m) => encode_pb(TcpMessageType::ChannelRemove, m),
158        ControlMessage::ChannelState(m) => encode_pb(TcpMessageType::ChannelState, m),
159        ControlMessage::UserRemove(m) => encode_pb(TcpMessageType::UserRemove, m),
160        ControlMessage::UserState(m) => encode_pb(TcpMessageType::UserState, m),
161        ControlMessage::BanList(m) => encode_pb(TcpMessageType::BanList, m),
162        ControlMessage::TextMessage(m) => encode_pb(TcpMessageType::TextMessage, m),
163        ControlMessage::PermissionDenied(m) => encode_pb(TcpMessageType::PermissionDenied, m),
164        ControlMessage::Acl(m) => encode_pb(TcpMessageType::Acl, m),
165        ControlMessage::QueryUsers(m) => encode_pb(TcpMessageType::QueryUsers, m),
166        ControlMessage::CryptSetup(m) => encode_pb(TcpMessageType::CryptSetup, m),
167        ControlMessage::ContextActionModify(m) => encode_pb(TcpMessageType::ContextActionModify, m),
168        ControlMessage::ContextAction(m) => encode_pb(TcpMessageType::ContextAction, m),
169        ControlMessage::UserList(m) => encode_pb(TcpMessageType::UserList, m),
170        ControlMessage::VoiceTarget(m) => encode_pb(TcpMessageType::VoiceTarget, m),
171        ControlMessage::PermissionQuery(m) => encode_pb(TcpMessageType::PermissionQuery, m),
172        ControlMessage::CodecVersion(m) => encode_pb(TcpMessageType::CodecVersion, m),
173        ControlMessage::UserStats(m) => encode_pb(TcpMessageType::UserStats, m),
174        ControlMessage::RequestBlob(m) => encode_pb(TcpMessageType::RequestBlob, m),
175        ControlMessage::ServerConfig(m) => encode_pb(TcpMessageType::ServerConfig, m),
176        ControlMessage::SuggestConfig(m) => encode_pb(TcpMessageType::SuggestConfig, m),
177        ControlMessage::PluginDataTransmission(m) => {
178            encode_pb(TcpMessageType::PluginDataTransmission, m)
179        }
180    }
181}
182
183/// Encode a control message as a complete TCP frame (6-byte header plus payload)
184/// appended to `out`. Composes [`encode_control`] with [`write_frame`], so
185/// `decode_frame(parse_frame(encode_frame(m))) == m`.
186pub fn encode_frame(message: &ControlMessage, out: &mut Vec<u8>) -> Result<(), FramingError> {
187    let (message_type, payload) = encode_control(message);
188    write_frame(message_type, &payload, out)
189}
190
191/// Prost-encode a message and tag it with its wire type code.
192fn encode_pb<M: Message>(message_type: TcpMessageType, message: &M) -> (u16, Vec<u8>) {
193    (u16::from(message_type), message.encode_to_vec())
194}
195
196/// Prost-decode a payload into a message, attaching the message type and length
197/// on failure so a decode error names what and where it went wrong.
198fn decode_pb<M: Message + Default>(
199    message_type: TcpMessageType,
200    payload: &[u8],
201) -> Result<M, DecodeError> {
202    M::decode(payload).map_err(|source| DecodeError::Protobuf {
203        message_type,
204        payload_len: payload.len(),
205        source,
206    })
207}
208
209#[cfg(test)]
210mod tests {
211    // See framing.rs for why the test module allows expect_used under -D warnings.
212    #![allow(clippy::expect_used)]
213
214    use super::*;
215
216    #[test]
217    fn decodes_a_normal_protobuf_message() {
218        let version = tcp::Version {
219            release: Some("mumble-server-runtime".to_string()),
220            ..Default::default()
221        };
222        let decoded = decode_control(0, &version.encode_to_vec()).expect("decode Version");
223        assert_eq!(decoded, ControlMessage::Version(version));
224    }
225
226    #[test]
227    fn udp_tunnel_payload_is_returned_raw() {
228        // A raw audio packet, deliberately not a valid `UDPTunnel` protobuf.
229        let raw_audio = vec![0x80u8, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF];
230        let decoded = decode_control(1, &raw_audio).expect("decode UDPTunnel");
231        assert_eq!(decoded, ControlMessage::UdpTunnel(raw_audio));
232    }
233
234    #[test]
235    fn udp_tunnel_is_raw_even_when_bytes_are_valid_protobuf() {
236        // The strongest form of the guard: bytes that ARE a valid `UDPTunnel`
237        // protobuf must still come back verbatim as raw audio, never unwrapped
238        // into the protobuf's `packet` field. If the dispatch is ever changed to
239        // prost-decode type 1, this assertion breaks.
240        let looks_like_protobuf = tcp::UdpTunnel {
241            packet: vec![0xDE, 0xAD, 0xBE, 0xEF],
242        }
243        .encode_to_vec();
244        let decoded = decode_control(1, &looks_like_protobuf).expect("decode UDPTunnel");
245        assert_eq!(decoded, ControlMessage::UdpTunnel(looks_like_protobuf));
246    }
247
248    #[test]
249    fn unknown_type_code_fails_closed() {
250        match decode_control(99, &[]) {
251            Err(DecodeError::Framing(FramingError::UnknownMessageType(99))) => {}
252            other => panic!("expected unknown-type error, got {other:?}"),
253        }
254    }
255
256    #[test]
257    fn encode_then_decode_roundtrips_a_protobuf_message() {
258        let message = ControlMessage::ServerSync(tcp::ServerSync {
259            session: Some(42),
260            max_bandwidth: Some(72_000),
261            welcome_text: Some("hi".to_string()),
262            permissions: Some(0),
263        });
264        let (message_type, payload) = encode_control(&message);
265        assert_eq!(
266            decode_control(message_type, &payload).expect("re-decode"),
267            message
268        );
269    }
270
271    #[test]
272    fn encode_preserves_udp_tunnel_raw_bytes() {
273        // The raw-audio special case must survive a round trip byte-for-byte and
274        // stay on type code 1, never become a protobuf-encoded UDPTunnel.
275        let raw_audio = vec![0x00u8, 0x18, 0x02, 0x20, 0xB0, 0x05, 0xDE, 0xAD];
276        let message = ControlMessage::UdpTunnel(raw_audio.clone());
277        let (message_type, payload) = encode_control(&message);
278        assert_eq!(message_type, u16::from(TcpMessageType::UdpTunnel));
279        assert_eq!(payload, raw_audio);
280        assert_eq!(
281            decode_control(message_type, &payload).expect("re-decode"),
282            message
283        );
284    }
285
286    #[test]
287    fn encode_frame_then_parse_roundtrips() {
288        let message = ControlMessage::Version(tcp::Version {
289            release: Some("mumble-server-runtime".to_string()),
290            ..Default::default()
291        });
292        let mut framed = Vec::new();
293        encode_frame(&message, &mut framed).expect("encode frame");
294        let frame = crate::framing::parse_frame(&framed)
295            .expect("parse")
296            .expect("a complete frame");
297        assert_eq!(decode_frame(&frame).expect("decode frame"), message);
298    }
299
300    #[test]
301    fn malformed_protobuf_is_an_error_not_a_default() {
302        // A truncated varint for a Version field: prost must reject it rather
303        // than yielding a default message.
304        let malformed = [0x08u8]; // field 1, varint, but no value byte follows
305        match decode_control(0, &malformed) {
306            Err(DecodeError::Protobuf {
307                message_type: TcpMessageType::Version,
308                ..
309            }) => {}
310            other => panic!("expected protobuf decode error, got {other:?}"),
311        }
312    }
313}