feat(client): report relay RTT diagnostics
Expose the active QUIC connection RTT in shared client diagnostics and in the Windows client status line. This gives operators a live relay-path latency signal without pretending to measure end-to-end gateway or LAN latency. The new diagnostics field defaults to unknown when older JSON snapshots omit it, so consumers can read pre-change snapshots without special migration code. The user-facing diagnostics now print Relay RTT only when the client has an active QUIC measurement. Test Plan: - cargo fmt --check - git diff --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings Refs: PLAN.md logging and diagnostics section
This commit is contained in:
@@ -59,6 +59,7 @@ Platform-neutral remote client relay session:
|
|||||||
- client hello with room, virtual MAC, and datagram budget
|
- client hello with room, virtual MAC, and datagram budget
|
||||||
- welcome/reject handling with assigned peer id and effective TAP MTU
|
- welcome/reject handling with assigned peer id and effective TAP MTU
|
||||||
- QUIC DATAGRAM support and negotiated datagram budget diagnostics
|
- 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
|
- reliable relay control-event reads for peer lifecycle messages
|
||||||
- Ethernet frame send/receive helpers over QUIC DATAGRAM with budget checks and
|
- Ethernet frame send/receive helpers over QUIC DATAGRAM with budget checks and
|
||||||
local drop outcomes for malformed or oversized sends
|
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
|
already-initialized Windows TAP adapter may need to be disabled/enabled or
|
||||||
reinstalled before it reloads the configured `NetworkAddress`.
|
reinstalled before it reloads the configured `NetworkAddress`.
|
||||||
It prints and reports client diagnostics snapshots with relay reachability,
|
It prints and reports client diagnostics snapshots with relay reachability,
|
||||||
LAN-gateway presence, route-pinning, QUIC datagram budget, TAP status/IP,
|
LAN-gateway presence, route-pinning, QUIC datagram budget, relay RTT, TAP
|
||||||
broadcast frame flow, frame/datagram counters, and drops. The periodic
|
status/IP, broadcast frame flow, frame/datagram counters, and drops. The
|
||||||
diagnostics refresh the TAP unicast IP so DHCP results that arrive after
|
periodic diagnostics refresh the TAP unicast IP so DHCP results that arrive
|
||||||
bridging starts become visible in later status lines. Each snapshot also emits
|
after bridging starts become visible in later status lines. Each snapshot also
|
||||||
short user-facing lines such as relay/gateway connection status, relay-route
|
emits short user-facing lines such as relay/gateway connection status,
|
||||||
and TAP readiness warnings, DHCP address presence, and broadcast-flow
|
relay-route and TAP readiness warnings, DHCP address presence, relay RTT, and
|
||||||
confirmation when those signals are observed. Malformed TAP frames, jumbo
|
broadcast-flow confirmation when those signals are observed. Malformed TAP
|
||||||
frames, and TAP frames whose encoded datagrams exceed the negotiated QUIC budget
|
frames, jumbo frames, and TAP frames whose encoded datagrams exceed the
|
||||||
are counted and dropped before relay send without stopping the bridge.
|
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
|
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
|
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
|
so later leave logs can identify a disconnected LAN gateway or client MAC when
|
||||||
|
|||||||
@@ -265,8 +265,9 @@ impl ClientSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn quic_diagnostics(&self) -> QuicDiagnostics {
|
pub fn quic_diagnostics(&self) -> QuicDiagnostics {
|
||||||
QuicDiagnostics::new(true, Some(self.quic_max_datagram_size))
|
QuicDiagnostics::new(true, Some(self.quic_max_datagram_size))
|
||||||
|
.with_relay_rtt_ms(Some(duration_millis_saturating(self.connection.rtt())))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[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(
|
async fn request_control_message(
|
||||||
connection: &quinn::Connection,
|
connection: &quinn::Connection,
|
||||||
message: ControlMessage,
|
message: ControlMessage,
|
||||||
@@ -844,6 +849,7 @@ mod tests {
|
|||||||
client.quic_diagnostics().max_datagram_size(),
|
client.quic_diagnostics().max_datagram_size(),
|
||||||
Some(client.quic_max_datagram_size())
|
Some(client.quic_max_datagram_size())
|
||||||
);
|
);
|
||||||
|
assert!(client.quic_diagnostics().relay_rtt_ms().is_some());
|
||||||
let relay_io = client.relay_io();
|
let relay_io = client.relay_io();
|
||||||
assert_eq!(relay_io.welcome().peer_id(), 2);
|
assert_eq!(relay_io.welcome().peer_id(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -933,6 +939,12 @@ mod tests {
|
|||||||
assert_eq!(snapshot.malformed_frames(), 1);
|
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>) {
|
fn test_server_config() -> (ServerConfig, CertificateDer<'static>) {
|
||||||
let certified_key =
|
let certified_key =
|
||||||
rcgen::generate_simple_self_signed(vec!["lanparty-relay.local".into()]).unwrap();
|
rcgen::generate_simple_self_signed(vec!["lanparty-relay.local".into()]).unwrap();
|
||||||
|
|||||||
@@ -458,12 +458,13 @@ async fn print_and_report_client_diagnostics(
|
|||||||
fn format_client_diagnostics(diagnostics: &ClientDiagnostics) -> String {
|
fn format_client_diagnostics(diagnostics: &ClientDiagnostics) -> String {
|
||||||
let stats = diagnostics.stats();
|
let stats = diagnostics.stats();
|
||||||
format!(
|
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().reachable()),
|
||||||
yes_no(diagnostics.relay().gateway_connected()),
|
yes_no(diagnostics.relay().gateway_connected()),
|
||||||
yes_no(diagnostics.relay().route_pinned()),
|
yes_no(diagnostics.relay().route_pinned()),
|
||||||
yes_no(diagnostics.quic().datagram_supported()),
|
yes_no(diagnostics.quic().datagram_supported()),
|
||||||
optional_label(diagnostics.quic().max_datagram_size()),
|
optional_label(diagnostics.quic().max_datagram_size()),
|
||||||
|
optional_millis_label(diagnostics.quic().relay_rtt_ms()),
|
||||||
yes_no(diagnostics.tap().adapter_found()),
|
yes_no(diagnostics.tap().adapter_found()),
|
||||||
optional_label(diagnostics.tap().mac()),
|
optional_label(diagnostics.tap().mac()),
|
||||||
optional_label(diagnostics.tap().mtu()),
|
optional_label(diagnostics.tap().mtu()),
|
||||||
@@ -487,6 +488,10 @@ fn optional_label<T: std::fmt::Display>(value: Option<T>) -> String {
|
|||||||
value.map_or_else(|| "unknown".to_string(), |value| value.to_string())
|
value.map_or_else(|| "unknown".to_string(), |value| value.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn optional_millis_label(value: Option<u64>) -> String {
|
||||||
|
value.map_or_else(|| "unknown".to_string(), |value| format!("{value} ms"))
|
||||||
|
}
|
||||||
|
|
||||||
fn format_user_diagnostic(diagnostic: &UserDiagnostic) -> String {
|
fn format_user_diagnostic(diagnostic: &UserDiagnostic) -> String {
|
||||||
match diagnostic.level() {
|
match diagnostic.level() {
|
||||||
UserDiagnosticLevel::Info => diagnostic.message().to_owned(),
|
UserDiagnosticLevel::Info => diagnostic.message().to_owned(),
|
||||||
@@ -875,7 +880,7 @@ mod tests {
|
|||||||
fn formats_client_diagnostics_status_line() {
|
fn formats_client_diagnostics_status_line() {
|
||||||
let diagnostics = ClientDiagnostics::new(
|
let diagnostics = ClientDiagnostics::new(
|
||||||
RelayDiagnostics::new(true, true, true),
|
RelayDiagnostics::new(true, true, true),
|
||||||
QuicDiagnostics::new(true, Some(1400)),
|
QuicDiagnostics::new(true, Some(1400)).with_relay_rtt_ms(Some(23)),
|
||||||
TapDiagnostics::new(
|
TapDiagnostics::new(
|
||||||
true,
|
true,
|
||||||
Some(mac(1)),
|
Some(mac(1)),
|
||||||
@@ -887,7 +892,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_client_diagnostics(&diagnostics),
|
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!(
|
assert_eq!(
|
||||||
format_client_diagnostics(&diagnostics),
|
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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,6 +258,8 @@ impl RelayDiagnostics {
|
|||||||
pub struct QuicDiagnostics {
|
pub struct QuicDiagnostics {
|
||||||
datagram_supported: bool,
|
datagram_supported: bool,
|
||||||
max_datagram_size: Option<u16>,
|
max_datagram_size: Option<u16>,
|
||||||
|
#[serde(default)]
|
||||||
|
relay_rtt_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QuicDiagnostics {
|
impl QuicDiagnostics {
|
||||||
@@ -266,9 +268,16 @@ impl QuicDiagnostics {
|
|||||||
Self {
|
Self {
|
||||||
datagram_supported,
|
datagram_supported,
|
||||||
max_datagram_size,
|
max_datagram_size,
|
||||||
|
relay_rtt_ms: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn with_relay_rtt_ms(mut self, relay_rtt_ms: Option<u64>) -> Self {
|
||||||
|
self.relay_rtt_ms = relay_rtt_ms;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn datagram_supported(&self) -> bool {
|
pub const fn datagram_supported(&self) -> bool {
|
||||||
self.datagram_supported
|
self.datagram_supported
|
||||||
@@ -278,6 +287,11 @@ impl QuicDiagnostics {
|
|||||||
pub const fn max_datagram_size(&self) -> Option<u16> {
|
pub const fn max_datagram_size(&self) -> Option<u16> {
|
||||||
self.max_datagram_size
|
self.max_datagram_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn relay_rtt_ms(&self) -> Option<u64> {
|
||||||
|
self.relay_rtt_ms
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
@@ -408,6 +422,13 @@ impl ClientDiagnostics {
|
|||||||
diagnostics.push(UserDiagnostic::new(UserDiagnosticLevel::Info, message));
|
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 {
|
if self.stats.broadcast_frames_tx() > 0 || self.stats.broadcast_frames_rx() > 0 {
|
||||||
diagnostics.push(UserDiagnostic::new(
|
diagnostics.push(UserDiagnostic::new(
|
||||||
UserDiagnosticLevel::Info,
|
UserDiagnosticLevel::Info,
|
||||||
@@ -537,6 +558,7 @@ mod tests {
|
|||||||
assert!(diagnostics.relay().gateway_connected());
|
assert!(diagnostics.relay().gateway_connected());
|
||||||
assert!(diagnostics.quic().datagram_supported());
|
assert!(diagnostics.quic().datagram_supported());
|
||||||
assert_eq!(diagnostics.quic().max_datagram_size(), Some(1400));
|
assert_eq!(diagnostics.quic().max_datagram_size(), Some(1400));
|
||||||
|
assert_eq!(diagnostics.quic().relay_rtt_ms(), None);
|
||||||
assert!(diagnostics.tap().adapter_found());
|
assert!(diagnostics.tap().adapter_found());
|
||||||
assert_eq!(diagnostics.tap().mac(), Some(mac));
|
assert_eq!(diagnostics.tap().mac(), Some(mac));
|
||||||
assert_eq!(diagnostics.tap().mtu(), Some(1200));
|
assert_eq!(diagnostics.tap().mtu(), Some(1200));
|
||||||
@@ -564,11 +586,24 @@ mod tests {
|
|||||||
assert_eq!(stats.broadcast_frames_rx(), 0);
|
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]
|
#[test]
|
||||||
fn derives_user_diagnostics_from_client_snapshot() {
|
fn derives_user_diagnostics_from_client_snapshot() {
|
||||||
let diagnostics = ClientDiagnostics::new(
|
let diagnostics = ClientDiagnostics::new(
|
||||||
RelayDiagnostics::new(true, true, true),
|
RelayDiagnostics::new(true, true, true),
|
||||||
QuicDiagnostics::new(true, Some(1400)),
|
QuicDiagnostics::new(true, Some(1400)).with_relay_rtt_ms(Some(23)),
|
||||||
TapDiagnostics::new(
|
TapDiagnostics::new(
|
||||||
true,
|
true,
|
||||||
Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])),
|
Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])),
|
||||||
@@ -590,6 +625,7 @@ mod tests {
|
|||||||
"Connected to relay",
|
"Connected to relay",
|
||||||
"Connected to LAN gateway",
|
"Connected to LAN gateway",
|
||||||
"DHCP received: 10.73.42.51",
|
"DHCP received: 10.73.42.51",
|
||||||
|
"Relay RTT: 23 ms",
|
||||||
"Broadcast traffic flowing",
|
"Broadcast traffic flowing",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user