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
This commit is contained in:
2026-05-21 20:16:32 +02:00
parent e8d7cf7ff5
commit 2afdae44c6
4 changed files with 124 additions and 1 deletions
Generated
+1
View File
@@ -474,6 +474,7 @@ dependencies = [
"lanparty-client-route", "lanparty-client-route",
"lanparty-client-tap", "lanparty-client-tap",
"lanparty-ctrl", "lanparty-ctrl",
"lanparty-obs",
"lanparty-proto", "lanparty-proto",
"tokio", "tokio",
] ]
+2
View File
@@ -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 rechecks that the relay route remains pinned, then restores the previous route
policy on exit. Until automatic TAP MAC configuration is wired, startup fails 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. 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.
+1
View File
@@ -8,6 +8,7 @@ anyhow.workspace = true
clap.workspace = true clap.workspace = true
lanparty-client-core = { path = "../lanparty-client-core" } lanparty-client-core = { path = "../lanparty-client-core" }
lanparty-ctrl = { path = "../lanparty-ctrl" } lanparty-ctrl = { path = "../lanparty-ctrl" }
lanparty-obs = { path = "../lanparty-obs" }
lanparty-proto = { path = "../lanparty-proto" } lanparty-proto = { path = "../lanparty-proto" }
tokio.workspace = true tokio.workspace = true
+120 -1
View File
@@ -21,12 +21,15 @@ use lanparty_client_route::{
#[cfg(windows)] #[cfg(windows)]
use lanparty_client_tap::TapAdapter; use lanparty_client_tap::TapAdapter;
use lanparty_ctrl::RoomCode; use lanparty_ctrl::RoomCode;
use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics};
use lanparty_proto::MacAddr; use lanparty_proto::MacAddr;
#[cfg(windows)] #[cfg(windows)]
const TAP_INTERFACE_METRIC: u32 = 9_000; const TAP_INTERFACE_METRIC: u32 = 9_000;
#[cfg(windows)] #[cfg(windows)]
const RELAY_ROUTE_VERIFY_INTERVAL: Duration = Duration::from_secs(5); const RELAY_ROUTE_VERIFY_INTERVAL: Duration = Duration::from_secs(5);
#[cfg(windows)]
const CLIENT_DIAGNOSTICS_INTERVAL: Duration = Duration::from_secs(10);
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command( #[command(
@@ -130,11 +133,20 @@ async fn main() -> Result<()> {
#[cfg(windows)] #[cfg(windows)]
async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) -> Result<()> { 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 = let relay_route =
verify_relay_route_is_pinned(session.config().relay_addr().ip(), relay_route_pin) verify_relay_route_is_pinned(session.config().relay_addr().ip(), relay_route_pin)
.context("relay route changed after TAP activation")?; .context("relay route changed after TAP activation")?;
print_verified_relay_route(&relay_route); print_verified_relay_route(&relay_route);
print_client_diagnostics(&client_diagnostics_snapshot(
session,
true,
tap_diagnostics.clone(),
));
println!( println!(
"bridging TAP frames; relay route is pinned and TAP route policy is scoped; press Ctrl-C to stop" "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_VERIFY_INTERVAL,
); );
relay_route_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); 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 { loop {
tokio::select! { tokio::select! {
@@ -160,6 +177,13 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute)
) )
.context("relay route changed while bridging")?; .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))] #[cfg(not(windows))]
async fn run_client(session: &ClientSession) -> Result<()> { async fn run_client(session: &ClientSession) -> Result<()> {
open_tap_adapter(session); 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"); println!("TAP frame pump and route pinning are not wired yet; press Ctrl-C to stop");
tokio::signal::ctrl_c() tokio::signal::ctrl_c()
@@ -272,6 +301,7 @@ fn route_next_hop_label(next_hop: Option<std::net::IpAddr>) -> String {
#[cfg(windows)] #[cfg(windows)]
struct OpenedTapAdapter { struct OpenedTapAdapter {
tap: TapAdapter, tap: TapAdapter,
tap_diagnostics: TapDiagnostics,
_route_guard: TapRouteProtectionGuard, _route_guard: TapRouteProtectionGuard,
} }
@@ -312,13 +342,66 @@ fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
tap_interface.index(), tap_interface.index(),
tap_interface.luid() tap_interface.luid()
); );
let tap_diagnostics = TapDiagnostics::new(
true,
Some(driver_mac),
Some(session.welcome().effective_tap_mtu()),
None,
);
Ok(OpenedTapAdapter { Ok(OpenedTapAdapter {
tap, tap,
tap_diagnostics,
_route_guard: route_guard, _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<T: std::fmt::Display>(value: Option<T>) -> String {
value.map_or_else(|| "unknown".to_string(), |value| value.to_string())
}
#[cfg_attr(not(windows), allow(dead_code))] #[cfg_attr(not(windows), allow(dead_code))]
fn validate_tap_driver_mac(expected_mac: MacAddr, driver_mac: MacAddr) -> Result<()> { fn validate_tap_driver_mac(expected_mac: MacAddr, driver_mac: MacAddr) -> Result<()> {
if driver_mac != expected_mac { if driver_mac != expected_mac {
@@ -524,6 +607,7 @@ fn open_tap_adapter(_session: &ClientSession) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use lanparty_obs::{QuicDiagnostics, TunnelStats};
#[test] #[test]
fn accepts_matching_tap_driver_mac() { fn accepts_matching_tap_driver_mac() {
@@ -537,6 +621,41 @@ mod tests {
assert!(error.to_string().contains("does not match tunnel identity")); 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 { const fn mac(last_octet: u8) -> MacAddr {
MacAddr::new([0x02, 0, 0, 0, 0, last_octet]) MacAddr::new([0x02, 0, 0, 0, 0, last_octet])
} }