fix(client): reuse existing relay host route
A crashed or forcibly killed Windows client can leave the scoped relay host route behind. The next run should still be allowed to start when Windows says that the exact route row already exists, because that route already protects the relay path from TAP default-route takeover. Handle ERROR_OBJECT_ALREADY_EXISTS from CreateIpForwardEntry2 as a successful borrowed pin. Routes created by the current client are still deleted on Drop; pre-existing routes are left alone so we do not remove administrator-managed or stale routes that this process did not create. The client startup log now marks whether the route was created or already existed, and the README and MVP test guide explain the behavior. Test Plan: All cargo commands used these environment variables: RUSTUP_HOME=/tmp/softlan-vpn-rustup CARGO_HOME=/tmp/softlan-vpn-cargo - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo check -p lanparty-client-route --tests --target x86_64-pc-windows-gnu - cargo check -p lanparty-client-route --tests --target x86_64-pc-windows-msvc - cargo clippy -p lanparty-client-route --tests --target x86_64-pc-windows-gnu -- -D warnings - cargo clippy -p lanparty-client-route --tests --target x86_64-pc-windows-msvc -- -D warnings - git diff --check Known limitation: full lanparty-client-win Windows cross-check is still blocked on this Linux host by the external ring toolchain setup. The GNU target lacks x86_64-w64-mingw32-gcc, and the MSVC target lacks lib.exe/MSVC environment. Refs: PLAN.md route-protection requirement
This commit is contained in:
@@ -79,6 +79,7 @@ Windows route-table boundary:
|
|||||||
- scoped default-route suppression with restore-on-drop behavior
|
- scoped default-route suppression with restore-on-drop behavior
|
||||||
- unicast IP address snapshots for TAP diagnostics
|
- unicast IP address snapshots for TAP diagnostics
|
||||||
- scoped host-route pinning for the relay IP on the pre-TAP interface
|
- 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
|
- non-Windows builds return a clear unsupported-platform error
|
||||||
|
|
||||||
### `lanparty-client-tap`
|
### `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
|
until shutdown. `--relay` accepts a DNS name or socket address; bare hosts
|
||||||
default to UDP/443. Before opening the adapter, it writes the
|
default to UDP/443. Before opening the adapter, it writes the
|
||||||
generated tunnel MAC to the TAP driver's `NetworkAddress` registry setting.
|
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
|
If the exact relay host route already exists, the client uses it and leaves it
|
||||||
room.
|
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
|
`--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
|
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
|
TAP media connected for the scoped client run, and reports the driver MAC/MTU
|
||||||
|
|||||||
+7
-2
@@ -125,6 +125,10 @@ TAP driver reports MAC ... and MTU ...
|
|||||||
client diagnostics: relay reachable yes gateway connected yes route pinned yes ...
|
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
|
The first diagnostics line may show `IP unknown`. After DHCP succeeds, a later
|
||||||
line should show:
|
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
|
## Cleanup
|
||||||
|
|
||||||
Stop client, gateway, and relay with Ctrl-C. The Windows client restores the TAP
|
Stop client, gateway, and relay with Ctrl-C. The Windows client removes the
|
||||||
route policy and marks TAP media disconnected when it exits normally.
|
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
|
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.
|
next run. Delete it only when you intentionally want a new client identity.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use windows_sys::Win32::{
|
use windows_sys::Win32::{
|
||||||
Foundation::ERROR_SUCCESS,
|
Foundation::{ERROR_OBJECT_ALREADY_EXISTS, ERROR_SUCCESS},
|
||||||
NetworkManagement::{
|
NetworkManagement::{
|
||||||
IpHelper::{
|
IpHelper::{
|
||||||
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
|
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
|
||||||
@@ -439,25 +439,39 @@ pub fn pin_relay_route(route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
|
|||||||
// valid host destination, next hop, and interface identity.
|
// valid host destination, next hop, and interface identity.
|
||||||
CreateIpForwardEntry2(&pinned.row)
|
CreateIpForwardEntry2(&pinned.row)
|
||||||
};
|
};
|
||||||
windows_status(status).with_context(|| {
|
let disposition = route_create_disposition(status).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to pin relay route to {} via interface index {}",
|
"failed to pin relay route to {} via interface index {}",
|
||||||
route.destination(),
|
route.destination(),
|
||||||
route.interface_index()
|
route.interface_index()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
pinned.active = true;
|
pinned.delete_on_drop = disposition == RouteCreateDisposition::Created;
|
||||||
|
|
||||||
Ok(pinned)
|
Ok(pinned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum RouteCreateDisposition {
|
||||||
|
Created,
|
||||||
|
AlreadyExists,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn route_create_disposition(status: u32) -> io::Result<RouteCreateDisposition> {
|
||||||
|
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 {
|
pub struct PinnedRelayRoute {
|
||||||
row: MIB_IPFORWARD_ROW2,
|
row: MIB_IPFORWARD_ROW2,
|
||||||
destination: IpAddr,
|
destination: IpAddr,
|
||||||
next_hop: Option<IpAddr>,
|
next_hop: Option<IpAddr>,
|
||||||
interface_index: u32,
|
interface_index: u32,
|
||||||
interface_luid: u64,
|
interface_luid: u64,
|
||||||
active: bool,
|
delete_on_drop: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PinnedRelayRoute {
|
impl PinnedRelayRoute {
|
||||||
@@ -480,6 +494,11 @@ impl PinnedRelayRoute {
|
|||||||
pub const fn interface_luid(&self) -> u64 {
|
pub const fn interface_luid(&self) -> u64 {
|
||||||
self.interface_luid
|
self.interface_luid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn created_by_client(&self) -> bool {
|
||||||
|
self.delete_on_drop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for PinnedRelayRoute {
|
impl fmt::Debug for PinnedRelayRoute {
|
||||||
@@ -489,14 +508,14 @@ impl fmt::Debug for PinnedRelayRoute {
|
|||||||
.field("next_hop", &self.next_hop)
|
.field("next_hop", &self.next_hop)
|
||||||
.field("interface_index", &self.interface_index)
|
.field("interface_index", &self.interface_index)
|
||||||
.field("interface_luid", &self.interface_luid)
|
.field("interface_luid", &self.interface_luid)
|
||||||
.field("active", &self.active)
|
.field("delete_on_drop", &self.delete_on_drop)
|
||||||
.finish_non_exhaustive()
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for PinnedRelayRoute {
|
impl Drop for PinnedRelayRoute {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if !self.active {
|
if !self.delete_on_drop {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +551,7 @@ fn pinned_route_row(route: &RouteSnapshot) -> PinnedRelayRoute {
|
|||||||
next_hop,
|
next_hop,
|
||||||
interface_index: route.interface_index(),
|
interface_index: route.interface_index(),
|
||||||
interface_luid: route.interface_luid(),
|
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.SitePrefixLength, 32);
|
||||||
assert_eq!(pinned.row.Metric, 0);
|
assert_eq!(pinned.row.Metric, 0);
|
||||||
assert_eq!(pinned.row.Protocol, RouteProtocolNetMgmt);
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -332,13 +332,18 @@ fn print_relay_route(route: &RouteSnapshot) {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn print_pinned_relay_route(route: &PinnedRelayRoute) {
|
fn print_pinned_relay_route(route: &PinnedRelayRoute) {
|
||||||
println!(
|
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.destination(),
|
||||||
route
|
route
|
||||||
.next_hop()
|
.next_hop()
|
||||||
.map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string()),
|
.map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string()),
|
||||||
route.interface_index(),
|
route.interface_index(),
|
||||||
route.interface_luid()
|
route.interface_luid(),
|
||||||
|
if route.created_by_client() {
|
||||||
|
"created"
|
||||||
|
} else {
|
||||||
|
"already existed"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user