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
+71 -2
View File
@@ -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<InterfaceMtuSnapshot> {
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<ScopedInterfaceMtu> {
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 {