use std::{fs, net::SocketAddr, path::PathBuf}; #[cfg(windows)] use std::{ sync::{Arc, 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::RoomCode; 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); #[derive(Debug, Parser)] #[command( name = "lanparty-client-win", about = "Windows TAP client for the LAN party L2 tunnel" )] struct ClientArgs { /// Relay UDP socket address, for example 203.0.113.10:443. #[arg(long)] relay: SocketAddr, /// 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()?, }; ClientSessionConfig::new( self.relay, 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 {}", session.welcome().peer_id(), session.welcome().room_id(), session.welcome().effective_tap_mtu() ); #[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 OpenedTapAdapter { tap, _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); 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 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); loop { tokio::select! { result = &mut frame_pump => 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")?; } } } } #[cfg(not(windows))] async fn run_client(session: &ClientSession) -> Result<()> { open_tap_adapter(session); println!("TAP frame pump and route pinning are not wired yet; press Ctrl-C to stop"); tokio::signal::ctrl_c() .await .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, _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 = lanparty_client_tap::open_first_adapter()?; 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() ); Ok(OpenedTapAdapter { tap, _route_guard: route_guard, }) } #[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}; automatic MAC configuration is not wired yet" ); } 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::*; #[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")); } const fn mac(last_octet: u8) -> MacAddr { MacAddr::new([0x02, 0, 0, 0, 0, last_octet]) } }