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:
@@ -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
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user