feat(relay): filter invalid source MACs

PLAN.md requires rejecting broadcast, multicast, and otherwise invalid source
MACs. Client ingress already had a forged-source check against the registered
client identity, but gateway ingress could still forward Ethernet frames whose
source MAC was not valid unicast.

Add an explicit `InvalidSourceMac` drop reason and filter invalid Ethernet
source MACs before client source authorization and last-seen refresh. This keeps
invalid source addresses out of both remote-client and gateway forwarding paths
while preserving `UnauthorizedSourceMac` for valid unicast sources that simply
belong to another client identity.

Document the invalid-source filter in the relay README decomposition.

Test Plan:
- cargo fmt --check
- cargo test -p lanparty-obs -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 22:17:32 +02:00
parent d2cf20f597
commit 881dee5491
3 changed files with 26 additions and 1 deletions
+2 -1
View File
@@ -99,7 +99,8 @@ Public relay binary and relay-owned room state:
- stable effective room MTU chosen before Ethernet datagrams flow - stable effective room MTU chosen before Ethernet datagrams flow
- live Ethernet datagram forwarding with no ingress reflection - live Ethernet datagram forwarding with no ingress reflection
- reliable `PeerJoined`/`PeerLeft` notifications to existing room peers - reliable `PeerJoined`/`PeerLeft` notifications to existing room peers
- L2 safety filters for jumbo, switch-control, DHCP-server, and IPv6-RA frames - L2 safety filters for invalid-source, jumbo, switch-control, DHCP-server,
and IPv6-RA frames
- client broadcast/multicast, unknown-unicast, and total bandwidth limiting - client broadcast/multicast, unknown-unicast, and total bandwidth limiting
- malformed peer datagram disconnect threshold - malformed peer datagram disconnect threshold
- peer stats control events retained for relay diagnostics - peer stats control events retained for relay diagnostics
+1
View File
@@ -30,6 +30,7 @@ pub enum FrameAction {
pub enum DropReason { pub enum DropReason {
Malformed, Malformed,
JumboFrame, JumboFrame,
InvalidSourceMac,
UnauthorizedSourceMac, UnauthorizedSourceMac,
DuplicateMac, DuplicateMac,
ControlPlaneEtherType, ControlPlaneEtherType,
+23
View File
@@ -516,6 +516,10 @@ impl Room {
Err(_) => return Ok(ForwardingDecision::dropped(DropReason::Malformed)), Err(_) => return Ok(ForwardingDecision::dropped(DropReason::Malformed)),
}; };
if !frame.source().is_valid_unicast() {
return Ok(ForwardingDecision::filtered(DropReason::InvalidSourceMac));
}
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 {
@@ -1302,6 +1306,25 @@ mod tests {
assert_filtered(&decision, DropReason::UnauthorizedSourceMac); assert_filtered(&decision, DropReason::UnauthorizedSourceMac);
} }
#[test]
fn filters_invalid_source_macs_from_clients_and_gateway() {
let mut registry = RoomRegistry::default();
let gateway = registry.join(gateway_hello()).unwrap();
let client = registry.join(client_hello(1)).unwrap();
let client_frame = ethernet(MacAddr::BROADCAST, MacAddr::BROADCAST);
let gateway_frame = ethernet(mac(1), MacAddr::ZERO);
let client_decision = registry
.forward_ethernet(&room(), client.peer().peer_id(), &client_frame)
.unwrap();
let gateway_decision = registry
.forward_ethernet(&room(), gateway.peer().peer_id(), &gateway_frame)
.unwrap();
assert_filtered(&client_decision, DropReason::InvalidSourceMac);
assert_filtered(&gateway_decision, DropReason::InvalidSourceMac);
}
#[test] #[test]
fn filters_jumbo_frames() { fn filters_jumbo_frames() {
let mut registry = RoomRegistry::default(); let mut registry = RoomRegistry::default();