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/<iface>`, 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
This commit is contained in:
2026-05-21 22:54:58 +02:00
parent d4c96569e3
commit 77025e6564
2 changed files with 65 additions and 0 deletions
+2
View File
@@ -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 exceed the negotiated QUIC budget are counted, dropped, and logged instead of
stopping the bridge. stopping the bridge.
`--relay` accepts a DNS name or socket address; bare hosts default to UDP/443. `--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 It tracks remote-client source MACs seen from relay traffic and periodically
emits small CAM refresh frames so the physical switch keeps those MACs emits small CAM refresh frames so the physical switch keeps those MACs
associated with the gateway port. Gateway associated with the gateway port. Gateway
+63
View File
@@ -2,11 +2,14 @@ use std::{
ffi::CString, ffi::CString,
io, io,
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
path::Path,
}; };
use lanparty_proto::MacAddr; use lanparty_proto::MacAddr;
const ETH_P_ALL: u16 = libc::ETH_P_ALL as u16; 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)] #[derive(Debug)]
pub struct PacketSocket { pub struct PacketSocket {
@@ -19,6 +22,7 @@ pub struct PacketSocket {
impl PacketSocket { impl PacketSocket {
pub fn open(interface: &str) -> io::Result<Self> { pub fn open(interface: &str) -> io::Result<Self> {
let interface_index = interface_index(interface)?; let interface_index = interface_index(interface)?;
reject_wireless_interface(interface)?;
let protocol = i32::from(ETH_P_ALL.to_be()); let protocol = i32::from(ETH_P_ALL.to_be());
let raw_fd = unsafe { let raw_fd = unsafe {
// SAFETY: socket is called with constant domain/type/protocol values and returns // SAFETY: socket is called with constant domain/type/protocol values and returns
@@ -193,6 +197,13 @@ fn interface_name(interface: &str) -> io::Result<CString> {
)); ));
} }
if interface.contains('/') {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"interface name cannot contain path separators",
));
}
let name = CString::new(interface).map_err(|_| { let name = CString::new(interface).map_err(|_| {
io::Error::new( io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
@@ -209,6 +220,23 @@ fn interface_name(interface: &str) -> io::Result<CString> {
Ok(name) 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<MacAddr> { fn interface_hardware_addr(fd: RawFd, interface: &str) -> io::Result<MacAddr> {
let name = interface_name(interface)?; let name = interface_name(interface)?;
let mut request = unsafe { let mut request = unsafe {
@@ -269,6 +297,11 @@ fn is_inbound_packet_type(packet_type: u8) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{
fs,
time::{SystemTime, UNIX_EPOCH},
};
use super::*; use super::*;
#[test] #[test]
@@ -281,6 +314,10 @@ mod tests {
interface_index("eth0\0bad").unwrap_err().kind(), interface_index("eth0\0bad").unwrap_err().kind(),
io::ErrorKind::InvalidInput io::ErrorKind::InvalidInput
); );
assert_eq!(
interface_index("eth0/bad").unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
} }
#[test] #[test]
@@ -307,4 +344,30 @@ mod tests {
assert!(is_inbound_packet_type(libc::PACKET_BROADCAST)); assert!(is_inbound_packet_type(libc::PACKET_BROADCAST));
assert!(is_inbound_packet_type(libc::PACKET_MULTICAST)); 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()))
}
} }