bee6e468bb
The Windows client relies on scoped guards to restore TAP MTU, interface metric, default-route policy, and the relay host route when the client exits. Those cleanups are still best-effort because they run from Drop, but silently ignoring errors makes the manual MVP test harder to recover from if Windows refuses one of the restore operations. Log cleanup failures with the interface family, index, LUID, or relay route identity involved. Successful cleanup remains quiet, and the guard ownership model is unchanged. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-route - cargo check -p lanparty-client-route --target x86_64-pc-windows-msvc - cargo clippy -p lanparty-client-route --all-targets -- -D warnings - cargo clippy -p lanparty-client-route --target x86_64-pc-windows-msvc \ -- -D warnings - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check - git diff --cached --check Refs: MVP Windows cleanup diagnostics
878 lines
26 KiB
Rust
878 lines
26 KiB
Rust
use std::{
|
|
fmt, io,
|
|
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
|
ptr::{null, null_mut},
|
|
slice,
|
|
};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use windows_sys::Win32::{
|
|
Foundation::{ERROR_OBJECT_ALREADY_EXISTS, ERROR_SUCCESS},
|
|
NetworkManagement::{
|
|
IpHelper::{
|
|
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
|
|
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, 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, InterfaceUnicastAddress, IpInterfaceFamily,
|
|
};
|
|
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 interface_metric(
|
|
identity: NetworkInterfaceIdentity,
|
|
family: IpInterfaceFamily,
|
|
) -> Result<InterfaceMetricSnapshot> {
|
|
let row = get_interface_row(identity, family)?;
|
|
|
|
Ok(metric_snapshot(identity, family, row))
|
|
}
|
|
|
|
pub fn interface_mtu(
|
|
identity: NetworkInterfaceIdentity,
|
|
family: IpInterfaceFamily,
|
|
) -> Result<InterfaceMtuSnapshot> {
|
|
let row = get_interface_row(identity, family)?;
|
|
|
|
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,
|
|
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 fn set_scoped_default_routes_disabled(
|
|
identity: NetworkInterfaceIdentity,
|
|
family: IpInterfaceFamily,
|
|
disabled: bool,
|
|
) -> Result<ScopedDefaultRoutes> {
|
|
let previous = interface_metric(identity, family)?;
|
|
let mut row = get_interface_row(identity, family)?;
|
|
row.DisableDefaultRoutes = disabled;
|
|
set_interface_row(&mut row).with_context(|| {
|
|
format!("failed to set {family:?} default-route disabled state to {disabled}")
|
|
})?;
|
|
|
|
Ok(ScopedDefaultRoutes {
|
|
previous,
|
|
active: true,
|
|
})
|
|
}
|
|
|
|
pub fn set_scoped_interface_mtu(
|
|
identity: NetworkInterfaceIdentity,
|
|
family: IpInterfaceFamily,
|
|
mtu: u32,
|
|
) -> Result<ScopedInterfaceMtu> {
|
|
if mtu == 0 {
|
|
bail!("interface MTU must be nonzero");
|
|
}
|
|
|
|
let previous = interface_mtu(identity, family)?;
|
|
let mut row = get_interface_row(identity, family)?;
|
|
row.NlMtu = mtu;
|
|
set_interface_row(&mut row)
|
|
.with_context(|| format!("failed to set {family:?} interface MTU to {mtu}"))?;
|
|
|
|
Ok(ScopedInterfaceMtu {
|
|
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;
|
|
}
|
|
|
|
if let Err(error) = restore_interface_metric(self.previous) {
|
|
eprintln!(
|
|
"failed to restore {:?} interface metric for index {} LUID {}: {error:#}",
|
|
self.previous.family(),
|
|
self.previous.identity().index(),
|
|
self.previous.identity().luid(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ScopedDefaultRoutes {
|
|
previous: InterfaceMetricSnapshot,
|
|
active: bool,
|
|
}
|
|
|
|
impl ScopedDefaultRoutes {
|
|
#[must_use]
|
|
pub const fn previous(&self) -> InterfaceMetricSnapshot {
|
|
self.previous
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for ScopedDefaultRoutes {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("ScopedDefaultRoutes")
|
|
.field("previous", &self.previous)
|
|
.field("active", &self.active)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Drop for ScopedDefaultRoutes {
|
|
fn drop(&mut self) {
|
|
if !self.active {
|
|
return;
|
|
}
|
|
|
|
if let Err(error) = restore_default_routes(self.previous) {
|
|
eprintln!(
|
|
"failed to restore {:?} default-route state for index {} LUID {}: {error:#}",
|
|
self.previous.family(),
|
|
self.previous.identity().index(),
|
|
self.previous.identity().luid(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ScopedInterfaceMtu {
|
|
previous: InterfaceMtuSnapshot,
|
|
active: bool,
|
|
}
|
|
|
|
impl ScopedInterfaceMtu {
|
|
#[must_use]
|
|
pub const fn previous(&self) -> InterfaceMtuSnapshot {
|
|
self.previous
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for ScopedInterfaceMtu {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("ScopedInterfaceMtu")
|
|
.field("previous", &self.previous)
|
|
.field("active", &self.active)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Drop for ScopedInterfaceMtu {
|
|
fn drop(&mut self) {
|
|
if !self.active {
|
|
return;
|
|
}
|
|
|
|
if let Err(error) = restore_interface_mtu(self.previous) {
|
|
eprintln!(
|
|
"failed to restore {:?} interface MTU for index {} LUID {}: {error:#}",
|
|
self.previous.family(),
|
|
self.previous.identity().index(),
|
|
self.previous.identity().luid(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
|
|
let destination_sockaddr = sockaddr_from_ip(destination);
|
|
let mut route = MIB_IPFORWARD_ROW2::default();
|
|
let mut source = SOCKADDR_INET::default();
|
|
let status = unsafe {
|
|
// SAFETY: destination_sockaddr, route, and source point to valid initialized storage.
|
|
// Null interface/source inputs ask Windows to choose the best current route.
|
|
GetBestRoute2(
|
|
null(),
|
|
0,
|
|
null(),
|
|
&destination_sockaddr,
|
|
0,
|
|
&mut route,
|
|
&mut source,
|
|
)
|
|
};
|
|
windows_status(status).with_context(|| format!("failed to find route to {destination}"))?;
|
|
|
|
let source = ip_from_sockaddr(&source).context("best route returned no source address")?;
|
|
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 = luid_value(route.InterfaceLuid);
|
|
|
|
Ok(RouteSnapshot::new(
|
|
destination,
|
|
source,
|
|
next_hop,
|
|
route_prefix,
|
|
route.DestinationPrefix.PrefixLength,
|
|
route.InterfaceIndex,
|
|
interface_luid,
|
|
route.Metric,
|
|
))
|
|
}
|
|
|
|
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 restore_default_routes(snapshot: InterfaceMetricSnapshot) -> Result<()> {
|
|
let mut row = get_interface_row(snapshot.identity(), snapshot.family())?;
|
|
row.DisableDefaultRoutes = snapshot.disable_default_routes();
|
|
set_interface_row(&mut row)
|
|
}
|
|
|
|
fn restore_interface_mtu(snapshot: InterfaceMtuSnapshot) -> Result<()> {
|
|
let mut row = get_interface_row(snapshot.identity(), snapshot.family())?;
|
|
row.NlMtu = snapshot.mtu();
|
|
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,
|
|
)
|
|
}
|
|
|
|
fn mtu_snapshot(
|
|
identity: NetworkInterfaceIdentity,
|
|
family: IpInterfaceFamily,
|
|
row: MIB_IPINTERFACE_ROW,
|
|
) -> InterfaceMtuSnapshot {
|
|
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,
|
|
IpInterfaceFamily::Ipv6 => AF_INET6,
|
|
}
|
|
}
|
|
|
|
pub fn pin_relay_route(route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
|
|
let mut pinned = pinned_route_row(route);
|
|
let status = unsafe {
|
|
// SAFETY: pinned.row was initialized with InitializeIpForwardEntry and populated with a
|
|
// valid host destination, next hop, and interface identity.
|
|
CreateIpForwardEntry2(&pinned.row)
|
|
};
|
|
let disposition = route_create_disposition(status).with_context(|| {
|
|
format!(
|
|
"failed to pin relay route to {} via interface index {}",
|
|
route.destination(),
|
|
route.interface_index()
|
|
)
|
|
})?;
|
|
pinned.delete_on_drop = disposition == RouteCreateDisposition::Created;
|
|
|
|
Ok(pinned)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RouteCreateDisposition {
|
|
Created,
|
|
AlreadyExists,
|
|
}
|
|
|
|
fn route_create_disposition(status: u32) -> io::Result<RouteCreateDisposition> {
|
|
match status {
|
|
ERROR_SUCCESS => Ok(RouteCreateDisposition::Created),
|
|
ERROR_OBJECT_ALREADY_EXISTS => Ok(RouteCreateDisposition::AlreadyExists),
|
|
_ => Err(io::Error::from_raw_os_error(status as i32)),
|
|
}
|
|
}
|
|
|
|
pub struct PinnedRelayRoute {
|
|
row: MIB_IPFORWARD_ROW2,
|
|
destination: IpAddr,
|
|
next_hop: Option<IpAddr>,
|
|
interface_index: u32,
|
|
interface_luid: u64,
|
|
delete_on_drop: bool,
|
|
}
|
|
|
|
impl PinnedRelayRoute {
|
|
#[must_use]
|
|
pub const fn destination(&self) -> IpAddr {
|
|
self.destination
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn next_hop(&self) -> Option<IpAddr> {
|
|
self.next_hop
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn interface_index(&self) -> u32 {
|
|
self.interface_index
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn interface_luid(&self) -> u64 {
|
|
self.interface_luid
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn created_by_client(&self) -> bool {
|
|
self.delete_on_drop
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for PinnedRelayRoute {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("PinnedRelayRoute")
|
|
.field("destination", &self.destination)
|
|
.field("next_hop", &self.next_hop)
|
|
.field("interface_index", &self.interface_index)
|
|
.field("interface_luid", &self.interface_luid)
|
|
.field("delete_on_drop", &self.delete_on_drop)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl Drop for PinnedRelayRoute {
|
|
fn drop(&mut self) {
|
|
if !self.delete_on_drop {
|
|
return;
|
|
}
|
|
|
|
let status = unsafe {
|
|
// SAFETY: self.row is the same route row that was successfully created for this guard.
|
|
DeleteIpForwardEntry2(&self.row)
|
|
};
|
|
if let Err(error) = windows_status(status) {
|
|
eprintln!(
|
|
"failed to delete relay host route to {} on interface index {} LUID {}: {error:#}",
|
|
self.destination, self.interface_index, self.interface_luid,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn pinned_route_row(route: &RouteSnapshot) -> PinnedRelayRoute {
|
|
let destination = route.destination();
|
|
let next_hop = route.next_hop();
|
|
let mut row = MIB_IPFORWARD_ROW2::default();
|
|
unsafe {
|
|
// SAFETY: row points to valid writable storage for Windows to initialize.
|
|
InitializeIpForwardEntry(&mut row);
|
|
}
|
|
row.InterfaceLuid = NET_LUID_LH {
|
|
Value: route.interface_luid(),
|
|
};
|
|
row.InterfaceIndex = route.interface_index();
|
|
row.DestinationPrefix = host_prefix(destination);
|
|
row.NextHop = sockaddr_from_ip(next_hop.unwrap_or_else(|| unspecified_for(destination)));
|
|
row.SitePrefixLength = host_prefix_len(destination);
|
|
row.Metric = 0;
|
|
row.Protocol = RouteProtocolNetMgmt;
|
|
|
|
PinnedRelayRoute {
|
|
row,
|
|
destination,
|
|
next_hop,
|
|
interface_index: route.interface_index(),
|
|
interface_luid: route.interface_luid(),
|
|
delete_on_drop: false,
|
|
}
|
|
}
|
|
|
|
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 {
|
|
IP_ADDRESS_PREFIX {
|
|
Prefix: sockaddr_from_ip(addr),
|
|
PrefixLength: host_prefix_len(addr),
|
|
}
|
|
}
|
|
|
|
const fn host_prefix_len(addr: IpAddr) -> u8 {
|
|
match addr {
|
|
IpAddr::V4(_) => 32,
|
|
IpAddr::V6(_) => 128,
|
|
}
|
|
}
|
|
|
|
const fn unspecified_for(addr: IpAddr) -> IpAddr {
|
|
match addr {
|
|
IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
|
IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
|
|
}
|
|
}
|
|
|
|
fn sockaddr_from_ip(addr: IpAddr) -> SOCKADDR_INET {
|
|
match addr {
|
|
IpAddr::V4(addr) => SOCKADDR_INET {
|
|
Ipv4: SOCKADDR_IN {
|
|
sin_family: AF_INET,
|
|
sin_port: 0,
|
|
sin_addr: IN_ADDR {
|
|
S_un: IN_ADDR_0 {
|
|
S_addr: u32::from_ne_bytes(addr.octets()),
|
|
},
|
|
},
|
|
sin_zero: [0; 8],
|
|
},
|
|
},
|
|
IpAddr::V6(addr) => SOCKADDR_INET {
|
|
Ipv6: SOCKADDR_IN6 {
|
|
sin6_family: AF_INET6,
|
|
sin6_port: 0,
|
|
sin6_flowinfo: 0,
|
|
sin6_addr: IN6_ADDR {
|
|
u: IN6_ADDR_0 {
|
|
Byte: addr.octets(),
|
|
},
|
|
},
|
|
Anonymous: SOCKADDR_IN6_0 { sin6_scope_id: 0 },
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
fn ip_from_sockaddr(sockaddr: &SOCKADDR_INET) -> Option<IpAddr> {
|
|
match unsafe {
|
|
// SAFETY: Reading the discriminator is valid for any SOCKADDR_INET initialized by us or
|
|
// by Windows APIs.
|
|
sockaddr.si_family
|
|
} {
|
|
AF_INET => {
|
|
let ipv4 = unsafe {
|
|
// SAFETY: si_family reports that the IPv4 union field is active.
|
|
sockaddr.Ipv4
|
|
};
|
|
let octets = unsafe {
|
|
// SAFETY: S_addr is the compact IPv4 representation in network byte order.
|
|
ipv4.sin_addr.S_un.S_addr.to_ne_bytes()
|
|
};
|
|
Some(IpAddr::V4(Ipv4Addr::from(octets)))
|
|
}
|
|
AF_INET6 => {
|
|
let ipv6 = unsafe {
|
|
// SAFETY: si_family reports that the IPv6 union field is active.
|
|
sockaddr.Ipv6
|
|
};
|
|
let octets = unsafe {
|
|
// SAFETY: Byte is the compact IPv6 representation in network byte order.
|
|
ipv6.sin6_addr.u.Byte
|
|
};
|
|
Some(IpAddr::V6(Ipv6Addr::from(octets)))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn windows_status(status: u32) -> io::Result<()> {
|
|
if status == ERROR_SUCCESS {
|
|
Ok(())
|
|
} else {
|
|
Err(io::Error::from_raw_os_error(status as i32))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn builds_ipv4_host_route_row() {
|
|
let route = RouteSnapshot::new(
|
|
ip("203.0.113.10"),
|
|
ip("192.0.2.44"),
|
|
Some(ip("192.0.2.1")),
|
|
ip("0.0.0.0"),
|
|
0,
|
|
12,
|
|
34,
|
|
25,
|
|
);
|
|
let pinned = pinned_route_row(&route);
|
|
|
|
assert_eq!(pinned.destination(), ip("203.0.113.10"));
|
|
assert_eq!(pinned.next_hop(), Some(ip("192.0.2.1")));
|
|
assert_eq!(pinned.interface_index(), 12);
|
|
assert_eq!(pinned.interface_luid(), 34);
|
|
assert_eq!(pinned.row.DestinationPrefix.PrefixLength, 32);
|
|
assert_eq!(pinned.row.SitePrefixLength, 32);
|
|
assert_eq!(pinned.row.Metric, 0);
|
|
assert_eq!(pinned.row.Protocol, RouteProtocolNetMgmt);
|
|
assert!(!pinned.created_by_client());
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_route_create_statuses() {
|
|
use windows_sys::Win32::Foundation::ERROR_INVALID_PARAMETER;
|
|
|
|
assert_eq!(
|
|
route_create_disposition(ERROR_SUCCESS).unwrap(),
|
|
RouteCreateDisposition::Created
|
|
);
|
|
assert_eq!(
|
|
route_create_disposition(ERROR_OBJECT_ALREADY_EXISTS).unwrap(),
|
|
RouteCreateDisposition::AlreadyExists
|
|
);
|
|
assert!(route_create_disposition(ERROR_INVALID_PARAMETER).is_err());
|
|
}
|
|
|
|
#[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_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_mtu_snapshots_from_rows() {
|
|
let identity = NetworkInterfaceIdentity::new(12, 34);
|
|
let mut row = interface_row_key(identity, IpInterfaceFamily::Ipv4);
|
|
row.NlMtu = 1200;
|
|
let snapshot = mtu_snapshot(identity, IpInterfaceFamily::Ipv4, row);
|
|
|
|
assert_eq!(snapshot.identity(), identity);
|
|
assert_eq!(snapshot.family(), IpInterfaceFamily::Ipv4);
|
|
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(
|
|
ip("2001:db8::10"),
|
|
ip("2001:db8::44"),
|
|
None,
|
|
ip("2001:db8::"),
|
|
64,
|
|
12,
|
|
34,
|
|
25,
|
|
);
|
|
let pinned = pinned_route_row(&route);
|
|
|
|
assert_eq!(pinned.destination(), ip("2001:db8::10"));
|
|
assert_eq!(pinned.next_hop(), None);
|
|
assert_eq!(pinned.row.DestinationPrefix.PrefixLength, 128);
|
|
assert_eq!(pinned.row.SitePrefixLength, 128);
|
|
assert_eq!(ip_from_sockaddr(&pinned.row.NextHop), Some(ip("::")));
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|