From 81878133d200a380d9b6b6204ebcdee9bdc245d7 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 07:51:44 +0200 Subject: [PATCH] test(route): cover relay host-route pin matching 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 --- README.md | 1 + crates/lanparty-client-route/src/lib.rs | 93 +++++++++++++++++++++++++ crates/lanparty-client-win/src/main.rs | 11 +-- 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2a87768..931a60f 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Windows route-table boundary: - scoped default-route suppression with restore-on-drop behavior - unicast IP address snapshots for TAP diagnostics - scoped host-route pinning for the relay IP on the pre-TAP interface +- host-route pin matching for relay-route verification after TAP activation - reuse of an already-existing matching relay host route without deleting it on exit - non-Windows builds return a clear unsupported-platform error diff --git a/crates/lanparty-client-route/src/lib.rs b/crates/lanparty-client-route/src/lib.rs index 78710cb..11e7b92 100644 --- a/crates/lanparty-client-route/src/lib.rs +++ b/crates/lanparty-client-route/src/lib.rs @@ -255,6 +255,21 @@ impl RouteSnapshot { 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 { @@ -425,6 +440,84 @@ mod tests { 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); diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 9bb8680..23ef312 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -436,11 +436,12 @@ fn verify_relay_route_is_pinned( #[cfg(windows)] fn relay_route_matches_pin(route: &RouteSnapshot, pin: &PinnedRelayRoute) -> bool { - route.destination() == pin.destination() - && route.is_host_route_to(pin.destination()) - && route.next_hop() == pin.next_hop() - && route.interface_index() == pin.interface_index() - && route.interface_luid() == pin.interface_luid() + route.matches_pinned_host_route( + pin.destination(), + pin.next_hop(), + pin.interface_index(), + pin.interface_luid(), + ) } #[cfg(windows)]