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:
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user