From 77025e6564067c1e4f0e23675f69e0b94498e75b Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 22:54:58 +0200 Subject: [PATCH] fix(gateway): reject wireless LAN interfaces The plan explicitly keeps the physical LAN gateway wired-only for the MVP. Managed Wi-Fi adapters are not reliable for arbitrary source-MAC injection, but the gateway previously accepted any interface that could be opened as an Ethernet-like packet socket. Reject Linux interfaces that sysfs marks as wireless before opening the raw packet socket. The check looks for the common `wireless` and `phy80211` markers under `/sys/class/net/`, and keeps path separators out of interface names so the sysfs lookup stays scoped to a single netdev name. Document the wired-only enforcement in the gateway README section. Test Plan: - cargo fmt --check - git diff --check - cargo test -p lanparty-gateway - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings Refs: PLAN.md --- README.md | 2 + crates/lanparty-gateway/src/packet.rs | 63 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/README.md b/README.md index 1884861..3415d92 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,8 @@ tunnel. It never fragments Ethernet frames; LAN frames whose encoded datagrams exceed the negotiated QUIC budget are counted, dropped, and logged instead of stopping the bridge. `--relay` accepts a DNS name or socket address; bare hosts default to UDP/443. +The gateway rejects Linux interfaces that sysfs identifies as Wi-Fi; managed +wireless NICs are not supported for the physical LAN bridge. 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. Gateway diff --git a/crates/lanparty-gateway/src/packet.rs b/crates/lanparty-gateway/src/packet.rs index f090341..c0d20ca 100644 --- a/crates/lanparty-gateway/src/packet.rs +++ b/crates/lanparty-gateway/src/packet.rs @@ -2,11 +2,14 @@ use std::{ ffi::CString, io, os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, + path::Path, }; use lanparty_proto::MacAddr; const ETH_P_ALL: u16 = libc::ETH_P_ALL as u16; +const SYS_CLASS_NET: &str = "/sys/class/net"; +const WIRELESS_INTERFACE_MARKERS: &[&str] = &["wireless", "phy80211"]; #[derive(Debug)] pub struct PacketSocket { @@ -19,6 +22,7 @@ pub struct PacketSocket { impl PacketSocket { pub fn open(interface: &str) -> io::Result { let interface_index = interface_index(interface)?; + reject_wireless_interface(interface)?; let protocol = i32::from(ETH_P_ALL.to_be()); let raw_fd = unsafe { // SAFETY: socket is called with constant domain/type/protocol values and returns @@ -193,6 +197,13 @@ fn interface_name(interface: &str) -> io::Result { )); } + if interface.contains('/') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "interface name cannot contain path separators", + )); + } + let name = CString::new(interface).map_err(|_| { io::Error::new( io::ErrorKind::InvalidInput, @@ -209,6 +220,23 @@ fn interface_name(interface: &str) -> io::Result { Ok(name) } +fn reject_wireless_interface(interface: &str) -> io::Result<()> { + if interface_is_wireless_in_sysfs(Path::new(SYS_CLASS_NET), interface) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("wireless interface {interface} cannot be used as a LAN gateway"), + )); + } + + Ok(()) +} + +fn interface_is_wireless_in_sysfs(sys_class_net: &Path, interface: &str) -> bool { + WIRELESS_INTERFACE_MARKERS + .iter() + .any(|marker| sys_class_net.join(interface).join(marker).exists()) +} + fn interface_hardware_addr(fd: RawFd, interface: &str) -> io::Result { let name = interface_name(interface)?; let mut request = unsafe { @@ -269,6 +297,11 @@ fn is_inbound_packet_type(packet_type: u8) -> bool { #[cfg(test)] mod tests { + use std::{ + fs, + time::{SystemTime, UNIX_EPOCH}, + }; + use super::*; #[test] @@ -281,6 +314,10 @@ mod tests { interface_index("eth0\0bad").unwrap_err().kind(), io::ErrorKind::InvalidInput ); + assert_eq!( + interface_index("eth0/bad").unwrap_err().kind(), + io::ErrorKind::InvalidInput + ); } #[test] @@ -307,4 +344,30 @@ mod tests { assert!(is_inbound_packet_type(libc::PACKET_BROADCAST)); assert!(is_inbound_packet_type(libc::PACKET_MULTICAST)); } + + #[test] + fn detects_wireless_interfaces_from_sysfs_markers() { + let sys_class_net = unique_temp_dir("lanparty-gateway-sysfs"); + let wired = sys_class_net.join("eth0"); + let wireless = sys_class_net.join("wlan0"); + let phy_wireless = sys_class_net.join("wlp1s0"); + fs::create_dir_all(&wired).unwrap(); + fs::create_dir_all(wireless.join("wireless")).unwrap(); + fs::create_dir_all(phy_wireless.join("phy80211")).unwrap(); + + assert!(!interface_is_wireless_in_sysfs(&sys_class_net, "eth0")); + assert!(interface_is_wireless_in_sysfs(&sys_class_net, "wlan0")); + assert!(interface_is_wireless_in_sysfs(&sys_class_net, "wlp1s0")); + + fs::remove_dir_all(sys_class_net).unwrap(); + } + + fn unique_temp_dir(prefix: &str) -> std::path::PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + + std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id())) + } }