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:
@@ -173,16 +173,18 @@ cargo run -p lanparty-gateway -- \
|
|||||||
The gateway first opens the wired LAN interface as an AF_PACKET socket with
|
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`
|
promiscuous packet membership, then connects to the relay as `role = gateway`
|
||||||
and completes the control-stream hello/welcome handshake. That startup order
|
and completes the control-stream hello/welcome handshake. That startup order
|
||||||
keeps an invalid or wireless interface from briefly advertising a gateway that
|
keeps an invalid, wireless, or unplugged interface from briefly advertising a
|
||||||
cannot bridge. Once both sides are ready, it bridges Ethernet frames between the
|
gateway that cannot bridge. Once both sides are ready, it bridges Ethernet
|
||||||
relay and wired LAN until shutdown. It captures whole LAN frames up to the
|
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
|
overlay payload-length ceiling before deciding whether they fit the tunnel. It
|
||||||
never fragments Ethernet frames; LAN frames whose encoded datagrams exceed the
|
never fragments Ethernet frames; LAN frames whose encoded datagrams exceed the
|
||||||
negotiated QUIC budget are counted, dropped, and logged instead of stopping the
|
negotiated QUIC budget are counted, dropped, and logged instead of stopping the
|
||||||
bridge.
|
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
|
The gateway rejects Linux interfaces that sysfs identifies as Wi-Fi, and rejects
|
||||||
wireless NICs are not supported for the physical LAN bridge.
|
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
|
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
|
||||||
|
|||||||
+6
-1
@@ -72,7 +72,8 @@ sudo ./target/release/lanparty-gateway \
|
|||||||
--iface eth0
|
--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:
|
Expected gateway output:
|
||||||
|
|
||||||
@@ -219,6 +220,10 @@ drop_reason=ControlPlaneEtherType
|
|||||||
If the client says `Waiting for LAN gateway`, check that the gateway uses the
|
If the client says `Waiting for LAN gateway`, check that the gateway uses the
|
||||||
same room code and is connected to the same relay.
|
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,
|
If startup fails before the relay connection while preparing the TAP adapter,
|
||||||
check that the terminal is elevated, TAP-Windows6 is installed, and
|
check that the terminal is elevated, TAP-Windows6 is installed, and
|
||||||
`--tap-instance-id` selects the intended adapter when more than one TAP adapter
|
`--tap-instance-id` selects the intended adapter when more than one TAP adapter
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
ffi::CString,
|
ffi::CString,
|
||||||
io,
|
fs, io,
|
||||||
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
|
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
|
||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
@@ -10,6 +10,7 @@ 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 SYS_CLASS_NET: &str = "/sys/class/net";
|
||||||
const WIRELESS_INTERFACE_MARKERS: &[&str] = &["wireless", "phy80211"];
|
const WIRELESS_INTERFACE_MARKERS: &[&str] = &["wireless", "phy80211"];
|
||||||
|
const CARRIER_FILE: &str = "carrier";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PacketSocket {
|
pub struct PacketSocket {
|
||||||
@@ -23,6 +24,7 @@ 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)?;
|
reject_wireless_interface(interface)?;
|
||||||
|
reject_disconnected_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
|
||||||
@@ -231,12 +233,38 @@ fn reject_wireless_interface(interface: &str) -> io::Result<()> {
|
|||||||
Ok(())
|
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 {
|
fn interface_is_wireless_in_sysfs(sys_class_net: &Path, interface: &str) -> bool {
|
||||||
WIRELESS_INTERFACE_MARKERS
|
WIRELESS_INTERFACE_MARKERS
|
||||||
.iter()
|
.iter()
|
||||||
.any(|marker| sys_class_net.join(interface).join(marker).exists())
|
.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> {
|
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 {
|
||||||
@@ -362,6 +390,39 @@ mod tests {
|
|||||||
fs::remove_dir_all(sys_class_net).unwrap();
|
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 {
|
fn unique_temp_dir(prefix: &str) -> std::path::PathBuf {
|
||||||
let nanos = SystemTime::now()
|
let nanos = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
Reference in New Issue
Block a user