diff --git a/README.md b/README.md index 0cc579f..2486804 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ Malformed peer datagrams log their per-peer count before the relay disconnects peers that cross the malformed-datagram threshold. Relay egress skips caused by a target peer's smaller datagram budget are logged with the ingress peer, target peer, encoded length, and target budget. +Ingress datagrams larger than the sending peer's negotiated datagram budget are +dropped before decode/forwarding and logged with `reason=datagram_budget`. 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 32fc18c..58d5b0b 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -66,6 +66,7 @@ struct PeerSession { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PeerDatagramOutcome { Accepted, + Dropped(DropReason), Malformed, } @@ -336,6 +337,7 @@ async fn run_peer_io( match forward_peer_datagram(rooms, sessions, accepted, datagram).await { Ok(PeerDatagramOutcome::Accepted) => {} + Ok(PeerDatagramOutcome::Dropped(_reason)) => {} Ok(PeerDatagramOutcome::Malformed) => { if let Some(reason) = malformed_tracker.record_malformed() { eprintln!( @@ -396,6 +398,11 @@ async fn forward_peer_datagram( accepted: &AcceptedPeer, datagram: Bytes, ) -> Result { + if datagram.len() > accepted.max_datagram_size { + eprintln!("{}", ingress_budget_drop_log_line(accepted, datagram.len())); + return Ok(PeerDatagramOutcome::Dropped(DropReason::DatagramBudget)); + } + let Ok(packet) = decode_datagram(&datagram) else { return Ok(PeerDatagramOutcome::Malformed); }; @@ -626,6 +633,17 @@ fn egress_budget_skip_log_line( ) } +fn ingress_budget_drop_log_line(accepted: &AcceptedPeer, datagram_len: usize) -> String { + format!( + "relay ingress dropped room={} peer_id={} role={:?} len={} max_datagram_size={} reason=datagram_budget", + accepted.room, + accepted.peer.peer_id(), + accepted.peer.role(), + datagram_len, + accepted.max_datagram_size + ) +} + async fn collect_target_sessions( sessions: &Arc>>, room: &RoomCode, @@ -1158,6 +1176,23 @@ mod tests { assert!(line.contains("reason=datagram_budget")); } + #[tokio::test] + async fn formats_ingress_budget_drop_log_line() { + let rooms = Arc::new(Mutex::new(RoomRegistry::default())); + let mut accepted = accepted_client_for_forwarding(&rooms, client_mac(1)).await; + accepted.max_datagram_size = 64; + + let line = ingress_budget_drop_log_line(&accepted, 65); + + assert!(line.contains("relay ingress dropped")); + assert!(line.contains("room=TESTROOM")); + assert!(line.contains(&format!("peer_id={}", accepted.peer.peer_id()))); + assert!(line.contains("role=Client")); + assert!(line.contains("len=65")); + assert!(line.contains("max_datagram_size=64")); + assert!(line.contains("reason=datagram_budget")); + } + #[tokio::test] async fn classifies_bad_peer_datagrams_as_malformed() { let rooms = Arc::new(Mutex::new(RoomRegistry::default())); @@ -1193,6 +1228,32 @@ mod tests { assert_eq!(outcome, PeerDatagramOutcome::Malformed); } + #[tokio::test] + async fn drops_peer_datagrams_above_negotiated_ingress_budget() { + let rooms = Arc::new(Mutex::new(RoomRegistry::default())); + let sessions = Arc::new(Mutex::new(HashMap::new())); + let mut accepted = accepted_client_for_forwarding(&rooms, client_mac(1)).await; + accepted.max_datagram_size = 32; + let datagram = encode_datagram( + FrameType::Ethernet, + accepted.welcome.room_id(), + accepted.peer.peer_id(), + OVERLAY_FLAGS_NONE, + ðernet_frame(client_mac(2), client_mac(1)), + ) + .unwrap(); + + assert!(datagram.len() > accepted.max_datagram_size); + let outcome = forward_peer_datagram(&rooms, &sessions, &accepted, Bytes::from(datagram)) + .await + .unwrap(); + + assert_eq!( + outcome, + PeerDatagramOutcome::Dropped(DropReason::DatagramBudget) + ); + } + #[tokio::test] async fn forwards_ethernet_datagrams_between_joined_peers() { let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM);