feat(client): monitor relay route after TAP activation
The Windows client now verifies that the relay path still uses the pinned host route after the TAP adapter is activated, and keeps checking that invariant while frames are being bridged. If Windows starts choosing a different prefix, next hop, or interface for the relay IP, the client exits instead of silently letting the tunnel route itself through the TAP side. This closes the remaining detection half of the route-protection startup flow from PLAN.md. The previous commits installed the pre-TAP host route, scoped TAP metrics, and disabled TAP default routes; this commit proves those protections are still winning after TAP activation and catches later DHCP-driven route changes during the session. The route crate now exposes small helpers for route-interface identity and host route matching so the client does not duplicate route-prefix semantics inline. The full Windows client target check still cannot complete on this Linux host: `ring` fails while compiling for `x86_64-pc-windows-msvc` because the Windows C header `assert.h` is unavailable, before `lanparty-client-win` is typechecked. The independent Windows-target route crate checks do pass. Test Plan: - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo check -p lanparty-client-route --target x86_64-pc-windows-msvc - cargo clippy -p lanparty-client-route --target x86_64-pc-windows-msvc --all-targets -- -D warnings - git diff --check Refs: PLAN.md
This commit is contained in:
@@ -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
|
with a generated locally administered virtual MAC persisted in
|
||||||
`lanparty-client-identity.json`, completes the control-stream hello/welcome
|
`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,
|
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
|
verifies that the relay route still uses that pinned host route after TAP
|
||||||
adapter until shutdown.
|
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
|
`--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
|
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
|
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,
|
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
|
periodically rechecks that the relay route remains pinned, then restores the
|
||||||
configuration is wired, startup fails before bridging if the driver-reported
|
previous route policy on exit. Until automatic TAP MAC/MTU configuration is
|
||||||
MAC or MTU does not match the tunnel settings.
|
wired, startup fails before bridging if the driver-reported MAC or MTU does not
|
||||||
|
match the tunnel settings.
|
||||||
|
|||||||
@@ -163,10 +163,27 @@ impl RouteSnapshot {
|
|||||||
self.interface_luid
|
self.interface_luid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn interface_identity(&self) -> NetworkInterfaceIdentity {
|
||||||
|
NetworkInterfaceIdentity::new(self.interface_index, self.interface_luid)
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn metric(&self) -> u32 {
|
pub const fn metric(&self) -> u32 {
|
||||||
self.metric
|
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)]
|
#[cfg(windows)]
|
||||||
@@ -263,7 +280,40 @@ mod tests {
|
|||||||
assert_eq!(snapshot.route_prefix_len(), 0);
|
assert_eq!(snapshot.route_prefix_len(), 0);
|
||||||
assert_eq!(snapshot.interface_index(), 12);
|
assert_eq!(snapshot.interface_index(), 12);
|
||||||
assert_eq!(snapshot.interface_luid(), 34);
|
assert_eq!(snapshot.interface_luid(), 34);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.interface_identity(),
|
||||||
|
NetworkInterfaceIdentity::new(12, 34)
|
||||||
|
);
|
||||||
assert_eq!(snapshot.metric(), 25);
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::{fs, net::SocketAddr, path::PathBuf};
|
|||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, mpsc},
|
sync::{Arc, mpsc},
|
||||||
thread,
|
thread,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
@@ -24,6 +25,8 @@ use lanparty_proto::MacAddr;
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const TAP_INTERFACE_METRIC: u32 = 9_000;
|
const TAP_INTERFACE_METRIC: u32 = 9_000;
|
||||||
|
#[cfg(windows)]
|
||||||
|
const RELAY_ROUTE_VERIFY_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
@@ -114,6 +117,9 @@ async fn main() -> Result<()> {
|
|||||||
return Err(error);
|
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;
|
let run_result = run_client(&session).await;
|
||||||
session.shutdown("client shutting down").await;
|
session.shutdown("client shutting down").await;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -123,15 +129,38 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[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 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!(
|
println!(
|
||||||
"bridging TAP frames; relay route is pinned and TAP route policy is scoped; press Ctrl-C to stop"
|
"bridging TAP frames; relay route is pinned and TAP route policy is scoped; press Ctrl-C to stop"
|
||||||
);
|
);
|
||||||
|
|
||||||
tokio::select! {
|
let frame_pump = run_tap_frame_pump(session.relay_io(), tap);
|
||||||
result = run_tap_frame_pump(session.relay_io(), tap) => result,
|
tokio::pin!(frame_pump);
|
||||||
result = tokio::signal::ctrl_c() => result.context("failed to wait for Ctrl-C"),
|
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<RouteSnapshot> {
|
||||||
|
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<std::net::IpAddr>) -> String {
|
||||||
|
next_hop.map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
struct OpenedTapAdapter {
|
struct OpenedTapAdapter {
|
||||||
tap: TapAdapter,
|
tap: TapAdapter,
|
||||||
|
|||||||
Reference in New Issue
Block a user