From 3e2648abc18ba69fff917e6d7aba8d71b94044e1 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 20:00:58 +0200 Subject: [PATCH] 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 --- README.md | 15 ++-- crates/lanparty-client-route/src/lib.rs | 73 ++++++++++++++++- crates/lanparty-client-route/src/windows.rs | 89 ++++++++++++++++++++- crates/lanparty-client-win/src/main.rs | 81 ++++++++++--------- 4 files changed, 212 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index ba8344a..7afc47d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Windows route-table boundary: - read-only best-route lookup for a relay destination IP - selected source address, next hop, interface index/LUID, prefix, and metric - interface index/LUID lookup from Windows network adapter GUIDs +- scoped IP interface MTU overrides with restore-on-drop behavior - scoped IP interface metric overrides with restore-on-drop behavior - scoped default-route suppression with restore-on-drop behavior - scoped host-route pinning for the relay IP on the pre-TAP interface @@ -141,10 +142,10 @@ verifies that the relay route still uses that pinned host route after TAP activation, 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. The client applies -a scoped TAP interface metric and disables TAP default routes while it runs, -periodically rechecks that the relay route remains pinned, then restores the -previous route policy on exit. Until automatic TAP MAC/MTU configuration is -wired, startup fails before bridging if the driver-reported MAC or MTU does not -match the tunnel settings. +Windows it sets the TAP IP interface MTU to the relay-selected MTU, marks the +TAP media connected, and reports the driver MAC/MTU before forwarding frames, +along with the TAP interface index/LUID. The client applies a scoped TAP +interface metric and disables TAP default routes while it runs, periodically +rechecks that the relay route remains pinned, then restores the previous route +policy on exit. Until automatic TAP MAC configuration is wired, startup fails +before bridging if the driver-reported MAC does not match the tunnel identity. diff --git a/crates/lanparty-client-route/src/lib.rs b/crates/lanparty-client-route/src/lib.rs index c28d914..b8100ef 100644 --- a/crates/lanparty-client-route/src/lib.rs +++ b/crates/lanparty-client-route/src/lib.rs @@ -59,6 +59,13 @@ pub struct InterfaceMetricSnapshot { disable_default_routes: bool, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InterfaceMtuSnapshot { + identity: NetworkInterfaceIdentity, + family: IpInterfaceFamily, + mtu: u32, +} + impl InterfaceMetricSnapshot { #[cfg_attr(not(windows), allow(dead_code))] const fn new( @@ -103,6 +110,32 @@ impl InterfaceMetricSnapshot { } } +impl InterfaceMtuSnapshot { + #[cfg_attr(not(windows), allow(dead_code))] + const fn new(identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, mtu: u32) -> Self { + Self { + identity, + family, + mtu, + } + } + + #[must_use] + pub const fn identity(self) -> NetworkInterfaceIdentity { + self.identity + } + + #[must_use] + pub const fn family(self) -> IpInterfaceFamily { + self.family + } + + #[must_use] + pub const fn mtu(self) -> u32 { + self.mtu + } +} + impl RouteSnapshot { #[cfg_attr(not(windows), allow(dead_code))] #[allow(clippy::too_many_arguments)] @@ -193,8 +226,9 @@ mod windows; pub use windows::{PinnedRelayRoute, best_route_to, interface_identity_from_guid, pin_relay_route}; #[cfg(windows)] pub use windows::{ - ScopedDefaultRoutes, ScopedInterfaceMetric, interface_metric, - set_scoped_default_routes_disabled, set_scoped_interface_metric, + ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu, interface_metric, + interface_mtu, set_scoped_default_routes_disabled, set_scoped_interface_metric, + set_scoped_interface_mtu, }; #[cfg(not(windows))] @@ -256,6 +290,29 @@ pub fn set_scoped_default_routes_disabled( bail!("Windows interface default-route updates are only available on Windows"); } +#[cfg(not(windows))] +pub fn interface_mtu( + _identity: NetworkInterfaceIdentity, + _family: IpInterfaceFamily, +) -> Result { + bail!("Windows interface MTU lookup is only available on Windows"); +} + +#[cfg(not(windows))] +#[derive(Debug)] +pub struct ScopedInterfaceMtu { + _private: (), +} + +#[cfg(not(windows))] +pub fn set_scoped_interface_mtu( + _identity: NetworkInterfaceIdentity, + _family: IpInterfaceFamily, + _mtu: u32, +) -> Result { + bail!("Windows interface MTU updates are only available on Windows"); +} + #[cfg(test)] mod tests { use super::*; @@ -337,6 +394,16 @@ mod tests { assert!(!snapshot.disable_default_routes()); } + #[test] + fn exposes_interface_mtu_snapshot_fields() { + let identity = NetworkInterfaceIdentity::new(12, 34); + let snapshot = InterfaceMtuSnapshot::new(identity, IpInterfaceFamily::Ipv6, 1200); + + assert_eq!(snapshot.identity(), identity); + assert_eq!(snapshot.family(), IpInterfaceFamily::Ipv6); + assert_eq!(snapshot.mtu(), 1200); + } + #[cfg(not(windows))] #[test] fn rejects_route_inspection_on_non_windows() { @@ -376,6 +443,8 @@ mod tests { assert!( set_scoped_default_routes_disabled(identity, IpInterfaceFamily::Ipv4, true).is_err() ); + assert!(interface_mtu(identity, IpInterfaceFamily::Ipv4).is_err()); + assert!(set_scoped_interface_mtu(identity, IpInterfaceFamily::Ipv4, 1200).is_err()); } fn ip(value: &str) -> IpAddr { diff --git a/crates/lanparty-client-route/src/windows.rs b/crates/lanparty-client-route/src/windows.rs index 8728496..ed6ae33 100644 --- a/crates/lanparty-client-route/src/windows.rs +++ b/crates/lanparty-client-route/src/windows.rs @@ -23,7 +23,7 @@ use windows_sys::Win32::{ }; use windows_sys::core::GUID; -use crate::{InterfaceMetricSnapshot, IpInterfaceFamily}; +use crate::{InterfaceMetricSnapshot, InterfaceMtuSnapshot, IpInterfaceFamily}; use crate::{NetworkInterfaceIdentity, RouteSnapshot}; pub fn interface_identity_from_guid(interface_guid: &str) -> Result { @@ -60,6 +60,15 @@ pub fn interface_metric( Ok(metric_snapshot(identity, family, row)) } +pub fn interface_mtu( + identity: NetworkInterfaceIdentity, + family: IpInterfaceFamily, +) -> Result { + let row = get_interface_row(identity, family)?; + + Ok(mtu_snapshot(identity, family, row)) +} + pub fn set_scoped_interface_metric( identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, @@ -96,6 +105,27 @@ pub fn set_scoped_default_routes_disabled( }) } +pub fn set_scoped_interface_mtu( + identity: NetworkInterfaceIdentity, + family: IpInterfaceFamily, + mtu: u32, +) -> Result { + if mtu == 0 { + bail!("interface MTU must be nonzero"); + } + + let previous = interface_mtu(identity, family)?; + let mut row = get_interface_row(identity, family)?; + row.NlMtu = mtu; + set_interface_row(&mut row) + .with_context(|| format!("failed to set {family:?} interface MTU to {mtu}"))?; + + Ok(ScopedInterfaceMtu { + previous, + active: true, + }) +} + pub struct ScopedInterfaceMetric { previous: InterfaceMetricSnapshot, active: bool, @@ -158,6 +188,37 @@ impl Drop for ScopedDefaultRoutes { } } +pub struct ScopedInterfaceMtu { + previous: InterfaceMtuSnapshot, + active: bool, +} + +impl ScopedInterfaceMtu { + #[must_use] + pub const fn previous(&self) -> InterfaceMtuSnapshot { + self.previous + } +} + +impl fmt::Debug for ScopedInterfaceMtu { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ScopedInterfaceMtu") + .field("previous", &self.previous) + .field("active", &self.active) + .finish() + } +} + +impl Drop for ScopedInterfaceMtu { + fn drop(&mut self) { + if !self.active { + return; + } + + let _ = restore_interface_mtu(self.previous); + } +} + pub fn best_route_to(destination: IpAddr) -> Result { let destination_sockaddr = sockaddr_from_ip(destination); let mut route = MIB_IPFORWARD_ROW2::default(); @@ -238,6 +299,12 @@ fn restore_default_routes(snapshot: InterfaceMetricSnapshot) -> Result<()> { set_interface_row(&mut row) } +fn restore_interface_mtu(snapshot: InterfaceMtuSnapshot) -> Result<()> { + let mut row = get_interface_row(snapshot.identity(), snapshot.family())?; + row.NlMtu = snapshot.mtu(); + set_interface_row(&mut row) +} + fn interface_row_key( identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, @@ -270,6 +337,14 @@ fn metric_snapshot( ) } +fn mtu_snapshot( + identity: NetworkInterfaceIdentity, + family: IpInterfaceFamily, + row: MIB_IPINTERFACE_ROW, +) -> InterfaceMtuSnapshot { + InterfaceMtuSnapshot::new(identity, family, row.NlMtu) +} + const fn address_family(family: IpInterfaceFamily) -> u16 { match family { IpInterfaceFamily::Ipv4 => AF_INET, @@ -571,6 +646,18 @@ mod tests { assert!(snapshot.disable_default_routes()); } + #[test] + fn builds_mtu_snapshots_from_rows() { + let identity = NetworkInterfaceIdentity::new(12, 34); + let mut row = interface_row_key(identity, IpInterfaceFamily::Ipv4); + row.NlMtu = 1200; + let snapshot = mtu_snapshot(identity, IpInterfaceFamily::Ipv4, row); + + assert_eq!(snapshot.identity(), identity); + assert_eq!(snapshot.family(), IpInterfaceFamily::Ipv4); + assert_eq!(snapshot.mtu(), 1200); + } + #[test] fn builds_ipv6_on_link_host_route_row() { let route = RouteSnapshot::new( diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 533d4cc..a011b47 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -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, _ipv6_default_routes: Option, + _ipv6_mtu: Option, } #[cfg(windows)] @@ -289,15 +291,10 @@ fn open_tap_adapter(session: &ClientSession) -> Result { 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 { } #[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 { +fn protect_tap_routes( + identity: NetworkInterfaceIdentity, + tap_mtu: u16, +) -> Result { + 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 { + 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 MacAddr { MacAddr::new([0x02, 0, 0, 0, 0, last_octet]) }