diff --git a/README.md b/README.md index 8c92f95..90d33b8 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,8 @@ wired interfaces whose sysfs carrier state reports no link; managed wireless NICs are not supported for the physical LAN bridge. It tracks remote-client MACs from relay lifecycle events and periodically emits small CAM refresh frames so the physical switch keeps those MACs associated -with the gateway port. Gateway +with the gateway port. A newly observed client also triggers an immediate CAM +refresh frame instead of waiting for the first periodic refresh tick. Gateway frame logs include direction, peer id when present, MACs, ethertype/length, frame length, action, and drop reason. The gateway also tracks frame/datagram counters and periodically sends stats snapshots to the relay. Malformed or runt diff --git a/TESTING.md b/TESTING.md index 9117d19..39adfd1 100644 --- a/TESTING.md +++ b/TESTING.md @@ -190,6 +190,7 @@ Gateway LAN traffic: ```text gateway frame interface=eth0 direction=LanToRemote ... action=Forwarded gateway frame interface=eth0 direction=RemoteToLan ... action=Forwarded +gateway CAM refresh interface=eth0 peer_id=... mac=... reason=peer_joined ``` Client health: diff --git a/crates/lanparty-gateway/src/lib.rs b/crates/lanparty-gateway/src/lib.rs index 540717a..8201899 100644 --- a/crates/lanparty-gateway/src/lib.rs +++ b/crates/lanparty-gateway/src/lib.rs @@ -435,8 +435,20 @@ impl GatewayConnection { let control_stream = control_stream .context("failed to accept gateway control event stream")?; let control_event = read_gateway_control_event(control_stream).await?; - remote_clients.observe_control_event(&control_event); + let immediate_refresh = remote_clients.observe_control_event(&control_event); println!("{}", format_gateway_control_event(&control_event)); + if let Some(refresh) = immediate_refresh { + write_lan_ethernet(&packet_socket, refresh.frame()).await?; + println!( + "{}", + gateway_cam_refresh_log_line( + packet_socket.get_ref().interface(), + refresh.peer_id(), + refresh.mac(), + "peer_joined", + ) + ); + } } } } @@ -760,6 +772,16 @@ fn gateway_frame_log_line( ) } +#[cfg(target_os = "linux")] +fn gateway_cam_refresh_log_line( + interface: &str, + peer_id: u32, + mac: MacAddr, + reason: &str, +) -> String { + format!("gateway CAM refresh interface={interface} peer_id={peer_id} mac={mac} reason={reason}") +} + #[cfg(target_os = "linux")] async fn read_lan_ethernet(packet_socket: &AsyncFd) -> Result { loop { @@ -803,6 +825,37 @@ struct RemoteClientTable { remote_clients: BTreeMap, } +#[cfg(target_os = "linux")] +#[derive(Debug, Clone, PartialEq, Eq)] +struct CamRefresh { + peer_id: u32, + mac: MacAddr, + frame: Vec, +} + +#[cfg(target_os = "linux")] +impl CamRefresh { + fn new(peer_id: u32, mac: MacAddr, gateway_mac: MacAddr) -> Self { + Self { + peer_id, + mac, + frame: cam_refresh_frame(mac, gateway_mac), + } + } + + const fn peer_id(&self) -> u32 { + self.peer_id + } + + const fn mac(&self) -> MacAddr { + self.mac + } + + fn frame(&self) -> &[u8] { + &self.frame + } +} + #[cfg(target_os = "linux")] impl RemoteClientTable { fn new(gateway_mac: MacAddr) -> Self { @@ -824,21 +877,27 @@ impl RemoteClientTable { Ok(None) } - fn observe_control_event(&mut self, event: &ControlMessage) { + fn observe_control_event(&mut self, event: &ControlMessage) -> Option { match event { ControlMessage::PeerJoined(peer) => self.observe_peer_joined(peer), - ControlMessage::PeerLeft { peer_id, .. } => self.observe_peer_left(*peer_id), - _ => {} + ControlMessage::PeerLeft { peer_id, .. } => { + self.observe_peer_left(*peer_id); + None + } + _ => None, } } - fn observe_peer_joined(&mut self, peer: &PeerInfo) { + fn observe_peer_joined(&mut self, peer: &PeerInfo) -> Option { if peer.role() == Role::Client && let Some(mac) = peer.mac() && mac.is_valid_client_identity() { self.remote_clients.insert(peer.peer_id(), mac); + return Some(CamRefresh::new(peer.peer_id(), mac, self.gateway_mac)); } + + None } fn observe_peer_left(&mut self, peer_id: u32) { @@ -1243,9 +1302,10 @@ mod tests { Some(DropReason::UnauthorizedSourceMac) ); - remote_clients.observe_control_event(&ControlMessage::PeerJoined( + let refresh = remote_clients.observe_control_event(&ControlMessage::PeerJoined( PeerInfo::new(7, Role::Client, Some(remote_mac)).unwrap(), )); + assert!(refresh.is_some()); assert_eq!( remote_clients @@ -1274,9 +1334,16 @@ mod tests { let remote_mac = MacAddr::new([0x02, 0, 0, 0, 0, 2]); let mut refresh = RemoteClientTable::new(gateway_mac); - refresh.observe_control_event(&ControlMessage::PeerJoined( + let immediate_refresh = refresh.observe_control_event(&ControlMessage::PeerJoined( PeerInfo::new(7, Role::Client, Some(remote_mac)).unwrap(), )); + let immediate_refresh = immediate_refresh.expect("client joins trigger CAM refresh"); + let immediate_frame = EthernetFrame::parse(immediate_refresh.frame()).unwrap(); + + assert_eq!(immediate_refresh.peer_id(), 7); + assert_eq!(immediate_refresh.mac(), remote_mac); + assert_eq!(immediate_frame.source(), remote_mac); + assert_eq!(immediate_frame.destination(), gateway_mac); assert_eq!(refresh.remote_mac_count(), 1); assert_eq!( EthernetFrame::parse(&refresh.refresh_frames()[0]) @@ -1285,10 +1352,13 @@ mod tests { remote_mac ); - refresh.observe_control_event(&ControlMessage::PeerLeft { - peer_id: 7, - reason: DisconnectReason::Normal, - }); + assert_eq!( + refresh.observe_control_event(&ControlMessage::PeerLeft { + peer_id: 7, + reason: DisconnectReason::Normal, + }), + None + ); assert_eq!(refresh.remote_mac_count(), 0); assert!(refresh.refresh_frames().is_empty()); } @@ -1311,6 +1381,20 @@ mod tests { ); } + #[cfg(target_os = "linux")] + #[test] + fn formats_gateway_cam_refresh_log_line() { + assert_eq!( + gateway_cam_refresh_log_line( + "eth0", + 7, + MacAddr::new([0x02, 0, 0, 0, 0, 2]), + "peer_joined" + ), + "gateway CAM refresh interface=eth0 peer_id=7 mac=02:00:00:00:00:02 reason=peer_joined" + ); + } + #[cfg(target_os = "linux")] #[test] fn formats_gateway_datagram_budget_drops() {