diff --git a/README.md b/README.md index 5c3ebf6..48d2deb 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,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 +- reuse of an already-existing matching relay host route without deleting it on exit - non-Windows builds return a clear unsupported-platform error ### `lanparty-client-tap` @@ -214,8 +215,9 @@ bridges Ethernet frames between the relay and the first TAP-Windows6 adapter until shutdown. `--relay` accepts a DNS name or socket address; bare hosts default to UDP/443. Before opening the adapter, it writes the generated tunnel MAC to the TAP driver's `NetworkAddress` registry setting. -The startup status reports whether the relay already has a LAN gateway for the -room. +If the exact relay host route already exists, the client uses it and leaves it +alone on exit. The startup status reports whether the relay already has a LAN +gateway for the room. `--virtual-mac` can still override the stored identity for manual testing. On Windows it sets the TAP IP interface MTU to the relay-selected MTU, marks the TAP media connected for the scoped client run, and reports the driver MAC/MTU diff --git a/TESTING.md b/TESTING.md index d2982f2..b4bc4bc 100644 --- a/TESTING.md +++ b/TESTING.md @@ -125,6 +125,10 @@ TAP driver reports MAC ... and MTU ... client diagnostics: relay reachable yes gateway connected yes route pinned yes ... ``` +The route pin line ends with `(created)` or `(already existed)`. Either is OK. +`already existed` usually means a matching relay host route was already present, +for example after a previous crashed test run. + The first diagnostics line may show `IP unknown`. After DHCP succeeds, a later line should show: @@ -231,8 +235,9 @@ Uncommon LAN subnets such as `10.73.42.0/24` are safer than `192.168.0.0/24`. ## Cleanup -Stop client, gateway, and relay with Ctrl-C. The Windows client restores the TAP -route policy and marks TAP media disconnected when it exits normally. +Stop client, gateway, and relay with Ctrl-C. The Windows client removes the +relay host route only when it created that route itself, restores the TAP route +policy, and marks TAP media disconnected when it exits normally. Keep `lanparty-client-identity.json` if you want the same virtual MAC on the next run. Delete it only when you intentionally want a new client identity. diff --git a/crates/lanparty-client-route/src/windows.rs b/crates/lanparty-client-route/src/windows.rs index ae26b53..67a72f1 100644 --- a/crates/lanparty-client-route/src/windows.rs +++ b/crates/lanparty-client-route/src/windows.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::{Context, Result, bail}; use windows_sys::Win32::{ - Foundation::ERROR_SUCCESS, + Foundation::{ERROR_OBJECT_ALREADY_EXISTS, ERROR_SUCCESS}, NetworkManagement::{ IpHelper::{ ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2, @@ -439,25 +439,39 @@ pub fn pin_relay_route(route: &RouteSnapshot) -> Result { // valid host destination, next hop, and interface identity. CreateIpForwardEntry2(&pinned.row) }; - windows_status(status).with_context(|| { + let disposition = route_create_disposition(status).with_context(|| { format!( "failed to pin relay route to {} via interface index {}", route.destination(), route.interface_index() ) })?; - pinned.active = true; + pinned.delete_on_drop = disposition == RouteCreateDisposition::Created; Ok(pinned) } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RouteCreateDisposition { + Created, + AlreadyExists, +} + +fn route_create_disposition(status: u32) -> io::Result { + match status { + ERROR_SUCCESS => Ok(RouteCreateDisposition::Created), + ERROR_OBJECT_ALREADY_EXISTS => Ok(RouteCreateDisposition::AlreadyExists), + _ => Err(io::Error::from_raw_os_error(status as i32)), + } +} + pub struct PinnedRelayRoute { row: MIB_IPFORWARD_ROW2, destination: IpAddr, next_hop: Option, interface_index: u32, interface_luid: u64, - active: bool, + delete_on_drop: bool, } impl PinnedRelayRoute { @@ -480,6 +494,11 @@ impl PinnedRelayRoute { pub const fn interface_luid(&self) -> u64 { self.interface_luid } + + #[must_use] + pub const fn created_by_client(&self) -> bool { + self.delete_on_drop + } } impl fmt::Debug for PinnedRelayRoute { @@ -489,14 +508,14 @@ impl fmt::Debug for PinnedRelayRoute { .field("next_hop", &self.next_hop) .field("interface_index", &self.interface_index) .field("interface_luid", &self.interface_luid) - .field("active", &self.active) + .field("delete_on_drop", &self.delete_on_drop) .finish_non_exhaustive() } } impl Drop for PinnedRelayRoute { fn drop(&mut self) { - if !self.active { + if !self.delete_on_drop { return; } @@ -532,7 +551,7 @@ fn pinned_route_row(route: &RouteSnapshot) -> PinnedRelayRoute { next_hop, interface_index: route.interface_index(), interface_luid: route.interface_luid(), - active: false, + delete_on_drop: false, } } @@ -685,6 +704,22 @@ mod tests { assert_eq!(pinned.row.SitePrefixLength, 32); assert_eq!(pinned.row.Metric, 0); assert_eq!(pinned.row.Protocol, RouteProtocolNetMgmt); + assert!(!pinned.created_by_client()); + } + + #[test] + fn classifies_route_create_statuses() { + use windows_sys::Win32::Foundation::ERROR_INVALID_PARAMETER; + + assert_eq!( + route_create_disposition(ERROR_SUCCESS).unwrap(), + RouteCreateDisposition::Created + ); + assert_eq!( + route_create_disposition(ERROR_OBJECT_ALREADY_EXISTS).unwrap(), + RouteCreateDisposition::AlreadyExists + ); + assert!(route_create_disposition(ERROR_INVALID_PARAMETER).is_err()); } #[test] diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 0fcf4c7..a89ea13 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -332,13 +332,18 @@ fn print_relay_route(route: &RouteSnapshot) { #[cfg(windows)] fn print_pinned_relay_route(route: &PinnedRelayRoute) { println!( - "relay route pinned before TAP: destination {} next hop {} interface index {} LUID {}", + "relay route pinned before TAP: destination {} next hop {} interface index {} LUID {} ({})", route.destination(), route .next_hop() .map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string()), route.interface_index(), - route.interface_luid() + route.interface_luid(), + if route.created_by_client() { + "created" + } else { + "already existed" + } ); }