From 432d1d08d1f45c2fb990809387076a95d6697f56 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 19:16:02 +0200 Subject: [PATCH] feat(client): resolve Windows interface identity by GUID Add a small route-crate API that resolves a Windows network adapter GUID into its interface LUID and index. The TAP adapter discovery already gives us the NetCfgInstanceId GUID, and the route/metric work needs the corresponding IP interface identity before it can safely inspect or adjust TAP metrics. The implementation keeps GUID parsing local and dependency-free, then delegates the actual identity lookup to Windows IP Helper calls. Non-Windows builds expose the same API shape with a clear unsupported-platform error. 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 --- README.md | 1 + crates/lanparty-client-route/src/lib.rs | 44 ++++++++++- crates/lanparty-client-route/src/windows.rs | 85 +++++++++++++++++++-- 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0f0c7f5..52f2287 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Windows route-table boundary: - read-only best-route lookup for a relay destination IP - selected source address, next hop, interface index/LUID, prefix, and metric +- interface index/LUID lookup from Windows network adapter GUIDs - scoped host-route pinning for the relay IP on the pre-TAP interface - non-Windows builds return a clear unsupported-platform error diff --git a/crates/lanparty-client-route/src/lib.rs b/crates/lanparty-client-route/src/lib.rs index 097985c..807bbd8 100644 --- a/crates/lanparty-client-route/src/lib.rs +++ b/crates/lanparty-client-route/src/lib.rs @@ -21,6 +21,29 @@ pub struct RouteSnapshot { metric: u32, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NetworkInterfaceIdentity { + index: u32, + luid: u64, +} + +impl NetworkInterfaceIdentity { + #[cfg_attr(not(windows), allow(dead_code))] + const fn new(index: u32, luid: u64) -> Self { + Self { index, luid } + } + + #[must_use] + pub const fn index(self) -> u32 { + self.index + } + + #[must_use] + pub const fn luid(self) -> u64 { + self.luid + } +} + impl RouteSnapshot { #[cfg_attr(not(windows), allow(dead_code))] #[allow(clippy::too_many_arguments)] @@ -91,7 +114,7 @@ impl RouteSnapshot { mod windows; #[cfg(windows)] -pub use windows::{PinnedRelayRoute, best_route_to, pin_relay_route}; +pub use windows::{PinnedRelayRoute, best_route_to, interface_identity_from_guid, pin_relay_route}; #[cfg(not(windows))] pub fn best_route_to(_destination: IpAddr) -> Result { @@ -109,6 +132,11 @@ pub fn pin_relay_route(_route: &RouteSnapshot) -> Result { bail!("Windows route pinning is only available on Windows"); } +#[cfg(not(windows))] +pub fn interface_identity_from_guid(_interface_guid: &str) -> Result { + bail!("Windows interface identity lookup is only available on Windows"); +} + #[cfg(test)] mod tests { use super::*; @@ -136,6 +164,14 @@ mod tests { assert_eq!(snapshot.metric(), 25); } + #[test] + fn exposes_network_interface_identity_fields() { + let identity = NetworkInterfaceIdentity::new(12, 34); + + assert_eq!(identity.index(), 12); + assert_eq!(identity.luid(), 34); + } + #[cfg(not(windows))] #[test] fn rejects_route_inspection_on_non_windows() { @@ -159,6 +195,12 @@ mod tests { assert!(pin_relay_route(&snapshot).is_err()); } + #[cfg(not(windows))] + #[test] + fn rejects_interface_lookup_on_non_windows() { + assert!(interface_identity_from_guid("{00112233-4455-6677-8899-AABBCCDDEEFF}").is_err()); + } + fn ip(value: &str) -> IpAddr { value.parse().unwrap() } diff --git a/crates/lanparty-client-route/src/windows.rs b/crates/lanparty-client-route/src/windows.rs index 186cdde..e761232 100644 --- a/crates/lanparty-client-route/src/windows.rs +++ b/crates/lanparty-client-route/src/windows.rs @@ -4,13 +4,14 @@ use std::{ ptr::null, }; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use windows_sys::Win32::{ Foundation::ERROR_SUCCESS, NetworkManagement::{ IpHelper::{ - CreateIpForwardEntry2, DeleteIpForwardEntry2, GetBestRoute2, IP_ADDRESS_PREFIX, - InitializeIpForwardEntry, MIB_IPFORWARD_ROW2, + ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2, + DeleteIpForwardEntry2, GetBestRoute2, IP_ADDRESS_PREFIX, InitializeIpForwardEntry, + MIB_IPFORWARD_ROW2, }, Ndis::NET_LUID_LH, }, @@ -19,8 +20,34 @@ use windows_sys::Win32::{ SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_IN6_0, SOCKADDR_INET, }, }; +use windows_sys::core::GUID; -use crate::RouteSnapshot; +use crate::{NetworkInterfaceIdentity, RouteSnapshot}; + +pub fn interface_identity_from_guid(interface_guid: &str) -> Result { + 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 best_route_to(destination: IpAddr) -> Result { let destination_sockaddr = sockaddr_from_ip(destination); @@ -45,10 +72,7 @@ pub fn best_route_to(destination: IpAddr) -> Result { 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 = unsafe { - // SAFETY: GetBestRoute2 initialized the route row, including InterfaceLuid. - route.InterfaceLuid.Value - }; + let interface_luid = luid_value(route.InterfaceLuid); Ok(RouteSnapshot::new( destination, @@ -166,6 +190,38 @@ fn pinned_route_row(route: &RouteSnapshot) -> PinnedRelayRoute { } } +fn parse_interface_guid(interface_guid: &str) -> Result { + 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), @@ -285,6 +341,19 @@ mod tests { 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_ipv6_on_link_host_route_row() { let route = RouteSnapshot::new(