diff --git a/README.md b/README.md index 3415d92..1dc92f9 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Platform-neutral remote client relay session: - client hello with room, virtual MAC, and datagram budget - welcome/reject handling with assigned peer id and effective TAP MTU - QUIC DATAGRAM support and negotiated datagram budget diagnostics +- relay RTT diagnostics from the active QUIC connection - reliable relay control-event reads for peer lifecycle messages - Ethernet frame send/receive helpers over QUIC DATAGRAM with budget checks and local drop outcomes for malformed or oversized sends @@ -222,15 +223,16 @@ if the driver-reported MAC does not match the tunnel identity, because an already-initialized Windows TAP adapter may need to be disabled/enabled or reinstalled before it reloads the configured `NetworkAddress`. 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. Each snapshot also emits -short user-facing lines such as relay/gateway connection status, relay-route -and TAP readiness warnings, DHCP address presence, and broadcast-flow -confirmation when those signals are observed. Malformed TAP frames, jumbo -frames, and TAP frames whose encoded datagrams exceed the negotiated QUIC budget -are counted and dropped before relay send without stopping the bridge. +LAN-gateway presence, route-pinning, QUIC datagram budget, relay RTT, 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. Each snapshot also +emits short user-facing lines such as relay/gateway connection status, +relay-route and TAP readiness warnings, DHCP address presence, relay RTT, and +broadcast-flow confirmation when those signals are observed. Malformed TAP +frames, jumbo frames, and TAP frames whose encoded datagrams exceed the +negotiated QUIC budget are counted and dropped before relay send without +stopping the bridge. 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 diff --git a/crates/lanparty-client-core/src/lib.rs b/crates/lanparty-client-core/src/lib.rs index a43ac21..403120c 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -265,8 +265,9 @@ impl ClientSession { } #[must_use] - pub const fn quic_diagnostics(&self) -> QuicDiagnostics { + pub fn quic_diagnostics(&self) -> QuicDiagnostics { QuicDiagnostics::new(true, Some(self.quic_max_datagram_size)) + .with_relay_rtt_ms(Some(duration_millis_saturating(self.connection.rtt()))) } #[must_use] @@ -568,6 +569,10 @@ fn client_bind_addr(relay_addr: SocketAddr) -> SocketAddr { } } +fn duration_millis_saturating(duration: Duration) -> u64 { + u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) +} + async fn request_control_message( connection: &quinn::Connection, message: ControlMessage, @@ -844,6 +849,7 @@ mod tests { client.quic_diagnostics().max_datagram_size(), Some(client.quic_max_datagram_size()) ); + assert!(client.quic_diagnostics().relay_rtt_ms().is_some()); let relay_io = client.relay_io(); assert_eq!(relay_io.welcome().peer_id(), 2); assert_eq!( @@ -933,6 +939,12 @@ mod tests { assert_eq!(snapshot.malformed_frames(), 1); } + #[test] + fn converts_relay_rtt_to_milliseconds() { + assert_eq!(duration_millis_saturating(Duration::from_micros(999)), 0); + assert_eq!(duration_millis_saturating(Duration::from_millis(23)), 23); + } + fn test_server_config() -> (ServerConfig, CertificateDer<'static>) { let certified_key = rcgen::generate_simple_self_signed(vec!["lanparty-relay.local".into()]).unwrap(); diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index e3145b2..fb357eb 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -458,12 +458,13 @@ async fn print_and_report_client_diagnostics( fn format_client_diagnostics(diagnostics: &ClientDiagnostics) -> String { let stats = diagnostics.stats(); format!( - "client diagnostics: relay reachable {} gateway connected {} route pinned {}; QUIC datagrams {} max {}; TAP found {} MAC {} MTU {} IP {}; frames tx {} rx {} broadcast tx {} rx {} datagrams tx {} rx {} drops {} malformed {}", + "client diagnostics: relay reachable {} gateway connected {} route pinned {}; QUIC datagrams {} max {} relay RTT {}; TAP found {} MAC {} MTU {} IP {}; frames tx {} rx {} broadcast tx {} rx {} datagrams tx {} rx {} drops {} malformed {}", yes_no(diagnostics.relay().reachable()), yes_no(diagnostics.relay().gateway_connected()), yes_no(diagnostics.relay().route_pinned()), yes_no(diagnostics.quic().datagram_supported()), optional_label(diagnostics.quic().max_datagram_size()), + optional_millis_label(diagnostics.quic().relay_rtt_ms()), yes_no(diagnostics.tap().adapter_found()), optional_label(diagnostics.tap().mac()), optional_label(diagnostics.tap().mtu()), @@ -487,6 +488,10 @@ fn optional_label(value: Option) -> String { value.map_or_else(|| "unknown".to_string(), |value| value.to_string()) } +fn optional_millis_label(value: Option) -> String { + value.map_or_else(|| "unknown".to_string(), |value| format!("{value} ms")) +} + fn format_user_diagnostic(diagnostic: &UserDiagnostic) -> String { match diagnostic.level() { UserDiagnosticLevel::Info => diagnostic.message().to_owned(), @@ -875,7 +880,7 @@ mod tests { fn formats_client_diagnostics_status_line() { let diagnostics = ClientDiagnostics::new( RelayDiagnostics::new(true, true, true), - QuicDiagnostics::new(true, Some(1400)), + QuicDiagnostics::new(true, Some(1400)).with_relay_rtt_ms(Some(23)), TapDiagnostics::new( true, Some(mac(1)), @@ -887,7 +892,7 @@ mod tests { assert_eq!( format_client_diagnostics(&diagnostics), - "client diagnostics: relay reachable yes gateway connected 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 broadcast tx 7 rx 8 datagrams tx 3 rx 4 drops 5 malformed 6" + "client diagnostics: relay reachable yes gateway connected yes route pinned yes; QUIC datagrams yes max 1400 relay RTT 23 ms; TAP found yes MAC 02:00:00:00:00:01 MTU 1200 IP 10.73.42.51; frames tx 1 rx 2 broadcast tx 7 rx 8 datagrams tx 3 rx 4 drops 5 malformed 6" ); } @@ -902,7 +907,7 @@ mod tests { assert_eq!( format_client_diagnostics(&diagnostics), - "client diagnostics: relay reachable yes gateway connected no route pinned no; QUIC datagrams no max unknown; TAP found no MAC unknown MTU unknown IP unknown; frames tx 0 rx 0 broadcast tx 0 rx 0 datagrams tx 0 rx 0 drops 0 malformed 0" + "client diagnostics: relay reachable yes gateway connected no route pinned no; QUIC datagrams no max unknown relay RTT unknown; TAP found no MAC unknown MTU unknown IP unknown; frames tx 0 rx 0 broadcast tx 0 rx 0 datagrams tx 0 rx 0 drops 0 malformed 0" ); } diff --git a/crates/lanparty-obs/src/lib.rs b/crates/lanparty-obs/src/lib.rs index 6678964..c6094e9 100644 --- a/crates/lanparty-obs/src/lib.rs +++ b/crates/lanparty-obs/src/lib.rs @@ -258,6 +258,8 @@ impl RelayDiagnostics { pub struct QuicDiagnostics { datagram_supported: bool, max_datagram_size: Option, + #[serde(default)] + relay_rtt_ms: Option, } impl QuicDiagnostics { @@ -266,9 +268,16 @@ impl QuicDiagnostics { Self { datagram_supported, max_datagram_size, + relay_rtt_ms: None, } } + #[must_use] + pub const fn with_relay_rtt_ms(mut self, relay_rtt_ms: Option) -> Self { + self.relay_rtt_ms = relay_rtt_ms; + self + } + #[must_use] pub const fn datagram_supported(&self) -> bool { self.datagram_supported @@ -278,6 +287,11 @@ impl QuicDiagnostics { pub const fn max_datagram_size(&self) -> Option { self.max_datagram_size } + + #[must_use] + pub const fn relay_rtt_ms(&self) -> Option { + self.relay_rtt_ms + } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] @@ -408,6 +422,13 @@ impl ClientDiagnostics { diagnostics.push(UserDiagnostic::new(UserDiagnosticLevel::Info, message)); } + if let Some(rtt_ms) = self.quic.relay_rtt_ms() { + diagnostics.push(UserDiagnostic::new( + UserDiagnosticLevel::Info, + format!("Relay RTT: {rtt_ms} ms"), + )); + } + if self.stats.broadcast_frames_tx() > 0 || self.stats.broadcast_frames_rx() > 0 { diagnostics.push(UserDiagnostic::new( UserDiagnosticLevel::Info, @@ -537,6 +558,7 @@ mod tests { assert!(diagnostics.relay().gateway_connected()); assert!(diagnostics.quic().datagram_supported()); assert_eq!(diagnostics.quic().max_datagram_size(), Some(1400)); + assert_eq!(diagnostics.quic().relay_rtt_ms(), None); assert!(diagnostics.tap().adapter_found()); assert_eq!(diagnostics.tap().mac(), Some(mac)); assert_eq!(diagnostics.tap().mtu(), Some(1200)); @@ -564,11 +586,24 @@ mod tests { assert_eq!(stats.broadcast_frames_rx(), 0); } + #[test] + fn defaults_missing_relay_rtt_to_unknown() { + let diagnostics: QuicDiagnostics = serde_json::from_str( + r#"{ + "datagram_supported": true, + "max_datagram_size": 1400 + }"#, + ) + .unwrap(); + + assert_eq!(diagnostics.relay_rtt_ms(), None); + } + #[test] fn derives_user_diagnostics_from_client_snapshot() { let diagnostics = ClientDiagnostics::new( RelayDiagnostics::new(true, true, true), - QuicDiagnostics::new(true, Some(1400)), + QuicDiagnostics::new(true, Some(1400)).with_relay_rtt_ms(Some(23)), TapDiagnostics::new( true, Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])), @@ -590,6 +625,7 @@ mod tests { "Connected to relay", "Connected to LAN gateway", "DHCP received: 10.73.42.51", + "Relay RTT: 23 ms", "Broadcast traffic flowing", ] );