fix(gateway): validate LAN interface before relay join

The gateway previously joined the relay room before opening the Linux
AF_PACKET socket. If the operator passed the wrong interface, a wireless
interface, or an interface that could not be opened, the relay could briefly
advertise a gateway that was never able to bridge LAN traffic.

Parse CLI args first, then keep platform handling in a small cfg-gated run
function. On Linux, build the config, open the AF_PACKET socket, and only then
join the relay as the room gateway. On non-Linux targets, fail before reading
relay certificate files, resolving relay names, or opening network connections.

Update the README and MVP test guide so the documented gateway startup order
matches the binary output.

Test Plan:
- cargo fmt --check
- cargo test -p lanparty-gateway
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check

Refs: MVP gateway startup hardening
This commit is contained in:
2026-05-21 23:19:26 +02:00
parent 2f0802dfcf
commit 47f66b7d04
3 changed files with 36 additions and 26 deletions
+10 -8
View File
@@ -168,14 +168,16 @@ cargo run -p lanparty-gateway -- \
--iface eth0
```
The gateway connects to the relay as `role = gateway`, completes the
control-stream hello/welcome handshake, opens an AF_PACKET socket on the LAN
interface with promiscuous packet membership, and 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.
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
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.
+2 -1
View File
@@ -77,8 +77,9 @@ Use the real wired LAN interface name for `--iface`. Do not use Wi-Fi.
Expected gateway output:
```text
lanparty-gateway connected as peer ...
lanparty-gateway opening interface eth0 and connecting to relay ...
lanparty-gateway opened AF_PACKET socket on eth0 ...
lanparty-gateway connected as peer ...
lanparty-gateway bridging frames; press Ctrl-C to stop
```
+22 -15
View File
@@ -1,18 +1,31 @@
use clap::Parser;
use lanparty_gateway::GatewayArgs;
#[cfg(target_os = "linux")]
use lanparty_gateway::PacketSocket;
use lanparty_gateway::{GatewayArgs, connect_gateway};
use lanparty_gateway::{PacketSocket, connect_gateway};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = GatewayArgs::parse().into_config()?;
let args = GatewayArgs::parse();
run(args).await
}
#[cfg(target_os = "linux")]
async fn run(args: GatewayArgs) -> anyhow::Result<()> {
let config = args.into_config()?;
println!(
"lanparty-gateway connecting interface {} to relay {} room {}",
"lanparty-gateway opening interface {} and connecting to relay {} room {}",
config.interface(),
config.relay_addr(),
config.room()
);
let socket = PacketSocket::open(config.interface())?;
println!(
"lanparty-gateway opened AF_PACKET socket on {} (ifindex {})",
socket.interface(),
socket.interface_index()
);
let gateway = connect_gateway(config).await?;
println!(
"lanparty-gateway connected as peer {} in room id {} with TAP MTU {} over {}",
@@ -21,19 +34,13 @@ async fn main() -> anyhow::Result<()> {
gateway.welcome().effective_tap_mtu(),
gateway.welcome().mode()
);
#[cfg(target_os = "linux")]
{
let socket = PacketSocket::open(gateway.config().interface())?;
println!(
"lanparty-gateway opened AF_PACKET socket on {} (ifindex {})",
socket.interface(),
socket.interface_index()
);
println!("lanparty-gateway bridging frames; press Ctrl-C to stop");
gateway.bridge_until_shutdown(socket).await?;
}
#[cfg(not(target_os = "linux"))]
anyhow::bail!("lanparty-gateway requires Linux AF_PACKET support");
Ok(())
}
#[cfg(not(target_os = "linux"))]
async fn run(_args: GatewayArgs) -> anyhow::Result<()> {
anyhow::bail!("lanparty-gateway requires Linux AF_PACKET support");
}