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, /// 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 { 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 { 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 { 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) -> 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, _ipv6_default_routes: Option, _ipv6_mtu: Option, } #[cfg(windows)] fn open_tap_adapter(session: &ClientSession) -> Result { 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 { 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) -> 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(value: Option) -> 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, } 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, } 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 { 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 { 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 { 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, relay_io: ClientRelayIo, tap_error_tx: mpsc::Sender, ) -> 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::().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]) } }