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
This commit is contained in:
2026-05-22 05:47:27 +02:00
parent 563a073d24
commit 2c490b2693
2 changed files with 92 additions and 18 deletions
+81 -8
View File
@@ -90,6 +90,17 @@ struct ClientArgs {
max_datagram_size: u16,
}
#[derive(Debug)]
struct ClientStartupConfig {
relay: RelayEndpoint,
server_name: String,
relay_ca_cert: Vec<u8>,
room: RoomCode,
identity: ClientIdentity,
tap_instance_id: Option<String>,
max_datagram_size: u16,
}
#[derive(Debug)]
struct ClientRuntimeConfig {
session: ClientSessionConfig,
@@ -97,7 +108,7 @@ struct ClientRuntimeConfig {
}
impl ClientArgs {
fn into_config(self) -> Result<ClientRuntimeConfig> {
fn into_startup_config(self) -> Result<ClientStartupConfig> {
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<ClientRuntimeConfig> {
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()))
}
}