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]) }