fix(relay): keep unknown unicast on gateway path

The relay models the physical LAN as the gateway port, not as another remote
client. Client-originated unknown unicast now forwards only to the gateway, and
gateway-originated unknown unicast is dropped unless it resolves to a registered
remote client. Broadcast and multicast fanout is unchanged.

This prevents promiscuous gateway capture of unrelated LAN unicast from being
flooded to every remote client. It also keeps client-to-LAN traffic from
needlessly leaking to other clients in the room.

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:
2026-05-21 20:03:13 +02:00
parent 3e2648abc1
commit 587b0516cd
2 changed files with 33 additions and 7 deletions
+2
View File
@@ -105,6 +105,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.
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.
## Gateway
+31 -7
View File
@@ -507,7 +507,10 @@ impl Room {
{
return Ok(ForwardingDecision::rate_limited());
}
self.all_peer_ids_except(ingress_peer_id)
match ingress_role {
Role::Client => self.gateway_peer_id_except(ingress_peer_id),
Role::Gateway => Vec::new(),
}
};
if targets.is_empty() {
@@ -564,6 +567,13 @@ impl Room {
peer_ids.sort_unstable();
peer_ids
}
fn gateway_peer_id_except(&self, ingress_peer_id: u32) -> Vec<u32> {
self.gateway
.as_ref()
.filter(|gateway| gateway.peer_id() != ingress_peer_id)
.map_or_else(Vec::new, |gateway| vec![gateway.peer_id()])
}
}
#[derive(Debug, Clone)]
@@ -933,18 +943,35 @@ mod tests {
fn forwards_unknown_client_unicast_to_gateway() {
let mut registry = RoomRegistry::default();
let gateway = registry.join(gateway_hello()).unwrap();
let client = registry.join(client_hello(1)).unwrap();
let client_one = registry.join(client_hello(1)).unwrap();
let client_two = registry.join(client_hello(2)).unwrap();
let frame = ethernet(MacAddr::new([0x00, 1, 2, 3, 4, 5]), mac(1));
let decision = registry
.forward_ethernet(&room(), client.peer().peer_id(), &frame)
.forward_ethernet(&room(), client_one.peer().peer_id(), &frame)
.unwrap();
assert_eq!(decision.action(), FrameAction::Forwarded);
assert_eq!(decision.targets(), &[gateway.peer().peer_id()]);
assert!(!decision.targets().contains(&client_two.peer().peer_id()));
assert_eq!(decision.drop_reason(), None);
}
#[test]
fn drops_gateway_unicast_to_unknown_remote_mac() {
let mut registry = RoomRegistry::default();
let gateway = registry.join(gateway_hello()).unwrap();
registry.join(client_hello(1)).unwrap();
registry.join(client_hello(2)).unwrap();
let frame = ethernet(physical_mac(), MacAddr::new([0x00, 1, 2, 3, 4, 5]));
let decision = registry
.forward_ethernet(&room(), gateway.peer().peer_id(), &frame)
.unwrap();
assert_dropped(&decision, DropReason::UnknownDestination);
}
#[test]
fn forwards_gateway_unicast_to_matching_client() {
let mut registry = RoomRegistry::default();
@@ -1032,10 +1059,7 @@ mod tests {
.forward_ethernet_at(&room(), client_one.peer().peer_id(), &unknown_unicast, now)
.unwrap();
assert_eq!(decision.action(), FrameAction::Forwarded);
assert_eq!(
decision.targets(),
&[gateway.peer().peer_id(), client_two.peer().peer_id()]
);
assert_eq!(decision.targets(), &[gateway.peer().peer_id()]);
}
let decision = registry