feat(client): add scoped interface metric override

Add a reversible IP interface metric boundary to `lanparty-client-route`. The
crate can now read an IPv4 or IPv6 interface metric snapshot and temporarily set
a manual interface metric with an RAII guard that restores the previous metric
and automatic-metric state on drop.

This prepares the TAP metric handling without wiring policy into the Windows
client yet. Default-route disabling is captured in snapshots for diagnostics and
future decisions, but this slice deliberately changes only `UseAutomaticMetric`
and `Metric`.

Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- cargo check -p lanparty-client-route --target x86_64-pc-windows-msvc
- cargo clippy -p lanparty-client-route --target x86_64-pc-windows-msvc --all-targets -- -D warnings
- git diff --check

Refs: PLAN.md
Refs: https://learn.microsoft.com/en-us/windows-hardware/drivers/network/initializeipinterfaceentry
Refs: https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-setipinterfaceentry
This commit is contained in:
2026-05-21 19:20:01 +02:00
parent 96bfbd0dbc
commit 61481eaf46
3 changed files with 271 additions and 2 deletions
+164 -2
View File
@@ -10,8 +10,9 @@ use windows_sys::Win32::{
NetworkManagement::{
IpHelper::{
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
DeleteIpForwardEntry2, GetBestRoute2, IP_ADDRESS_PREFIX, InitializeIpForwardEntry,
MIB_IPFORWARD_ROW2,
DeleteIpForwardEntry2, GetBestRoute2, GetIpInterfaceEntry, IP_ADDRESS_PREFIX,
InitializeIpForwardEntry, InitializeIpInterfaceEntry, MIB_IPFORWARD_ROW2,
MIB_IPINTERFACE_ROW, SetIpInterfaceEntry,
},
Ndis::NET_LUID_LH,
},
@@ -22,6 +23,7 @@ use windows_sys::Win32::{
};
use windows_sys::core::GUID;
use crate::{InterfaceMetricSnapshot, IpInterfaceFamily};
use crate::{NetworkInterfaceIdentity, RouteSnapshot};
pub fn interface_identity_from_guid(interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
@@ -49,6 +51,64 @@ pub fn interface_identity_from_guid(interface_guid: &str) -> Result<NetworkInter
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 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 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 fn best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
let destination_sockaddr = sockaddr_from_ip(destination);
let mut route = MIB_IPFORWARD_ROW2::default();
@@ -86,6 +146,82 @@ pub fn best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
))
}
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 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,
)
}
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 {
@@ -354,6 +490,32 @@ mod tests {
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_ipv6_on_link_host_route_row() {
let route = RouteSnapshot::new(