3e2648abc1
The Windows client now sets the TAP IP-interface MTU to the relay-selected MTU before it starts bridging frames. The override is scoped like the existing metric and default-route guards, so the previous MTU is restored when the client exits. The route crate now exposes `InterfaceMtuSnapshot` and `ScopedInterfaceMtu` around `MIB_IPINTERFACE_ROW.NlMtu`, reusing the same `GetIpInterfaceEntry` and `SetIpInterfaceEntry` path already used for metrics and default-route policy. IPv4 MTU setup is required for startup, while IPv6 MTU setup is best-effort to match the existing IPv6 route-protection behavior. This intentionally leaves TAP MAC configuration as fail-fast. TAP-Windows6 does not expose a matching set-MAC IOCTL in the driver header, so that should remain a separate design decision. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-route - cargo test -p lanparty-client-win - cargo clippy -p lanparty-client-route -p lanparty-client-win --all-targets -- -D warnings - cargo check -p lanparty-client-route --target x86_64-pc-windows-gnu - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check - cargo check -p lanparty-client-win --target x86_64-pc-windows-gnu (fails before this crate in ring: missing x86_64-w64-mingw32-gcc) Refs: PLAN.md
544 lines
17 KiB
Rust
544 lines
17 KiB
Rust
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<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()?,
|
|
};
|
|
|
|
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<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,
|
|
_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 = 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<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::*;
|
|
|
|
#[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])
|
|
}
|
|
}
|