From 2c490b2693512fd45369ce8a2d15b7d83c1625ac Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 05:47:27 +0200 Subject: [PATCH] fix(client): clear TAP before resolving relay The previous startup ordering loaded the virtual MAC before touching TAP, but it also resolved the relay endpoint before clearing stale TAP media state. That left a post-crash TAP adapter able to influence DNS or route selection before the client had pinned the relay path. Split Windows client startup config into a local phase and a resolved runtime phase. The local phase reads the certificate, room, TAP adapter selection, and client identity without performing DNS. Windows startup now writes the TAP NetworkAddress value and marks TAP media disconnected before resolving the relay endpoint or opening the QUIC connection. A regression test uses an intentionally unresolved relay hostname to prove that building the startup config does not resolve DNS. The client still resolves the relay before activation and still validates the driver-reported TAP MAC before bridging. Test Plan: - cargo test -p lanparty-client-win - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo fmt --check - git diff --check - cargo check -p lanparty-client-win --target x86_64-pc-windows-gnu - blocked by missing x86_64-w64-mingw32-gcc for ring on this host Refs: PLAN.md Windows routing / metric handling --- README.md | 21 +++--- crates/lanparty-client-win/src/main.rs | 89 +++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 95f4d7a..8c92f95 100644 --- a/README.md +++ b/README.md @@ -217,16 +217,17 @@ cargo run -p lanparty-client-win -- \ 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`. It resolves the relay DNS name before TAP -activation, writes the generated tunnel MAC to the selected TAP driver's -`NetworkAddress` registry setting, and marks TAP media disconnected before -connecting to the relay. That clears stale connected state from a previous -crashed run without letting the TAP adapter influence relay routing. The client -then completes the control-stream hello/welcome handshake, pins a host route -for the resolved relay IP on the current pre-TAP interface, verifies that the -relay route still uses that pinned host route after TAP activation, and bridges -Ethernet frames between the relay and the TAP-Windows6 adapter until shutdown. -`--relay` accepts a DNS name or socket address; bare hosts default to UDP/443. +`lanparty-client-identity.json`. Before resolving or connecting to the relay, +it writes the generated tunnel MAC to the selected TAP driver's +`NetworkAddress` registry setting and marks TAP media disconnected. That clears +stale connected state from a previous crashed run without letting the TAP +adapter influence relay DNS or route selection. The client then resolves the +relay endpoint, completes the control-stream hello/welcome handshake, pins a +host route for the resolved relay IP on the current pre-TAP interface, verifies +that the relay route still uses that pinned host route after TAP activation, +and bridges Ethernet frames between the relay and the TAP-Windows6 adapter +until shutdown. `--relay` accepts a DNS name or socket address; bare hosts +default to UDP/443. TAP frames whose source MAC does not match that generated tunnel MAC are dropped locally before they can consume relay bandwidth; the relay still enforces the same source-MAC rule. diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 8ba158d..b9dae9b 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -90,6 +90,17 @@ struct ClientArgs { max_datagram_size: u16, } +#[derive(Debug)] +struct ClientStartupConfig { + relay: RelayEndpoint, + server_name: String, + relay_ca_cert: Vec, + room: RoomCode, + identity: ClientIdentity, + tap_instance_id: Option, + max_datagram_size: u16, +} + #[derive(Debug)] struct ClientRuntimeConfig { session: ClientSessionConfig, @@ -97,7 +108,7 @@ struct ClientRuntimeConfig { } impl ClientArgs { - fn into_config(self) -> Result { + fn into_startup_config(self) -> Result { if self.list_tap_adapters { bail!("--list-tap-adapters exits before building a tunnel config"); } @@ -117,16 +128,31 @@ impl ClientArgs { None => ClientIdentityStore::new(self.identity_file)?.load_or_create()?, }; - let relay_addr = relay + Ok(ClientStartupConfig { + relay, + server_name: self.server_name, + relay_ca_cert, + room, + identity, + tap_instance_id: self.tap_instance_id, + max_datagram_size: self.max_datagram_size, + }) + } +} + +impl ClientStartupConfig { + fn into_runtime_config(self) -> Result { + let relay_addr = self + .relay .resolve() .context("failed to resolve relay endpoint")?; let session = ClientSessionConfig::new( relay_addr, self.server_name, - relay_ca_cert, - room, - identity.virtual_mac(), + self.relay_ca_cert, + self.room, + self.identity.virtual_mac(), self.max_datagram_size, )?; @@ -145,12 +171,13 @@ async fn main() -> Result<()> { return Ok(()); } - let config = args.into_config()?; + let startup_config = args.into_startup_config()?; #[cfg(windows)] prepare_tap_before_relay_connect( - config.tap_instance_id.as_deref(), - config.session.virtual_mac(), + startup_config.tap_instance_id.as_deref(), + startup_config.identity.virtual_mac(), )?; + let config = startup_config.into_runtime_config()?; println!( "lanparty-client-win connecting virtual MAC {} to relay {} room {}", config.session.virtual_mac(), @@ -1053,6 +1080,11 @@ fn open_tap_adapter(_session: &ClientSession) { #[cfg(test)] mod tests { + use std::{ + fs, + time::{SystemTime, UNIX_EPOCH}, + }; + use super::*; use lanparty_ctrl::{DisconnectReason, PeerInfo}; use lanparty_net::DEFAULT_RELAY_PORT; @@ -1181,6 +1213,38 @@ mod tests { assert_eq!(relay.port(), DEFAULT_RELAY_PORT); } + #[test] + fn builds_startup_config_without_resolving_relay() { + let cert_path = unique_temp_file("lanparty-client-cert"); + fs::write(&cert_path, [1_u8, 2, 3]).unwrap(); + let cert_path_string = cert_path.display().to_string(); + let args = ClientArgs::parse_from([ + "lanparty-client-win", + "--relay", + "unresolved.invalid", + "--relay-ca-cert", + &cert_path_string, + "--room", + "ROOM1", + "--virtual-mac", + "02:00:00:00:00:09", + "--tap-instance-id", + "{01234567-89AB-CDEF-0123-456789ABCDEF}", + ]); + + let startup = args.into_startup_config().unwrap(); + fs::remove_file(cert_path).unwrap(); + + assert_eq!(startup.relay.host(), "unresolved.invalid"); + assert_eq!(startup.relay_ca_cert, vec![1, 2, 3]); + assert_eq!(startup.room.as_str(), "ROOM1"); + assert_eq!(startup.identity.virtual_mac(), mac(9)); + assert_eq!( + startup.tap_instance_id.as_deref(), + Some("{01234567-89AB-CDEF-0123-456789ABCDEF}") + ); + } + #[test] fn accepts_tap_adapter_listing_without_tunnel_args() { let args = ClientArgs::parse_from(["lanparty-client-win", "--list-tap-adapters"]); @@ -1323,4 +1387,13 @@ mod tests { fn tap_info(instance_id: &str) -> TapAdapterInfo { TapAdapterInfo::new(instance_id, "tap0901").unwrap() } + + fn unique_temp_file(prefix: &str) -> std::path::PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + + std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id())) + } }