From 4033b7c2d2164bb626198e8127e801a49cfefa9c Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 19:37:14 +0200 Subject: [PATCH] fix(gateway): request promiscuous AF_PACKET membership The gateway now asks Linux for PACKET_MR_PROMISC membership on the bound AF_PACKET socket. The tunnel depends on receiving LAN frames addressed to remote client MACs, not only frames addressed to the gateway NIC's own MAC. The switch may learn those remote MACs on the gateway port, but the NIC can still filter the unicast frames unless the packet socket requests promiscuous packet membership. This keeps the promiscuous lifetime scoped to the packet socket. If the kernel rejects the membership request, gateway startup fails instead of running in a mode that cannot reliably capture remote-client replies from the LAN. Test Plan: - cargo fmt --check - cargo test -p lanparty-gateway - cargo clippy -p lanparty-gateway --all-targets -- -D warnings - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md --- README.md | 8 ++--- crates/lanparty-gateway/src/packet.rs | 42 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c6926bb..7fbc8ca 100644 --- a/README.md +++ b/README.md @@ -116,10 +116,10 @@ cargo run -p lanparty-gateway -- \ The gateway connects to the relay as `role = gateway`, completes the control-stream hello/welcome handshake, opens an AF_PACKET socket on the LAN -interface, and bridges Ethernet frames between the relay and wired LAN until -shutdown. It tracks remote-client source MACs seen from relay traffic and -periodically emits small CAM refresh frames so the physical switch keeps those -MACs associated with the gateway port. +interface with promiscuous packet membership, and bridges Ethernet frames +between the relay and wired LAN until shutdown. It tracks remote-client source +MACs seen from relay traffic and periodically emits small CAM refresh frames so +the physical switch keeps those MACs associated with the gateway port. ## Windows Client diff --git a/crates/lanparty-gateway/src/packet.rs b/crates/lanparty-gateway/src/packet.rs index 8136655..f090341 100644 --- a/crates/lanparty-gateway/src/packet.rs +++ b/crates/lanparty-gateway/src/packet.rs @@ -58,6 +58,7 @@ impl PacketSocket { if result < 0 { return Err(io::Error::last_os_error()); } + enable_promiscuous_membership(fd.as_raw_fd(), interface_index)?; let interface_mac = interface_hardware_addr(fd.as_raw_fd(), interface)?; Ok(Self { @@ -153,6 +154,37 @@ pub fn interface_index(interface: &str) -> io::Result { Ok(index) } +fn enable_promiscuous_membership(fd: RawFd, interface_index: u32) -> io::Result<()> { + let membership = promiscuous_membership(interface_index); + let result = unsafe { + // SAFETY: membership points to a fully initialized packet_mreq and the provided length + // matches that kernel ABI struct. The socket fd is owned by PacketSocket::open. + libc::setsockopt( + fd, + libc::SOL_PACKET, + libc::PACKET_ADD_MEMBERSHIP, + (&membership as *const libc::packet_mreq).cast::(), + std::mem::size_of::() as libc::socklen_t, + ) + }; + if result < 0 { + return Err(io::Error::last_os_error()); + } + + Ok(()) +} + +fn promiscuous_membership(interface_index: u32) -> libc::packet_mreq { + let mut membership = unsafe { + // SAFETY: packet_mreq is a plain kernel ABI struct; zeroing leaves unused fields empty. + std::mem::zeroed::() + }; + membership.mr_ifindex = interface_index as libc::c_int; + membership.mr_type = libc::PACKET_MR_PROMISC as libc::c_ushort; + + membership +} + fn interface_name(interface: &str) -> io::Result { if interface.trim().is_empty() { return Err(io::Error::new( @@ -258,6 +290,16 @@ mod tests { assert_ne!(error.kind(), io::ErrorKind::InvalidInput); } + #[test] + fn builds_promiscuous_packet_membership_request() { + let membership = promiscuous_membership(12); + + assert_eq!(membership.mr_ifindex, 12); + assert_eq!(membership.mr_type, libc::PACKET_MR_PROMISC as u16); + assert_eq!(membership.mr_alen, 0); + assert_eq!(membership.mr_address, [0; 8]); + } + #[test] fn classifies_inbound_packet_types() { assert!(!is_inbound_packet_type(libc::PACKET_OUTGOING));