feat(client): require explicit TAP selection when ambiguous
Add a Windows client --tap-instance-id option for selecting a specific TAP-Windows6 adapter by NetCfgInstanceId / InterfaceGuid. The client still opens the only installed TAP adapter automatically, but now refuses to choose an arbitrary adapter when multiple TAP-Windows6 adapters are present. This keeps the MVP test run from silently configuring and opening the wrong TAP adapter on machines that already have VPN or test TAP devices installed. The error lists available adapter instance ids so the operator can rerun with the intended value. 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:
@@ -20,6 +20,8 @@ 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;
|
||||
use lanparty_obs::{
|
||||
@@ -68,13 +70,23 @@ struct ClientArgs {
|
||||
#[arg(long)]
|
||||
virtual_mac: Option<MacAddr>,
|
||||
|
||||
/// TAP-Windows6 NetCfgInstanceId/InterfaceGuid to open.
|
||||
#[arg(long, value_name = "GUID")]
|
||||
tap_instance_id: Option<String>,
|
||||
|
||||
/// Client's advertised QUIC datagram budget before relay clamping.
|
||||
#[arg(long, default_value_t = 1400)]
|
||||
max_datagram_size: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientRuntimeConfig {
|
||||
session: ClientSessionConfig,
|
||||
tap_instance_id: Option<String>,
|
||||
}
|
||||
|
||||
impl ClientArgs {
|
||||
fn into_config(self) -> Result<ClientSessionConfig> {
|
||||
fn into_config(self) -> Result<ClientRuntimeConfig> {
|
||||
let relay_ca_cert = fs::read(&self.relay_ca_cert).with_context(|| {
|
||||
format!(
|
||||
"failed to read relay CA certificate {}",
|
||||
@@ -92,14 +104,19 @@ impl ClientArgs {
|
||||
.resolve()
|
||||
.with_context(|| format!("failed to resolve relay endpoint {}", self.relay))?;
|
||||
|
||||
ClientSessionConfig::new(
|
||||
let session = ClientSessionConfig::new(
|
||||
relay_addr,
|
||||
self.server_name,
|
||||
relay_ca_cert,
|
||||
self.room,
|
||||
identity.virtual_mac(),
|
||||
self.max_datagram_size,
|
||||
)
|
||||
)?;
|
||||
|
||||
Ok(ClientRuntimeConfig {
|
||||
session,
|
||||
tap_instance_id: self.tap_instance_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,12 +125,12 @@ async fn main() -> Result<()> {
|
||||
let config = ClientArgs::parse().into_config()?;
|
||||
println!(
|
||||
"lanparty-client-win connecting virtual MAC {} to relay {} room {}",
|
||||
config.virtual_mac(),
|
||||
config.relay_addr(),
|
||||
config.room()
|
||||
config.session.virtual_mac(),
|
||||
config.session.relay_addr(),
|
||||
config.session.room()
|
||||
);
|
||||
|
||||
let session = connect_client(config).await?;
|
||||
let session = connect_client(config.session).await?;
|
||||
println!(
|
||||
"lanparty-client-win connected as peer {} in room id {} with TAP MTU {} over {}; LAN gateway connected {}",
|
||||
session.welcome().peer_id(),
|
||||
@@ -131,7 +148,14 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let run_result = run_client(&session, &relay_route_pin).await;
|
||||
let run_result = run_client(
|
||||
&session,
|
||||
&relay_route_pin,
|
||||
config.tap_instance_id.as_deref(),
|
||||
)
|
||||
.await;
|
||||
#[cfg(not(windows))]
|
||||
let _ = config.tap_instance_id;
|
||||
#[cfg(not(windows))]
|
||||
let run_result = run_client(&session).await;
|
||||
session.shutdown("client shutting down").await;
|
||||
@@ -142,14 +166,18 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) -> Result<()> {
|
||||
async fn run_client(
|
||||
session: &ClientSession,
|
||||
relay_route_pin: &PinnedRelayRoute,
|
||||
tap_instance_id: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let relay_status = ClientRelayStatus::from_welcome(session);
|
||||
let OpenedTapAdapter {
|
||||
tap,
|
||||
tap_diagnostics,
|
||||
tap_interface,
|
||||
_route_guard,
|
||||
} = open_tap_adapter(session)?;
|
||||
} = open_tap_adapter(session, tap_instance_id)?;
|
||||
let relay_route =
|
||||
verify_relay_route_is_pinned(session.config().relay_addr().ip(), relay_route_pin)
|
||||
.context("relay route changed after TAP activation")?;
|
||||
@@ -353,8 +381,11 @@ struct TapRouteProtectionGuard {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
|
||||
let tap = open_configured_tap_adapter(session.config().virtual_mac())?;
|
||||
fn open_tap_adapter(
|
||||
session: &ClientSession,
|
||||
tap_instance_id: Option<&str>,
|
||||
) -> Result<OpenedTapAdapter> {
|
||||
let tap = open_configured_tap_adapter(session.config().virtual_mac(), tap_instance_id)?;
|
||||
let tap_interface =
|
||||
lanparty_client_route::interface_identity_from_guid(tap.info().instance_id())
|
||||
.context("failed to resolve TAP interface identity")?;
|
||||
@@ -395,12 +426,12 @@ fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn open_configured_tap_adapter(virtual_mac: MacAddr) -> Result<TapAdapter> {
|
||||
let mut adapters = lanparty_client_tap::available_adapters()?;
|
||||
let info = adapters
|
||||
.drain(..)
|
||||
.next()
|
||||
.context("no TAP-Windows6 adapters found")?;
|
||||
fn open_configured_tap_adapter(
|
||||
virtual_mac: MacAddr,
|
||||
tap_instance_id: Option<&str>,
|
||||
) -> Result<TapAdapter> {
|
||||
let adapters = lanparty_client_tap::available_adapters()?;
|
||||
let info = select_tap_adapter(adapters, tap_instance_id)?;
|
||||
lanparty_client_tap::configure_adapter_mac(&info, virtual_mac).with_context(|| {
|
||||
format!(
|
||||
"failed to persist TAP MAC {virtual_mac} for adapter {}",
|
||||
@@ -411,6 +442,52 @@ fn open_configured_tap_adapter(virtual_mac: MacAddr) -> Result<TapAdapter> {
|
||||
TapAdapter::open(info)
|
||||
}
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
fn select_tap_adapter(
|
||||
mut adapters: Vec<TapAdapterInfo>,
|
||||
requested_instance_id: Option<&str>,
|
||||
) -> Result<TapAdapterInfo> {
|
||||
if let Some(requested) = requested_instance_id {
|
||||
let requested = requested.trim();
|
||||
if requested.is_empty() {
|
||||
bail!("TAP adapter instance id cannot be empty");
|
||||
}
|
||||
if let Some(index) = adapters
|
||||
.iter()
|
||||
.position(|adapter| adapter.instance_id().eq_ignore_ascii_case(requested))
|
||||
{
|
||||
return Ok(adapters.remove(index));
|
||||
}
|
||||
|
||||
bail!(
|
||||
"TAP adapter instance id {requested:?} was not found; available TAP adapters: {}",
|
||||
available_tap_adapter_label(&adapters)
|
||||
);
|
||||
}
|
||||
|
||||
match adapters.len() {
|
||||
0 => bail!("no TAP-Windows6 adapters found"),
|
||||
1 => Ok(adapters.remove(0)),
|
||||
_ => bail!(
|
||||
"multiple TAP-Windows6 adapters found; pass --tap-instance-id with one of: {}",
|
||||
available_tap_adapter_label(&adapters)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
fn available_tap_adapter_label(adapters: &[TapAdapterInfo]) -> String {
|
||||
if adapters.is_empty() {
|
||||
return "none".to_owned();
|
||||
}
|
||||
|
||||
adapters
|
||||
.iter()
|
||||
.map(TapAdapterInfo::instance_id)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
fn client_diagnostics_snapshot(
|
||||
session: &ClientSession,
|
||||
route_pinned: bool,
|
||||
@@ -986,6 +1063,72 @@ mod tests {
|
||||
assert_eq!(args.relay.port(), DEFAULT_RELAY_PORT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_tap_instance_id() {
|
||||
let args = ClientArgs::parse_from([
|
||||
"lanparty-client-win",
|
||||
"--relay",
|
||||
"relay.example.test",
|
||||
"--relay-ca-cert",
|
||||
"relay-cert.der",
|
||||
"--room",
|
||||
"ROOM1",
|
||||
"--tap-instance-id",
|
||||
"{01234567-89AB-CDEF-0123-456789ABCDEF}",
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
args.tap_instance_id.as_deref(),
|
||||
Some("{01234567-89AB-CDEF-0123-456789ABCDEF}")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_only_tap_adapter_by_default() {
|
||||
let selected = select_tap_adapter(vec![tap_info("tap-one")], None).unwrap();
|
||||
|
||||
assert_eq!(selected.instance_id(), "tap-one");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_tap_adapters_without_selection() {
|
||||
let error = select_tap_adapter(vec![tap_info("tap-one"), tap_info("tap-two")], None)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(error.contains("multiple TAP-Windows6 adapters"));
|
||||
assert!(error.contains("tap-one"));
|
||||
assert!(error.contains("tap-two"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_requested_tap_adapter_case_insensitively() {
|
||||
let selected = select_tap_adapter(
|
||||
vec![tap_info("{AAAAAAAA-0000-0000-0000-000000000001}")],
|
||||
Some("{aaaaaaaa-0000-0000-0000-000000000001}"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
selected.instance_id(),
|
||||
"{AAAAAAAA-0000-0000-0000-000000000001}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_available_tap_adapters_when_selection_is_missing() {
|
||||
let error = select_tap_adapter(
|
||||
vec![tap_info("tap-one"), tap_info("tap-two")],
|
||||
Some("tap-missing"),
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(error.contains("tap-missing"));
|
||||
assert!(error.contains("tap-one"));
|
||||
assert!(error.contains("tap-two"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_relay_lifecycle_events() {
|
||||
let gateway = ControlMessage::PeerJoined(PeerInfo::new(1, Role::Gateway, None).unwrap());
|
||||
@@ -1033,4 +1176,8 @@ mod tests {
|
||||
const fn mac(last_octet: u8) -> MacAddr {
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, last_octet])
|
||||
}
|
||||
|
||||
fn tap_info(instance_id: &str) -> TapAdapterInfo {
|
||||
TapAdapterInfo::new(instance_id, "tap0901").unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user