feat(client): list TAP adapters before connecting
Add a --list-tap-adapters mode to the Windows client. The command prints the TAP-Windows6 adapter instance ids and exits without requiring relay, room, or certificate arguments. This makes the manual MVP test smoother on machines with multiple TAP adapters: the operator can ask the binary for the exact ids, then rerun with --tap-instance-id instead of relying on a separate PowerShell query or waiting for the ambiguous-adapter startup error. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-win - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md Windows client TAP adapter responsibility
This commit is contained in:
@@ -227,7 +227,8 @@ reinstalled before it reloads the configured `NetworkAddress`.
|
|||||||
If exactly one TAP-Windows6 adapter is installed, the client opens it
|
If exactly one TAP-Windows6 adapter is installed, the client opens it
|
||||||
automatically. If multiple TAP-Windows6 adapters are installed, startup fails
|
automatically. If multiple TAP-Windows6 adapters are installed, startup fails
|
||||||
until `--tap-instance-id` selects the intended adapter by NetCfgInstanceId /
|
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,
|
It prints and reports client diagnostics snapshots with relay reachability,
|
||||||
LAN-gateway presence, route-pinning, QUIC datagram budget, relay RTT, TAP
|
LAN-gateway presence, route-pinning, QUIC datagram budget, relay RTT, TAP
|
||||||
status/IP, broadcast frame flow, frame/datagram counters, and drops. The
|
status/IP, broadcast frame flow, frame/datagram counters, and drops. The
|
||||||
|
|||||||
+1
-2
@@ -104,8 +104,7 @@ If the Windows machine has multiple TAP-Windows6 adapters, select the intended
|
|||||||
one explicitly:
|
one explicitly:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-NetAdapter | Where-Object InterfaceDescription -Like "*TAP*" |
|
.\target\release\lanparty-client-win.exe --list-tap-adapters
|
||||||
Select-Object Name, InterfaceGuid, InterfaceDescription
|
|
||||||
|
|
||||||
.\target\release\lanparty-client-win.exe `
|
.\target\release\lanparty-client-win.exe `
|
||||||
--relay relay.example.net:8443 `
|
--relay relay.example.net:8443 `
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use lanparty_client_route::{
|
|||||||
};
|
};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use lanparty_client_tap::TapAdapter;
|
use lanparty_client_tap::TapAdapter;
|
||||||
#[cfg(any(windows, test))]
|
|
||||||
use lanparty_client_tap::TapAdapterInfo;
|
use lanparty_client_tap::TapAdapterInfo;
|
||||||
use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode};
|
use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode};
|
||||||
use lanparty_net::RelayEndpoint;
|
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"
|
about = "Windows TAP client for the LAN party L2 tunnel"
|
||||||
)]
|
)]
|
||||||
struct ClientArgs {
|
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.
|
/// Relay DNS name or UDP socket address; bare hosts default to UDP/443.
|
||||||
#[arg(long, value_name = "HOST[:PORT]")]
|
#[arg(
|
||||||
relay: RelayEndpoint,
|
long,
|
||||||
|
value_name = "HOST[:PORT]",
|
||||||
|
required_unless_present = "list_tap_adapters"
|
||||||
|
)]
|
||||||
|
relay: Option<RelayEndpoint>,
|
||||||
|
|
||||||
/// TLS server name expected in the relay certificate.
|
/// TLS server name expected in the relay certificate.
|
||||||
#[arg(long, default_value = "lanparty-relay.local")]
|
#[arg(long, default_value = "lanparty-relay.local")]
|
||||||
server_name: String,
|
server_name: String,
|
||||||
|
|
||||||
/// DER-encoded relay CA/certificate to trust.
|
/// DER-encoded relay CA/certificate to trust.
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(
|
||||||
relay_ca_cert: PathBuf,
|
long,
|
||||||
|
value_name = "PATH",
|
||||||
|
required_unless_present = "list_tap_adapters"
|
||||||
|
)]
|
||||||
|
relay_ca_cert: Option<PathBuf>,
|
||||||
|
|
||||||
/// Room code to join as a remote client.
|
/// Room code to join as a remote client.
|
||||||
#[arg(long)]
|
#[arg(long, required_unless_present = "list_tap_adapters")]
|
||||||
room: RoomCode,
|
room: Option<RoomCode>,
|
||||||
|
|
||||||
/// Identity JSON file used to persist the generated virtual MAC.
|
/// Identity JSON file used to persist the generated virtual MAC.
|
||||||
#[arg(
|
#[arg(
|
||||||
@@ -87,10 +98,17 @@ struct ClientRuntimeConfig {
|
|||||||
|
|
||||||
impl ClientArgs {
|
impl ClientArgs {
|
||||||
fn into_config(self) -> Result<ClientRuntimeConfig> {
|
fn into_config(self) -> Result<ClientRuntimeConfig> {
|
||||||
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!(
|
format!(
|
||||||
"failed to read relay CA certificate {}",
|
"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()?,
|
None => ClientIdentityStore::new(self.identity_file)?.load_or_create()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let relay_addr = self
|
let relay_addr = relay
|
||||||
.relay
|
|
||||||
.resolve()
|
.resolve()
|
||||||
.with_context(|| format!("failed to resolve relay endpoint {}", self.relay))?;
|
.context("failed to resolve relay endpoint")?;
|
||||||
|
|
||||||
let session = ClientSessionConfig::new(
|
let session = ClientSessionConfig::new(
|
||||||
relay_addr,
|
relay_addr,
|
||||||
self.server_name,
|
self.server_name,
|
||||||
relay_ca_cert,
|
relay_ca_cert,
|
||||||
self.room,
|
room,
|
||||||
identity.virtual_mac(),
|
identity.virtual_mac(),
|
||||||
self.max_datagram_size,
|
self.max_datagram_size,
|
||||||
)?;
|
)?;
|
||||||
@@ -122,7 +139,13 @@ impl ClientArgs {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
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!(
|
println!(
|
||||||
"lanparty-client-win connecting virtual MAC {} to relay {} room {}",
|
"lanparty-client-win connecting virtual MAC {} to relay {} room {}",
|
||||||
config.session.virtual_mac(),
|
config.session.virtual_mac(),
|
||||||
@@ -165,6 +188,15 @@ async fn main() -> Result<()> {
|
|||||||
run_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)]
|
#[cfg(windows)]
|
||||||
async fn run_client(
|
async fn run_client(
|
||||||
session: &ClientSession,
|
session: &ClientSession,
|
||||||
@@ -488,6 +520,29 @@ fn available_tap_adapter_label(adapters: &[TapAdapterInfo]) -> String {
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_tap_adapter_list(adapters: &[TapAdapterInfo]) -> Vec<String> {
|
||||||
|
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(
|
fn client_diagnostics_snapshot(
|
||||||
session: &ClientSession,
|
session: &ClientSession,
|
||||||
route_pinned: bool,
|
route_pinned: bool,
|
||||||
@@ -1059,8 +1114,19 @@ mod tests {
|
|||||||
"ROOM1",
|
"ROOM1",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert_eq!(args.relay.host(), "relay.example.test");
|
let relay = args.relay.unwrap();
|
||||||
assert_eq!(args.relay.port(), DEFAULT_RELAY_PORT);
|
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]
|
#[test]
|
||||||
@@ -1129,6 +1195,21 @@ mod tests {
|
|||||||
assert!(error.contains("tap-two"));
|
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]
|
#[test]
|
||||||
fn formats_relay_lifecycle_events() {
|
fn formats_relay_lifecycle_events() {
|
||||||
let gateway = ControlMessage::PeerJoined(PeerInfo::new(1, Role::Gateway, None).unwrap());
|
let gateway = ControlMessage::PeerJoined(PeerInfo::new(1, Role::Gateway, None).unwrap());
|
||||||
|
|||||||
Reference in New Issue
Block a user