3e2648abc1
The Windows client now sets the TAP IP-interface MTU to the relay-selected MTU before it starts bridging frames. The override is scoped like the existing metric and default-route guards, so the previous MTU is restored when the client exits. The route crate now exposes `InterfaceMtuSnapshot` and `ScopedInterfaceMtu` around `MIB_IPINTERFACE_ROW.NlMtu`, reusing the same `GetIpInterfaceEntry` and `SetIpInterfaceEntry` path already used for metrics and default-route policy. IPv4 MTU setup is required for startup, while IPv6 MTU setup is best-effort to match the existing IPv6 route-protection behavior. This intentionally leaves TAP MAC configuration as fail-fast. TAP-Windows6 does not expose a matching set-MAC IOCTL in the driver header, so that should remain a separate design decision. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-route - cargo test -p lanparty-client-win - cargo clippy -p lanparty-client-route -p lanparty-client-win --all-targets -- -D warnings - cargo check -p lanparty-client-route --target x86_64-pc-windows-gnu - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check - cargo check -p lanparty-client-win --target x86_64-pc-windows-gnu (fails before this crate in ring: missing x86_64-w64-mingw32-gcc) Refs: PLAN.md
686 lines
20 KiB
Rust
686 lines
20 KiB
Rust
use std::{
|
|
fmt, io,
|
|
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
|
ptr::null,
|
|
};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use windows_sys::Win32::{
|
|
Foundation::ERROR_SUCCESS,
|
|
NetworkManagement::{
|
|
IpHelper::{
|
|
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
|
|
DeleteIpForwardEntry2, GetBestRoute2, GetIpInterfaceEntry, IP_ADDRESS_PREFIX,
|
|
InitializeIpForwardEntry, InitializeIpInterfaceEntry, MIB_IPFORWARD_ROW2,
|
|
MIB_IPINTERFACE_ROW, 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,
|
|
},
|
|
};
|
|
use windows_sys::core::GUID;
|
|
|
|
use crate::{InterfaceMetricSnapshot, InterfaceMtuSnapshot, 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 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;
|
|
}
|
|
|
|
let _ = restore_interface_metric(self.previous);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
let _ = restore_default_routes(self.previous);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
let _ = restore_interface_mtu(self.previous);
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
};
|
|
windows_status(status).with_context(|| {
|
|
format!(
|
|
"failed to pin relay route to {} via interface index {}",
|
|
route.destination(),
|
|
route.interface_index()
|
|
)
|
|
})?;
|
|
pinned.active = true;
|
|
|
|
Ok(pinned)
|
|
}
|
|
|
|
pub struct PinnedRelayRoute {
|
|
row: MIB_IPFORWARD_ROW2,
|
|
destination: IpAddr,
|
|
next_hop: Option<IpAddr>,
|
|
interface_index: u32,
|
|
interface_luid: u64,
|
|
active: 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
|
|
}
|
|
}
|
|
|
|
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("active", &self.active)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl Drop for PinnedRelayRoute {
|
|
fn drop(&mut self) {
|
|
if !self.active {
|
|
return;
|
|
}
|
|
|
|
unsafe {
|
|
// SAFETY: self.row is the same route row that was successfully created for this guard.
|
|
// Deletion failure is intentionally ignored during Drop.
|
|
DeleteIpForwardEntry2(&self.row);
|
|
}
|
|
}
|
|
}
|
|
|
|
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(),
|
|
active: 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);
|
|
}
|
|
|
|
#[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_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()
|
|
}
|
|
}
|