diff --git a/README.md b/README.md index 765d821..3530880 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ certificate handling remains future work. Ethernet forwarding decisions are logged with room, peer, MAC, ethertype, action, drop reason, and target count. Safety-policy rejects use the `filtered` action so they are distinguishable from malformed/unknown-destination drops and rate limits. +Malformed peer datagrams log their per-peer count before the relay disconnects +peers that cross the malformed-datagram threshold. Unknown unicast from a client is forwarded only to the gateway port; unknown unicast from the gateway is dropped instead of flooded to every remote client. When a peer joins or leaves, the relay sends a reliable lifecycle control event diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index 4a09139..fba5570 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -111,6 +111,10 @@ impl MalformedDatagramTracker { None } } + + const fn count(&self) -> usize { + self.count + } } impl RelayServer { @@ -321,9 +325,25 @@ async fn run_peer_io( Ok(PeerDatagramOutcome::Accepted) => {} Ok(PeerDatagramOutcome::Malformed) => { if let Some(reason) = malformed_tracker.record_malformed() { + eprintln!( + "{}", + malformed_datagram_log_line( + accepted, + malformed_tracker.count(), + true, + ) + ); connection.close(0_u32.into(), reason.as_bytes()); return PeerClose::protocol_error(reason); } + eprintln!( + "{}", + malformed_datagram_log_line( + accepted, + malformed_tracker.count(), + false, + ) + ); } Err(error) => { eprintln!( @@ -554,6 +574,22 @@ fn peer_stats_log_line(accepted: &AcceptedPeer, stats: &TunnelStats) -> String { ) } +fn malformed_datagram_log_line( + accepted: &AcceptedPeer, + malformed_count: usize, + disconnecting: bool, +) -> String { + format!( + "malformed peer datagram room={} peer_id={} role={:?} count={} threshold={} disconnecting={}", + accepted.room, + accepted.peer.peer_id(), + accepted.peer.role(), + malformed_count, + MAX_MALFORMED_DATAGRAMS_PER_PEER, + disconnecting + ) +} + async fn collect_target_sessions( sessions: &Arc>>, room: &RoomCode, @@ -967,13 +1003,33 @@ mod tests { for _ in 1..MAX_MALFORMED_DATAGRAMS_PER_PEER { assert_eq!(tracker.record_malformed(), None); } + assert_eq!(tracker.count(), MAX_MALFORMED_DATAGRAMS_PER_PEER - 1); let reason = tracker .record_malformed() .expect("threshold should disconnect peer"); + assert_eq!(tracker.count(), MAX_MALFORMED_DATAGRAMS_PER_PEER); assert!(reason.contains("malformed datagrams")); } + #[tokio::test] + async fn formats_malformed_datagram_log_line() { + let rooms = Arc::new(Mutex::new(RoomRegistry::default())); + let accepted = accepted_client_for_forwarding(&rooms, client_mac(1)).await; + + let line = malformed_datagram_log_line(&accepted, 3, false); + assert!(line.contains("malformed peer datagram")); + assert!(line.contains("room=TESTROOM")); + assert!(line.contains(&format!("peer_id={}", accepted.peer.peer_id()))); + assert!(line.contains("role=Client")); + assert!(line.contains("count=3")); + assert!(line.contains(&format!("threshold={MAX_MALFORMED_DATAGRAMS_PER_PEER}"))); + assert!(line.contains("disconnecting=false")); + + let line = malformed_datagram_log_line(&accepted, MAX_MALFORMED_DATAGRAMS_PER_PEER, true); + assert!(line.contains("disconnecting=true")); + } + #[tokio::test] async fn classifies_bad_peer_datagrams_as_malformed() { let rooms = Arc::new(Mutex::new(RoomRegistry::default()));