feat(client): add Windows route snapshot helper
Add `lanparty-client-route` as the Win32 boundary for route-table work. The first API is intentionally read-only: `best_route_to` wraps `GetBestRoute2` and returns the selected source address, next hop, route prefix, interface index/LUID, and route metric for a relay destination IP. This keeps the route-protection work separate from the QUIC client binary, so we can typecheck the Windows IP Helper calls on this Linux host without pulling in the `ring` build path that currently blocks full Windows binary checks. Actual route pinning and metric mutation remain later slices. 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 -- -D warnings - git diff --check Refs: PLAN.md
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
use std::{
|
||||
io,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
ptr::null,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::ERROR_SUCCESS,
|
||||
NetworkManagement::IpHelper::{GetBestRoute2, MIB_IPFORWARD_ROW2},
|
||||
Networking::WinSock::{
|
||||
AF_INET, AF_INET6, IN_ADDR, IN_ADDR_0, IN6_ADDR, IN6_ADDR_0, SOCKADDR_IN, SOCKADDR_IN6,
|
||||
SOCKADDR_IN6_0, SOCKADDR_INET,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::RouteSnapshot;
|
||||
|
||||
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 = unsafe {
|
||||
// SAFETY: GetBestRoute2 initialized the route row, including InterfaceLuid.
|
||||
route.InterfaceLuid.Value
|
||||
};
|
||||
|
||||
Ok(RouteSnapshot::new(
|
||||
destination,
|
||||
source,
|
||||
next_hop,
|
||||
route_prefix,
|
||||
route.DestinationPrefix.PrefixLength,
|
||||
route.InterfaceIndex,
|
||||
interface_luid,
|
||||
route.Metric,
|
||||
))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user