diff --git a/README.md b/README.md index dba34f1..b1bb208 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,8 @@ reinstalled before it reloads the configured `NetworkAddress`. If exactly one TAP-Windows6 adapter is installed, the client opens it automatically. If multiple TAP-Windows6 adapters are installed, startup fails until `--tap-instance-id` selects the intended adapter by NetCfgInstanceId / -InterfaceGuid. +InterfaceGuid. `--list-tap-adapters` prints the TAP adapter ids and exits +without connecting. It prints and reports client diagnostics snapshots with relay reachability, LAN-gateway presence, route-pinning, QUIC datagram budget, relay RTT, TAP status/IP, broadcast frame flow, frame/datagram counters, and drops. The diff --git a/TESTING.md b/TESTING.md index 897c57e..e145adc 100644 --- a/TESTING.md +++ b/TESTING.md @@ -104,8 +104,7 @@ If the Windows machine has multiple TAP-Windows6 adapters, select the intended one explicitly: ```powershell -Get-NetAdapter | Where-Object InterfaceDescription -Like "*TAP*" | - Select-Object Name, InterfaceGuid, InterfaceDescription +.\target\release\lanparty-client-win.exe --list-tap-adapters .\target\release\lanparty-client-win.exe ` --relay relay.example.net:8443 ` diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 0d7fa44..c9bcdcb 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -20,7 +20,6 @@ use lanparty_client_route::{ }; #[cfg(windows)] use lanparty_client_tap::TapAdapter; -#[cfg(any(windows, test))] use lanparty_client_tap::TapAdapterInfo; use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode}; use lanparty_net::RelayEndpoint; @@ -42,21 +41,33 @@ const CLIENT_DIAGNOSTICS_INTERVAL: Duration = Duration::from_secs(10); about = "Windows TAP client for the LAN party L2 tunnel" )] struct ClientArgs { + /// List TAP-Windows6 adapter ids and exit without connecting. + #[arg(long)] + list_tap_adapters: bool, + /// Relay DNS name or UDP socket address; bare hosts default to UDP/443. - #[arg(long, value_name = "HOST[:PORT]")] - relay: RelayEndpoint, + #[arg( + long, + value_name = "HOST[:PORT]", + required_unless_present = "list_tap_adapters" + )] + relay: Option, /// TLS server name expected in the relay certificate. #[arg(long, default_value = "lanparty-relay.local")] server_name: String, /// DER-encoded relay CA/certificate to trust. - #[arg(long, value_name = "PATH")] - relay_ca_cert: PathBuf, + #[arg( + long, + value_name = "PATH", + required_unless_present = "list_tap_adapters" + )] + relay_ca_cert: Option, /// Room code to join as a remote client. - #[arg(long)] - room: RoomCode, + #[arg(long, required_unless_present = "list_tap_adapters")] + room: Option, /// Identity JSON file used to persist the generated virtual MAC. #[arg( @@ -87,10 +98,17 @@ struct ClientRuntimeConfig { impl ClientArgs { fn into_config(self) -> Result { - let relay_ca_cert = fs::read(&self.relay_ca_cert).with_context(|| { + if self.list_tap_adapters { + bail!("--list-tap-adapters exits before building a tunnel config"); + } + + let relay = self.relay.context("--relay is required")?; + let relay_ca_cert_path = self.relay_ca_cert.context("--relay-ca-cert is required")?; + let room = self.room.context("--room is required")?; + let relay_ca_cert = fs::read(&relay_ca_cert_path).with_context(|| { format!( "failed to read relay CA certificate {}", - self.relay_ca_cert.display() + relay_ca_cert_path.display() ) })?; @@ -99,16 +117,15 @@ impl ClientArgs { None => ClientIdentityStore::new(self.identity_file)?.load_or_create()?, }; - let relay_addr = self - .relay + let relay_addr = relay .resolve() - .with_context(|| format!("failed to resolve relay endpoint {}", self.relay))?; + .context("failed to resolve relay endpoint")?; let session = ClientSessionConfig::new( relay_addr, self.server_name, relay_ca_cert, - self.room, + room, identity.virtual_mac(), self.max_datagram_size, )?; @@ -122,7 +139,13 @@ impl ClientArgs { #[tokio::main] async fn main() -> Result<()> { - let config = ClientArgs::parse().into_config()?; + let args = ClientArgs::parse(); + if args.list_tap_adapters { + print_available_tap_adapters()?; + return Ok(()); + } + + let config = args.into_config()?; println!( "lanparty-client-win connecting virtual MAC {} to relay {} room {}", config.session.virtual_mac(), @@ -165,6 +188,15 @@ async fn main() -> Result<()> { run_result } +fn print_available_tap_adapters() -> Result<()> { + let adapters = lanparty_client_tap::available_adapters()?; + for line in format_tap_adapter_list(&adapters) { + println!("{line}"); + } + + Ok(()) +} + #[cfg(windows)] async fn run_client( session: &ClientSession, @@ -488,6 +520,29 @@ fn available_tap_adapter_label(adapters: &[TapAdapterInfo]) -> String { .join(", ") } +fn format_tap_adapter_list(adapters: &[TapAdapterInfo]) -> Vec { + if adapters.is_empty() { + return vec!["No TAP-Windows6 adapters found".to_owned()]; + } + + let mut lines = Vec::with_capacity(adapters.len() + 1); + lines.push("TAP-Windows6 adapters:".to_owned()); + lines.extend(adapters.iter().map(|adapter| { + let driver_key = adapter + .driver_key_name() + .map(|driver_key| format!(" driver-key={driver_key}")) + .unwrap_or_default(); + format!( + " {} component={}{}", + adapter.instance_id(), + adapter.component_id(), + driver_key + ) + })); + + lines +} + fn client_diagnostics_snapshot( session: &ClientSession, route_pinned: bool, @@ -1059,8 +1114,19 @@ mod tests { "ROOM1", ]); - assert_eq!(args.relay.host(), "relay.example.test"); - assert_eq!(args.relay.port(), DEFAULT_RELAY_PORT); + let relay = args.relay.unwrap(); + assert_eq!(relay.host(), "relay.example.test"); + assert_eq!(relay.port(), DEFAULT_RELAY_PORT); + } + + #[test] + fn accepts_tap_adapter_listing_without_tunnel_args() { + let args = ClientArgs::parse_from(["lanparty-client-win", "--list-tap-adapters"]); + + assert!(args.list_tap_adapters); + assert!(args.relay.is_none()); + assert!(args.relay_ca_cert.is_none()); + assert!(args.room.is_none()); } #[test] @@ -1129,6 +1195,21 @@ mod tests { assert!(error.contains("tap-two")); } + #[test] + fn formats_tap_adapter_listing() { + assert_eq!( + format_tap_adapter_list(&[]), + vec!["No TAP-Windows6 adapters found".to_owned()] + ); + assert_eq!( + format_tap_adapter_list(&[tap_info("tap-one")]), + vec![ + "TAP-Windows6 adapters:".to_owned(), + " tap-one component=tap0901".to_owned(), + ] + ); + } + #[test] fn formats_relay_lifecycle_events() { let gateway = ControlMessage::PeerJoined(PeerInfo::new(1, Role::Gateway, None).unwrap());