diff --git a/README.md b/README.md index 1bc057a..8f38555 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Public relay binary and relay-owned room state: - one gateway per room, duplicate client MAC rejection, and room limits - stable effective room MTU chosen before Ethernet datagrams flow - Ethernet datagram forwarding decisions with no ingress reflection +- peer leave cleanup for room membership and MAC indexes ## Build diff --git a/crates/lanparty-relay/src/lib.rs b/crates/lanparty-relay/src/lib.rs index f15e9be..85fdae4 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -73,6 +73,29 @@ impl RoomSnapshot { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LeaveResult { + peer: PeerInfo, + room_removed: bool, +} + +impl LeaveResult { + #[must_use] + const fn new(peer: PeerInfo, room_removed: bool) -> Self { + Self { peer, room_removed } + } + + #[must_use] + pub const fn peer(&self) -> &PeerInfo { + &self.peer + } + + #[must_use] + pub const fn room_removed(&self) -> bool { + self.room_removed + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ForwardingDecision { targets: Vec, @@ -183,6 +206,21 @@ impl RoomRegistry { self.rooms.get(room).map(Room::snapshot) } + pub fn leave(&mut self, room: &RoomCode, peer_id: u32) -> Result { + let room_state = self + .rooms + .get_mut(room) + .ok_or_else(|| ForwardingError::UnknownRoom(room.clone()))?; + let peer = room_state.leave(room, peer_id)?; + let room_removed = room_state.is_empty(); + + if room_removed { + self.rooms.remove(room); + } + + Ok(LeaveResult::new(peer, room_removed)) + } + pub fn forward_ethernet( &self, room: &RoomCode, @@ -331,6 +369,32 @@ impl Room { } } + fn leave(&mut self, room_code: &RoomCode, peer_id: u32) -> Result { + if self + .gateway + .as_ref() + .is_some_and(|gateway| gateway.peer_id() == peer_id) + { + return Ok(self.gateway.take().expect("gateway exists")); + } + + let peer = + self.clients + .remove(&peer_id) + .ok_or_else(|| ForwardingError::UnknownIngressPeer { + room: room_code.clone(), + peer_id, + })?; + let mac = peer.mac().expect("client peer info has MAC"); + self.clients_by_mac.remove(&mac); + + Ok(peer) + } + + fn is_empty(&self) -> bool { + self.gateway.is_none() && self.clients.is_empty() + } + fn forward_ethernet( &self, room_code: &RoomCode, @@ -522,6 +586,66 @@ mod tests { assert_eq!(gateway.welcome().effective_tap_mtu(), 972); } + #[test] + fn removes_client_from_room_indexes() { + let mut registry = RoomRegistry::default(); + let client = registry.join(client_hello(1)).unwrap(); + registry.join(client_hello(2)).unwrap(); + + let result = registry.leave(&room(), client.peer().peer_id()).unwrap(); + let snapshot = registry.snapshot(&room()).unwrap(); + + assert_eq!(result.peer(), client.peer()); + assert!(!result.room_removed()); + assert_eq!(snapshot.clients().len(), 1); + assert!(registry.join(client_hello(1)).is_ok()); + } + + #[test] + fn removes_empty_room_after_last_peer_leaves() { + let mut registry = RoomRegistry::default(); + let client = registry.join(client_hello(1)).unwrap(); + + let result = registry.leave(&room(), client.peer().peer_id()).unwrap(); + + assert_eq!(result.peer(), client.peer()); + assert!(result.room_removed()); + assert_eq!(registry.room_count(), 0); + assert!(registry.snapshot(&room()).is_none()); + } + + #[test] + fn removes_gateway_without_removing_room_when_clients_remain() { + let mut registry = RoomRegistry::default(); + let gateway = registry.join(gateway_hello()).unwrap(); + registry.join(client_hello(1)).unwrap(); + + let result = registry.leave(&room(), gateway.peer().peer_id()).unwrap(); + let snapshot = registry.snapshot(&room()).unwrap(); + + assert_eq!(result.peer(), gateway.peer()); + assert!(!result.room_removed()); + assert!(snapshot.gateway().is_none()); + assert_eq!(snapshot.clients().len(), 1); + assert!(registry.join(gateway_hello()).is_ok()); + } + + #[test] + fn reports_unknown_peer_on_leave() { + let mut registry = RoomRegistry::default(); + registry.join(client_hello(1)).unwrap(); + + let error = registry.leave(&room(), 99).unwrap_err(); + + assert_eq!( + error, + ForwardingError::UnknownIngressPeer { + room: room(), + peer_id: 99, + } + ); + } + #[test] fn forwards_unknown_client_unicast_to_gateway() { let mut registry = RoomRegistry::default();