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
+120 -1
View File
@@ -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<std::net::IpAddr>) -> String {
#[cfg(windows)]
struct OpenedTapAdapter {
tap: TapAdapter,
tap_diagnostics: TapDiagnostics,
_route_guard: TapRouteProtectionGuard,
}
@@ -312,13 +342,66 @@ fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
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<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))]
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])
}