diff --git a/README.md b/README.md index 31d5054..a022c5c 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,10 @@ It prints and reports client diagnostics snapshots with relay reachability, LAN-gateway presence, route-pinning, QUIC datagram budget, TAP status/IP, broadcast frame flow, frame/datagram counters, and drops. The periodic diagnostics refresh the TAP unicast IP so DHCP results that arrive after -bridging starts become visible in later status lines. Relay lifecycle events -are logged as they arrive, including gateway joins and peer leaves. The client -remembers peer identities from join and catch-up events so later leave logs can -identify a disconnected LAN gateway or client MAC when that peer was known. +bridging starts become visible in later status lines. Each snapshot also emits +short user-facing lines such as relay/gateway connection status, DHCP address +presence, and broadcast-flow confirmation when those signals are observed. +Relay lifecycle events are logged as they arrive, including gateway joins and +peer leaves. The client remembers peer identities from join and catch-up events +so later leave logs can identify a disconnected LAN gateway or client MAC when +that peer was known. diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index c68e07b..cec5a39 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -22,7 +22,9 @@ use lanparty_client_route::{ use lanparty_client_tap::TapAdapter; use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode}; use lanparty_net::RelayEndpoint; -use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics}; +use lanparty_obs::{ + ClientDiagnostics, RelayDiagnostics, TapDiagnostics, UserDiagnostic, UserDiagnosticLevel, +}; use lanparty_proto::MacAddr; #[cfg(windows)] @@ -438,6 +440,9 @@ fn tap_diagnostics_with_ip(base: &TapDiagnostics, ip: Option) -> TapDiag fn print_client_diagnostics(diagnostics: &ClientDiagnostics) { println!("{}", format_client_diagnostics(diagnostics)); + for diagnostic in diagnostics.user_diagnostics() { + println!("{}", format_user_diagnostic(&diagnostic)); + } } async fn print_and_report_client_diagnostics( @@ -482,6 +487,14 @@ fn optional_label(value: Option) -> String { value.map_or_else(|| "unknown".to_string(), |value| value.to_string()) } +fn format_user_diagnostic(diagnostic: &UserDiagnostic) -> String { + match diagnostic.level() { + UserDiagnosticLevel::Info => diagnostic.message().to_owned(), + UserDiagnosticLevel::Warning => format!("Warning: {}", diagnostic.message()), + UserDiagnosticLevel::Error => format!("Error: {}", diagnostic.message()), + } +} + async fn run_control_event_log( session: &ClientSession, relay_status: ClientRelayStatus, @@ -887,6 +900,31 @@ mod tests { ); } + #[test] + fn formats_user_diagnostic_levels() { + assert_eq!( + format_user_diagnostic(&UserDiagnostic::new( + UserDiagnosticLevel::Info, + "Connected to relay" + )), + "Connected to relay" + ); + assert_eq!( + format_user_diagnostic(&UserDiagnostic::new( + UserDiagnosticLevel::Warning, + "Waiting for LAN gateway" + )), + "Warning: Waiting for LAN gateway" + ); + assert_eq!( + format_user_diagnostic(&UserDiagnostic::new( + UserDiagnosticLevel::Error, + "Relay not reachable" + )), + "Error: Relay not reachable" + ); + } + #[test] fn refreshes_tap_diagnostics_ip_without_losing_static_fields() { let base = TapDiagnostics::new( diff --git a/crates/lanparty-obs/src/lib.rs b/crates/lanparty-obs/src/lib.rs index 1d3d653..6f15472 100644 --- a/crates/lanparty-obs/src/lib.rs +++ b/crates/lanparty-obs/src/lib.rs @@ -367,6 +367,44 @@ impl ClientDiagnostics { pub const fn stats(&self) -> &TunnelStats { &self.stats } + + #[must_use] + pub fn user_diagnostics(&self) -> Vec { + let mut diagnostics = Vec::new(); + + diagnostics.push(if self.relay.reachable() { + UserDiagnostic::new(UserDiagnosticLevel::Info, "Connected to relay") + } else { + UserDiagnostic::new(UserDiagnosticLevel::Error, "Relay not reachable") + }); + + diagnostics.push(if self.relay.gateway_connected() { + UserDiagnostic::new(UserDiagnosticLevel::Info, "Connected to LAN gateway") + } else { + UserDiagnostic::new(UserDiagnosticLevel::Warning, "Waiting for LAN gateway") + }); + + if let Some(message) = tap_ip_user_message(self.tap.ip()) { + diagnostics.push(UserDiagnostic::new(UserDiagnosticLevel::Info, message)); + } + + if self.stats.broadcast_frames_tx() > 0 || self.stats.broadcast_frames_rx() > 0 { + diagnostics.push(UserDiagnostic::new( + UserDiagnosticLevel::Info, + "Broadcast traffic flowing", + )); + } + + diagnostics + } +} + +fn tap_ip_user_message(ip: Option) -> Option { + match ip { + Some(IpAddr::V4(ip)) if !ip.is_link_local() => Some(format!("DHCP received: {ip}")), + Some(ip) => Some(format!("TAP IP detected: {ip}")), + None => None, + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] @@ -505,4 +543,74 @@ mod tests { assert_eq!(stats.broadcast_frames_tx(), 0); assert_eq!(stats.broadcast_frames_rx(), 0); } + + #[test] + fn derives_user_diagnostics_from_client_snapshot() { + let diagnostics = ClientDiagnostics::new( + RelayDiagnostics::new(true, true, true), + QuicDiagnostics::new(true, Some(1400)), + TapDiagnostics::new( + true, + Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])), + Some(1200), + Some("10.73.42.51".parse().unwrap()), + ), + TunnelStats::new(1, 2, 3, 4, 5, 6).with_broadcast_frames(7, 8), + ); + + let user_diagnostics = diagnostics.user_diagnostics(); + let messages: Vec<_> = user_diagnostics + .iter() + .map(UserDiagnostic::message) + .collect(); + + assert_eq!( + messages, + [ + "Connected to relay", + "Connected to LAN gateway", + "DHCP received: 10.73.42.51", + "Broadcast traffic flowing", + ] + ); + assert!( + user_diagnostics + .iter() + .all(|diagnostic| diagnostic.level() == UserDiagnosticLevel::Info) + ); + } + + #[test] + fn reports_user_diagnostic_warnings_for_missing_connections() { + let diagnostics = ClientDiagnostics::new( + RelayDiagnostics::new(false, false, false), + QuicDiagnostics::new(false, None), + TapDiagnostics::new(false, None, None, None), + TunnelStats::default(), + ); + + let user_diagnostics = diagnostics.user_diagnostics(); + + assert_eq!( + user_diagnostics + .iter() + .map(UserDiagnostic::message) + .collect::>(), + ["Relay not reachable", "Waiting for LAN gateway"] + ); + assert_eq!(user_diagnostics[0].level(), UserDiagnosticLevel::Error); + assert_eq!(user_diagnostics[1].level(), UserDiagnosticLevel::Warning); + } + + #[test] + fn avoids_calling_link_local_tap_ip_dhcp() { + assert_eq!( + tap_ip_user_message(Some("169.254.10.20".parse().unwrap())), + Some("TAP IP detected: 169.254.10.20".to_string()) + ); + assert_eq!( + tap_ip_user_message(Some("fe80::1".parse().unwrap())), + Some("TAP IP detected: fe80::1".to_string()) + ); + } }