//! 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, 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, 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 { 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 { 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 { bail!("Windows route pinning is only available on Windows"); } #[cfg(not(windows))] pub fn interface_identity_from_guid(_interface_guid: &str) -> Result { 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 { 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 { 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 { 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() } }