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
This commit is contained in:
@@ -55,6 +55,7 @@ Windows route-table boundary:
|
|||||||
|
|
||||||
- read-only best-route lookup for a relay destination IP
|
- read-only best-route lookup for a relay destination IP
|
||||||
- selected source address, next hop, interface index/LUID, prefix, and metric
|
- 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
|
- scoped host-route pinning for the relay IP on the pre-TAP interface
|
||||||
- non-Windows builds return a clear unsupported-platform error
|
- non-Windows builds return a clear unsupported-platform error
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,29 @@ pub struct RouteSnapshot {
|
|||||||
metric: u32,
|
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 {
|
impl RouteSnapshot {
|
||||||
#[cfg_attr(not(windows), allow(dead_code))]
|
#[cfg_attr(not(windows), allow(dead_code))]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -91,7 +114,7 @@ impl RouteSnapshot {
|
|||||||
mod windows;
|
mod windows;
|
||||||
|
|
||||||
#[cfg(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))]
|
#[cfg(not(windows))]
|
||||||
pub fn best_route_to(_destination: IpAddr) -> Result<RouteSnapshot> {
|
pub fn best_route_to(_destination: IpAddr) -> Result<RouteSnapshot> {
|
||||||
@@ -109,6 +132,11 @@ pub fn pin_relay_route(_route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
|
|||||||
bail!("Windows route pinning is only available on Windows");
|
bail!("Windows route pinning is only available on Windows");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn interface_identity_from_guid(_interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
|
||||||
|
bail!("Windows interface identity lookup is only available on Windows");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -136,6 +164,14 @@ mod tests {
|
|||||||
assert_eq!(snapshot.metric(), 25);
|
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))]
|
#[cfg(not(windows))]
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_route_inspection_on_non_windows() {
|
fn rejects_route_inspection_on_non_windows() {
|
||||||
@@ -159,6 +195,12 @@ mod tests {
|
|||||||
assert!(pin_relay_route(&snapshot).is_err());
|
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 {
|
fn ip(value: &str) -> IpAddr {
|
||||||
value.parse().unwrap()
|
value.parse().unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ use std::{
|
|||||||
ptr::null,
|
ptr::null,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use windows_sys::Win32::{
|
use windows_sys::Win32::{
|
||||||
Foundation::ERROR_SUCCESS,
|
Foundation::ERROR_SUCCESS,
|
||||||
NetworkManagement::{
|
NetworkManagement::{
|
||||||
IpHelper::{
|
IpHelper::{
|
||||||
CreateIpForwardEntry2, DeleteIpForwardEntry2, GetBestRoute2, IP_ADDRESS_PREFIX,
|
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
|
||||||
InitializeIpForwardEntry, MIB_IPFORWARD_ROW2,
|
DeleteIpForwardEntry2, GetBestRoute2, IP_ADDRESS_PREFIX, InitializeIpForwardEntry,
|
||||||
|
MIB_IPFORWARD_ROW2,
|
||||||
},
|
},
|
||||||
Ndis::NET_LUID_LH,
|
Ndis::NET_LUID_LH,
|
||||||
},
|
},
|
||||||
@@ -19,8 +20,34 @@ use windows_sys::Win32::{
|
|||||||
SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_IN6_0, SOCKADDR_INET,
|
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<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 best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
|
pub fn best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
|
||||||
let destination_sockaddr = sockaddr_from_ip(destination);
|
let destination_sockaddr = sockaddr_from_ip(destination);
|
||||||
@@ -45,10 +72,7 @@ pub fn best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
|
|||||||
let route_prefix = ip_from_sockaddr(&route.DestinationPrefix.Prefix)
|
let route_prefix = ip_from_sockaddr(&route.DestinationPrefix.Prefix)
|
||||||
.context("best route returned no route prefix")?;
|
.context("best route returned no route prefix")?;
|
||||||
let next_hop = ip_from_sockaddr(&route.NextHop).filter(|next_hop| !next_hop.is_unspecified());
|
let next_hop = ip_from_sockaddr(&route.NextHop).filter(|next_hop| !next_hop.is_unspecified());
|
||||||
let interface_luid = unsafe {
|
let interface_luid = luid_value(route.InterfaceLuid);
|
||||||
// SAFETY: GetBestRoute2 initialized the route row, including InterfaceLuid.
|
|
||||||
route.InterfaceLuid.Value
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(RouteSnapshot::new(
|
Ok(RouteSnapshot::new(
|
||||||
destination,
|
destination,
|
||||||
@@ -166,6 +190,38 @@ fn pinned_route_row(route: &RouteSnapshot) -> PinnedRelayRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
fn host_prefix(addr: IpAddr) -> IP_ADDRESS_PREFIX {
|
||||||
IP_ADDRESS_PREFIX {
|
IP_ADDRESS_PREFIX {
|
||||||
Prefix: sockaddr_from_ip(addr),
|
Prefix: sockaddr_from_ip(addr),
|
||||||
@@ -285,6 +341,19 @@ mod tests {
|
|||||||
assert_eq!(pinned.row.Protocol, RouteProtocolNetMgmt);
|
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]
|
#[test]
|
||||||
fn builds_ipv6_on_link_host_route_row() {
|
fn builds_ipv6_on_link_host_route_row() {
|
||||||
let route = RouteSnapshot::new(
|
let route = RouteSnapshot::new(
|
||||||
|
|||||||
Reference in New Issue
Block a user