fix(gateway): reject LAN interfaces without carrier
A gateway with the selected Ethernet cable unplugged can still open an AF_PACKET socket and join the relay room. That makes clients see a connected LAN gateway even though DHCP and LAN discovery cannot make the physical round trip. Check the selected Linux interface's sysfs carrier file before creating the raw socket. If sysfs reports carrier 0, fail before the gateway joins the relay. Missing or unrecognized carrier files remain allowed so this does not reject interfaces where the kernel cannot expose link state in that form. README and TESTING now document the preflight and the operator fix. Test Plan: All cargo commands used these environment variables: RUSTUP_HOME=/tmp/softlan-vpn-rustup CARGO_HOME=/tmp/softlan-vpn-cargo - cargo test -p lanparty-gateway \ packet::tests::detects_disconnected_interfaces_from_sysfs_carrier - cargo test -p lanparty-gateway - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md wired Ethernet gateway requirement
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
ffi::CString,
|
||||
io,
|
||||
fs, io,
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
|
||||
path::Path,
|
||||
};
|
||||
@@ -10,6 +10,7 @@ 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"];
|
||||
const CARRIER_FILE: &str = "carrier";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PacketSocket {
|
||||
@@ -23,6 +24,7 @@ impl PacketSocket {
|
||||
pub fn open(interface: &str) -> io::Result<Self> {
|
||||
let interface_index = interface_index(interface)?;
|
||||
reject_wireless_interface(interface)?;
|
||||
reject_disconnected_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
|
||||
@@ -231,12 +233,38 @@ fn reject_wireless_interface(interface: &str) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reject_disconnected_interface(interface: &str) -> io::Result<()> {
|
||||
if interface_carrier_state(Path::new(SYS_CLASS_NET), interface)? == Some(false) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"wired interface {interface} reports no carrier; plug it into the LAN switch before starting the 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_carrier_state(sys_class_net: &Path, interface: &str) -> io::Result<Option<bool>> {
|
||||
let carrier_path = sys_class_net.join(interface).join(CARRIER_FILE);
|
||||
match fs::read_to_string(&carrier_path) {
|
||||
Ok(value) => match value.trim() {
|
||||
"0" => Ok(Some(false)),
|
||||
"1" => Ok(Some(true)),
|
||||
_ => Ok(None),
|
||||
},
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn interface_hardware_addr(fd: RawFd, interface: &str) -> io::Result<MacAddr> {
|
||||
let name = interface_name(interface)?;
|
||||
let mut request = unsafe {
|
||||
@@ -362,6 +390,39 @@ mod tests {
|
||||
fs::remove_dir_all(sys_class_net).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_disconnected_interfaces_from_sysfs_carrier() {
|
||||
let sys_class_net = unique_temp_dir("lanparty-gateway-carrier");
|
||||
let connected = sys_class_net.join("eth0");
|
||||
let disconnected = sys_class_net.join("eth1");
|
||||
let unknown = sys_class_net.join("eth2");
|
||||
fs::create_dir_all(&connected).unwrap();
|
||||
fs::create_dir_all(&disconnected).unwrap();
|
||||
fs::create_dir_all(&unknown).unwrap();
|
||||
fs::write(connected.join(CARRIER_FILE), "1\n").unwrap();
|
||||
fs::write(disconnected.join(CARRIER_FILE), "0\n").unwrap();
|
||||
fs::write(unknown.join(CARRIER_FILE), "unknown\n").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
interface_carrier_state(&sys_class_net, "eth0").unwrap(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
interface_carrier_state(&sys_class_net, "eth1").unwrap(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
interface_carrier_state(&sys_class_net, "eth2").unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
interface_carrier_state(&sys_class_net, "eth-missing").unwrap(),
|
||||
None
|
||||
);
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user