feat(client): report TAP IP in diagnostics

Add InterfaceUnicastAddress snapshots to client-route, backed by the Windows
unicast IP address table. The Windows client samples the TAP interface after it
resolves the adapter identity, preferring IPv4 for diagnostics and falling back
to the first address or unknown on lookup failure.

This keeps Win32 IP table handling in the route crate and fills the existing
TapDiagnostics IP field without making bridging depend on DHCP being present.
If DHCP has not assigned an address yet, diagnostics still make that visible as
unknown.

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

Attempted:
- cargo check -p lanparty-client-route -p lanparty-client-win
  --target x86_64-pc-windows-gnu

Blocked because ring needs missing x86_64-w64-mingw32-gcc here.

Refs: PLAN.md
This commit is contained in:
2026-05-21 20:20:55 +02:00
parent 2afdae44c6
commit c6dbb78cfc
4 changed files with 235 additions and 11 deletions
+138 -7
View File
@@ -1,7 +1,8 @@
use std::{
fmt, io,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
ptr::null,
ptr::{null, null_mut},
slice,
};
use anyhow::{Context, Result, bail};
@@ -10,20 +11,23 @@ use windows_sys::Win32::{
NetworkManagement::{
IpHelper::{
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
DeleteIpForwardEntry2, GetBestRoute2, GetIpInterfaceEntry, IP_ADDRESS_PREFIX,
InitializeIpForwardEntry, InitializeIpInterfaceEntry, MIB_IPFORWARD_ROW2,
MIB_IPINTERFACE_ROW, SetIpInterfaceEntry,
DeleteIpForwardEntry2, FreeMibTable, GetBestRoute2, GetIpInterfaceEntry,
GetUnicastIpAddressTable, IP_ADDRESS_PREFIX, InitializeIpForwardEntry,
InitializeIpInterfaceEntry, MIB_IPFORWARD_ROW2, MIB_IPINTERFACE_ROW,
MIB_UNICASTIPADDRESS_ROW, MIB_UNICASTIPADDRESS_TABLE, 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,
AF_INET, AF_INET6, AF_UNSPEC, 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::{
InterfaceMetricSnapshot, InterfaceMtuSnapshot, InterfaceUnicastAddress, IpInterfaceFamily,
};
use crate::{NetworkInterfaceIdentity, RouteSnapshot};
pub fn interface_identity_from_guid(interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
@@ -69,6 +73,33 @@ pub fn interface_mtu(
Ok(mtu_snapshot(identity, family, row))
}
pub fn interface_unicast_addresses(
identity: NetworkInterfaceIdentity,
) -> Result<Vec<InterfaceUnicastAddress>> {
let mut table = null_mut();
let status = unsafe {
// SAFETY: table points to writable storage for the returned heap table pointer.
GetUnicastIpAddressTable(AF_UNSPEC, &mut table)
};
windows_status(status).with_context(|| {
format!(
"failed to read unicast IP address table for interface index {} LUID {}",
identity.index(),
identity.luid()
)
})?;
if table.is_null() {
bail!("Windows returned a null unicast IP address table");
}
let table = MibUnicastAddressTable(table);
let rows = table.rows();
Ok(rows
.iter()
.filter_map(|row| unicast_address_snapshot(identity, row))
.collect())
}
pub fn set_scoped_interface_metric(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
@@ -345,6 +376,55 @@ fn mtu_snapshot(
InterfaceMtuSnapshot::new(identity, family, row.NlMtu)
}
fn unicast_address_snapshot(
identity: NetworkInterfaceIdentity,
row: &MIB_UNICASTIPADDRESS_ROW,
) -> Option<InterfaceUnicastAddress> {
if row.InterfaceIndex != identity.index() || luid_value(row.InterfaceLuid) != identity.luid() {
return None;
}
let address = ip_from_sockaddr(&row.Address)?;
if address.is_unspecified() {
return None;
}
let family = match address {
IpAddr::V4(_) => IpInterfaceFamily::Ipv4,
IpAddr::V6(_) => IpInterfaceFamily::Ipv6,
};
Some(InterfaceUnicastAddress::new(
identity,
family,
address,
row.OnLinkPrefixLength,
))
}
struct MibUnicastAddressTable(*mut MIB_UNICASTIPADDRESS_TABLE);
impl MibUnicastAddressTable {
fn rows(&self) -> &[MIB_UNICASTIPADDRESS_ROW] {
let table = unsafe {
// SAFETY: self.0 is checked non-null after GetUnicastIpAddressTable succeeds and is
// owned by this guard until Drop frees it.
&*self.0
};
unsafe {
// SAFETY: Windows allocates NumEntries contiguous rows starting at Table.
slice::from_raw_parts(table.Table.as_ptr(), table.NumEntries as usize)
}
}
}
impl Drop for MibUnicastAddressTable {
fn drop(&mut self) {
unsafe {
// SAFETY: self.0 was allocated by GetUnicastIpAddressTable and is freed exactly once.
FreeMibTable(self.0.cast());
}
}
}
const fn address_family(family: IpInterfaceFamily) -> u16 {
match family {
IpInterfaceFamily::Ipv4 => AF_INET,
@@ -658,6 +738,37 @@ mod tests {
assert_eq!(snapshot.mtu(), 1200);
}
#[test]
fn builds_unicast_address_snapshots_from_rows() {
let identity = NetworkInterfaceIdentity::new(12, 34);
let row = unicast_row(identity, ip("10.73.42.51"), 24);
let snapshot = unicast_address_snapshot(identity, &row).unwrap();
assert_eq!(snapshot.identity(), identity);
assert_eq!(snapshot.family(), IpInterfaceFamily::Ipv4);
assert_eq!(snapshot.address(), ip("10.73.42.51"));
assert_eq!(snapshot.prefix_len(), 24);
}
#[test]
fn filters_unicast_address_rows_for_other_interfaces() {
let identity = NetworkInterfaceIdentity::new(12, 34);
let mut row = unicast_row(identity, ip("10.73.42.51"), 24);
row.InterfaceIndex = 99;
assert_eq!(unicast_address_snapshot(identity, &row), None);
}
#[test]
fn filters_unspecified_unicast_addresses() {
let identity = NetworkInterfaceIdentity::new(12, 34);
let row = unicast_row(identity, ip("0.0.0.0"), 0);
assert_eq!(unicast_address_snapshot(identity, &row), None);
}
#[test]
fn builds_ipv6_on_link_host_route_row() {
let route = RouteSnapshot::new(
@@ -682,4 +793,24 @@ mod tests {
fn ip(value: &str) -> IpAddr {
value.parse().unwrap()
}
fn unicast_row(
identity: NetworkInterfaceIdentity,
address: IpAddr,
prefix_len: u8,
) -> MIB_UNICASTIPADDRESS_ROW {
let mut row = unsafe {
// SAFETY: MIB_UNICASTIPADDRESS_ROW is a plain Win32 data structure; tests populate
// the fields read by unicast_address_snapshot before using it.
std::mem::zeroed::<MIB_UNICASTIPADDRESS_ROW>()
};
row.Address = sockaddr_from_ip(address);
row.InterfaceLuid = NET_LUID_LH {
Value: identity.luid(),
};
row.InterfaceIndex = identity.index();
row.OnLinkPrefixLength = prefix_len;
row
}
}