diff --git a/README.md b/README.md index a95a891..5842933 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ 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 +- 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 @@ -148,6 +149,8 @@ handshake, pins a host route for the relay IP on the current pre-TAP interface, verifies that the relay route still uses that pinned host route after TAP activation, and then bridges Ethernet frames between the relay and the first TAP-Windows6 adapter until shutdown. +The startup status reports whether the relay already has a LAN gateway for the +room. `--virtual-mac` can still override the stored identity for manual testing. On Windows it sets the TAP IP interface MTU to the relay-selected MTU, marks the TAP media connected, and reports the driver MAC/MTU before forwarding frames, diff --git a/crates/lanparty-client-core/src/lib.rs b/crates/lanparty-client-core/src/lib.rs index 2d13e51..973bcf8 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -674,6 +674,7 @@ mod tests { ); assert_eq!(client.welcome().room_id(), 7); assert_eq!(client.welcome().peer_id(), 2); + assert!(!client.welcome().gateway_connected()); assert!(client.quic_max_datagram_size() <= 1400); assert!(client.quic_diagnostics().datagram_supported()); assert_eq!( diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index b8e92f4..070de86 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -109,10 +109,11 @@ async fn main() -> Result<()> { let session = connect_client(config).await?; println!( - "lanparty-client-win connected as peer {} in room id {} with TAP MTU {}", + "lanparty-client-win connected as peer {} in room id {} with TAP MTU {}; LAN gateway connected {}", session.welcome().peer_id(), session.welcome().room_id(), - session.welcome().effective_tap_mtu() + session.welcome().effective_tap_mtu(), + yes_no(session.welcome().gateway_connected()) ); #[cfg(windows)] let relay_route_pin = match pin_relay_route_before_tap(session.config().relay_addr().ip()) { diff --git a/crates/lanparty-ctrl/src/lib.rs b/crates/lanparty-ctrl/src/lib.rs index 18dd7ad..85ba43c 100644 --- a/crates/lanparty-ctrl/src/lib.rs +++ b/crates/lanparty-ctrl/src/lib.rs @@ -189,6 +189,8 @@ pub struct ServerWelcome { room_id: u64, peer_id: u32, effective_tap_mtu: u16, + #[serde(default)] + gateway_connected: bool, } impl ServerWelcome { @@ -209,9 +211,16 @@ impl ServerWelcome { room_id, peer_id, effective_tap_mtu, + gateway_connected: false, }) } + #[must_use] + pub const fn with_gateway_connected(mut self, gateway_connected: bool) -> Self { + self.gateway_connected = gateway_connected; + self + } + pub fn validate(&self) -> Result<(), ControlError> { if self.protocol_version != CONTROL_PROTOCOL_VERSION { return Err(ControlError::UnsupportedVersion { @@ -253,6 +262,11 @@ impl ServerWelcome { pub const fn effective_tap_mtu(&self) -> u16 { self.effective_tap_mtu } + + #[must_use] + pub const fn gateway_connected(&self) -> bool { + self.gateway_connected + } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] @@ -489,6 +503,14 @@ mod tests { )); } + #[test] + fn server_welcome_reports_gateway_presence() { + let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16).unwrap(); + + assert!(!welcome.gateway_connected()); + assert!(welcome.with_gateway_connected(true).gateway_connected()); + } + #[test] fn peer_info_enforces_role_mac_rules() { let client = PeerInfo::new(7, Role::Client, Some(client_mac())).unwrap(); diff --git a/crates/lanparty-relay/src/lib.rs b/crates/lanparty-relay/src/lib.rs index e3fafff..683bd9e 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -327,8 +327,10 @@ impl Room { format!("invalid peer: {error}"), ) })?; - let welcome = - ServerWelcome::new(self.room_id, peer_id, effective_tap_mtu).map_err(|error| { + let gateway_connected = matches!(peer.role(), Role::Gateway) || self.gateway.is_some(); + let welcome = ServerWelcome::new(self.room_id, peer_id, effective_tap_mtu) + .map(|welcome| welcome.with_gateway_connected(gateway_connected)) + .map_err(|error| { Reject::new( RejectReason::InternalError, format!("welcome failed: {error}"), @@ -817,13 +819,24 @@ mod tests { assert_eq!(registry.room_count(), 1); assert_eq!(gateway.peer().role(), Role::Gateway); assert_eq!(gateway.welcome().peer_id(), 1); + assert!(gateway.welcome().gateway_connected()); assert_eq!(client.peer().role(), Role::Client); assert_eq!(client.welcome().peer_id(), 2); + assert!(client.welcome().gateway_connected()); assert_eq!(snapshot.gateway().unwrap().peer_id(), 1); assert_eq!(snapshot.clients().len(), 1); assert_eq!(snapshot.effective_tap_mtu(), 1200); } + #[test] + fn reports_missing_gateway_to_client_joining_first() { + let mut registry = RoomRegistry::default(); + + let client = registry.join(client_hello(1)).unwrap(); + + assert!(!client.welcome().gateway_connected()); + } + #[test] fn rejects_second_gateway() { let mut registry = RoomRegistry::default();