//! Reliable control-plane messages for the LAN party tunnel. //! //! QUIC streams carry these messages as length-prefixed JSON frames. The crate //! defines the typed handshake/status model and the small framing layer needed //! by client, relay, and gateway stream handlers. use std::{fmt, str::FromStr}; mod codec; pub use codec::{ CONTROL_LENGTH_PREFIX_LEN, ControlCodecError, MAX_CONTROL_MESSAGE_LEN, complete_control_frame_len, decode_control_frame, encode_control_message, }; pub use lanparty_obs::TunnelStats; use lanparty_proto::{MIN_USEFUL_TAP_MTU, MacAddr, MtuError, recommended_tap_mtu}; use thiserror::Error; pub const RELAY_ALPN: &[u8] = b"lanparty-l2/1"; pub const CONTROL_PROTOCOL_VERSION: u16 = 1; pub const MIN_ROOM_CODE_LEN: usize = 1; pub const MAX_ROOM_CODE_LEN: usize = 64; #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum ControlError { #[error("room code cannot be empty")] EmptyRoomCode, #[error("room code is too long: got {len}, maximum is {max}")] RoomCodeTooLong { len: usize, max: usize }, #[error("invalid room code character at byte {index}: {byte:#04x}")] InvalidRoomCodeByte { index: usize, byte: u8 }, #[error("unsupported control protocol version {actual}; supported version is {supported}")] UnsupportedVersion { actual: u16, supported: u16 }, #[error("client hello must announce a MAC address")] MissingClientMac, #[error("gateway hello must not announce a client MAC address")] UnexpectedGatewayMac, #[error("client MAC {mac} is not a locally administered unicast address")] InvalidClientMac { mac: MacAddr }, #[error("peer id 0 is reserved")] InvalidPeerId, #[error("effective TAP MTU {mtu} is below the minimum useful MTU {minimum}")] EffectiveMtuTooSmall { mtu: u16, minimum: usize }, #[error(transparent)] Mtu(#[from] MtuError), } #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] #[serde(try_from = "String", into = "String")] pub struct RoomCode(String); impl RoomCode { pub fn new(value: impl Into) -> Result { let value = value.into(); validate_room_code(&value)?; Ok(Self(value)) } #[must_use] pub fn as_str(&self) -> &str { &self.0 } } impl fmt::Display for RoomCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } impl FromStr for RoomCode { type Err = ControlError; fn from_str(value: &str) -> Result { Self::new(value) } } impl TryFrom for RoomCode { type Error = ControlError; fn try_from(value: String) -> Result { Self::new(value) } } impl From for String { fn from(value: RoomCode) -> Self { value.0 } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum Role { Client, Gateway, } #[derive( Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, )] pub enum ConnectionMode { #[default] #[serde(rename = "relay")] Relay, #[serde(rename = "direct-p2p")] DirectP2p, #[serde(rename = "direct-failed-relay-fallback")] DirectFailedRelayFallback, } impl fmt::Display for ConnectionMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Relay => f.write_str("relay"), Self::DirectP2p => f.write_str("direct-p2p"), Self::DirectFailedRelayFallback => f.write_str("direct-failed-relay-fallback"), } } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub struct EndpointHello { protocol_version: u16, role: Role, room: RoomCode, announced_mac: Option, max_datagram_size: u16, } impl EndpointHello { pub fn client( room: RoomCode, announced_mac: MacAddr, max_datagram_size: u16, ) -> Result { let hello = Self { protocol_version: CONTROL_PROTOCOL_VERSION, role: Role::Client, room, announced_mac: Some(announced_mac), max_datagram_size, }; hello.validate()?; Ok(hello) } pub fn gateway(room: RoomCode, max_datagram_size: u16) -> Result { let hello = Self { protocol_version: CONTROL_PROTOCOL_VERSION, role: Role::Gateway, room, announced_mac: None, max_datagram_size, }; hello.validate()?; Ok(hello) } pub fn validate(&self) -> Result<(), ControlError> { if self.protocol_version != CONTROL_PROTOCOL_VERSION { return Err(ControlError::UnsupportedVersion { actual: self.protocol_version, supported: CONTROL_PROTOCOL_VERSION, }); } recommended_tap_mtu(usize::from(self.max_datagram_size))?; match (self.role, self.announced_mac) { (Role::Client, Some(mac)) if mac.is_valid_client_identity() => Ok(()), (Role::Client, Some(mac)) => Err(ControlError::InvalidClientMac { mac }), (Role::Client, None) => Err(ControlError::MissingClientMac), (Role::Gateway, Some(_)) => Err(ControlError::UnexpectedGatewayMac), (Role::Gateway, None) => Ok(()), } } #[must_use] pub const fn protocol_version(&self) -> u16 { self.protocol_version } #[must_use] pub const fn role(&self) -> Role { self.role } #[must_use] pub fn room(&self) -> &RoomCode { &self.room } #[must_use] pub const fn announced_mac(&self) -> Option { self.announced_mac } #[must_use] pub const fn max_datagram_size(&self) -> u16 { self.max_datagram_size } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub struct ServerWelcome { protocol_version: u16, room_id: u64, peer_id: u32, effective_tap_mtu: u16, #[serde(default)] mode: ConnectionMode, #[serde(default)] gateway_connected: bool, #[serde(default, skip_serializing_if = "Option::is_none")] gateway_peer_id: Option, } impl ServerWelcome { pub fn new(room_id: u64, peer_id: u32, effective_tap_mtu: u16) -> Result { if peer_id == 0 { return Err(ControlError::InvalidPeerId); } if usize::from(effective_tap_mtu) < MIN_USEFUL_TAP_MTU { return Err(ControlError::EffectiveMtuTooSmall { mtu: effective_tap_mtu, minimum: MIN_USEFUL_TAP_MTU, }); } Ok(Self { protocol_version: CONTROL_PROTOCOL_VERSION, room_id, peer_id, effective_tap_mtu, mode: ConnectionMode::Relay, gateway_connected: false, gateway_peer_id: None, }) } #[must_use] pub const fn with_mode(mut self, mode: ConnectionMode) -> Self { self.mode = mode; self } #[must_use] pub const fn with_gateway_connected(mut self, gateway_connected: bool) -> Self { self.gateway_connected = gateway_connected; if !gateway_connected { self.gateway_peer_id = None; } self } #[must_use] pub const fn with_gateway_peer_id(mut self, gateway_peer_id: Option) -> Self { self.gateway_peer_id = gateway_peer_id; if let Some(_peer_id) = gateway_peer_id { self.gateway_connected = true; } self } pub fn validate(&self) -> Result<(), ControlError> { if self.protocol_version != CONTROL_PROTOCOL_VERSION { return Err(ControlError::UnsupportedVersion { actual: self.protocol_version, supported: CONTROL_PROTOCOL_VERSION, }); } if self.peer_id == 0 { return Err(ControlError::InvalidPeerId); } if usize::from(self.effective_tap_mtu) < MIN_USEFUL_TAP_MTU { return Err(ControlError::EffectiveMtuTooSmall { mtu: self.effective_tap_mtu, minimum: MIN_USEFUL_TAP_MTU, }); } if self.gateway_peer_id == Some(0) { return Err(ControlError::InvalidPeerId); } Ok(()) } #[must_use] pub const fn protocol_version(&self) -> u16 { self.protocol_version } #[must_use] pub const fn room_id(&self) -> u64 { self.room_id } #[must_use] pub const fn peer_id(&self) -> u32 { self.peer_id } #[must_use] pub const fn effective_tap_mtu(&self) -> u16 { self.effective_tap_mtu } #[must_use] pub const fn mode(&self) -> ConnectionMode { self.mode } #[must_use] pub const fn gateway_connected(&self) -> bool { self.gateway_connected } #[must_use] pub const fn gateway_peer_id(&self) -> Option { self.gateway_peer_id } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub struct PeerInfo { peer_id: u32, role: Role, mac: Option, } impl PeerInfo { pub fn new(peer_id: u32, role: Role, mac: Option) -> Result { if peer_id == 0 { return Err(ControlError::InvalidPeerId); } match (role, mac) { (Role::Client, Some(mac)) if mac.is_valid_client_identity() => Ok(Self { peer_id, role, mac: Some(mac), }), (Role::Client, Some(mac)) => Err(ControlError::InvalidClientMac { mac }), (Role::Client, None) => Err(ControlError::MissingClientMac), (Role::Gateway, Some(_)) => Err(ControlError::UnexpectedGatewayMac), (Role::Gateway, None) => Ok(Self { peer_id, role, mac: None, }), } } pub fn validate(&self) -> Result<(), ControlError> { if self.peer_id == 0 { return Err(ControlError::InvalidPeerId); } match (self.role, self.mac) { (Role::Client, Some(mac)) if mac.is_valid_client_identity() => Ok(()), (Role::Client, Some(mac)) => Err(ControlError::InvalidClientMac { mac }), (Role::Client, None) => Err(ControlError::MissingClientMac), (Role::Gateway, Some(_)) => Err(ControlError::UnexpectedGatewayMac), (Role::Gateway, None) => Ok(()), } } #[must_use] pub const fn peer_id(&self) -> u32 { self.peer_id } #[must_use] pub const fn role(&self) -> Role { self.role } #[must_use] pub const fn mac(&self) -> Option { self.mac } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum RejectReason { UnsupportedVersion, RoomFull, GatewayAlreadyConnected, DuplicateMac, InvalidMac, MtuTooSmall, MalformedHello, Unauthorized, InternalError, } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum DisconnectReason { Normal, RelayShutdown, ProtocolError, TimedOut, Replaced, } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub struct Reject { reason: RejectReason, message: String, } impl Reject { #[must_use] pub fn new(reason: RejectReason, message: impl Into) -> Self { Self { reason, message: message.into(), } } #[must_use] pub const fn reason(&self) -> &RejectReason { &self.reason } #[must_use] pub fn message(&self) -> &str { &self.message } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(tag = "type", content = "payload", rename_all = "snake_case")] pub enum ControlMessage { Hello(EndpointHello), Welcome(ServerWelcome), Reject(Reject), PeerJoined(PeerInfo), PeerLeft { peer_id: u32, reason: DisconnectReason, }, Stats(TunnelStats), Disconnect { reason: DisconnectReason, message: String, }, } impl ControlMessage { pub fn validate(&self) -> Result<(), ControlError> { match self { Self::Hello(hello) => hello.validate(), Self::Welcome(welcome) => welcome.validate(), Self::Reject(_) | Self::Stats(_) | Self::Disconnect { .. } => Ok(()), Self::PeerJoined(peer) => peer.validate(), Self::PeerLeft { peer_id, .. } if *peer_id == 0 => Err(ControlError::InvalidPeerId), Self::PeerLeft { .. } => Ok(()), } } } fn validate_room_code(value: &str) -> Result<(), ControlError> { if value.len() < MIN_ROOM_CODE_LEN { return Err(ControlError::EmptyRoomCode); } if value.len() > MAX_ROOM_CODE_LEN { return Err(ControlError::RoomCodeTooLong { len: value.len(), max: MAX_ROOM_CODE_LEN, }); } for (index, byte) in value.bytes().enumerate() { if !byte.is_ascii_alphanumeric() && byte != b'-' && byte != b'_' { return Err(ControlError::InvalidRoomCodeByte { index, byte }); } } Ok(()) } #[cfg(test)] mod tests { use super::*; fn room() -> RoomCode { RoomCode::new("ABCD-1234").unwrap() } fn client_mac() -> MacAddr { MacAddr::new([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0xee]) } #[test] fn validates_room_codes() { assert_eq!(RoomCode::new("").unwrap_err(), ControlError::EmptyRoomCode); assert!(RoomCode::new("ABCD_1234-z").is_ok()); assert!(matches!( RoomCode::new("bad room").unwrap_err(), ControlError::InvalidRoomCodeByte { index: 3, byte: b' ' } )); assert!(matches!( RoomCode::new("a".repeat(MAX_ROOM_CODE_LEN + 1)).unwrap_err(), ControlError::RoomCodeTooLong { .. } )); } #[test] fn builds_valid_client_hello() { let hello = EndpointHello::client(room(), client_mac(), 1400).unwrap(); assert_eq!(hello.protocol_version(), CONTROL_PROTOCOL_VERSION); assert_eq!(hello.role(), Role::Client); assert_eq!(hello.announced_mac(), Some(client_mac())); assert_eq!(hello.max_datagram_size(), 1400); } #[test] fn client_hello_rejects_invalid_mac_and_tiny_datagram_budget() { assert!(matches!( EndpointHello::client(room(), MacAddr::BROADCAST, 1400).unwrap_err(), ControlError::InvalidClientMac { .. } )); assert!(matches!( EndpointHello::client(room(), client_mac(), 128).unwrap_err(), ControlError::Mtu(MtuError::DatagramTooSmall { .. }) )); } #[test] fn builds_valid_gateway_hello() { let hello = EndpointHello::gateway(room(), 1400).unwrap(); assert_eq!(hello.role(), Role::Gateway); assert_eq!(hello.announced_mac(), None); } #[test] fn server_welcome_rejects_reserved_peer_id_and_tiny_mtu() { assert_eq!( ServerWelcome::new(1, 0, MIN_USEFUL_TAP_MTU as u16).unwrap_err(), ControlError::InvalidPeerId ); assert!(matches!( ServerWelcome::new(1, 2, (MIN_USEFUL_TAP_MTU - 1) as u16).unwrap_err(), ControlError::EffectiveMtuTooSmall { .. } )); assert_eq!( ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16) .unwrap() .with_gateway_peer_id(Some(0)) .validate() .unwrap_err(), ControlError::InvalidPeerId ); } #[test] fn server_welcome_reports_gateway_presence() { let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16).unwrap(); assert_eq!(welcome.mode(), ConnectionMode::Relay); assert!(!welcome.gateway_connected()); assert_eq!(welcome.gateway_peer_id(), None); let with_gateway = welcome.with_gateway_peer_id(Some(7)); assert!(with_gateway.gateway_connected()); assert_eq!(with_gateway.gateway_peer_id(), Some(7)); let without_gateway = with_gateway.with_gateway_connected(false); assert!(!without_gateway.gateway_connected()); assert_eq!(without_gateway.gateway_peer_id(), None); } #[test] fn server_welcome_reports_connection_mode() { let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16) .unwrap() .with_mode(ConnectionMode::DirectFailedRelayFallback); assert_eq!(welcome.mode(), ConnectionMode::DirectFailedRelayFallback); assert_eq!(ConnectionMode::Relay.to_string(), "relay"); assert_eq!(ConnectionMode::DirectP2p.to_string(), "direct-p2p"); assert_eq!( ConnectionMode::DirectFailedRelayFallback.to_string(), "direct-failed-relay-fallback" ); assert_eq!( serde_json::to_string(&ConnectionMode::DirectP2p).unwrap(), r#""direct-p2p""# ); assert_eq!( serde_json::from_str::(r#""direct-failed-relay-fallback""#).unwrap(), ConnectionMode::DirectFailedRelayFallback ); } #[test] fn server_welcome_defaults_missing_mode_to_relay() { let json = format!( r#"{{"protocol_version":1,"room_id":1,"peer_id":2,"effective_tap_mtu":{}}}"#, MIN_USEFUL_TAP_MTU ); let welcome: ServerWelcome = serde_json::from_str(&json).unwrap(); assert_eq!(welcome.mode(), ConnectionMode::Relay); assert_eq!(welcome.gateway_peer_id(), None); } #[test] fn peer_info_enforces_role_mac_rules() { let client = PeerInfo::new(7, Role::Client, Some(client_mac())).unwrap(); assert_eq!(client.peer_id(), 7); assert_eq!(client.mac(), Some(client_mac())); assert!(matches!( PeerInfo::new(8, Role::Client, None).unwrap_err(), ControlError::MissingClientMac )); assert!(matches!( PeerInfo::new(9, Role::Gateway, Some(client_mac())).unwrap_err(), ControlError::UnexpectedGatewayMac )); } #[test] fn exposes_stats_counters() { let stats = TunnelStats::new(1, 2, 3, 4, 5, 6); assert_eq!(stats.ethernet_frames_tx(), 1); assert_eq!(stats.ethernet_frames_rx(), 2); assert_eq!(stats.datagrams_tx(), 3); assert_eq!(stats.datagrams_rx(), 4); assert_eq!(stats.dropped_frames(), 5); assert_eq!(stats.malformed_frames(), 6); } }