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
This commit is contained in:
2026-05-21 19:24:01 +02:00
parent 61481eaf46
commit c6a4a9da89
2 changed files with 75 additions and 8 deletions
+71 -5
View File
@@ -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<lanparty_client_tap::TapAdapter> {
struct OpenedTapAdapter {
tap: TapAdapter,
_metric_guard: TapMetricGuard,
}
#[cfg(windows)]
struct TapMetricGuard {
_ipv4: ScopedInterfaceMetric,
_ipv6: Option<ScopedInterfaceMetric>,
}
#[cfg(windows)]
fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
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<lanparty_client_tap::TapA
);
}
Ok(tap)
Ok(OpenedTapAdapter {
tap,
_metric_guard: metric_guard,
})
}
#[cfg(windows)]
fn protect_tap_metrics(identity: NetworkInterfaceIdentity) -> Result<TapMetricGuard> {
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)]