81878133d2
The Windows client protects the relay connection by pinning a host route before activating TAP, then checking that the best route still matches that pinned host route after TAP route policy changes. That predicate is part of the route boundary, not the Windows binary's frame-pump logic. Move the exact match check onto RouteSnapshot and cover the important mismatch cases: default-route fallback, wrong next hop, wrong interface index/LUID, and IPv6 on-link host routes. The Windows client keeps the same behavior but calls the route-crate helper. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-route matches_pinned_host_route_identity - cargo test -p lanparty-client-route matches_ipv6_on_link_pinned_host_route - cargo test -p lanparty-client-route - cargo test -p lanparty-client-win - cargo test --workspace - cargo clippy -p lanparty-client-route --all-targets -- -D warnings - cargo clippy -p lanparty-client-win --all-targets -- -D warnings - cargo clippy --workspace --all-targets -- -D warnings - cargo check -p lanparty-client-route --target x86_64-pc-windows-msvc - git diff --check - git diff --cached --check Refs: MVP relay-route protection
612 lines
16 KiB
Rust
612 lines
16 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,
|
|
}
|
|
|
|
#[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<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)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn matches_pinned_host_route(
|
|
&self,
|
|
destination: IpAddr,
|
|
next_hop: Option<IpAddr>,
|
|
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<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(not(windows))]
|
|
pub fn interface_mtu(
|
|
_identity: NetworkInterfaceIdentity,
|
|
_family: IpInterfaceFamily,
|
|
) -> Result<InterfaceMtuSnapshot> {
|
|
bail!("Windows interface MTU lookup is only available on Windows");
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn interface_unicast_addresses(
|
|
_identity: NetworkInterfaceIdentity,
|
|
) -> Result<Vec<InterfaceUnicastAddress>> {
|
|
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<ScopedInterfaceMtu> {
|
|
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()
|
|
}
|
|
}
|