feat(client): include gateway presence in diagnostics
PLAN.md calls for user-facing diagnostics that can say whether the remote client is connected to the LAN gateway. Startup already reported the initial welcome bit, but periodic diagnostics only carried relay reachability and route pinning. Add gateway_connected to RelayDiagnostics and seed the client status from the welcome. The client control-event logger now updates that status when gateway join and leave events arrive, so later diagnostics reflect relay lifecycle changes while the tunnel is running. Test Plan: - cargo fmt --check - cargo test -p lanparty-obs -p lanparty-client-win - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md
This commit is contained in:
@@ -202,10 +202,10 @@ 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
|
adapter may need to be disabled/enabled or reinstalled before it reloads the
|
||||||
configured `NetworkAddress`.
|
configured `NetworkAddress`.
|
||||||
It prints and reports client diagnostics snapshots with relay reachability,
|
It prints and reports client diagnostics snapshots with relay reachability,
|
||||||
route-pinning, QUIC datagram budget, TAP status/IP, frame/datagram counters,
|
LAN-gateway presence, route-pinning, QUIC datagram budget, TAP status/IP,
|
||||||
and drops. The periodic diagnostics refresh the TAP unicast IP so DHCP results
|
frame/datagram counters, and drops. The periodic diagnostics refresh the TAP
|
||||||
that arrive after bridging starts become visible in later status lines.
|
unicast IP so DHCP results that arrive after bridging starts become visible in
|
||||||
Relay lifecycle events are logged as they arrive, including gateway joins and
|
later status lines. Relay lifecycle events are logged as they arrive, including
|
||||||
peer leaves. The client remembers peer identities from join and catch-up events
|
gateway joins and peer leaves. The client remembers peer identities from join
|
||||||
so later leave logs can identify a disconnected LAN gateway or client MAC when
|
and catch-up events so later leave logs can identify a disconnected LAN gateway
|
||||||
that peer was known.
|
or client MAC when that peer was known.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
use std::sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
};
|
||||||
use std::{collections::BTreeMap, fs, net::IpAddr, path::PathBuf};
|
use std::{collections::BTreeMap, fs, net::IpAddr, path::PathBuf};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::{
|
use std::{sync::mpsc, thread, time::Duration};
|
||||||
sync::{Arc, mpsc},
|
|
||||||
thread,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -141,6 +141,7 @@ 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 relay_status = ClientRelayStatus::from_welcome(session);
|
||||||
let OpenedTapAdapter {
|
let OpenedTapAdapter {
|
||||||
tap,
|
tap,
|
||||||
tap_diagnostics,
|
tap_diagnostics,
|
||||||
@@ -156,6 +157,7 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute)
|
|||||||
&client_diagnostics_snapshot(
|
&client_diagnostics_snapshot(
|
||||||
session,
|
session,
|
||||||
true,
|
true,
|
||||||
|
&relay_status,
|
||||||
current_tap_diagnostics(&tap_diagnostics, tap_interface),
|
current_tap_diagnostics(&tap_diagnostics, tap_interface),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -166,7 +168,7 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute)
|
|||||||
|
|
||||||
let frame_pump = run_tap_frame_pump(session.relay_io(), tap);
|
let frame_pump = run_tap_frame_pump(session.relay_io(), tap);
|
||||||
tokio::pin!(frame_pump);
|
tokio::pin!(frame_pump);
|
||||||
let control_events = run_control_event_log(session);
|
let control_events = run_control_event_log(session, relay_status.clone());
|
||||||
tokio::pin!(control_events);
|
tokio::pin!(control_events);
|
||||||
let shutdown = tokio::signal::ctrl_c();
|
let shutdown = tokio::signal::ctrl_c();
|
||||||
tokio::pin!(shutdown);
|
tokio::pin!(shutdown);
|
||||||
@@ -199,6 +201,7 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute)
|
|||||||
&client_diagnostics_snapshot(
|
&client_diagnostics_snapshot(
|
||||||
session,
|
session,
|
||||||
true,
|
true,
|
||||||
|
&relay_status,
|
||||||
current_tap_diagnostics(&tap_diagnostics, tap_interface),
|
current_tap_diagnostics(&tap_diagnostics, tap_interface),
|
||||||
),
|
),
|
||||||
).await;
|
).await;
|
||||||
@@ -209,15 +212,21 @@ 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<()> {
|
||||||
|
let relay_status = ClientRelayStatus::from_welcome(session);
|
||||||
open_tap_adapter(session);
|
open_tap_adapter(session);
|
||||||
print_and_report_client_diagnostics(
|
print_and_report_client_diagnostics(
|
||||||
session,
|
session,
|
||||||
&client_diagnostics_snapshot(session, false, TapDiagnostics::new(false, None, None, None)),
|
&client_diagnostics_snapshot(
|
||||||
|
session,
|
||||||
|
false,
|
||||||
|
&relay_status,
|
||||||
|
TapDiagnostics::new(false, None, None, None),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
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");
|
||||||
|
|
||||||
let control_events = run_control_event_log(session);
|
let control_events = run_control_event_log(session, relay_status);
|
||||||
tokio::pin!(control_events);
|
tokio::pin!(control_events);
|
||||||
let shutdown = tokio::signal::ctrl_c();
|
let shutdown = tokio::signal::ctrl_c();
|
||||||
tokio::pin!(shutdown);
|
tokio::pin!(shutdown);
|
||||||
@@ -403,10 +412,11 @@ fn open_configured_tap_adapter(virtual_mac: MacAddr) -> Result<TapAdapter> {
|
|||||||
fn client_diagnostics_snapshot(
|
fn client_diagnostics_snapshot(
|
||||||
session: &ClientSession,
|
session: &ClientSession,
|
||||||
route_pinned: bool,
|
route_pinned: bool,
|
||||||
|
relay_status: &ClientRelayStatus,
|
||||||
tap: TapDiagnostics,
|
tap: TapDiagnostics,
|
||||||
) -> ClientDiagnostics {
|
) -> ClientDiagnostics {
|
||||||
ClientDiagnostics::new(
|
ClientDiagnostics::new(
|
||||||
RelayDiagnostics::new(true, route_pinned),
|
RelayDiagnostics::new(true, route_pinned, relay_status.gateway_connected()),
|
||||||
session.quic_diagnostics(),
|
session.quic_diagnostics(),
|
||||||
tap,
|
tap,
|
||||||
session.stats_snapshot(),
|
session.stats_snapshot(),
|
||||||
@@ -443,8 +453,9 @@ 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 {} route pinned {}; QUIC datagrams {} max {}; TAP found {} MAC {} MTU {} IP {}; frames tx {} rx {} datagrams tx {} rx {} drops {} malformed {}",
|
"client diagnostics: relay reachable {} gateway connected {} 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().reachable()),
|
||||||
|
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()),
|
||||||
@@ -469,7 +480,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())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_control_event_log(session: &ClientSession) -> Result<()> {
|
async fn run_control_event_log(
|
||||||
|
session: &ClientSession,
|
||||||
|
relay_status: ClientRelayStatus,
|
||||||
|
) -> Result<()> {
|
||||||
let mut formatter = ControlEventFormatter::default();
|
let mut formatter = ControlEventFormatter::default();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -477,13 +491,40 @@ async fn run_control_event_log(session: &ClientSession) -> Result<()> {
|
|||||||
.recv_control_event()
|
.recv_control_event()
|
||||||
.await
|
.await
|
||||||
.context("failed to receive relay control event")?;
|
.context("failed to receive relay control event")?;
|
||||||
println!("{}", formatter.format(&event));
|
println!("{}", formatter.format(&event, &relay_status));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn format_control_event(event: &ControlMessage) -> String {
|
fn format_control_event(event: &ControlMessage) -> String {
|
||||||
ControlEventFormatter::default().format(event)
|
let relay_status = ClientRelayStatus::new(false);
|
||||||
|
ControlEventFormatter::default().format(event, &relay_status)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ClientRelayStatus {
|
||||||
|
gateway_connected: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientRelayStatus {
|
||||||
|
fn new(gateway_connected: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
gateway_connected: Arc::new(AtomicBool::new(gateway_connected)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_welcome(session: &ClientSession) -> Self {
|
||||||
|
Self::new(session.welcome().gateway_connected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gateway_connected(&self) -> bool {
|
||||||
|
self.gateway_connected.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_gateway_connected(&self, gateway_connected: bool) {
|
||||||
|
self.gateway_connected
|
||||||
|
.store(gateway_connected, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@@ -492,10 +533,13 @@ struct ControlEventFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ControlEventFormatter {
|
impl ControlEventFormatter {
|
||||||
fn format(&mut self, event: &ControlMessage) -> String {
|
fn format(&mut self, event: &ControlMessage, relay_status: &ClientRelayStatus) -> String {
|
||||||
match event {
|
match event {
|
||||||
ControlMessage::PeerJoined(peer) => {
|
ControlMessage::PeerJoined(peer) => {
|
||||||
self.peers.insert(peer.peer_id(), peer.clone());
|
self.peers.insert(peer.peer_id(), peer.clone());
|
||||||
|
if peer.role() == Role::Gateway {
|
||||||
|
relay_status.set_gateway_connected(true);
|
||||||
|
}
|
||||||
format_peer_joined(peer)
|
format_peer_joined(peer)
|
||||||
}
|
}
|
||||||
ControlMessage::PeerLeft { peer_id, reason } => {
|
ControlMessage::PeerLeft { peer_id, reason } => {
|
||||||
@@ -505,6 +549,7 @@ impl ControlEventFormatter {
|
|||||||
|
|
||||||
match peer.role() {
|
match peer.role() {
|
||||||
Role::Gateway => {
|
Role::Gateway => {
|
||||||
|
relay_status.set_gateway_connected(false);
|
||||||
format!(
|
format!(
|
||||||
"relay event: LAN gateway disconnected (peer {}, {reason:?})",
|
"relay event: LAN gateway disconnected (peer {}, {reason:?})",
|
||||||
peer.peer_id()
|
peer.peer_id()
|
||||||
@@ -798,7 +843,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn formats_client_diagnostics_status_line() {
|
fn formats_client_diagnostics_status_line() {
|
||||||
let diagnostics = ClientDiagnostics::new(
|
let diagnostics = ClientDiagnostics::new(
|
||||||
RelayDiagnostics::new(true, true),
|
RelayDiagnostics::new(true, true, true),
|
||||||
QuicDiagnostics::new(true, Some(1400)),
|
QuicDiagnostics::new(true, Some(1400)),
|
||||||
TapDiagnostics::new(
|
TapDiagnostics::new(
|
||||||
true,
|
true,
|
||||||
@@ -811,14 +856,14 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_client_diagnostics(&diagnostics),
|
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"
|
"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 datagrams tx 3 rx 4 drops 5 malformed 6"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn formats_missing_client_diagnostics_as_unknown() {
|
fn formats_missing_client_diagnostics_as_unknown() {
|
||||||
let diagnostics = ClientDiagnostics::new(
|
let diagnostics = ClientDiagnostics::new(
|
||||||
RelayDiagnostics::new(true, false),
|
RelayDiagnostics::new(true, false, false),
|
||||||
QuicDiagnostics::new(false, None),
|
QuicDiagnostics::new(false, None),
|
||||||
TapDiagnostics::new(false, None, None, None),
|
TapDiagnostics::new(false, None, None, None),
|
||||||
TunnelStats::default(),
|
TunnelStats::default(),
|
||||||
@@ -826,7 +871,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_client_diagnostics(&diagnostics),
|
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"
|
"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 datagrams tx 0 rx 0 drops 0 malformed 0"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,13 +927,15 @@ mod tests {
|
|||||||
reason: DisconnectReason::Normal,
|
reason: DisconnectReason::Normal,
|
||||||
};
|
};
|
||||||
let mut formatter = ControlEventFormatter::default();
|
let mut formatter = ControlEventFormatter::default();
|
||||||
|
let relay_status = ClientRelayStatus::new(false);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.format(&gateway),
|
formatter.format(&gateway, &relay_status),
|
||||||
"relay event: LAN gateway connected as peer 1"
|
"relay event: LAN gateway connected as peer 1"
|
||||||
);
|
);
|
||||||
|
assert!(relay_status.gateway_connected());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.format(&client),
|
formatter.format(&client, &relay_status),
|
||||||
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
|
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -896,13 +943,14 @@ mod tests {
|
|||||||
"relay event: peer 3 left (Normal)"
|
"relay event: peer 3 left (Normal)"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.format(&client_left),
|
formatter.format(&client_left, &relay_status),
|
||||||
"relay event: client peer 2 with MAC 02:00:00:00:00:02 left (Normal)"
|
"relay event: client peer 2 with MAC 02:00:00:00:00:02 left (Normal)"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.format(&gateway_left),
|
formatter.format(&gateway_left, &relay_status),
|
||||||
"relay event: LAN gateway disconnected (peer 1, Normal)"
|
"relay event: LAN gateway disconnected (peer 1, Normal)"
|
||||||
);
|
);
|
||||||
|
assert!(!relay_status.gateway_connected());
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn mac(last_octet: u8) -> MacAddr {
|
const fn mac(last_octet: u8) -> MacAddr {
|
||||||
|
|||||||
@@ -196,14 +196,16 @@ impl TunnelStats {
|
|||||||
pub struct RelayDiagnostics {
|
pub struct RelayDiagnostics {
|
||||||
reachable: bool,
|
reachable: bool,
|
||||||
route_pinned: bool,
|
route_pinned: bool,
|
||||||
|
gateway_connected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RelayDiagnostics {
|
impl RelayDiagnostics {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new(reachable: bool, route_pinned: bool) -> Self {
|
pub const fn new(reachable: bool, route_pinned: bool, gateway_connected: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
reachable,
|
reachable,
|
||||||
route_pinned,
|
route_pinned,
|
||||||
|
gateway_connected,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +218,11 @@ impl RelayDiagnostics {
|
|||||||
pub const fn route_pinned(&self) -> bool {
|
pub const fn route_pinned(&self) -> bool {
|
||||||
self.route_pinned
|
self.route_pinned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn gateway_connected(&self) -> bool {
|
||||||
|
self.gateway_connected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
@@ -428,7 +435,7 @@ mod tests {
|
|||||||
let mac = MacAddr::new([0x02, 1, 2, 3, 4, 5]);
|
let mac = MacAddr::new([0x02, 1, 2, 3, 4, 5]);
|
||||||
let stats = TunnelStats::new(1, 2, 3, 4, 5, 6);
|
let stats = TunnelStats::new(1, 2, 3, 4, 5, 6);
|
||||||
let diagnostics = ClientDiagnostics::new(
|
let diagnostics = ClientDiagnostics::new(
|
||||||
RelayDiagnostics::new(true, true),
|
RelayDiagnostics::new(true, true, true),
|
||||||
QuicDiagnostics::new(true, Some(1400)),
|
QuicDiagnostics::new(true, Some(1400)),
|
||||||
TapDiagnostics::new(
|
TapDiagnostics::new(
|
||||||
true,
|
true,
|
||||||
@@ -441,6 +448,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(diagnostics.relay().reachable());
|
assert!(diagnostics.relay().reachable());
|
||||||
assert!(diagnostics.relay().route_pinned());
|
assert!(diagnostics.relay().route_pinned());
|
||||||
|
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!(diagnostics.tap().adapter_found());
|
assert!(diagnostics.tap().adapter_found());
|
||||||
|
|||||||
Reference in New Issue
Block a user