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
+67 -2
View File
@@ -66,6 +66,14 @@ pub struct InterfaceMtuSnapshot {
mtu: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InterfaceUnicastAddress {
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
address: IpAddr,
prefix_len: u8,
}
impl InterfaceMetricSnapshot {
#[cfg_attr(not(windows), allow(dead_code))]
const fn new(
@@ -136,6 +144,43 @@ impl InterfaceMtuSnapshot {
}
}
impl InterfaceUnicastAddress {
#[cfg_attr(not(windows), allow(dead_code))]
const fn new(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
address: IpAddr,
prefix_len: u8,
) -> Self {
Self {
identity,
family,
address,
prefix_len,
}
}
#[must_use]
pub const fn identity(self) -> NetworkInterfaceIdentity {
self.identity
}
#[must_use]
pub const fn family(self) -> IpInterfaceFamily {
self.family
}
#[must_use]
pub const fn address(self) -> IpAddr {
self.address
}
#[must_use]
pub const fn prefix_len(self) -> u8 {
self.prefix_len
}
}
impl RouteSnapshot {
#[cfg_attr(not(windows), allow(dead_code))]
#[allow(clippy::too_many_arguments)]
@@ -227,8 +272,8 @@ pub use windows::{PinnedRelayRoute, best_route_to, interface_identity_from_guid,
#[cfg(windows)]
pub use windows::{
ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu, interface_metric,
interface_mtu, set_scoped_default_routes_disabled, set_scoped_interface_metric,
set_scoped_interface_mtu,
interface_mtu, interface_unicast_addresses, set_scoped_default_routes_disabled,
set_scoped_interface_metric, set_scoped_interface_mtu,
};
#[cfg(not(windows))]
@@ -298,6 +343,13 @@ pub fn interface_mtu(
bail!("Windows interface MTU lookup is only available on Windows");
}
#[cfg(not(windows))]
pub fn interface_unicast_addresses(
_identity: NetworkInterfaceIdentity,
) -> Result<Vec<InterfaceUnicastAddress>> {
bail!("Windows interface address lookup is only available on Windows");
}
#[cfg(not(windows))]
#[derive(Debug)]
pub struct ScopedInterfaceMtu {
@@ -404,6 +456,18 @@ mod tests {
assert_eq!(snapshot.mtu(), 1200);
}
#[test]
fn exposes_interface_unicast_address_fields() {
let identity = NetworkInterfaceIdentity::new(12, 34);
let snapshot =
InterfaceUnicastAddress::new(identity, IpInterfaceFamily::Ipv4, ip("10.73.42.51"), 24);
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);
}
#[cfg(not(windows))]
#[test]
fn rejects_route_inspection_on_non_windows() {
@@ -444,6 +508,7 @@ mod tests {
set_scoped_default_routes_disabled(identity, IpInterfaceFamily::Ipv4, true).is_err()
);
assert!(interface_mtu(identity, IpInterfaceFamily::Ipv4).is_err());
assert!(interface_unicast_addresses(identity).is_err());
assert!(set_scoped_interface_mtu(identity, IpInterfaceFamily::Ipv4, 1200).is_err());
}