diff --git a/README.md b/README.md index d03306f..74b63bd 100644 --- a/README.md +++ b/README.md @@ -173,16 +173,18 @@ cargo run -p lanparty-gateway -- \ The gateway first opens the wired LAN interface as an AF_PACKET socket with promiscuous packet membership, then connects to the relay as `role = gateway` and completes the control-stream hello/welcome handshake. That startup order -keeps an invalid or wireless interface from briefly advertising a gateway that -cannot bridge. Once both sides are ready, it bridges Ethernet frames between the -relay and wired LAN until shutdown. It captures whole LAN frames up to the +keeps an invalid, wireless, or unplugged interface from briefly advertising a +gateway that cannot bridge. Once both sides are ready, it bridges Ethernet +frames between the relay and wired LAN until shutdown. It captures whole LAN +frames up to the overlay payload-length ceiling before deciding whether they fit the 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. +The gateway rejects Linux interfaces that sysfs identifies as Wi-Fi, and rejects +wired interfaces whose sysfs carrier state reports no link; 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/TESTING.md b/TESTING.md index b8421a3..3ff356f 100644 --- a/TESTING.md +++ b/TESTING.md @@ -72,7 +72,8 @@ sudo ./target/release/lanparty-gateway \ --iface eth0 ``` -Use the real wired LAN interface name for `--iface`. Do not use Wi-Fi. +Use the real wired LAN interface name for `--iface`. Do not use Wi-Fi. The +gateway fails before joining the relay if sysfs reports no Ethernet carrier. Expected gateway output: @@ -219,6 +220,10 @@ drop_reason=ControlPlaneEtherType If the client says `Waiting for LAN gateway`, check that the gateway uses the same room code and is connected to the same relay. +If the gateway fails with `reports no carrier`, plug the selected Ethernet +interface into the LAN party switch, bring the interface up, and restart the +gateway. + If startup fails before the relay connection while preparing the TAP adapter, check that the terminal is elevated, TAP-Windows6 is installed, and `--tap-instance-id` selects the intended adapter when more than one TAP adapter diff --git a/crates/lanparty-gateway/src/packet.rs b/crates/lanparty-gateway/src/packet.rs index c0d20ca..c32f222 100644 --- a/crates/lanparty-gateway/src/packet.rs +++ b/crates/lanparty-gateway/src/packet.rs @@ -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 { 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> { + 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 { 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)