2a8e6467c4
The Windows client now verifies that the relay path still uses the pinned host route after the TAP adapter is activated, and keeps checking that invariant while frames are being bridged. If Windows starts choosing a different prefix, next hop, or interface for the relay IP, the client exits instead of silently letting the tunnel route itself through the TAP side. This closes the remaining detection half of the route-protection startup flow from PLAN.md. The previous commits installed the pre-TAP host route, scoped TAP metrics, and disabled TAP default routes; this commit proves those protections are still winning after TAP activation and catches later DHCP-driven route changes during the session. The route crate now exposes small helpers for route-interface identity and host route matching so the client does not duplicate route-prefix semantics inline. The full Windows client target check still cannot complete on this Linux host: `ring` fails while compiling for `x86_64-pc-windows-msvc` because the Windows C header `assert.h` is unavailable, before `lanparty-client-win` is typechecked. The independent Windows-target route crate checks do pass. 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 --all-targets -- -D warnings - git diff --check Refs: PLAN.md
385 lines
9.6 KiB
Rust
385 lines
9.6 KiB
Rust
//! Windows route-table inspection for protecting the relay path.
|
|
//!
|
|
//! The client binary uses this crate to keep Win32 route/metric calls out of
|
|
//! the relay session code. The crate can snapshot the current relay route and
|
|
//! install scoped route/interface overrides that are restored when dropped.
|
|
|
|
use std::net::IpAddr;
|
|
|
|
#[cfg(not(windows))]
|
|
use anyhow::{Result, bail};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct RouteSnapshot {
|
|
destination: IpAddr,
|
|
source: IpAddr,
|
|
next_hop: Option<IpAddr>,
|
|
route_prefix: IpAddr,
|
|
route_prefix_len: u8,
|
|
interface_index: u32,
|
|
interface_luid: u64,
|
|
metric: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct NetworkInterfaceIdentity {
|
|
index: u32,
|
|
luid: u64,
|
|
}
|
|
|
|
impl NetworkInterfaceIdentity {
|
|
#[cfg_attr(not(windows), allow(dead_code))]
|
|
const fn new(index: u32, luid: u64) -> Self {
|
|
Self { index, luid }
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn index(self) -> u32 {
|
|
self.index
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn luid(self) -> u64 {
|
|
self.luid
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum IpInterfaceFamily {
|
|
Ipv4,
|
|
Ipv6,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct InterfaceMetricSnapshot {
|
|
identity: NetworkInterfaceIdentity,
|
|
family: IpInterfaceFamily,
|
|
automatic_metric: bool,
|
|
metric: u32,
|
|
disable_default_routes: bool,
|
|
}
|
|
|
|
impl InterfaceMetricSnapshot {
|
|
#[cfg_attr(not(windows), allow(dead_code))]
|
|
const fn new(
|
|
identity: NetworkInterfaceIdentity,
|
|
family: IpInterfaceFamily,
|
|
automatic_metric: bool,
|
|
metric: u32,
|
|
disable_default_routes: bool,
|
|
) -> Self {
|
|
Self {
|
|
identity,
|
|
family,
|
|
automatic_metric,
|
|
metric,
|
|
disable_default_routes,
|
|
}
|
|
}
|
|
|
|
#[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 automatic_metric(self) -> bool {
|
|
self.automatic_metric
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn metric(self) -> u32 {
|
|
self.metric
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn disable_default_routes(self) -> bool {
|
|
self.disable_default_routes
|
|
}
|
|
}
|
|
|
|
impl RouteSnapshot {
|
|
#[cfg_attr(not(windows), allow(dead_code))]
|
|
#[allow(clippy::too_many_arguments)]
|
|
const fn new(
|
|
destination: IpAddr,
|
|
source: IpAddr,
|
|
next_hop: Option<IpAddr>,
|
|
route_prefix: IpAddr,
|
|
route_prefix_len: u8,
|
|
interface_index: u32,
|
|
interface_luid: u64,
|
|
metric: u32,
|
|
) -> Self {
|
|
Self {
|
|
destination,
|
|
source,
|
|
next_hop,
|
|
route_prefix,
|
|
route_prefix_len,
|
|
interface_index,
|
|
interface_luid,
|
|
metric,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn destination(&self) -> IpAddr {
|
|
self.destination
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn source(&self) -> IpAddr {
|
|
self.source
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn next_hop(&self) -> Option<IpAddr> {
|
|
self.next_hop
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn route_prefix(&self) -> IpAddr {
|
|
self.route_prefix
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn route_prefix_len(&self) -> u8 {
|
|
self.route_prefix_len
|
|
}
|
|
|
|
#[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 interface_identity(&self) -> NetworkInterfaceIdentity {
|
|
NetworkInterfaceIdentity::new(self.interface_index, self.interface_luid)
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn metric(&self) -> u32 {
|
|
self.metric
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn is_host_route_to(&self, destination: IpAddr) -> bool {
|
|
self.route_prefix == destination && self.route_prefix_len == host_prefix_len(destination)
|
|
}
|
|
}
|
|
|
|
const fn host_prefix_len(destination: IpAddr) -> u8 {
|
|
match destination {
|
|
IpAddr::V4(_) => 32,
|
|
IpAddr::V6(_) => 128,
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
mod windows;
|
|
|
|
#[cfg(windows)]
|
|
pub use windows::{PinnedRelayRoute, best_route_to, interface_identity_from_guid, pin_relay_route};
|
|
#[cfg(windows)]
|
|
pub use windows::{
|
|
ScopedDefaultRoutes, ScopedInterfaceMetric, interface_metric,
|
|
set_scoped_default_routes_disabled, set_scoped_interface_metric,
|
|
};
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn best_route_to(_destination: IpAddr) -> Result<RouteSnapshot> {
|
|
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(not(windows))]
|
|
pub fn interface_identity_from_guid(_interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
|
|
bail!("Windows interface identity lookup is only available on Windows");
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
#[derive(Debug)]
|
|
pub struct ScopedInterfaceMetric {
|
|
_private: (),
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn interface_metric(
|
|
_identity: NetworkInterfaceIdentity,
|
|
_family: IpInterfaceFamily,
|
|
) -> Result<InterfaceMetricSnapshot> {
|
|
bail!("Windows interface metric lookup is only available on Windows");
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn set_scoped_interface_metric(
|
|
_identity: NetworkInterfaceIdentity,
|
|
_family: IpInterfaceFamily,
|
|
_metric: u32,
|
|
) -> Result<ScopedInterfaceMetric> {
|
|
bail!("Windows interface metric updates are only available on Windows");
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
#[derive(Debug)]
|
|
pub struct ScopedDefaultRoutes {
|
|
_private: (),
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn set_scoped_default_routes_disabled(
|
|
_identity: NetworkInterfaceIdentity,
|
|
_family: IpInterfaceFamily,
|
|
_disabled: bool,
|
|
) -> Result<ScopedDefaultRoutes> {
|
|
bail!("Windows interface default-route updates are only available on Windows");
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn exposes_route_snapshot_fields() {
|
|
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_eq!(snapshot.destination(), ip("203.0.113.10"));
|
|
assert_eq!(snapshot.source(), ip("192.0.2.44"));
|
|
assert_eq!(snapshot.next_hop(), Some(ip("192.0.2.1")));
|
|
assert_eq!(snapshot.route_prefix(), ip("0.0.0.0"));
|
|
assert_eq!(snapshot.route_prefix_len(), 0);
|
|
assert_eq!(snapshot.interface_index(), 12);
|
|
assert_eq!(snapshot.interface_luid(), 34);
|
|
assert_eq!(
|
|
snapshot.interface_identity(),
|
|
NetworkInterfaceIdentity::new(12, 34)
|
|
);
|
|
assert_eq!(snapshot.metric(), 25);
|
|
assert!(!snapshot.is_host_route_to(ip("203.0.113.10")));
|
|
}
|
|
|
|
#[test]
|
|
fn identifies_host_routes_to_destination() {
|
|
let snapshot = RouteSnapshot::new(
|
|
ip("203.0.113.10"),
|
|
ip("192.0.2.44"),
|
|
Some(ip("192.0.2.1")),
|
|
ip("203.0.113.10"),
|
|
32,
|
|
12,
|
|
34,
|
|
0,
|
|
);
|
|
assert!(snapshot.is_host_route_to(ip("203.0.113.10")));
|
|
assert!(!snapshot.is_host_route_to(ip("203.0.113.11")));
|
|
|
|
let snapshot = RouteSnapshot::new(
|
|
ip("2001:db8::10"),
|
|
ip("2001:db8::44"),
|
|
None,
|
|
ip("2001:db8::10"),
|
|
128,
|
|
12,
|
|
34,
|
|
0,
|
|
);
|
|
assert!(snapshot.is_host_route_to(ip("2001:db8::10")));
|
|
}
|
|
|
|
#[test]
|
|
fn exposes_network_interface_identity_fields() {
|
|
let identity = NetworkInterfaceIdentity::new(12, 34);
|
|
|
|
assert_eq!(identity.index(), 12);
|
|
assert_eq!(identity.luid(), 34);
|
|
}
|
|
|
|
#[test]
|
|
fn exposes_interface_metric_snapshot_fields() {
|
|
let identity = NetworkInterfaceIdentity::new(12, 34);
|
|
let snapshot =
|
|
InterfaceMetricSnapshot::new(identity, IpInterfaceFamily::Ipv4, true, 25, false);
|
|
|
|
assert_eq!(snapshot.identity(), identity);
|
|
assert_eq!(snapshot.family(), IpInterfaceFamily::Ipv4);
|
|
assert!(snapshot.automatic_metric());
|
|
assert_eq!(snapshot.metric(), 25);
|
|
assert!(!snapshot.disable_default_routes());
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
#[test]
|
|
fn rejects_route_inspection_on_non_windows() {
|
|
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());
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
#[test]
|
|
fn rejects_interface_lookup_on_non_windows() {
|
|
assert!(interface_identity_from_guid("{00112233-4455-6677-8899-AABBCCDDEEFF}").is_err());
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
#[test]
|
|
fn rejects_interface_metric_operations_on_non_windows() {
|
|
let identity = NetworkInterfaceIdentity::new(12, 34);
|
|
|
|
assert!(interface_metric(identity, IpInterfaceFamily::Ipv4).is_err());
|
|
assert!(set_scoped_interface_metric(identity, IpInterfaceFamily::Ipv4, 500).is_err());
|
|
assert!(
|
|
set_scoped_default_routes_disabled(identity, IpInterfaceFamily::Ipv4, true).is_err()
|
|
);
|
|
}
|
|
|
|
fn ip(value: &str) -> IpAddr {
|
|
value.parse().unwrap()
|
|
}
|
|
}
|