feat(client): pin relay route before opening TAP

Create the relay host-route pin in the Windows client before the TAP adapter is
opened and marked connected. The guard is held until after `ClientSession`
shutdown so both the active tunnel and the QUIC close path keep using the
pre-TAP interface.

Route inspection or route creation failure now aborts startup before TAP
activation, and the client explicitly closes the relay session in that failure
path. Once the pin is installed, the client reports both the original best-route
snapshot and the pinned route. Default-route takeover detection/neutralization
is still future work.

Verification note: I attempted to check `lanparty-client-win` for
`x86_64-pc-windows-msvc`, but this host still lacks the Windows C headers
needed by `ring`; the build stops at `assert.h` before the binary crate can be
typechecked for Windows.

Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- cargo check -p lanparty-client-route --target x86_64-pc-windows-msvc
- cargo clippy -p lanparty-client-route --target x86_64-pc-windows-msvc --all-targets -- -D warnings
- git diff --check

Refs: PLAN.md
This commit is contained in:
2026-05-21 19:13:57 +02:00
parent 4fa1c1cabb
commit d90c06dd70
2 changed files with 41 additions and 14 deletions
+5 -4
View File
@@ -131,9 +131,10 @@ cargo run -p lanparty-client-win -- \
The Windows client binary currently connects to the relay as `role = client` The Windows client binary currently connects to the relay as `role = client`
with a generated locally administered virtual MAC persisted in with a generated locally administered virtual MAC persisted in
`lanparty-client-identity.json`, completes the control-stream hello/welcome `lanparty-client-identity.json`, completes the control-stream hello/welcome
handshake, snapshots the current relay route, and then bridges Ethernet frames handshake, pins a host route for the relay IP on the current pre-TAP interface,
between the relay and the first TAP-Windows6 adapter until shutdown. and then bridges Ethernet frames between the relay and the first TAP-Windows6
adapter until shutdown.
`--virtual-mac` can still override the stored identity for manual testing. On `--virtual-mac` can still override the stored identity for manual testing. On
Windows it marks the TAP media connected and reports the driver MAC/MTU before Windows it marks the TAP media connected and reports the driver MAC/MTU before
forwarding frames. Route pinning and automatic TAP MAC/MTU configuration are forwarding frames. Default-route takeover neutralization and automatic TAP
not wired yet. MAC/MTU configuration are not wired yet.
+36 -10
View File
@@ -13,7 +13,7 @@ use lanparty_client_core::{
ClientIdentity, ClientIdentityStore, ClientSession, ClientSessionConfig, connect_client, ClientIdentity, ClientIdentityStore, ClientSession, ClientSessionConfig, connect_client,
}; };
#[cfg(windows)] #[cfg(windows)]
use lanparty_client_route::RouteSnapshot; use lanparty_client_route::{PinnedRelayRoute, RouteSnapshot};
#[cfg(windows)] #[cfg(windows)]
use lanparty_client_tap::TapAdapter; use lanparty_client_tap::TapAdapter;
use lanparty_ctrl::RoomCode; use lanparty_ctrl::RoomCode;
@@ -100,17 +100,28 @@ async fn main() -> Result<()> {
session.welcome().room_id(), session.welcome().room_id(),
session.welcome().effective_tap_mtu() session.welcome().effective_tap_mtu()
); );
#[cfg(windows)]
let relay_route_pin = match pin_relay_route_before_tap(session.config().relay_addr().ip()) {
Ok(pin) => pin,
Err(error) => {
session.shutdown("client startup failed").await;
return Err(error);
}
};
let run_result = run_client(&session).await; let run_result = run_client(&session).await;
session.shutdown("client shutting down").await; session.shutdown("client shutting down").await;
#[cfg(windows)]
drop(relay_route_pin);
run_result run_result
} }
#[cfg(windows)] #[cfg(windows)]
async fn run_client(session: &ClientSession) -> Result<()> { async fn run_client(session: &ClientSession) -> Result<()> {
log_relay_route_before_tap(session.config().relay_addr().ip());
let tap = open_tap_adapter(session)?; let tap = open_tap_adapter(session)?;
println!("bridging TAP frames; route pinning is not wired yet; press Ctrl-C to stop"); println!(
"bridging TAP frames; relay route is pinned; default-route neutralization is not wired yet; press Ctrl-C to stop"
);
tokio::select! { tokio::select! {
result = run_tap_frame_pump(session.relay_io(), tap) => result, result = run_tap_frame_pump(session.relay_io(), tap) => result,
@@ -129,13 +140,15 @@ async fn run_client(session: &ClientSession) -> Result<()> {
} }
#[cfg(windows)] #[cfg(windows)]
fn log_relay_route_before_tap(destination: std::net::IpAddr) { fn pin_relay_route_before_tap(destination: std::net::IpAddr) -> Result<PinnedRelayRoute> {
match lanparty_client_route::best_route_to(destination) { let route = lanparty_client_route::best_route_to(destination)
Ok(route) => print_relay_route(&route), .context("failed to inspect relay route before TAP activation")?;
Err(error) => eprintln!( print_relay_route(&route);
"failed to inspect relay route before TAP activation; route pinning is not wired yet: {error:#}" let pin = lanparty_client_route::pin_relay_route(&route)
), .context("failed to pin relay route before TAP activation")?;
} print_pinned_relay_route(&pin);
Ok(pin)
} }
#[cfg(windows)] #[cfg(windows)]
@@ -155,6 +168,19 @@ fn print_relay_route(route: &RouteSnapshot) {
); );
} }
#[cfg(windows)]
fn print_pinned_relay_route(route: &PinnedRelayRoute) {
println!(
"relay route pinned before TAP: destination {} next hop {} interface index {} LUID {}",
route.destination(),
route
.next_hop()
.map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string()),
route.interface_index(),
route.interface_luid()
);
}
#[cfg(windows)] #[cfg(windows)]
fn open_tap_adapter(session: &ClientSession) -> Result<lanparty_client_tap::TapAdapter> { fn open_tap_adapter(session: &ClientSession) -> Result<lanparty_client_tap::TapAdapter> {
let tap = lanparty_client_tap::open_first_adapter()?; let tap = lanparty_client_tap::open_first_adapter()?;