From 2afdae44c6ebd49a37ea06ee6c44ee2bfe1a40f2 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 20:16:32 +0200 Subject: [PATCH] feat(client): print runtime diagnostics snapshots The client now builds ClientDiagnostics snapshots from the connected session, known TAP state, route-pinning status, and tunnel counters. Windows prints one snapshot after TAP and relay-route setup, then repeats snapshots while bridging so frame/datagram counters and drops are visible during manual phase-1 tests. Non-Windows builds print the same relay and QUIC diagnostics with TAP fields marked unknown before waiting for Ctrl-C. TAP IP remains unknown until a later Windows adapter IP inspection slice wires that source of truth. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-win - cargo clippy -p lanparty-client-win --all-targets -- -D warnings - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md --- Cargo.lock | 1 + README.md | 2 + crates/lanparty-client-win/Cargo.toml | 1 + crates/lanparty-client-win/src/main.rs | 121 ++++++++++++++++++++++++- 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index faed813..7169e97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,7 @@ dependencies = [ "lanparty-client-route", "lanparty-client-tap", "lanparty-ctrl", + "lanparty-obs", "lanparty-proto", "tokio", ] diff --git a/README.md b/README.md index e363c9f..66e1643 100644 --- a/README.md +++ b/README.md @@ -155,3 +155,5 @@ interface metric and disables TAP default routes while it runs, periodically rechecks that the relay route remains pinned, then restores the previous route policy on exit. Until automatic TAP MAC configuration is wired, startup fails before bridging if the driver-reported MAC does not match the tunnel identity. +It prints client diagnostics snapshots with relay reachability, route-pinning, +QUIC datagram budget, TAP status, frame/datagram counters, and drops. diff --git a/crates/lanparty-client-win/Cargo.toml b/crates/lanparty-client-win/Cargo.toml index d26509f..7b79099 100644 --- a/crates/lanparty-client-win/Cargo.toml +++ b/crates/lanparty-client-win/Cargo.toml @@ -8,6 +8,7 @@ anyhow.workspace = true clap.workspace = true lanparty-client-core = { path = "../lanparty-client-core" } lanparty-ctrl = { path = "../lanparty-ctrl" } +lanparty-obs = { path = "../lanparty-obs" } lanparty-proto = { path = "../lanparty-proto" } tokio.workspace = true diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index a011b47..dd537cb 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -21,12 +21,15 @@ use lanparty_client_route::{ #[cfg(windows)] use lanparty_client_tap::TapAdapter; use lanparty_ctrl::RoomCode; +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( @@ -130,11 +133,20 @@ async fn main() -> Result<()> { #[cfg(windows)] async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) -> Result<()> { - let OpenedTapAdapter { tap, _route_guard } = open_tap_adapter(session)?; + let OpenedTapAdapter { + tap, + tap_diagnostics, + _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_client_diagnostics(&client_diagnostics_snapshot( + session, + true, + tap_diagnostics.clone(), + )); println!( "bridging TAP frames; relay route is pinned and TAP route policy is scoped; press Ctrl-C to stop" ); @@ -148,6 +160,11 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) 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! { @@ -160,6 +177,13 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) ) .context("relay route changed while bridging")?; } + _ = diagnostics_check.tick() => { + print_client_diagnostics(&client_diagnostics_snapshot( + session, + true, + tap_diagnostics.clone(), + )); + } } } } @@ -167,6 +191,11 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) #[cfg(not(windows))] async fn run_client(session: &ClientSession) -> Result<()> { open_tap_adapter(session); + print_client_diagnostics(&client_diagnostics_snapshot( + session, + false, + TapDiagnostics::new(false, None, None, None), + )); println!("TAP frame pump and route pinning are not wired yet; press Ctrl-C to stop"); tokio::signal::ctrl_c() @@ -272,6 +301,7 @@ fn route_next_hop_label(next_hop: Option) -> String { #[cfg(windows)] struct OpenedTapAdapter { tap: TapAdapter, + tap_diagnostics: TapDiagnostics, _route_guard: TapRouteProtectionGuard, } @@ -312,13 +342,66 @@ fn open_tap_adapter(session: &ClientSession) -> Result { 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, _route_guard: route_guard, }) } +fn client_diagnostics_snapshot( + session: &ClientSession, + route_pinned: bool, + tap: TapDiagnostics, +) -> ClientDiagnostics { + ClientDiagnostics::new( + RelayDiagnostics::new(true, route_pinned), + session.quic_diagnostics(), + tap, + session.stats_snapshot(), + ) +} + +fn print_client_diagnostics(diagnostics: &ClientDiagnostics) { + println!("{}", format_client_diagnostics(diagnostics)); +} + +fn format_client_diagnostics(diagnostics: &ClientDiagnostics) -> String { + let stats = diagnostics.stats(); + format!( + "client diagnostics: relay reachable {} route pinned {}; QUIC datagrams {} max {}; TAP found {} MAC {} MTU {} IP {}; frames tx {} rx {} datagrams tx {} rx {} drops {} malformed {}", + yes_no(diagnostics.relay().reachable()), + 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.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()) +} + #[cfg_attr(not(windows), allow(dead_code))] fn validate_tap_driver_mac(expected_mac: MacAddr, driver_mac: MacAddr) -> Result<()> { if driver_mac != expected_mac { @@ -524,6 +607,7 @@ fn open_tap_adapter(_session: &ClientSession) { #[cfg(test)] mod tests { use super::*; + use lanparty_obs::{QuicDiagnostics, TunnelStats}; #[test] fn accepts_matching_tap_driver_mac() { @@ -537,6 +621,41 @@ mod tests { 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), + 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), + ); + + assert_eq!( + format_client_diagnostics(&diagnostics), + "client diagnostics: relay reachable 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 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), + QuicDiagnostics::new(false, None), + TapDiagnostics::new(false, None, None, None), + TunnelStats::default(), + ); + + assert_eq!( + format_client_diagnostics(&diagnostics), + "client diagnostics: relay reachable yes route pinned no; QUIC datagrams no max unknown; TAP found no MAC unknown MTU unknown IP unknown; frames tx 0 rx 0 datagrams tx 0 rx 0 drops 0 malformed 0" + ); + } + const fn mac(last_octet: u8) -> MacAddr { MacAddr::new([0x02, 0, 0, 0, 0, last_octet]) }