feat(client): add scoped relay host route pin
Add the first route-table mutation API to `lanparty-client-route`. Given the pre-TAP best-route snapshot, the crate can now create a host route for the relay IP on that same interface and return an RAII guard that deletes the route when it is dropped. The route row is initialized with `InitializeIpForwardEntry`, uses a /32 or /128 destination prefix for the relay IP, preserves the pre-TAP interface index/LUID, and uses the remembered next hop or an on-link unspecified next hop when Windows reported no gateway. The guard tracks whether creation actually succeeded so pure row-construction tests and failed creates do not try to delete routes they did not install. This remains a crate-level boundary only. The Windows client still reports the pre-TAP route but does not yet hold a pin across TAP activation; that is the next integration slice. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-route - cargo clippy -p lanparty-client-route --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 --all-targets -- -D warnings - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md Refs: https://learn.microsoft.com/en-us/windows-hardware/drivers/network/createipforwardentry2 Refs: https://learn.microsoft.com/en-us/windows-hardware/drivers/network/initializeipforwardentry
This commit is contained in:
@@ -55,6 +55,7 @@ Windows route-table boundary:
|
|||||||
|
|
||||||
- read-only best-route lookup for a relay destination IP
|
- read-only best-route lookup for a relay destination IP
|
||||||
- selected source address, next hop, interface index/LUID, prefix, and metric
|
- selected source address, next hop, interface index/LUID, prefix, and metric
|
||||||
|
- scoped host-route pinning for the relay IP on the pre-TAP interface
|
||||||
- non-Windows builds return a clear unsupported-platform error
|
- non-Windows builds return a clear unsupported-platform error
|
||||||
|
|
||||||
### `lanparty-client-tap`
|
### `lanparty-client-tap`
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! Windows route-table inspection for protecting the relay path.
|
//! Windows route-table inspection for protecting the relay path.
|
||||||
//!
|
//!
|
||||||
//! The client binary uses this crate to keep Win32 route/metric calls out of
|
//! The client binary uses this crate to keep Win32 route/metric calls out of
|
||||||
//! the relay session code. This crate starts with read-only route snapshots;
|
//! the relay session code. The crate can snapshot the current relay route and
|
||||||
//! later route pinning can build on the same typed boundary.
|
//! install a scoped host route that pins the relay IP to the pre-TAP interface.
|
||||||
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
@@ -91,13 +91,24 @@ impl RouteSnapshot {
|
|||||||
mod windows;
|
mod windows;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub use windows::best_route_to;
|
pub use windows::{PinnedRelayRoute, best_route_to, pin_relay_route};
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
pub fn best_route_to(_destination: IpAddr) -> Result<RouteSnapshot> {
|
pub fn best_route_to(_destination: IpAddr) -> Result<RouteSnapshot> {
|
||||||
bail!("Windows route inspection is only available on Windows");
|
bail!("Windows route inspection is only available on Windows");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PinnedRelayRoute {
|
||||||
|
_private: (),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn pin_relay_route(_route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
|
||||||
|
bail!("Windows route pinning is only available on Windows");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -131,6 +142,23 @@ mod tests {
|
|||||||
assert!(best_route_to(ip("203.0.113.10")).is_err());
|
assert!(best_route_to(ip("203.0.113.10")).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[test]
|
||||||
|
fn rejects_route_pinning_on_non_windows() {
|
||||||
|
let snapshot = 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(pin_relay_route(&snapshot).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
fn ip(value: &str) -> IpAddr {
|
fn ip(value: &str) -> IpAddr {
|
||||||
value.parse().unwrap()
|
value.parse().unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io,
|
fmt, io,
|
||||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||||
ptr::null,
|
ptr::null,
|
||||||
};
|
};
|
||||||
@@ -7,10 +7,16 @@ use std::{
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use windows_sys::Win32::{
|
use windows_sys::Win32::{
|
||||||
Foundation::ERROR_SUCCESS,
|
Foundation::ERROR_SUCCESS,
|
||||||
NetworkManagement::IpHelper::{GetBestRoute2, MIB_IPFORWARD_ROW2},
|
NetworkManagement::{
|
||||||
|
IpHelper::{
|
||||||
|
CreateIpForwardEntry2, DeleteIpForwardEntry2, GetBestRoute2, IP_ADDRESS_PREFIX,
|
||||||
|
InitializeIpForwardEntry, MIB_IPFORWARD_ROW2,
|
||||||
|
},
|
||||||
|
Ndis::NET_LUID_LH,
|
||||||
|
},
|
||||||
Networking::WinSock::{
|
Networking::WinSock::{
|
||||||
AF_INET, AF_INET6, IN_ADDR, IN_ADDR_0, IN6_ADDR, IN6_ADDR_0, SOCKADDR_IN, SOCKADDR_IN6,
|
AF_INET, AF_INET6, IN_ADDR, IN_ADDR_0, IN6_ADDR, IN6_ADDR_0, RouteProtocolNetMgmt,
|
||||||
SOCKADDR_IN6_0, SOCKADDR_INET,
|
SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_IN6_0, SOCKADDR_INET,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,6 +62,131 @@ pub fn best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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 {
|
fn sockaddr_from_ip(addr: IpAddr) -> SOCKADDR_INET {
|
||||||
match addr {
|
match addr {
|
||||||
IpAddr::V4(addr) => SOCKADDR_INET {
|
IpAddr::V4(addr) => SOCKADDR_INET {
|
||||||
@@ -125,3 +256,57 @@ fn windows_status(status: u32) -> io::Result<()> {
|
|||||||
Err(io::Error::from_raw_os_error(status as i32))
|
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 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user