feat(client): scope TAP interface MTU while running

The Windows client now sets the TAP IP-interface MTU to the relay-selected MTU
before it starts bridging frames. The override is scoped like the existing
metric and default-route guards, so the previous MTU is restored when the
client exits.

The route crate now exposes `InterfaceMtuSnapshot` and `ScopedInterfaceMtu`
around `MIB_IPINTERFACE_ROW.NlMtu`, reusing the same `GetIpInterfaceEntry` and
`SetIpInterfaceEntry` path already used for metrics and default-route policy.
IPv4 MTU setup is required for startup, while IPv6 MTU setup is best-effort to
match the existing IPv6 route-protection behavior.

This intentionally leaves TAP MAC configuration as fail-fast. TAP-Windows6 does
not expose a matching set-MAC IOCTL in the driver header, so that should remain
a separate design decision.

Test Plan:
- cargo fmt --check
- cargo test -p lanparty-client-route
- cargo test -p lanparty-client-win
- cargo clippy -p lanparty-client-route -p lanparty-client-win --all-targets
  -- -D warnings
- cargo check -p lanparty-client-route --target x86_64-pc-windows-gnu
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
- cargo check -p lanparty-client-win --target x86_64-pc-windows-gnu
  (fails before this crate in ring: missing x86_64-w64-mingw32-gcc)

