mumble_server_runtime_gateway/
handshake.rs

1//! The pure server-to-client handshake sequence.
2//!
3//! [`prelude`] and [`completion`] are the lifecycle frames that bracket the
4//! connection's first view: crypto and codec setup before it, `ServerSync` and
5//! `ServerConfig` after it. The view in between is whatever the shard pushed
6//! onto this connection's queue, so there is exactly one rendering path in the
7//! runtime and the handshake is not a second one.
8//!
9//! The order is authoritative, traced to Murmur (R1):
10//! REF: references/mumble/src/murmur/Messages.cpp : `Server::msgAuthenticate` -
11//!   CryptSetup, CodecVersion, ChannelState (root first, parents before
12//!   children), UserState(self), UserState(others), ServerSync, ServerConfig.
13//! REF: references/mumble/src/murmur/Server.cpp : `Server::encrypted` - the
14//!   server's own `Version` goes out as soon as TLS completes, BEFORE
15//!   Authenticate; it is therefore [`server_version`] and not part of this
16//!   sequence.
17
18use mumble_server_runtime_protocol::ControlMessage;
19use mumble_server_runtime_protocol::messages::tcp;
20use mumble_server_runtime_shard::SessionId;
21
22use crate::config::GatewayConfig;
23
24/// Effective-permission bits, as the Mumble client understands them.
25///
26/// Re-exported rather than restated: the shard answers `PermissionQuery` from
27/// the same bits, and two definitions of one constant is one definition too
28/// many.
29pub use mumble_server_runtime_shard::perm;
30
31/// The server's own `Version`, sent immediately after the TLS handshake
32/// completes and before the client's `Authenticate`.
33#[must_use]
34pub fn server_version(config: &GatewayConfig) -> ControlMessage {
35    let (major, minor, patch) = config.version;
36    ControlMessage::Version(tcp::Version {
37        // The legacy v1 field stays in sync for older introspection; a 1.5+
38        // client reads v2 (REF Version.h: each component is a u16).
39        version_v1: Some(
40            (u32::from(major) << 16) | (u32::from(minor) << 8) | u32::from(patch.min(0xFF)),
41        ),
42        version_v2: Some(config.version_v2()),
43        release: Some(format!("Mumble Server Runtime {major}.{minor}.{patch}")),
44        os: Some("Mumble Server Runtime".to_owned()),
45        os_version: None,
46    })
47}
48
49/// Lifecycle messages that precede the connection's first view.
50#[must_use]
51pub fn prelude(crypt_setup: tcp::CryptSetup) -> Vec<ControlMessage> {
52    vec![
53        ControlMessage::CryptSetup(crypt_setup),
54        // Opus only. REF Server.cpp: the reset state is
55        // `iCodecAlpha = iCodecBeta = 0; bPreferAlpha = false;`.
56        ControlMessage::CodecVersion(tcp::CodecVersion {
57            alpha: 0,
58            beta: 0,
59            prefer_alpha: false,
60            opus: Some(true),
61        }),
62    ]
63}
64
65/// Lifecycle messages that complete it.
66#[must_use]
67pub fn completion(config: &GatewayConfig, session: SessionId) -> Vec<ControlMessage> {
68    vec![
69        // The client learns its own session here, after the view that
70        // introduced it (invariant 6).
71        ControlMessage::ServerSync(tcp::ServerSync {
72            session: Some(session.0),
73            max_bandwidth: Some(config.max_bandwidth),
74            welcome_text: if config.welcome_text.is_empty() {
75                None
76            } else {
77                Some(config.welcome_text.clone())
78            },
79            permissions: Some(u64::from(perm::DEFAULT)),
80        }),
81        ControlMessage::ServerConfig(tcp::ServerConfig {
82            max_bandwidth: Some(config.max_bandwidth),
83            welcome_text: None,
84            allow_html: Some(config.allow_html),
85            message_length: Some(config.message_length),
86            image_message_length: None,
87            max_users: Some(config.max_users),
88            recording_allowed: Some(config.recording_allowed),
89        }),
90    ]
91}
92
93/// Refuse a connection before it is attached to anything.
94///
95/// REF: references/vendored/Mumble.proto : `Reject.RejectType`.
96#[must_use]
97pub fn reject(reason: &str) -> ControlMessage {
98    ControlMessage::Reject(tcp::Reject {
99        r#type: Some(i32::from(tcp::reject::RejectType::None)),
100        reason: Some(reason.to_owned()),
101    })
102}
103
104#[cfg(test)]
105mod tests {
106    #![allow(clippy::expect_used)]
107
108    use super::*;
109
110    fn crypt() -> tcp::CryptSetup {
111        tcp::CryptSetup {
112            key: Some(vec![0u8; 16]),
113            client_nonce: Some(vec![0u8; 16]),
114            server_nonce: Some(vec![0u8; 16]),
115        }
116    }
117
118    #[test]
119    fn the_prelude_sets_up_crypto_then_the_codec() {
120        let messages = prelude(crypt());
121        assert!(matches!(
122            messages.first(),
123            Some(ControlMessage::CryptSetup(_))
124        ));
125        assert!(matches!(
126            messages.get(1),
127            Some(ControlMessage::CodecVersion(_))
128        ));
129        assert_eq!(messages.len(), 2);
130    }
131
132    #[test]
133    fn the_completion_syncs_before_it_configures() {
134        let messages = completion(&GatewayConfig::default(), SessionId(7));
135        match messages.first() {
136            Some(ControlMessage::ServerSync(sync)) => assert_eq!(sync.session, Some(7)),
137            other => panic!("expected ServerSync first, got {other:?}"),
138        }
139        assert!(matches!(
140            messages.get(1),
141            Some(ControlMessage::ServerConfig(_))
142        ));
143    }
144}