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
+88 -1
View File
@@ -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<NetworkInterfaceIdentity> {
@@ -60,6 +60,15 @@ pub fn interface_metric(
Ok(metric_snapshot(identity, family, row))
}
pub fn interface_mtu(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
) -> Result<InterfaceMtuSnapshot> {
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<ScopedInterfaceMtu> {
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<RouteSnapshot> {
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(