Files
softlan-vpn/crates/lanparty-client-route/src/windows.rs
T
ddidderr 3e2648abc1 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
2026-05-21 20:00:58 +02:00

686 lines
20 KiB
Rust

use std::{
fmt, io,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
ptr::null,
};
use anyhow::{Context, Result, bail};
use windows_sys::Win32::{
Foundation::ERROR_SUCCESS,
NetworkManagement::{
IpHelper::{
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
DeleteIpForwardEntry2, GetBestRoute2, GetIpInterfaceEntry, IP_ADDRESS_PREFIX,
InitializeIpForwardEntry, InitializeIpInterfaceEntry, MIB_IPFORWARD_ROW2,
MIB_IPINTERFACE_ROW, SetIpInterfaceEntry,
},
Ndis::NET_LUID_LH,
},
Networking::WinSock::{
AF_INET, AF_INET6, IN_ADDR, IN_ADDR_0, IN6_ADDR, IN6_ADDR_0, RouteProtocolNetMgmt,
SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_IN6_0, SOCKADDR_INET,
},
};
use windows_sys::core::GUID;
use crate::{InterfaceMetricSnapshot, InterfaceMtuSnapshot, IpInterfaceFamily};
use crate::{NetworkInterfaceIdentity, RouteSnapshot};
pub fn interface_identity_from_guid(interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
let guid = parse_interface_guid(interface_guid)?;
let mut luid = NET_LUID_LH::default();
let status = unsafe {
// SAFETY: guid and luid point to valid initialized storage.
ConvertInterfaceGuidToLuid(&guid, &mut luid)
};
windows_status(status)
.with_context(|| format!("failed to resolve interface LUID for {interface_guid}"))?;
let mut index = 0_u32;
let status = unsafe {
// SAFETY: luid was returned by Windows and index points to writable storage.
ConvertInterfaceLuidToIndex(&luid, &mut index)
};
windows_status(status).with_context(|| {
format!(
"failed to resolve interface index for interface LUID {}",
luid_value(luid)
)
})?;
Ok(NetworkInterfaceIdentity::new(index, luid_value(luid)))
}
pub fn interface_metric(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
) -> Result<InterfaceMetricSnapshot> {
let row = get_interface_row(identity, family)?;
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,
metric: u32,
) -> Result<ScopedInterfaceMetric> {
let previous = interface_metric(identity, family)?;
let mut row = get_interface_row(identity, family)?;
row.UseAutomaticMetric = false;
row.Metric = metric;
set_interface_row(&mut row)
.with_context(|| format!("failed to set {family:?} interface metric to {metric}"))?;
Ok(ScopedInterfaceMetric {
previous,
active: true,
})
}
pub fn set_scoped_default_routes_disabled(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
disabled: bool,
) -> Result<ScopedDefaultRoutes> {
let previous = interface_metric(identity, family)?;
let mut row = get_interface_row(identity, family)?;
row.DisableDefaultRoutes = disabled;
set_interface_row(&mut row).with_context(|| {
format!("failed to set {family:?} default-route disabled state to {disabled}")
})?;
Ok(ScopedDefaultRoutes {
previous,
active: true,
})
}
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,
}
impl ScopedInterfaceMetric {
#[must_use]
pub const fn previous(&self) -> InterfaceMetricSnapshot {
self.previous
}
}
impl fmt::Debug for ScopedInterfaceMetric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScopedInterfaceMetric")
.field("previous", &self.previous)
.field("active", &self.active)
.finish()
}
}
impl Drop for ScopedInterfaceMetric {
fn drop(&mut self) {
if !self.active {
return;
}
let _ = restore_interface_metric(self.previous);
}
}
pub struct ScopedDefaultRoutes {
previous: InterfaceMetricSnapshot,
active: bool,
}
impl ScopedDefaultRoutes {
#[must_use]
pub const fn previous(&self) -> InterfaceMetricSnapshot {
self.previous
}
}
impl fmt::Debug for ScopedDefaultRoutes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScopedDefaultRoutes")
.field("previous", &self.previous)
.field("active", &self.active)
.finish()
}
}
impl Drop for ScopedDefaultRoutes {
fn drop(&mut self) {
if !self.active {
return;
}
let _ = restore_default_routes(self.previous);
}
}
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();
let mut source = SOCKADDR_INET::default();
let status = unsafe {
// SAFETY: destination_sockaddr, route, and source point to valid initialized storage.
// Null interface/source inputs ask Windows to choose the best current route.
GetBestRoute2(
null(),
0,
null(),
&destination_sockaddr,
0,
&mut route,
&mut source,
)
};
windows_status(status).with_context(|| format!("failed to find route to {destination}"))?;
let source = ip_from_sockaddr(&source).context("best route returned no source address")?;
let route_prefix = ip_from_sockaddr(&route.DestinationPrefix.Prefix)
.context("best route returned no route prefix")?;
let next_hop = ip_from_sockaddr(&route.NextHop).filter(|next_hop| !next_hop.is_unspecified());
let interface_luid = luid_value(route.InterfaceLuid);
Ok(RouteSnapshot::new(
destination,
source,
next_hop,
route_prefix,
route.DestinationPrefix.PrefixLength,
route.InterfaceIndex,
interface_luid,
route.Metric,
))
}
fn get_interface_row(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
) -> Result<MIB_IPINTERFACE_ROW> {
let mut row = interface_row_key(identity, family);
let status = unsafe {
// SAFETY: row is initialized with the family and interface identity Windows needs to
// retrieve the IP interface entry.
GetIpInterfaceEntry(&mut row)
};
windows_status(status).with_context(|| {
format!(
"failed to read {family:?} interface row for index {} LUID {}",
identity.index(),
identity.luid()
)
})?;
Ok(row)
}
fn set_interface_row(row: &mut MIB_IPINTERFACE_ROW) -> Result<()> {
let status = unsafe {
// SAFETY: row was obtained from GetIpInterfaceEntry and only mutable configuration fields
// are changed before calling SetIpInterfaceEntry.
SetIpInterfaceEntry(row)
};
windows_status(status).context("failed to update IP interface row")
}
fn restore_interface_metric(snapshot: InterfaceMetricSnapshot) -> Result<()> {
let mut row = get_interface_row(snapshot.identity(), snapshot.family())?;
row.UseAutomaticMetric = snapshot.automatic_metric();
row.Metric = snapshot.metric();
set_interface_row(&mut row)
}
fn restore_default_routes(snapshot: InterfaceMetricSnapshot) -> Result<()> {
let mut row = get_interface_row(snapshot.identity(), snapshot.family())?;
row.DisableDefaultRoutes = snapshot.disable_default_routes();
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,
) -> MIB_IPINTERFACE_ROW {
let mut row = MIB_IPINTERFACE_ROW::default();
unsafe {
// SAFETY: row points to valid writable storage for Windows to initialize.
InitializeIpInterfaceEntry(&mut row);
}
row.Family = address_family(family);
row.InterfaceLuid = NET_LUID_LH {
Value: identity.luid(),
};
row.InterfaceIndex = identity.index();
row
}
fn metric_snapshot(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
row: MIB_IPINTERFACE_ROW,
) -> InterfaceMetricSnapshot {
InterfaceMetricSnapshot::new(
identity,
family,
row.UseAutomaticMetric,
row.Metric,
row.DisableDefaultRoutes,
)
}
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,
IpInterfaceFamily::Ipv6 => AF_INET6,
}
}
pub fn pin_relay_route(route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
let mut pinned = pinned_route_row(route);
let status = unsafe {
// SAFETY: pinned.row was initialized with InitializeIpForwardEntry and populated with a
// valid host destination, next hop, and interface identity.
CreateIpForwardEntry2(&pinned.row)
};
windows_status(status).with_context(|| {
format!(
"failed to pin relay route to {} via interface index {}",
route.destination(),
route.interface_index()
)
})?;
pinned.active = true;
Ok(pinned)
}
pub struct PinnedRelayRoute {
row: MIB_IPFORWARD_ROW2,
destination: IpAddr,
next_hop: Option<IpAddr>,
interface_index: u32,
interface_luid: u64,
active: bool,
}
impl PinnedRelayRoute {
#[must_use]
pub const fn destination(&self) -> IpAddr {
self.destination
}
#[must_use]
pub const fn next_hop(&self) -> Option<IpAddr> {
self.next_hop
}
#[must_use]
pub const fn interface_index(&self) -> u32 {
self.interface_index
}
#[must_use]
pub const fn interface_luid(&self) -> u64 {
self.interface_luid
}
}
impl fmt::Debug for PinnedRelayRoute {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PinnedRelayRoute")
.field("destination", &self.destination)
.field("next_hop", &self.next_hop)
.field("interface_index", &self.interface_index)
.field("interface_luid", &self.interface_luid)
.field("active", &self.active)
.finish_non_exhaustive()
}
}
impl Drop for PinnedRelayRoute {
fn drop(&mut self) {
if !self.active {
return;
}
unsafe {
// SAFETY: self.row is the same route row that was successfully created for this guard.
// Deletion failure is intentionally ignored during Drop.
DeleteIpForwardEntry2(&self.row);
}
}
}
fn pinned_route_row(route: &RouteSnapshot) -> PinnedRelayRoute {
let destination = route.destination();
let next_hop = route.next_hop();
let mut row = MIB_IPFORWARD_ROW2::default();
unsafe {
// SAFETY: row points to valid writable storage for Windows to initialize.
InitializeIpForwardEntry(&mut row);
}
row.InterfaceLuid = NET_LUID_LH {
Value: route.interface_luid(),
};
row.InterfaceIndex = route.interface_index();
row.DestinationPrefix = host_prefix(destination);
row.NextHop = sockaddr_from_ip(next_hop.unwrap_or_else(|| unspecified_for(destination)));
row.SitePrefixLength = host_prefix_len(destination);
row.Metric = 0;
row.Protocol = RouteProtocolNetMgmt;
PinnedRelayRoute {
row,
destination,
next_hop,
interface_index: route.interface_index(),
interface_luid: route.interface_luid(),
active: false,
}
}
fn parse_interface_guid(interface_guid: &str) -> Result<GUID> {
let value = interface_guid.trim();
let value = value
.strip_prefix('{')
.and_then(|value| value.strip_suffix('}'))
.unwrap_or(value);
let mut hex = String::with_capacity(32);
for ch in value.chars() {
if ch == '-' {
continue;
}
if !ch.is_ascii_hexdigit() {
bail!("interface GUID contains non-hex character {ch:?}");
}
hex.push(ch);
}
if hex.len() != 32 {
bail!("interface GUID must contain 32 hex digits");
}
let value = u128::from_str_radix(&hex, 16).context("failed to parse interface GUID")?;
Ok(GUID::from_u128(value))
}
fn luid_value(luid: NET_LUID_LH) -> u64 {
unsafe {
// SAFETY: Reading the Value view is valid for the NET_LUID_LH union returned by Windows
// or initialized from a Value.
luid.Value
}
}
fn host_prefix(addr: IpAddr) -> IP_ADDRESS_PREFIX {
IP_ADDRESS_PREFIX {
Prefix: sockaddr_from_ip(addr),
PrefixLength: host_prefix_len(addr),
}
}
const fn host_prefix_len(addr: IpAddr) -> u8 {
match addr {
IpAddr::V4(_) => 32,
IpAddr::V6(_) => 128,
}
}
const fn unspecified_for(addr: IpAddr) -> IpAddr {
match addr {
IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
}
}
fn sockaddr_from_ip(addr: IpAddr) -> SOCKADDR_INET {
match addr {
IpAddr::V4(addr) => SOCKADDR_INET {
Ipv4: SOCKADDR_IN {
sin_family: AF_INET,
sin_port: 0,
sin_addr: IN_ADDR {
S_un: IN_ADDR_0 {
S_addr: u32::from_ne_bytes(addr.octets()),
},
},
sin_zero: [0; 8],
},
},
IpAddr::V6(addr) => SOCKADDR_INET {
Ipv6: SOCKADDR_IN6 {
sin6_family: AF_INET6,
sin6_port: 0,
sin6_flowinfo: 0,
sin6_addr: IN6_ADDR {
u: IN6_ADDR_0 {
Byte: addr.octets(),
},
},
Anonymous: SOCKADDR_IN6_0 { sin6_scope_id: 0 },
},
},
}
}
fn ip_from_sockaddr(sockaddr: &SOCKADDR_INET) -> Option<IpAddr> {
match unsafe {
// SAFETY: Reading the discriminator is valid for any SOCKADDR_INET initialized by us or
// by Windows APIs.
sockaddr.si_family
} {
AF_INET => {
let ipv4 = unsafe {
// SAFETY: si_family reports that the IPv4 union field is active.
sockaddr.Ipv4
};
let octets = unsafe {
// SAFETY: S_addr is the compact IPv4 representation in network byte order.
ipv4.sin_addr.S_un.S_addr.to_ne_bytes()
};
Some(IpAddr::V4(Ipv4Addr::from(octets)))
}
AF_INET6 => {
let ipv6 = unsafe {
// SAFETY: si_family reports that the IPv6 union field is active.
sockaddr.Ipv6
};
let octets = unsafe {
// SAFETY: Byte is the compact IPv6 representation in network byte order.
ipv6.sin6_addr.u.Byte
};
Some(IpAddr::V6(Ipv6Addr::from(octets)))
}
_ => None,
}
}
fn windows_status(status: u32) -> io::Result<()> {
if status == ERROR_SUCCESS {
Ok(())
} else {
Err(io::Error::from_raw_os_error(status as i32))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_ipv4_host_route_row() {
let route = RouteSnapshot::new(
ip("203.0.113.10"),
ip("192.0.2.44"),
Some(ip("192.0.2.1")),
ip("0.0.0.0"),
0,
12,
34,
25,
);
let pinned = pinned_route_row(&route);
assert_eq!(pinned.destination(), ip("203.0.113.10"));
assert_eq!(pinned.next_hop(), Some(ip("192.0.2.1")));
assert_eq!(pinned.interface_index(), 12);
assert_eq!(pinned.interface_luid(), 34);
assert_eq!(pinned.row.DestinationPrefix.PrefixLength, 32);
assert_eq!(pinned.row.SitePrefixLength, 32);
assert_eq!(pinned.row.Metric, 0);
assert_eq!(pinned.row.Protocol, RouteProtocolNetMgmt);
}
#[test]
fn parses_interface_guid_strings() {
let guid = parse_interface_guid("{00112233-4455-6677-8899-AABBCCDDEEFF}").unwrap();
assert_eq!(guid.data1, 0x0011_2233);
assert_eq!(guid.data2, 0x4455);
assert_eq!(guid.data3, 0x6677);
assert_eq!(guid.data4, [0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
assert!(parse_interface_guid("00112233-4455-6677-8899-aabbccddeeff").is_ok());
assert!(parse_interface_guid("{00112233-4455-6677-8899-AABBCCDDEE}").is_err());
assert!(parse_interface_guid("{00112233-4455-6677-8899-AABBCCDDEEGG}").is_err());
}
#[test]
fn builds_interface_row_keys() {
let identity = NetworkInterfaceIdentity::new(12, 34);
let row = interface_row_key(identity, IpInterfaceFamily::Ipv4);
assert_eq!(row.Family, AF_INET);
assert_eq!(row.InterfaceIndex, 12);
assert_eq!(luid_value(row.InterfaceLuid), 34);
}
#[test]
fn builds_metric_snapshots_from_rows() {
let identity = NetworkInterfaceIdentity::new(12, 34);
let mut row = interface_row_key(identity, IpInterfaceFamily::Ipv6);
row.UseAutomaticMetric = true;
row.Metric = 500;
row.DisableDefaultRoutes = true;
let snapshot = metric_snapshot(identity, IpInterfaceFamily::Ipv6, row);
assert_eq!(snapshot.identity(), identity);
assert_eq!(snapshot.family(), IpInterfaceFamily::Ipv6);
assert!(snapshot.automatic_metric());
assert_eq!(snapshot.metric(), 500);
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(
ip("2001:db8::10"),
ip("2001:db8::44"),
None,
ip("2001:db8::"),
64,
12,
34,
25,
);
let pinned = pinned_route_row(&route);
assert_eq!(pinned.destination(), ip("2001:db8::10"));
assert_eq!(pinned.next_hop(), None);
assert_eq!(pinned.row.DestinationPrefix.PrefixLength, 128);
assert_eq!(pinned.row.SitePrefixLength, 128);
assert_eq!(ip_from_sockaddr(&pinned.row.NextHop), Some(ip("::")));
}
fn ip(value: &str) -> IpAddr {
value.parse().unwrap()
}
}