Files
softlan-vpn/crates/lanparty-client-win/src/main.rs
T
ddidderr 21a69626e0 feat(obs): report broadcast frame counters
PLAN.md calls out "Broadcast traffic flowing" as a user-facing diagnostic.
The tunnel stats only reported total Ethernet frame counts, so the client
could not distinguish whether broadcast traffic was actually crossing the
tunnel.

Add defaulted broadcast tx/rx counters to TunnelStats while preserving the
existing constructor and old JSON compatibility. Client and gateway accounting
now increments those counters from validated Ethernet frames, and the client
diagnostics line reports the broadcast flow next to total frame counts.

Relay peer stats logs include the new counters so operators can see broadcast
activity from forwarded stats snapshots too.

Test Plan:
- cargo fmt --check
- cargo test -p lanparty-obs -p lanparty-client-core -p lanparty-gateway \
  -p lanparty-client-win -p lanparty-relay
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check

Refs: PLAN.md
2026-05-21 21:54:35 +02:00

962 lines
31 KiB
Rust

use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::{collections::BTreeMap, fs, net::IpAddr, path::PathBuf};
#[cfg(windows)]
use std::{sync::mpsc, thread, time::Duration};
use anyhow::{Context, Result, bail};
use clap::Parser;
#[cfg(windows)]
use lanparty_client_core::ClientRelayIo;
use lanparty_client_core::{
ClientIdentity, ClientIdentityStore, ClientSession, ClientSessionConfig, connect_client,
};
#[cfg(windows)]
use lanparty_client_route::{
IpInterfaceFamily, NetworkInterfaceIdentity, PinnedRelayRoute, RouteSnapshot,
ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu,
};
#[cfg(windows)]
use lanparty_client_tap::TapAdapter;
use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode};
use lanparty_net::RelayEndpoint;
use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics};
use lanparty_proto::MacAddr;
#[cfg(windows)]
const TAP_INTERFACE_METRIC: u32 = 9_000;
#[cfg(windows)]
const RELAY_ROUTE_VERIFY_INTERVAL: Duration = Duration::from_secs(5);
#[cfg(windows)]
const CLIENT_DIAGNOSTICS_INTERVAL: Duration = Duration::from_secs(10);
#[derive(Debug, Parser)]
#[command(
name = "lanparty-client-win",
about = "Windows TAP client for the LAN party L2 tunnel"
)]
struct ClientArgs {
/// Relay DNS name or UDP socket address; bare hosts default to UDP/443.
#[arg(long, value_name = "HOST[:PORT]")]
relay: RelayEndpoint,
/// 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,
/// Room code to join as a remote client.
#[arg(long)]
room: RoomCode,
/// Identity JSON file used to persist the generated virtual MAC.
#[arg(
long = "identity-file",
value_name = "PATH",
default_value = "lanparty-client-identity.json"
)]
identity_file: PathBuf,
/// Override the generated locally administered TAP MAC address.
#[arg(long)]
virtual_mac: Option<MacAddr>,
/// Client's advertised QUIC datagram budget before relay clamping.
#[arg(long, default_value_t = 1400)]
max_datagram_size: u16,
}
impl ClientArgs {
fn into_config(self) -> Result<ClientSessionConfig> {
let relay_ca_cert = fs::read(&self.relay_ca_cert).with_context(|| {
format!(
"failed to read relay CA certificate {}",
self.relay_ca_cert.display()
)
})?;
let identity = match self.virtual_mac {
Some(virtual_mac) => ClientIdentity::new(virtual_mac)?,
None => ClientIdentityStore::new(self.identity_file)?.load_or_create()?,
};
let relay_addr = self
.relay
.resolve()
.with_context(|| format!("failed to resolve relay endpoint {}", self.relay))?;
ClientSessionConfig::new(
relay_addr,
self.server_name,
relay_ca_cert,
self.room,
identity.virtual_mac(),
self.max_datagram_size,
)
}
}
#[tokio::main]
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()
);
let session = connect_client(config).await?;
println!(
"lanparty-client-win connected as peer {} in room id {} with TAP MTU {} over {}; LAN gateway connected {}",
session.welcome().peer_id(),
session.welcome().room_id(),
session.welcome().effective_tap_mtu(),
session.welcome().mode(),
yes_no(session.welcome().gateway_connected())
);
#[cfg(windows)]
let relay_route_pin = match pin_relay_route_before_tap(session.config().relay_addr().ip()) {
Ok(pin) => pin,
Err(error) => {
session.shutdown("client startup failed").await;
return Err(error);
}
};
#[cfg(windows)]
let run_result = run_client(&session, &relay_route_pin).await;
#[cfg(not(windows))]
let run_result = run_client(&session).await;
session.shutdown("client shutting down").await;
#[cfg(windows)]
drop(relay_route_pin);
run_result
}
#[cfg(windows)]
async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) -> Result<()> {
let relay_status = ClientRelayStatus::from_welcome(session);
let OpenedTapAdapter {
tap,
tap_diagnostics,
tap_interface,
_route_guard,
} = open_tap_adapter(session)?;
let relay_route =
verify_relay_route_is_pinned(session.config().relay_addr().ip(), relay_route_pin)
.context("relay route changed after TAP activation")?;
print_verified_relay_route(&relay_route);
print_and_report_client_diagnostics(
session,
&client_diagnostics_snapshot(
session,
true,
&relay_status,
current_tap_diagnostics(&tap_diagnostics, tap_interface),
),
)
.await;
println!(
"bridging TAP frames; relay route is pinned and TAP route policy is scoped; press Ctrl-C to stop"
);
let frame_pump = run_tap_frame_pump(session.relay_io(), tap);
tokio::pin!(frame_pump);
let control_events = run_control_event_log(session, relay_status.clone());
tokio::pin!(control_events);
let shutdown = tokio::signal::ctrl_c();
tokio::pin!(shutdown);
let mut relay_route_check = tokio::time::interval_at(
tokio::time::Instant::now() + RELAY_ROUTE_VERIFY_INTERVAL,
RELAY_ROUTE_VERIFY_INTERVAL,
);
relay_route_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut diagnostics_check = tokio::time::interval_at(
tokio::time::Instant::now() + CLIENT_DIAGNOSTICS_INTERVAL,
CLIENT_DIAGNOSTICS_INTERVAL,
);
diagnostics_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
result = &mut frame_pump => return result,
result = &mut control_events => return result,
result = &mut shutdown => return result.context("failed to wait for Ctrl-C"),
_ = relay_route_check.tick() => {
verify_relay_route_is_pinned(
session.config().relay_addr().ip(),
relay_route_pin,
)
.context("relay route changed while bridging")?;
}
_ = diagnostics_check.tick() => {
print_and_report_client_diagnostics(
session,
&client_diagnostics_snapshot(
session,
true,
&relay_status,
current_tap_diagnostics(&tap_diagnostics, tap_interface),
),
).await;
}
}
}
}
#[cfg(not(windows))]
async fn run_client(session: &ClientSession) -> Result<()> {
let relay_status = ClientRelayStatus::from_welcome(session);
open_tap_adapter(session);
print_and_report_client_diagnostics(
session,
&client_diagnostics_snapshot(
session,
false,
&relay_status,
TapDiagnostics::new(false, None, None, None),
),
)
.await;
println!("TAP frame pump and route pinning are not wired yet; press Ctrl-C to stop");
let control_events = run_control_event_log(session, relay_status);
tokio::pin!(control_events);
let shutdown = tokio::signal::ctrl_c();
tokio::pin!(shutdown);
tokio::select! {
result = &mut control_events => result,
result = &mut shutdown => result.context("failed to wait for Ctrl-C"),
}
}
#[cfg(windows)]
fn pin_relay_route_before_tap(destination: std::net::IpAddr) -> Result<PinnedRelayRoute> {
let route = lanparty_client_route::best_route_to(destination)
.context("failed to inspect relay route before TAP activation")?;
print_relay_route(&route);
let pin = lanparty_client_route::pin_relay_route(&route)
.context("failed to pin relay route before TAP activation")?;
print_pinned_relay_route(&pin);
Ok(pin)
}
#[cfg(windows)]
fn print_relay_route(route: &RouteSnapshot) {
println!(
"relay route before TAP: destination {} source {} next hop {} interface index {} LUID {} prefix {}/{} metric {}",
route.destination(),
route.source(),
route
.next_hop()
.map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string()),
route.interface_index(),
route.interface_luid(),
route.route_prefix(),
route.route_prefix_len(),
route.metric()
);
}
#[cfg(windows)]
fn print_pinned_relay_route(route: &PinnedRelayRoute) {
println!(
"relay route pinned before TAP: destination {} next hop {} interface index {} LUID {}",
route.destination(),
route
.next_hop()
.map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string()),
route.interface_index(),
route.interface_luid()
);
}
#[cfg(windows)]
fn verify_relay_route_is_pinned(
destination: std::net::IpAddr,
pin: &PinnedRelayRoute,
) -> Result<RouteSnapshot> {
let route = lanparty_client_route::best_route_to(destination)
.context("failed to inspect relay route")?;
if !relay_route_matches_pin(&route, pin) {
bail!(
"relay route to {} uses prefix {}/{} next hop {} interface index {} LUID {}, expected pinned host route via next hop {} interface index {} LUID {}",
route.destination(),
route.route_prefix(),
route.route_prefix_len(),
route_next_hop_label(route.next_hop()),
route.interface_index(),
route.interface_luid(),
route_next_hop_label(pin.next_hop()),
pin.interface_index(),
pin.interface_luid(),
);
}
Ok(route)
}
#[cfg(windows)]
fn relay_route_matches_pin(route: &RouteSnapshot, pin: &PinnedRelayRoute) -> bool {
route.destination() == pin.destination()
&& route.is_host_route_to(pin.destination())
&& route.next_hop() == pin.next_hop()
&& route.interface_index() == pin.interface_index()
&& route.interface_luid() == pin.interface_luid()
}
#[cfg(windows)]
fn print_verified_relay_route(route: &RouteSnapshot) {
println!(
"relay route verified after TAP activation: destination {} next hop {} interface index {} LUID {} prefix {}/{} metric {}",
route.destination(),
route_next_hop_label(route.next_hop()),
route.interface_index(),
route.interface_luid(),
route.route_prefix(),
route.route_prefix_len(),
route.metric()
);
}
#[cfg(windows)]
fn route_next_hop_label(next_hop: Option<std::net::IpAddr>) -> String {
next_hop.map_or_else(|| "on-link".to_string(), |next_hop| next_hop.to_string())
}
#[cfg(windows)]
struct OpenedTapAdapter {
tap: TapAdapter,
tap_diagnostics: TapDiagnostics,
tap_interface: NetworkInterfaceIdentity,
_route_guard: TapRouteProtectionGuard,
}
#[cfg(windows)]
struct TapRouteProtectionGuard {
_ipv4_metric: ScopedInterfaceMetric,
_ipv4_default_routes: ScopedDefaultRoutes,
_ipv4_mtu: ScopedInterfaceMtu,
_ipv6_metric: Option<ScopedInterfaceMetric>,
_ipv6_default_routes: Option<ScopedDefaultRoutes>,
_ipv6_mtu: Option<ScopedInterfaceMtu>,
}
#[cfg(windows)]
fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
let tap = open_configured_tap_adapter(session.config().virtual_mac())?;
let tap_interface =
lanparty_client_route::interface_identity_from_guid(tap.info().instance_id())
.context("failed to resolve TAP interface identity")?;
let route_guard = protect_tap_routes(tap_interface, session.welcome().effective_tap_mtu())?;
let driver_mac = tap.driver_mac()?;
let driver_mtu = tap.driver_mtu()?;
validate_tap_driver_mac(session.config().virtual_mac(), driver_mac)?;
tap.set_media_connected(true)?;
println!(
"lanparty-client-win opened TAP adapter {}",
tap.info().device_path()
);
println!(
"TAP driver reports MAC {} and MTU {}; relay selected TAP MTU {}",
driver_mac,
driver_mtu,
session.welcome().effective_tap_mtu()
);
println!(
"TAP interface index {} LUID {}",
tap_interface.index(),
tap_interface.luid()
);
let tap_diagnostics = TapDiagnostics::new(
true,
Some(driver_mac),
Some(session.welcome().effective_tap_mtu()),
None,
);
Ok(OpenedTapAdapter {
tap,
tap_diagnostics,
tap_interface,
_route_guard: route_guard,
})
}
#[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")?;
lanparty_client_tap::configure_adapter_mac(&info, virtual_mac).with_context(|| {
format!(
"failed to persist TAP MAC {virtual_mac} for adapter {}",
info.instance_id()
)
})?;
TapAdapter::open(info)
}
fn client_diagnostics_snapshot(
session: &ClientSession,
route_pinned: bool,
relay_status: &ClientRelayStatus,
tap: TapDiagnostics,
) -> ClientDiagnostics {
ClientDiagnostics::new(
RelayDiagnostics::new(true, route_pinned, relay_status.gateway_connected()),
session.quic_diagnostics(),
tap,
session.stats_snapshot(),
)
}
#[cfg(windows)]
fn current_tap_diagnostics(
base: &TapDiagnostics,
identity: NetworkInterfaceIdentity,
) -> TapDiagnostics {
tap_diagnostics_with_ip(base, tap_unicast_ip(identity))
}
#[cfg_attr(not(windows), allow(dead_code))]
fn tap_diagnostics_with_ip(base: &TapDiagnostics, ip: Option<IpAddr>) -> TapDiagnostics {
TapDiagnostics::new(base.adapter_found(), base.mac(), base.mtu(), ip)
}
fn print_client_diagnostics(diagnostics: &ClientDiagnostics) {
println!("{}", format_client_diagnostics(diagnostics));
}
async fn print_and_report_client_diagnostics(
session: &ClientSession,
diagnostics: &ClientDiagnostics,
) {
print_client_diagnostics(diagnostics);
if let Err(error) = session.send_stats_snapshot().await {
eprintln!("failed to send client stats to relay: {error:#}");
}
}
fn format_client_diagnostics(diagnostics: &ClientDiagnostics) -> String {
let stats = diagnostics.stats();
format!(
"client diagnostics: relay reachable {} gateway connected {} route pinned {}; QUIC datagrams {} max {}; TAP found {} MAC {} MTU {} IP {}; frames tx {} rx {} broadcast tx {} rx {} datagrams tx {} rx {} drops {} malformed {}",
yes_no(diagnostics.relay().reachable()),
yes_no(diagnostics.relay().gateway_connected()),
yes_no(diagnostics.relay().route_pinned()),
yes_no(diagnostics.quic().datagram_supported()),
optional_label(diagnostics.quic().max_datagram_size()),
yes_no(diagnostics.tap().adapter_found()),
optional_label(diagnostics.tap().mac()),
optional_label(diagnostics.tap().mtu()),
optional_label(diagnostics.tap().ip()),
stats.ethernet_frames_tx(),
stats.ethernet_frames_rx(),
stats.broadcast_frames_tx(),
stats.broadcast_frames_rx(),
stats.datagrams_tx(),
stats.datagrams_rx(),
stats.dropped_frames(),
stats.malformed_frames()
)
}
const fn yes_no(value: bool) -> &'static str {
if value { "yes" } else { "no" }
}
fn optional_label<T: std::fmt::Display>(value: Option<T>) -> String {
value.map_or_else(|| "unknown".to_string(), |value| value.to_string())
}
async fn run_control_event_log(
session: &ClientSession,
relay_status: ClientRelayStatus,
) -> Result<()> {
let mut formatter = ControlEventFormatter::default();
loop {
let event = session
.recv_control_event()
.await
.context("failed to receive relay control event")?;
println!("{}", formatter.format(&event, &relay_status));
}
}
#[cfg(test)]
fn format_control_event(event: &ControlMessage) -> String {
let relay_status = ClientRelayStatus::new(false);
ControlEventFormatter::default().format(event, &relay_status)
}
#[derive(Debug, Clone)]
struct ClientRelayStatus {
gateway_connected: Arc<AtomicBool>,
}
impl ClientRelayStatus {
fn new(gateway_connected: bool) -> Self {
Self {
gateway_connected: Arc::new(AtomicBool::new(gateway_connected)),
}
}
fn from_welcome(session: &ClientSession) -> Self {
Self::new(session.welcome().gateway_connected())
}
fn gateway_connected(&self) -> bool {
self.gateway_connected.load(Ordering::Relaxed)
}
fn set_gateway_connected(&self, gateway_connected: bool) {
self.gateway_connected
.store(gateway_connected, Ordering::Relaxed);
}
}
#[derive(Debug, Default)]
struct ControlEventFormatter {
peers: BTreeMap<u32, PeerInfo>,
}
impl ControlEventFormatter {
fn format(&mut self, event: &ControlMessage, relay_status: &ClientRelayStatus) -> String {
match event {
ControlMessage::PeerJoined(peer) => {
self.peers.insert(peer.peer_id(), peer.clone());
if peer.role() == Role::Gateway {
relay_status.set_gateway_connected(true);
}
format_peer_joined(peer)
}
ControlMessage::PeerLeft { peer_id, reason } => {
let Some(peer) = self.peers.remove(peer_id) else {
return format!("relay event: peer {peer_id} left ({reason:?})");
};
match peer.role() {
Role::Gateway => {
relay_status.set_gateway_connected(false);
format!(
"relay event: LAN gateway disconnected (peer {}, {reason:?})",
peer.peer_id()
)
}
Role::Client => {
let mac = peer
.mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "unknown".to_string());
format!(
"relay event: client peer {} with MAC {} left ({reason:?})",
peer.peer_id(),
mac
)
}
}
}
_ => format!("relay event: {event:?}"),
}
}
}
fn format_peer_joined(peer: &PeerInfo) -> String {
match peer.role() {
Role::Gateway => {
format!(
"relay event: LAN gateway connected as peer {}",
peer.peer_id()
)
}
Role::Client => {
let mac = peer
.mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "unknown".to_string());
format!(
"relay event: client peer {} joined with MAC {}",
peer.peer_id(),
mac
)
}
}
}
#[cfg(windows)]
fn tap_unicast_ip(identity: NetworkInterfaceIdentity) -> Option<IpAddr> {
match lanparty_client_route::interface_unicast_addresses(identity) {
Ok(addresses) => preferred_tap_ip(&addresses),
Err(error) => {
eprintln!(
"failed to inspect TAP IP address; diagnostics will report unknown: {error:#}"
);
None
}
}
}
#[cfg(windows)]
fn preferred_tap_ip(
addresses: &[lanparty_client_route::InterfaceUnicastAddress],
) -> Option<IpAddr> {
addresses
.iter()
.find(|address| matches!(address.address(), IpAddr::V4(_)))
.or_else(|| addresses.first())
.map(|address| address.address())
}
#[cfg_attr(not(windows), allow(dead_code))]
fn validate_tap_driver_mac(expected_mac: MacAddr, driver_mac: MacAddr) -> Result<()> {
if driver_mac != expected_mac {
bail!(
"TAP driver MAC {driver_mac} does not match tunnel identity {expected_mac}; the NetworkAddress registry value was written before opening the adapter, but Windows may need the TAP adapter disabled/enabled or reinstalled before the driver reloads it"
);
}
Ok(())
}
#[cfg(windows)]
fn protect_tap_routes(
identity: NetworkInterfaceIdentity,
tap_mtu: u16,
) -> Result<TapRouteProtectionGuard> {
let tap_mtu = u32::from(tap_mtu);
let ipv4_mtu =
lanparty_client_route::set_scoped_interface_mtu(identity, IpInterfaceFamily::Ipv4, tap_mtu)
.context("failed to set TAP IPv4 MTU")?;
print_tap_mtu_override(IpInterfaceFamily::Ipv4, tap_mtu, &ipv4_mtu);
let ipv4_metric = lanparty_client_route::set_scoped_interface_metric(
identity,
IpInterfaceFamily::Ipv4,
TAP_INTERFACE_METRIC,
)
.context("failed to set TAP IPv4 interface metric")?;
print_tap_metric_override(IpInterfaceFamily::Ipv4, &ipv4_metric);
let ipv4_default_routes = lanparty_client_route::set_scoped_default_routes_disabled(
identity,
IpInterfaceFamily::Ipv4,
true,
)
.context("failed to disable TAP IPv4 default routes")?;
print_tap_default_routes_override(IpInterfaceFamily::Ipv4, &ipv4_default_routes);
let ipv6_mtu = match lanparty_client_route::set_scoped_interface_mtu(
identity,
IpInterfaceFamily::Ipv6,
tap_mtu,
) {
Ok(mtu) => {
print_tap_mtu_override(IpInterfaceFamily::Ipv6, tap_mtu, &mtu);
Some(mtu)
}
Err(error) => {
eprintln!("failed to set TAP IPv6 MTU; IPv6 may use the previous MTU: {error:#}");
None
}
};
let ipv6_metric = match lanparty_client_route::set_scoped_interface_metric(
identity,
IpInterfaceFamily::Ipv6,
TAP_INTERFACE_METRIC,
) {
Ok(metric) => {
print_tap_metric_override(IpInterfaceFamily::Ipv6, &metric);
Some(metric)
}
Err(error) => {
eprintln!(
"failed to set TAP IPv6 interface metric; IPv6 route protection may be incomplete: {error:#}"
);
None
}
};
let ipv6_default_routes = match lanparty_client_route::set_scoped_default_routes_disabled(
identity,
IpInterfaceFamily::Ipv6,
true,
) {
Ok(default_routes) => {
print_tap_default_routes_override(IpInterfaceFamily::Ipv6, &default_routes);
Some(default_routes)
}
Err(error) => {
eprintln!(
"failed to disable TAP IPv6 default routes; IPv6 route protection may be incomplete: {error:#}"
);
None
}
};
Ok(TapRouteProtectionGuard {
_ipv4_metric: ipv4_metric,
_ipv4_default_routes: ipv4_default_routes,
_ipv4_mtu: ipv4_mtu,
_ipv6_metric: ipv6_metric,
_ipv6_default_routes: ipv6_default_routes,
_ipv6_mtu: ipv6_mtu,
})
}
#[cfg(windows)]
fn print_tap_mtu_override(family: IpInterfaceFamily, mtu: u32, guard: &ScopedInterfaceMtu) {
let previous = guard.previous();
println!(
"TAP {family:?} MTU set to {mtu}; previous MTU {}",
previous.mtu()
);
}
#[cfg(windows)]
fn print_tap_metric_override(family: IpInterfaceFamily, metric: &ScopedInterfaceMetric) {
let previous = metric.previous();
println!(
"TAP {family:?} interface metric set to {TAP_INTERFACE_METRIC}; previous metric {} automatic {} default-routes-disabled {}",
previous.metric(),
previous.automatic_metric(),
previous.disable_default_routes()
);
}
#[cfg(windows)]
fn print_tap_default_routes_override(family: IpInterfaceFamily, routes: &ScopedDefaultRoutes) {
let previous = routes.previous();
println!(
"TAP {family:?} default routes disabled; previous default-routes-disabled {}",
previous.disable_default_routes()
);
}
#[cfg(windows)]
async fn run_tap_frame_pump(relay_io: ClientRelayIo, tap: TapAdapter) -> Result<()> {
let tap = Arc::new(tap);
let (tap_error_tx, tap_error_rx) = mpsc::channel();
spawn_tap_reader(Arc::clone(&tap), relay_io.clone(), tap_error_tx)?;
let tap_reader_error = tokio::task::spawn_blocking(move || tap_error_rx.recv());
tokio::pin!(tap_reader_error);
loop {
tokio::select! {
reader_result = &mut tap_reader_error => {
let reader_result = reader_result.context("TAP reader error watcher panicked")?;
let error = reader_result
.context("TAP reader thread stopped without reporting an error")?;
return Err::<(), _>(error).context("TAP reader thread stopped");
}
relay_frame = relay_io.recv_ethernet() => {
let relay_frame = relay_frame.context("failed to receive relay Ethernet frame")?;
let tap = Arc::clone(&tap);
let payload = relay_frame.payload().to_vec();
tokio::task::spawn_blocking(move || {
tap.write_ethernet_frame(&payload)
.context("failed to write relay Ethernet frame to TAP")
})
.await
.context("TAP writer task panicked")??;
}
}
}
}
#[cfg(windows)]
fn spawn_tap_reader(
tap: Arc<TapAdapter>,
relay_io: ClientRelayIo,
tap_error_tx: mpsc::Sender<anyhow::Error>,
) -> Result<()> {
thread::Builder::new()
.name("lanparty-tap-reader".to_string())
.spawn(move || {
let mut buffer = vec![0_u8; lanparty_client_tap::TAP_FRAME_BUFFER_LEN];
loop {
let result = read_and_relay_tap_frame(&tap, &relay_io, &mut buffer);
if let Err(error) = result {
let _ = tap_error_tx.send(error);
return;
}
}
})
.context("failed to spawn TAP reader thread")?;
Ok(())
}
#[cfg(windows)]
fn read_and_relay_tap_frame(
tap: &TapAdapter,
relay_io: &ClientRelayIo,
buffer: &mut [u8],
) -> Result<()> {
let len = tap
.read_ethernet_frame(buffer)
.context("failed to read TAP Ethernet frame")?;
relay_io
.send_ethernet(&buffer[..len])
.context("failed to send TAP Ethernet frame to relay")?;
Ok(())
}
#[cfg(not(windows))]
fn open_tap_adapter(_session: &ClientSession) {
println!("Windows TAP adapter opening is compiled only on Windows");
}
#[cfg(test)]
mod tests {
use super::*;
use lanparty_ctrl::{DisconnectReason, PeerInfo};
use lanparty_net::DEFAULT_RELAY_PORT;
use lanparty_obs::{QuicDiagnostics, TunnelStats};
#[test]
fn accepts_matching_tap_driver_mac() {
assert!(validate_tap_driver_mac(mac(1), mac(1)).is_ok());
}
#[test]
fn rejects_tap_driver_mac_mismatch() {
let error = validate_tap_driver_mac(mac(1), mac(2)).unwrap_err();
assert!(error.to_string().contains("does not match tunnel identity"));
}
#[test]
fn formats_client_diagnostics_status_line() {
let diagnostics = ClientDiagnostics::new(
RelayDiagnostics::new(true, true, true),
QuicDiagnostics::new(true, Some(1400)),
TapDiagnostics::new(
true,
Some(mac(1)),
Some(1200),
Some("10.73.42.51".parse().unwrap()),
),
TunnelStats::new(1, 2, 3, 4, 5, 6).with_broadcast_frames(7, 8),
);
assert_eq!(
format_client_diagnostics(&diagnostics),
"client diagnostics: relay reachable yes gateway connected yes route pinned yes; QUIC datagrams yes max 1400; TAP found yes MAC 02:00:00:00:00:01 MTU 1200 IP 10.73.42.51; frames tx 1 rx 2 broadcast tx 7 rx 8 datagrams tx 3 rx 4 drops 5 malformed 6"
);
}
#[test]
fn formats_missing_client_diagnostics_as_unknown() {
let diagnostics = ClientDiagnostics::new(
RelayDiagnostics::new(true, false, false),
QuicDiagnostics::new(false, None),
TapDiagnostics::new(false, None, None, None),
TunnelStats::default(),
);
assert_eq!(
format_client_diagnostics(&diagnostics),
"client diagnostics: relay reachable yes gateway connected no route pinned no; QUIC datagrams no max unknown; TAP found no MAC unknown MTU unknown IP unknown; frames tx 0 rx 0 broadcast tx 0 rx 0 datagrams tx 0 rx 0 drops 0 malformed 0"
);
}
#[test]
fn refreshes_tap_diagnostics_ip_without_losing_static_fields() {
let base = TapDiagnostics::new(
true,
Some(mac(1)),
Some(1200),
Some("169.254.10.20".parse().unwrap()),
);
let refreshed =
tap_diagnostics_with_ip(&base, Some("10.73.42.51".parse::<IpAddr>().unwrap()));
assert!(refreshed.adapter_found());
assert_eq!(refreshed.mac(), Some(mac(1)));
assert_eq!(refreshed.mtu(), Some(1200));
assert_eq!(refreshed.ip().unwrap().to_string(), "10.73.42.51");
}
#[test]
fn accepts_relay_domain_with_default_port() {
let args = ClientArgs::parse_from([
"lanparty-client-win",
"--relay",
"relay.example.test",
"--relay-ca-cert",
"relay-cert.der",
"--room",
"ROOM1",
]);
assert_eq!(args.relay.host(), "relay.example.test");
assert_eq!(args.relay.port(), DEFAULT_RELAY_PORT);
}
#[test]
fn formats_relay_lifecycle_events() {
let gateway = ControlMessage::PeerJoined(PeerInfo::new(1, Role::Gateway, None).unwrap());
let client =
ControlMessage::PeerJoined(PeerInfo::new(2, Role::Client, Some(mac(2))).unwrap());
let unknown_left = ControlMessage::PeerLeft {
peer_id: 3,
reason: DisconnectReason::Normal,
};
let client_left = ControlMessage::PeerLeft {
peer_id: 2,
reason: DisconnectReason::Normal,
};
let gateway_left = ControlMessage::PeerLeft {
peer_id: 1,
reason: DisconnectReason::Normal,
};
let mut formatter = ControlEventFormatter::default();
let relay_status = ClientRelayStatus::new(false);
assert_eq!(
formatter.format(&gateway, &relay_status),
"relay event: LAN gateway connected as peer 1"
);
assert!(relay_status.gateway_connected());
assert_eq!(
formatter.format(&client, &relay_status),
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
);
assert_eq!(
format_control_event(&unknown_left),
"relay event: peer 3 left (Normal)"
);
assert_eq!(
formatter.format(&client_left, &relay_status),
"relay event: client peer 2 with MAC 02:00:00:00:00:02 left (Normal)"
);
assert_eq!(
formatter.format(&gateway_left, &relay_status),
"relay event: LAN gateway disconnected (peer 1, Normal)"
);
assert!(!relay_status.gateway_connected());
}
const fn mac(last_octet: u8) -> MacAddr {
MacAddr::new([0x02, 0, 0, 0, 0, last_octet])
}
}