From c6a4a9da893fbc96d423b0e681a879153cd6583c Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 19:24:01 +0200 Subject: [PATCH] feat(client): scope TAP interface metrics while running The Windows client now applies a high manual metric to the TAP interface while the adapter is active. This keeps ordinary host routes preferred over TAP routes during the tunnel lifetime, and the route crate guard restores the previous metric and automatic-metric state when the client exits or startup unwinds. IPv4 metric protection is required because the tunnel depends on keeping the relay path reachable. IPv6 metric protection is attempted as a best-effort step so IPv4-only Windows setups can still run while dual-stack hosts receive similar protection when the IPv6 interface row exists. The metric guard is held for the same lifetime as the TAP frame pump. The relay host-route pin remains held through QUIC shutdown. Default-route takeover detection and automatic TAP MAC/MTU configuration are still follow-up work from PLAN.md. 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 --- README.md | 7 ++- crates/lanparty-client-win/src/main.rs | 76 ++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dc69388..bb62e1b 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ 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 Windows it marks the TAP media connected and reports the driver MAC/MTU before -forwarding frames, along with the TAP interface index/LUID. Default-route -takeover neutralization and automatic TAP MAC/MTU configuration are not wired -yet. +forwarding frames, along with the TAP interface index/LUID. The client applies +a scoped TAP interface metric while it runs and restores the previous metric on +exit. Default-route takeover detection and automatic TAP MAC/MTU configuration +are not wired yet. diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index baca39d..75913d3 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -13,12 +13,18 @@ use lanparty_client_core::{ ClientIdentity, ClientIdentityStore, ClientSession, ClientSessionConfig, connect_client, }; #[cfg(windows)] -use lanparty_client_route::{PinnedRelayRoute, RouteSnapshot}; +use lanparty_client_route::{ + IpInterfaceFamily, NetworkInterfaceIdentity, PinnedRelayRoute, RouteSnapshot, + ScopedInterfaceMetric, +}; #[cfg(windows)] use lanparty_client_tap::TapAdapter; use lanparty_ctrl::RoomCode; use lanparty_proto::MacAddr; +#[cfg(windows)] +const TAP_INTERFACE_METRIC: u32 = 9_000; + #[derive(Debug, Parser)] #[command( name = "lanparty-client-win", @@ -118,9 +124,9 @@ async fn main() -> Result<()> { #[cfg(windows)] async fn run_client(session: &ClientSession) -> Result<()> { - let tap = open_tap_adapter(session)?; + let OpenedTapAdapter { tap, _metric_guard } = open_tap_adapter(session)?; println!( - "bridging TAP frames; relay route is pinned; default-route neutralization is not wired yet; press Ctrl-C to stop" + "bridging TAP frames; relay route is pinned and TAP metrics are scoped; default-route takeover detection is not wired yet; press Ctrl-C to stop" ); tokio::select! { @@ -182,12 +188,25 @@ fn print_pinned_relay_route(route: &PinnedRelayRoute) { } #[cfg(windows)] -fn open_tap_adapter(session: &ClientSession) -> Result { +struct OpenedTapAdapter { + tap: TapAdapter, + _metric_guard: TapMetricGuard, +} + +#[cfg(windows)] +struct TapMetricGuard { + _ipv4: ScopedInterfaceMetric, + _ipv6: Option, +} + +#[cfg(windows)] +fn open_tap_adapter(session: &ClientSession) -> Result { let tap = lanparty_client_tap::open_first_adapter()?; tap.set_media_connected(true)?; let tap_interface = lanparty_client_route::interface_identity_from_guid(tap.info().instance_id()) .context("failed to resolve TAP interface identity")?; + let metric_guard = protect_tap_metrics(tap_interface)?; let driver_mac = tap.driver_mac()?; let driver_mtu = tap.driver_mtu()?; @@ -221,7 +240,54 @@ fn open_tap_adapter(session: &ClientSession) -> Result Result { + let ipv4 = lanparty_client_route::set_scoped_interface_metric( + identity, + IpInterfaceFamily::Ipv4, + TAP_INTERFACE_METRIC, + ) + .context("failed to set TAP IPv4 interface metric")?; + print_tap_metric_override(IpInterfaceFamily::Ipv4, &ipv4); + + let ipv6 = match lanparty_client_route::set_scoped_interface_metric( + identity, + IpInterfaceFamily::Ipv6, + TAP_INTERFACE_METRIC, + ) { + Ok(metric) => { + print_tap_metric_override(IpInterfaceFamily::Ipv6, &metric); + Some(metric) + } + Err(error) => { + eprintln!( + "failed to set TAP IPv6 interface metric; IPv6 route protection may be incomplete: {error:#}" + ); + None + } + }; + + Ok(TapMetricGuard { + _ipv4: ipv4, + _ipv6: ipv6, + }) +} + +#[cfg(windows)] +fn print_tap_metric_override(family: IpInterfaceFamily, metric: &ScopedInterfaceMetric) { + let previous = metric.previous(); + println!( + "TAP {family:?} interface metric set to {TAP_INTERFACE_METRIC}; previous metric {} automatic {} default-routes-disabled {}", + previous.metric(), + previous.automatic_metric(), + previous.disable_default_routes() + ); } #[cfg(windows)]