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
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
+63
View File
@@ -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<Self> {
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<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(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
@@ -209,6 +220,23 @@ fn interface_name(interface: &str) -> io::Result<CString> {
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> {
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()))
}
}