//! 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, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InterfaceMtuSnapshot { identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, mtu: u32, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InterfaceUnicastAddress { identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, address: IpAddr, prefix_len: u8, } 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 InterfaceMtuSnapshot { #[cfg_attr(not(windows), allow(dead_code))] const fn new(identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, mtu: u32) -> Self { Self { identity, family, mtu, } } #[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 mtu(self) -> u32 { self.mtu } } impl InterfaceUnicastAddress { #[cfg_attr(not(windows), allow(dead_code))] const fn new( identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, address: IpAddr, prefix_len: u8, ) -> Self { Self { identity, family, address, prefix_len, } } #[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 address(self) -> IpAddr { self.address } #[must_use] pub const fn prefix_len(self) -> u8 { self.prefix_len } } 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) } #[must_use] pub fn matches_pinned_host_route( &self, destination: IpAddr, next_hop: Option, interface_index: u32, interface_luid: u64, ) -> bool { self.destination == destination && self.is_host_route_to(destination) && self.next_hop == next_hop && self.interface_index == interface_index && self.interface_luid == interface_luid } } 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, ScopedInterfaceMtu, interface_metric, interface_mtu, interface_unicast_addresses, set_scoped_default_routes_disabled, set_scoped_interface_metric, set_scoped_interface_mtu, }; #[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(not(windows))] pub fn interface_mtu( _identity: NetworkInterfaceIdentity, _family: IpInterfaceFamily, ) -> Result { bail!("Windows interface MTU lookup is only available on Windows"); } #[cfg(not(windows))] pub fn interface_unicast_addresses( _identity: NetworkInterfaceIdentity, ) -> Result> { bail!("Windows interface address lookup is only available on Windows"); } #[cfg(not(windows))] #[derive(Debug)] pub struct ScopedInterfaceMtu { _private: (), } #[cfg(not(windows))] pub fn set_scoped_interface_mtu( _identity: NetworkInterfaceIdentity, _family: IpInterfaceFamily, _mtu: u32, ) -> Result { bail!("Windows interface MTU 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 matches_pinned_host_route_identity() { 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.matches_pinned_host_route( ip("203.0.113.10"), Some(ip("192.0.2.1")), 12, 34, )); assert!(!snapshot.matches_pinned_host_route( ip("203.0.113.10"), Some(ip("192.0.2.2")), 12, 34, )); assert!(!snapshot.matches_pinned_host_route( ip("203.0.113.10"), Some(ip("192.0.2.1")), 13, 34, )); assert!(!snapshot.matches_pinned_host_route( ip("203.0.113.10"), Some(ip("192.0.2.1")), 12, 35, )); let default_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, ); assert!(!default_route.matches_pinned_host_route( ip("203.0.113.10"), Some(ip("192.0.2.1")), 12, 34, )); } #[test] fn matches_ipv6_on_link_pinned_host_route() { let snapshot = RouteSnapshot::new( ip("2001:db8::10"), ip("2001:db8::44"), None, ip("2001:db8::10"), 128, 12, 34, 0, ); assert!(snapshot.matches_pinned_host_route(ip("2001:db8::10"), None, 12, 34)); assert!(!snapshot.matches_pinned_host_route( ip("2001:db8::10"), Some(ip("2001:db8::1")), 12, 34, )); } #[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()); } #[test] fn exposes_interface_mtu_snapshot_fields() { let identity = NetworkInterfaceIdentity::new(12, 34); let snapshot = InterfaceMtuSnapshot::new(identity, IpInterfaceFamily::Ipv6, 1200); assert_eq!(snapshot.identity(), identity); assert_eq!(snapshot.family(), IpInterfaceFamily::Ipv6); assert_eq!(snapshot.mtu(), 1200); } #[test] fn exposes_interface_unicast_address_fields() { let identity = NetworkInterfaceIdentity::new(12, 34); let snapshot = InterfaceUnicastAddress::new(identity, IpInterfaceFamily::Ipv4, ip("10.73.42.51"), 24); assert_eq!(snapshot.identity(), identity); assert_eq!(snapshot.family(), IpInterfaceFamily::Ipv4); assert_eq!(snapshot.address(), ip("10.73.42.51")); assert_eq!(snapshot.prefix_len(), 24); } #[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() ); assert!(interface_mtu(identity, IpInterfaceFamily::Ipv4).is_err()); assert!(interface_unicast_addresses(identity).is_err()); assert!(set_scoped_interface_mtu(identity, IpInterfaceFamily::Ipv4, 1200).is_err()); } fn ip(value: &str) -> IpAddr { value.parse().unwrap() } }