diff --git a/README.md b/README.md index d9e2314..5bcc429 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Transport-agnostic tunnel contract shared by all binaries: Reliable control-plane schema shared by the QUIC stream handlers: - endpoint hello messages with role, room, MAC, and datagram budget -- server welcome, reject, peer lifecycle, stats, and disconnect messages +- server welcome mode, reject, peer lifecycle, stats, and disconnect messages - initial room gateway-presence status in server welcomes - room-code, role/MAC, peer-id, and effective-MTU validation - length-prefixed JSON control frames for reliable QUIC streams diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index f3712d2..7d36ee5 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -113,10 +113,11 @@ async fn main() -> Result<()> { let session = connect_client(config).await?; println!( - "lanparty-client-win connected as peer {} in room id {} with TAP MTU {}; LAN gateway connected {}", + "lanparty-client-win connected as peer {} in room id {} with TAP MTU {} over {}; LAN gateway connected {}", session.welcome().peer_id(), session.welcome().room_id(), session.welcome().effective_tap_mtu(), + session.welcome().mode(), yes_no(session.welcome().gateway_connected()) ); #[cfg(windows)] diff --git a/crates/lanparty-ctrl/src/lib.rs b/crates/lanparty-ctrl/src/lib.rs index 85ba43c..c1fd913 100644 --- a/crates/lanparty-ctrl/src/lib.rs +++ b/crates/lanparty-ctrl/src/lib.rs @@ -98,6 +98,29 @@ pub enum Role { 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, @@ -190,6 +213,8 @@ pub struct ServerWelcome { peer_id: u32, effective_tap_mtu: u16, #[serde(default)] + mode: ConnectionMode, + #[serde(default)] gateway_connected: bool, } @@ -211,10 +236,17 @@ impl ServerWelcome { room_id, peer_id, effective_tap_mtu, + mode: ConnectionMode::Relay, gateway_connected: false, }) } + #[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; @@ -263,6 +295,11 @@ impl ServerWelcome { 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 @@ -507,10 +544,45 @@ mod tests { 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!(welcome.with_gateway_connected(true).gateway_connected()); } + #[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); + } + #[test] fn peer_info_enforces_role_mac_rules() { let client = PeerInfo::new(7, Role::Client, Some(client_mac())).unwrap(); diff --git a/crates/lanparty-gateway/src/main.rs b/crates/lanparty-gateway/src/main.rs index e8ad3dd..0bbd10c 100644 --- a/crates/lanparty-gateway/src/main.rs +++ b/crates/lanparty-gateway/src/main.rs @@ -15,10 +15,11 @@ async fn main() -> anyhow::Result<()> { let gateway = connect_gateway(config).await?; println!( - "lanparty-gateway connected as peer {} in room id {} with TAP MTU {}", + "lanparty-gateway connected as peer {} in room id {} with TAP MTU {} over {}", gateway.welcome().peer_id(), gateway.welcome().room_id(), - gateway.welcome().effective_tap_mtu() + gateway.welcome().effective_tap_mtu(), + gateway.welcome().mode() ); #[cfg(target_os = "linux")] {