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)]