feat(relay): classify safety rejects as filtered
PLAN.md describes relay frame logs with separate forwarded, dropped, filtered, and rate-limited actions. The relay had the shared `Filtered` action in its log vocabulary, but safety-policy rejects were still reported as generic drops. Classify forged client source MACs and L2 safety-filter matches as filtered forwarding decisions. Malformed frames and unknown destinations remain drops, while rate limits continue to use the rate-limited action. Document the distinction in the relay README section. Test Plan: - cargo fmt --check - cargo test -p lanparty-relay - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md
This commit is contained in:
@@ -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 so the gateway and client can pin it in development. Production
|
||||||
certificate handling remains future work. Ethernet forwarding decisions are
|
certificate handling remains future work. Ethernet forwarding decisions are
|
||||||
logged with room, peer, MAC, ethertype, action, drop reason, and target count.
|
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
|
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.
|
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
|
When a peer joins or leaves, the relay sends a reliable lifecycle control event
|
||||||
|
|||||||
@@ -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]
|
#[must_use]
|
||||||
fn rate_limited() -> Self {
|
fn rate_limited() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -510,7 +519,7 @@ impl Room {
|
|||||||
if ingress_role == Role::Client {
|
if ingress_role == Role::Client {
|
||||||
let expected_source = ingress_mac.expect("client peers have MAC addresses");
|
let expected_source = ingress_mac.expect("client peers have MAC addresses");
|
||||||
if frame.source() != expected_source {
|
if frame.source() != expected_source {
|
||||||
return Ok(ForwardingDecision::dropped(
|
return Ok(ForwardingDecision::filtered(
|
||||||
DropReason::UnauthorizedSourceMac,
|
DropReason::UnauthorizedSourceMac,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -519,7 +528,7 @@ impl Room {
|
|||||||
self.mark_peer_seen(ingress_peer_id, now);
|
self.mark_peer_seen(ingress_peer_id, now);
|
||||||
|
|
||||||
if let Some(drop_reason) = safety_drop_reason(ingress_role, frame) {
|
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
|
if ingress_role == Role::Client
|
||||||
@@ -859,6 +868,12 @@ mod tests {
|
|||||||
assert!(decision.targets().is_empty());
|
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) {
|
fn assert_rate_limited(decision: &ForwardingDecision) {
|
||||||
assert_eq!(decision.action(), FrameAction::RateLimited);
|
assert_eq!(decision.action(), FrameAction::RateLimited);
|
||||||
assert_eq!(decision.drop_reason(), Some(DropReason::RateLimit));
|
assert_eq!(decision.drop_reason(), Some(DropReason::RateLimit));
|
||||||
@@ -1139,7 +1154,7 @@ mod tests {
|
|||||||
before + Duration::from_secs(5),
|
before + Duration::from_secs(5),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_dropped(&decision, DropReason::UnauthorizedSourceMac);
|
assert_filtered(&decision, DropReason::UnauthorizedSourceMac);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
@@ -1275,7 +1290,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn drops_client_frames_with_forged_source_mac() {
|
fn filters_client_frames_with_forged_source_mac() {
|
||||||
let mut registry = RoomRegistry::default();
|
let mut registry = RoomRegistry::default();
|
||||||
let client = registry.join(client_hello(1)).unwrap();
|
let client = registry.join(client_hello(1)).unwrap();
|
||||||
let frame = ethernet(MacAddr::BROADCAST, mac(2));
|
let frame = ethernet(MacAddr::BROADCAST, mac(2));
|
||||||
@@ -1284,16 +1299,11 @@ mod tests {
|
|||||||
.forward_ethernet(&room(), client.peer().peer_id(), &frame)
|
.forward_ethernet(&room(), client.peer().peer_id(), &frame)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(decision.action(), FrameAction::Dropped);
|
assert_filtered(&decision, DropReason::UnauthorizedSourceMac);
|
||||||
assert_eq!(
|
|
||||||
decision.drop_reason(),
|
|
||||||
Some(DropReason::UnauthorizedSourceMac)
|
|
||||||
);
|
|
||||||
assert!(decision.targets().is_empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn drops_jumbo_frames() {
|
fn filters_jumbo_frames() {
|
||||||
let mut registry = RoomRegistry::default();
|
let mut registry = RoomRegistry::default();
|
||||||
registry.join(gateway_hello()).unwrap();
|
registry.join(gateway_hello()).unwrap();
|
||||||
let client = registry.join(client_hello(1)).unwrap();
|
let client = registry.join(client_hello(1)).unwrap();
|
||||||
@@ -1304,11 +1314,11 @@ mod tests {
|
|||||||
.forward_ethernet(&room(), client.peer().peer_id(), &frame)
|
.forward_ethernet(&room(), client.peer().peer_id(), &frame)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_dropped(&decision, DropReason::JumboFrame);
|
assert_filtered(&decision, DropReason::JumboFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 mut registry = RoomRegistry::default();
|
||||||
let gateway = registry.join(gateway_hello()).unwrap();
|
let gateway = registry.join(gateway_hello()).unwrap();
|
||||||
let client = registry.join(client_hello(1)).unwrap();
|
let client = registry.join(client_hello(1)).unwrap();
|
||||||
@@ -1324,12 +1334,12 @@ mod tests {
|
|||||||
.forward_ethernet(&room(), gateway.peer().peer_id(), &gateway_frame)
|
.forward_ethernet(&room(), gateway.peer().peer_id(), &gateway_frame)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_dropped(&client_decision, DropReason::ControlPlaneEtherType);
|
assert_filtered(&client_decision, DropReason::ControlPlaneEtherType);
|
||||||
assert_dropped(&gateway_decision, DropReason::ControlPlaneEtherType);
|
assert_filtered(&gateway_decision, DropReason::ControlPlaneEtherType);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 mut registry = RoomRegistry::default();
|
||||||
let gateway = registry.join(gateway_hello()).unwrap();
|
let gateway = registry.join(gateway_hello()).unwrap();
|
||||||
let client = registry.join(client_hello(1)).unwrap();
|
let client = registry.join(client_hello(1)).unwrap();
|
||||||
@@ -1346,13 +1356,13 @@ mod tests {
|
|||||||
.forward_ethernet(&room(), gateway.peer().peer_id(), &gateway_frame)
|
.forward_ethernet(&room(), gateway.peer().peer_id(), &gateway_frame)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_dropped(&client_decision, DropReason::DhcpServerReply);
|
assert_filtered(&client_decision, DropReason::DhcpServerReply);
|
||||||
assert_eq!(gateway_decision.action(), FrameAction::Forwarded);
|
assert_eq!(gateway_decision.action(), FrameAction::Forwarded);
|
||||||
assert_eq!(gateway_decision.targets(), &[client.peer().peer_id()]);
|
assert_eq!(gateway_decision.targets(), &[client.peer().peer_id()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn drops_remote_ipv6_router_advertisements() {
|
fn filters_remote_ipv6_router_advertisements() {
|
||||||
let mut registry = RoomRegistry::default();
|
let mut registry = RoomRegistry::default();
|
||||||
registry.join(gateway_hello()).unwrap();
|
registry.join(gateway_hello()).unwrap();
|
||||||
let client = registry.join(client_hello(1)).unwrap();
|
let client = registry.join(client_hello(1)).unwrap();
|
||||||
@@ -1368,7 +1378,7 @@ mod tests {
|
|||||||
.forward_ethernet(&room(), client.peer().peer_id(), &frame)
|
.forward_ethernet(&room(), client.peer().peer_id(), &frame)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_dropped(&decision, DropReason::Ipv6RouterAdvertisement);
|
assert_filtered(&decision, DropReason::Ipv6RouterAdvertisement);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user