diff --git a/README.md b/README.md index 9fc485a..c6926bb 100644 --- a/README.md +++ b/README.md @@ -135,12 +135,14 @@ The Windows client binary currently connects to the relay as `role = client` with a generated locally administered virtual MAC persisted in `lanparty-client-identity.json`, completes the control-stream hello/welcome handshake, pins a host route for the relay IP on the current pre-TAP interface, -and then bridges Ethernet frames between the relay and the first TAP-Windows6 -adapter until shutdown. +verifies that the relay route still uses that pinned host route after TAP +activation, and then bridges Ethernet frames between the relay and the first +TAP-Windows6 adapter until shutdown. `--virtual-mac` can still override the stored identity for manual testing. On Windows it marks the TAP media connected and reports the driver MAC/MTU before forwarding frames, along with the TAP interface index/LUID. The client applies a scoped TAP interface metric and disables TAP default routes while it runs, -then restores the previous route policy on exit. Until automatic TAP MAC/MTU -configuration is wired, startup fails before bridging if the driver-reported -MAC or MTU does not match the tunnel settings. +periodically rechecks that the relay route remains pinned, then restores the +previous route policy on exit. Until automatic TAP MAC/MTU configuration is +wired, startup fails before bridging if the driver-reported MAC or MTU does not +match the tunnel settings. diff --git a/crates/lanparty-client-route/src/lib.rs b/crates/lanparty-client-route/src/lib.rs index cefdb7a..c28d914 100644 --- a/crates/lanparty-client-route/src/lib.rs +++ b/crates/lanparty-client-route/src/lib.rs @@ -163,10 +163,27 @@ impl RouteSnapshot { 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)] @@ -263,7 +280,40 @@ mod tests { 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] diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 97214fa..533d4cc 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -3,6 +3,7 @@ use std::{fs, net::SocketAddr, path::PathBuf}; use std::{ sync::{Arc, mpsc}, thread, + time::Duration, }; use anyhow::{Context, Result, bail}; @@ -24,6 +25,8 @@ use lanparty_proto::MacAddr; #[cfg(windows)] const TAP_INTERFACE_METRIC: u32 = 9_000; +#[cfg(windows)] +const RELAY_ROUTE_VERIFY_INTERVAL: Duration = Duration::from_secs(5); #[derive(Debug, Parser)] #[command( @@ -114,6 +117,9 @@ async fn main() -> Result<()> { return Err(error); } }; + #[cfg(windows)] + let run_result = run_client(&session, &relay_route_pin).await; + #[cfg(not(windows))] let run_result = run_client(&session).await; session.shutdown("client shutting down").await; #[cfg(windows)] @@ -123,15 +129,38 @@ async fn main() -> Result<()> { } #[cfg(windows)] -async fn run_client(session: &ClientSession) -> Result<()> { +async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) -> Result<()> { let OpenedTapAdapter { tap, _route_guard } = open_tap_adapter(session)?; + let relay_route = + verify_relay_route_is_pinned(session.config().relay_addr().ip(), relay_route_pin) + .context("relay route changed after TAP activation")?; + print_verified_relay_route(&relay_route); println!( "bridging TAP frames; relay route is pinned and TAP route policy is scoped; press Ctrl-C to stop" ); - tokio::select! { - result = run_tap_frame_pump(session.relay_io(), tap) => result, - result = tokio::signal::ctrl_c() => result.context("failed to wait for Ctrl-C"), + let frame_pump = run_tap_frame_pump(session.relay_io(), tap); + tokio::pin!(frame_pump); + let shutdown = tokio::signal::ctrl_c(); + tokio::pin!(shutdown); + let mut relay_route_check = tokio::time::interval_at( + tokio::time::Instant::now() + RELAY_ROUTE_VERIFY_INTERVAL, + RELAY_ROUTE_VERIFY_INTERVAL, + ); + relay_route_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + + loop { + tokio::select! { + result = &mut frame_pump => return result, + result = &mut shutdown => return result.context("failed to wait for Ctrl-C"), + _ = relay_route_check.tick() => { + verify_relay_route_is_pinned( + session.config().relay_addr().ip(), + relay_route_pin, + ) + .context("relay route changed while bridging")?; + } + } } } @@ -187,6 +216,59 @@ fn print_pinned_relay_route(route: &PinnedRelayRoute) { ); } +#[cfg(windows)] +fn verify_relay_route_is_pinned( + destination: std::net::IpAddr, + pin: &PinnedRelayRoute, +) -> Result { + let route = lanparty_client_route::best_route_to(destination) + .context("failed to inspect relay route")?; + if !relay_route_matches_pin(&route, pin) { + bail!( + "relay route to {} uses prefix {}/{} next hop {} interface index {} LUID {}, expected pinned host route via next hop {} interface index {} LUID {}", + route.destination(), + route.route_prefix(), + route.route_prefix_len(), + route_next_hop_label(route.next_hop()), + route.interface_index(), + route.interface_luid(), + route_next_hop_label(pin.next_hop()), + pin.interface_index(), + pin.interface_luid(), + ); + } + + Ok(route) +} + +#[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() +} + +#[cfg(windows)] +fn print_verified_relay_route(route: &RouteSnapshot) { + println!( + "relay route verified after TAP activation: destination {} next hop {} interface index {} LUID {} prefix {}/{} metric {}", + route.destination(), + route_next_hop_label(route.next_hop()), + route.interface_index(), + route.interface_luid(), + route.route_prefix(), + route.route_prefix_len(), + route.metric() + ); +} + +#[cfg(windows)] +fn route_next_hop_label(next_hop: Option) -> String { + next_hop.map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string()) +} + #[cfg(windows)] struct OpenedTapAdapter { tap: TapAdapter,