diff --git a/README.md b/README.md index 35b8d9e..765d821 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,8 @@ self-signed development certificate; `--dev-cert-der-out` writes that certificate so the gateway and client can pin it in development. Production 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. 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/lib.rs b/crates/lanparty-relay/src/lib.rs index 6f65c77..4d273c5 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -146,6 +146,15 @@ impl ForwardingDecision { } } + #[must_use] + fn filtered(drop_reason: DropReason) -> Self { + Self { + targets: Vec::new(), + action: FrameAction::Filtered, + drop_reason: Some(drop_reason), + } + } + #[must_use] fn rate_limited() -> Self { Self { @@ -510,7 +519,7 @@ impl Room { if ingress_role == Role::Client { let expected_source = ingress_mac.expect("client peers have MAC addresses"); if frame.source() != expected_source { - return Ok(ForwardingDecision::dropped( + return Ok(ForwardingDecision::filtered( DropReason::UnauthorizedSourceMac, )); } @@ -519,7 +528,7 @@ impl Room { self.mark_peer_seen(ingress_peer_id, now); if let Some(drop_reason) = safety_drop_reason(ingress_role, frame) { - return Ok(ForwardingDecision::dropped(drop_reason)); + return Ok(ForwardingDecision::filtered(drop_reason)); } if ingress_role == Role::Client @@ -859,6 +868,12 @@ mod tests { assert!(decision.targets().is_empty()); } + fn assert_filtered(decision: &ForwardingDecision, reason: DropReason) { + assert_eq!(decision.action(), FrameAction::Filtered); + assert_eq!(decision.drop_reason(), Some(reason)); + assert!(decision.targets().is_empty()); + } + fn assert_rate_limited(decision: &ForwardingDecision) { assert_eq!(decision.action(), FrameAction::RateLimited); assert_eq!(decision.drop_reason(), Some(DropReason::RateLimit)); @@ -1139,7 +1154,7 @@ mod tests { before + Duration::from_secs(5), ) .unwrap(); - assert_dropped(&decision, DropReason::UnauthorizedSourceMac); + assert_filtered(&decision, DropReason::UnauthorizedSourceMac); assert_eq!( registry @@ -1275,7 +1290,7 @@ mod tests { } #[test] - fn drops_client_frames_with_forged_source_mac() { + fn filters_client_frames_with_forged_source_mac() { let mut registry = RoomRegistry::default(); let client = registry.join(client_hello(1)).unwrap(); let frame = ethernet(MacAddr::BROADCAST, mac(2)); @@ -1284,16 +1299,11 @@ mod tests { .forward_ethernet(&room(), client.peer().peer_id(), &frame) .unwrap(); - assert_eq!(decision.action(), FrameAction::Dropped); - assert_eq!( - decision.drop_reason(), - Some(DropReason::UnauthorizedSourceMac) - ); - assert!(decision.targets().is_empty()); + assert_filtered(&decision, DropReason::UnauthorizedSourceMac); } #[test] - fn drops_jumbo_frames() { + fn filters_jumbo_frames() { let mut registry = RoomRegistry::default(); registry.join(gateway_hello()).unwrap(); let client = registry.join(client_hello(1)).unwrap(); @@ -1304,11 +1314,11 @@ mod tests { .forward_ethernet(&room(), client.peer().peer_id(), &frame) .unwrap(); - assert_dropped(&decision, DropReason::JumboFrame); + assert_filtered(&decision, DropReason::JumboFrame); } #[test] - fn drops_l2_control_plane_frames_from_clients_and_gateway() { + fn filters_l2_control_plane_frames_from_clients_and_gateway() { let mut registry = RoomRegistry::default(); let gateway = registry.join(gateway_hello()).unwrap(); let client = registry.join(client_hello(1)).unwrap(); @@ -1324,12 +1334,12 @@ mod tests { .forward_ethernet(&room(), gateway.peer().peer_id(), &gateway_frame) .unwrap(); - assert_dropped(&client_decision, DropReason::ControlPlaneEtherType); - assert_dropped(&gateway_decision, DropReason::ControlPlaneEtherType); + assert_filtered(&client_decision, DropReason::ControlPlaneEtherType); + assert_filtered(&gateway_decision, DropReason::ControlPlaneEtherType); } #[test] - fn drops_remote_dhcp_server_replies_but_allows_lan_replies() { + fn filters_remote_dhcp_server_replies_but_allows_lan_replies() { let mut registry = RoomRegistry::default(); let gateway = registry.join(gateway_hello()).unwrap(); let client = registry.join(client_hello(1)).unwrap(); @@ -1346,13 +1356,13 @@ mod tests { .forward_ethernet(&room(), gateway.peer().peer_id(), &gateway_frame) .unwrap(); - assert_dropped(&client_decision, DropReason::DhcpServerReply); + assert_filtered(&client_decision, DropReason::DhcpServerReply); assert_eq!(gateway_decision.action(), FrameAction::Forwarded); assert_eq!(gateway_decision.targets(), &[client.peer().peer_id()]); } #[test] - fn drops_remote_ipv6_router_advertisements() { + fn filters_remote_ipv6_router_advertisements() { let mut registry = RoomRegistry::default(); registry.join(gateway_hello()).unwrap(); let client = registry.join(client_hello(1)).unwrap(); @@ -1368,7 +1378,7 @@ mod tests { .forward_ethernet(&room(), client.peer().peer_id(), &frame) .unwrap(); - assert_dropped(&decision, DropReason::Ipv6RouterAdvertisement); + assert_filtered(&decision, DropReason::Ipv6RouterAdvertisement); } #[test]