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())) + } }