Refs: PLAN.md
This commit is contained in:
2026-05-21 20:00:58 +02:00
parent 0299190c42
commit 3e2648abc1
4 changed files with 212 additions and 46 deletions
+45 -36
View File
@@ -16,7 +16,7 @@ use lanparty_client_core::{
#[cfg(windows)]
use lanparty_client_route::{
IpInterfaceFamily, NetworkInterfaceIdentity, PinnedRelayRoute, RouteSnapshot,
ScopedDefaultRoutes, ScopedInterfaceMetric,
ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu,
};
#[cfg(windows)]
use lanparty_client_tap::TapAdapter;
@@ -279,8 +279,10 @@ struct OpenedTapAdapter {
struct TapRouteProtectionGuard {
_ipv4_metric: ScopedInterfaceMetric,
_ipv4_default_routes: ScopedDefaultRoutes,
_ipv4_mtu: ScopedInterfaceMtu,
_ipv6_metric: Option<ScopedInterfaceMetric>,
_ipv6_default_routes: Option<ScopedDefaultRoutes>,
_ipv6_mtu: Option<ScopedInterfaceMtu>,
}
#[cfg(windows)]
@@ -289,15 +291,10 @@ fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
let tap_interface =
lanparty_client_route::interface_identity_from_guid(tap.info().instance_id())
.context("failed to resolve TAP interface identity")?;
let route_guard = protect_tap_routes(tap_interface)?;
let route_guard = protect_tap_routes(tap_interface, session.welcome().effective_tap_mtu())?;
let driver_mac = tap.driver_mac()?;
let driver_mtu = tap.driver_mtu()?;
validate_tap_driver_settings(
session.config().virtual_mac(),
session.welcome().effective_tap_mtu(),
driver_mac,
driver_mtu,
)?;
validate_tap_driver_mac(session.config().virtual_mac(), driver_mac)?;
tap.set_media_connected(true)?;
println!(
@@ -323,30 +320,27 @@ fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
}
#[cfg_attr(not(windows), allow(dead_code))]
fn validate_tap_driver_settings(
expected_mac: MacAddr,
expected_mtu: u16,
driver_mac: MacAddr,
driver_mtu: u32,
) -> Result<()> {
fn validate_tap_driver_mac(expected_mac: MacAddr, driver_mac: MacAddr) -> Result<()> {
if driver_mac != expected_mac {
bail!(
"TAP driver MAC {driver_mac} does not match tunnel identity {expected_mac}; automatic MAC configuration is not wired yet"
);
}
let expected_mtu = u32::from(expected_mtu);
if driver_mtu != expected_mtu {
bail!(
"TAP driver MTU {driver_mtu} does not match relay-selected MTU {expected_mtu}; automatic MTU configuration is not wired yet"
);
}
Ok(())
}
#[cfg(windows)]
fn protect_tap_routes(identity: NetworkInterfaceIdentity) -> Result<TapRouteProtectionGuard> {
fn protect_tap_routes(
identity: NetworkInterfaceIdentity,
tap_mtu: u16,
) -> Result<TapRouteProtectionGuard> {
let tap_mtu = u32::from(tap_mtu);
let ipv4_mtu =
lanparty_client_route::set_scoped_interface_mtu(identity, IpInterfaceFamily::Ipv4, tap_mtu)
.context("failed to set TAP IPv4 MTU")?;
print_tap_mtu_override(IpInterfaceFamily::Ipv4, tap_mtu, &ipv4_mtu);
let ipv4_metric = lanparty_client_route::set_scoped_interface_metric(
identity,
IpInterfaceFamily::Ipv4,
@@ -363,6 +357,21 @@ fn protect_tap_routes(identity: NetworkInterfaceIdentity) -> Result<TapRouteProt
.context("failed to disable TAP IPv4 default routes")?;
print_tap_default_routes_override(IpInterfaceFamily::Ipv4, &ipv4_default_routes);
let ipv6_mtu = match lanparty_client_route::set_scoped_interface_mtu(
identity,
IpInterfaceFamily::Ipv6,
tap_mtu,
) {
Ok(mtu) => {
print_tap_mtu_override(IpInterfaceFamily::Ipv6, tap_mtu, &mtu);
Some(mtu)
}
Err(error) => {
eprintln!("failed to set TAP IPv6 MTU; IPv6 may use the previous MTU: {error:#}");
None
}
};
let ipv6_metric = match lanparty_client_route::set_scoped_interface_metric(
identity,
IpInterfaceFamily::Ipv6,
@@ -400,11 +409,22 @@ fn protect_tap_routes(identity: NetworkInterfaceIdentity) -> Result<TapRouteProt
Ok(TapRouteProtectionGuard {
_ipv4_metric: ipv4_metric,
_ipv4_default_routes: ipv4_default_routes,
_ipv4_mtu: ipv4_mtu,
_ipv6_metric: ipv6_metric,
_ipv6_default_routes: ipv6_default_routes,
_ipv6_mtu: ipv6_mtu,
})
}
#[cfg(windows)]
fn print_tap_mtu_override(family: IpInterfaceFamily, mtu: u32, guard: &ScopedInterfaceMtu) {
let previous = guard.previous();
println!(
"TAP {family:?} MTU set to {mtu}; previous MTU {}",
previous.mtu()
);
}
#[cfg(windows)]
fn print_tap_metric_override(family: IpInterfaceFamily, metric: &ScopedInterfaceMetric) {
let previous = metric.previous();
@@ -506,28 +526,17 @@ mod tests {
use super::*;
#[test]
fn accepts_matching_tap_driver_settings() {
assert!(validate_tap_driver_settings(mac(1), 1200, mac(1), 1200).is_ok());
fn accepts_matching_tap_driver_mac() {
assert!(validate_tap_driver_mac(mac(1), mac(1)).is_ok());
}
#[test]
fn rejects_tap_driver_mac_mismatch() {
let error = validate_tap_driver_settings(mac(1), 1200, mac(2), 1200).unwrap_err();
let error = validate_tap_driver_mac(mac(1), mac(2)).unwrap_err();
assert!(error.to_string().contains("does not match tunnel identity"));
}
#[test]
fn rejects_tap_driver_mtu_mismatch() {
let error = validate_tap_driver_settings(mac(1), 1200, mac(1), 1500).unwrap_err();
assert!(
error
.to_string()
.contains("does not match relay-selected MTU")
);
}
const fn mac(last_octet: u8) -> MacAddr {
MacAddr::new([0x02, 0, 0, 0, 0, last_octet])
}