diff --git a/README.md b/README.md index be895c3..7af6936 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ Public relay binary and relay-owned room state: - live Ethernet datagram forwarding with no ingress reflection - forwarding drops for Ethernet frames above the negotiated TAP MTU - per-peer egress budget checks against the negotiated datagram size -- reliable `PeerJoined`/`PeerLeft` notifications to existing room peers +- reliable `PeerJoined`/`PeerLeft` notifications plus gateway identity in + welcome messages - L2 safety filters for invalid-source, jumbo, switch-control, remote VLAN tags, remote IPv6 fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames, including frames behind ordinary IPv6 extension headers diff --git a/crates/lanparty-client-core/src/lib.rs b/crates/lanparty-client-core/src/lib.rs index d8a0d0b..c514f71 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -921,6 +921,7 @@ mod tests { assert_eq!(client.welcome().room_id(), 7); assert_eq!(client.welcome().peer_id(), 2); assert!(!client.welcome().gateway_connected()); + assert_eq!(client.welcome().gateway_peer_id(), None); 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 a337f72..9edd02a 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -1,6 +1,6 @@ use std::sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU32, Ordering}, }; use std::{collections::BTreeMap, fs, net::IpAddr, path::PathBuf}; #[cfg(windows)] @@ -795,17 +795,23 @@ fn format_control_event(event: &ControlMessage) -> String { #[derive(Debug, Clone)] struct ClientRelayStatus { gateway_connected: Arc, + gateway_peer_id: Arc, } impl ClientRelayStatus { + const UNKNOWN_GATEWAY_PEER_ID: u32 = 0; + fn new(gateway_connected: bool) -> Self { Self { gateway_connected: Arc::new(AtomicBool::new(gateway_connected)), + gateway_peer_id: Arc::new(AtomicU32::new(Self::UNKNOWN_GATEWAY_PEER_ID)), } } fn from_welcome(session: &ClientSession) -> Self { - Self::new(session.welcome().gateway_connected()) + let status = Self::new(session.welcome().gateway_connected()); + status.set_gateway_peer_id(session.welcome().gateway_peer_id()); + status } fn gateway_connected(&self) -> bool { @@ -815,6 +821,23 @@ impl ClientRelayStatus { fn set_gateway_connected(&self, gateway_connected: bool) { self.gateway_connected .store(gateway_connected, Ordering::Relaxed); + if !gateway_connected { + self.set_gateway_peer_id(None); + } + } + + fn gateway_peer_id(&self) -> Option { + match self.gateway_peer_id.load(Ordering::Relaxed) { + Self::UNKNOWN_GATEWAY_PEER_ID => None, + peer_id => Some(peer_id), + } + } + + fn set_gateway_peer_id(&self, gateway_peer_id: Option) { + self.gateway_peer_id.store( + gateway_peer_id.unwrap_or(Self::UNKNOWN_GATEWAY_PEER_ID), + Ordering::Relaxed, + ); } } @@ -829,12 +852,19 @@ impl ControlEventFormatter { ControlMessage::PeerJoined(peer) => { self.peers.insert(peer.peer_id(), peer.clone()); if peer.role() == Role::Gateway { + relay_status.set_gateway_peer_id(Some(peer.peer_id())); relay_status.set_gateway_connected(true); } format_peer_joined(peer) } ControlMessage::PeerLeft { peer_id, reason } => { let Some(peer) = self.peers.remove(peer_id) else { + if relay_status.gateway_peer_id() == Some(*peer_id) { + relay_status.set_gateway_connected(false); + return format!( + "relay event: LAN gateway disconnected (peer {peer_id}, {reason:?})" + ); + } return format!("relay event: peer {peer_id} left ({reason:?})"); }; @@ -1502,6 +1532,7 @@ mod tests { "relay event: LAN gateway connected as peer 1" ); assert!(relay_status.gateway_connected()); + assert_eq!(relay_status.gateway_peer_id(), Some(1)); assert_eq!( formatter.format(&client, &relay_status), "relay event: client peer 2 joined with MAC 02:00:00:00:00:02" @@ -1519,6 +1550,25 @@ mod tests { "relay event: LAN gateway disconnected (peer 1, Normal)" ); assert!(!relay_status.gateway_connected()); + assert_eq!(relay_status.gateway_peer_id(), None); + } + + #[test] + fn clears_gateway_status_when_welcome_gateway_leaves_before_join_event() { + let gateway_left = ControlMessage::PeerLeft { + peer_id: 7, + reason: DisconnectReason::Normal, + }; + let mut formatter = ControlEventFormatter::default(); + let relay_status = ClientRelayStatus::new(true); + relay_status.set_gateway_peer_id(Some(7)); + + assert_eq!( + formatter.format(&gateway_left, &relay_status), + "relay event: LAN gateway disconnected (peer 7, Normal)" + ); + assert!(!relay_status.gateway_connected()); + assert_eq!(relay_status.gateway_peer_id(), None); } const fn mac(last_octet: u8) -> MacAddr { diff --git a/crates/lanparty-ctrl/src/lib.rs b/crates/lanparty-ctrl/src/lib.rs index c1fd913..28cab00 100644 --- a/crates/lanparty-ctrl/src/lib.rs +++ b/crates/lanparty-ctrl/src/lib.rs @@ -216,6 +216,8 @@ pub struct ServerWelcome { mode: ConnectionMode, #[serde(default)] gateway_connected: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + gateway_peer_id: Option, } impl ServerWelcome { @@ -238,6 +240,7 @@ impl ServerWelcome { effective_tap_mtu, mode: ConnectionMode::Relay, gateway_connected: false, + gateway_peer_id: None, }) } @@ -250,6 +253,18 @@ impl ServerWelcome { #[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 } @@ -272,6 +287,10 @@ impl ServerWelcome { }); } + if self.gateway_peer_id == Some(0) { + return Err(ControlError::InvalidPeerId); + } + Ok(()) } @@ -304,6 +323,11 @@ impl ServerWelcome { 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)] @@ -538,6 +562,14 @@ mod tests { 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] @@ -546,7 +578,15 @@ mod tests { assert_eq!(welcome.mode(), ConnectionMode::Relay); assert!(!welcome.gateway_connected()); - assert!(welcome.with_gateway_connected(true).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] @@ -581,6 +621,7 @@ mod tests { let welcome: ServerWelcome = serde_json::from_str(&json).unwrap(); assert_eq!(welcome.mode(), ConnectionMode::Relay); + assert_eq!(welcome.gateway_peer_id(), None); } #[test] diff --git a/crates/lanparty-relay/src/lib.rs b/crates/lanparty-relay/src/lib.rs index 4e2acf4..21205a1 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -375,9 +375,12 @@ impl Room { format!("invalid peer: {error}"), ) })?; - let gateway_connected = matches!(peer.role(), Role::Gateway) || self.gateway.is_some(); + let gateway_peer_id = match peer.role() { + Role::Gateway => Some(peer.peer_id()), + Role::Client => self.gateway.as_ref().map(|gateway| gateway.info.peer_id()), + }; let welcome = ServerWelcome::new(self.room_id, peer_id, effective_tap_mtu) - .map(|welcome| welcome.with_gateway_connected(gateway_connected)) + .map(|welcome| welcome.with_gateway_peer_id(gateway_peer_id)) .map_err(|error| { Reject::new( RejectReason::InternalError, @@ -912,9 +915,11 @@ mod tests { assert_eq!(gateway.peer().role(), Role::Gateway); assert_eq!(gateway.welcome().peer_id(), 1); assert!(gateway.welcome().gateway_connected()); + assert_eq!(gateway.welcome().gateway_peer_id(), Some(1)); assert_eq!(client.peer().role(), Role::Client); assert_eq!(client.welcome().peer_id(), 2); assert!(client.welcome().gateway_connected()); + assert_eq!(client.welcome().gateway_peer_id(), Some(1)); assert_eq!(snapshot.gateway().unwrap().peer_id(), 1); assert_eq!(snapshot.clients().len(), 1); assert_eq!(snapshot.effective_tap_mtu(), 1200); @@ -929,6 +934,7 @@ mod tests { let client = registry.join(client_hello(1)).unwrap(); assert!(!client.welcome().gateway_connected()); + assert_eq!(client.welcome().gateway_peer_id(), None); } #[test] diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index 58d5b0b..3f91866 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -1412,7 +1412,9 @@ mod tests { assert_eq!(gateway.welcome().room_id(), client.welcome().room_id()); assert_eq!(gateway.welcome().peer_id(), 1); assert_eq!(client.welcome().peer_id(), 2); + assert_eq!(gateway.welcome().gateway_peer_id(), Some(1)); assert!(client.welcome().gateway_connected()); + assert_eq!(client.welcome().gateway_peer_id(), Some(1)); assert_eq!( gateway.quic_max_datagram_size(), client.quic_max_datagram_size()