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
|
||||
configured `NetworkAddress`.
|
||||
It prints and reports client diagnostics snapshots with relay reachability,
|
||||
route-pinning, QUIC datagram budget, TAP status/IP, 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.
|
||||
LAN-gateway presence, route-pinning, QUIC datagram budget, TAP status/IP,
|
||||
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.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::{collections::BTreeMap, fs, net::IpAddr, path::PathBuf};
|
||||
#[cfg(windows)]
|
||||
use std::{
|
||||
sync::{Arc, mpsc},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{sync::mpsc, thread, time::Duration};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Parser;
|
||||
@@ -141,6 +141,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute) -> Result<()> {
|
||||
let relay_status = ClientRelayStatus::from_welcome(session);
|
||||
let OpenedTapAdapter {
|
||||
tap,
|
||||
tap_diagnostics,
|
||||
@@ -156,6 +157,7 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute)
|
||||
&client_diagnostics_snapshot(
|
||||
session,
|
||||
true,
|
||||
&relay_status,
|
||||
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);
|
||||
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);
|
||||
let shutdown = tokio::signal::ctrl_c();
|
||||
tokio::pin!(shutdown);
|
||||
@@ -199,6 +201,7 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute)
|
||||
&client_diagnostics_snapshot(
|
||||
session,
|
||||
true,
|
||||
&relay_status,
|
||||
current_tap_diagnostics(&tap_diagnostics, tap_interface),
|
||||
),
|
||||
).await;
|
||||
@@ -209,15 +212,21 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute)
|
||||
|
||||
#[cfg(not(windows))]
|
||||
async fn run_client(session: &ClientSession) -> Result<()> {
|
||||
let relay_status = ClientRelayStatus::from_welcome(session);
|
||||
open_tap_adapter(session);
|
||||
print_and_report_client_diagnostics(
|
||||
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;
|
||||
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);
|
||||
let shutdown = tokio::signal::ctrl_c();
|
||||
tokio::pin!(shutdown);
|
||||
@@ -403,10 +412,11 @@ fn open_configured_tap_adapter(virtual_mac: MacAddr) -> Result<TapAdapter> {
|
||||
fn client_diagnostics_snapshot(
|
||||
session: &ClientSession,
|
||||
route_pinned: bool,
|
||||
relay_status: &ClientRelayStatus,
|
||||
tap: TapDiagnostics,
|
||||
) -> ClientDiagnostics {
|
||||
ClientDiagnostics::new(
|
||||
RelayDiagnostics::new(true, route_pinned),
|
||||
RelayDiagnostics::new(true, route_pinned, relay_status.gateway_connected()),
|
||||
session.quic_diagnostics(),
|
||||
tap,
|
||||
session.stats_snapshot(),
|
||||
@@ -443,8 +453,9 @@ async fn print_and_report_client_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 {}",
|
||||
"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().gateway_connected()),
|
||||
yes_no(diagnostics.relay().route_pinned()),
|
||||
yes_no(diagnostics.quic().datagram_supported()),
|
||||
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())
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
loop {
|
||||
@@ -477,13 +491,40 @@ async fn run_control_event_log(session: &ClientSession) -> Result<()> {
|
||||
.recv_control_event()
|
||||
.await
|
||||
.context("failed to receive relay control event")?;
|
||||
println!("{}", formatter.format(&event));
|
||||
println!("{}", formatter.format(&event, &relay_status));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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)]
|
||||
@@ -492,10 +533,13 @@ struct ControlEventFormatter {
|
||||
}
|
||||
|
||||
impl ControlEventFormatter {
|
||||
fn format(&mut self, event: &ControlMessage) -> String {
|
||||
fn format(&mut self, event: &ControlMessage, relay_status: &ClientRelayStatus) -> String {
|
||||
match event {
|
||||
ControlMessage::PeerJoined(peer) => {
|
||||
self.peers.insert(peer.peer_id(), peer.clone());
|
||||
if peer.role() == Role::Gateway {
|
||||
relay_status.set_gateway_connected(true);
|
||||
}
|
||||
format_peer_joined(peer)
|
||||
}
|
||||
ControlMessage::PeerLeft { peer_id, reason } => {
|
||||
@@ -505,6 +549,7 @@ impl ControlEventFormatter {
|
||||
|
||||
match peer.role() {
|
||||
Role::Gateway => {
|
||||
relay_status.set_gateway_connected(false);
|
||||
format!(
|
||||
"relay event: LAN gateway disconnected (peer {}, {reason:?})",
|
||||
peer.peer_id()
|
||||
@@ -798,7 +843,7 @@ mod tests {
|
||||
#[test]
|
||||
fn formats_client_diagnostics_status_line() {
|
||||
let diagnostics = ClientDiagnostics::new(
|
||||
RelayDiagnostics::new(true, true),
|
||||
RelayDiagnostics::new(true, true, true),
|
||||
QuicDiagnostics::new(true, Some(1400)),
|
||||
TapDiagnostics::new(
|
||||
true,
|
||||
@@ -811,14 +856,14 @@ mod tests {
|
||||
|
||||
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"
|
||||
"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]
|
||||
fn formats_missing_client_diagnostics_as_unknown() {
|
||||
let diagnostics = ClientDiagnostics::new(
|
||||
RelayDiagnostics::new(true, false),
|
||||
RelayDiagnostics::new(true, false, false),
|
||||
QuicDiagnostics::new(false, None),
|
||||
TapDiagnostics::new(false, None, None, None),
|
||||
TunnelStats::default(),
|
||||
@@ -826,7 +871,7 @@ mod tests {
|
||||
|
||||
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"
|
||||
"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,
|
||||
};
|
||||
let mut formatter = ControlEventFormatter::default();
|
||||
let relay_status = ClientRelayStatus::new(false);
|
||||
|
||||
assert_eq!(
|
||||
formatter.format(&gateway),
|
||||
formatter.format(&gateway, &relay_status),
|
||||
"relay event: LAN gateway connected as peer 1"
|
||||
);
|
||||
assert!(relay_status.gateway_connected());
|
||||
assert_eq!(
|
||||
formatter.format(&client),
|
||||
formatter.format(&client, &relay_status),
|
||||
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -896,13 +943,14 @@ mod tests {
|
||||
"relay event: peer 3 left (Normal)"
|
||||
);
|
||||
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)"
|
||||
);
|
||||
assert_eq!(
|
||||
formatter.format(&gateway_left),
|
||||
formatter.format(&gateway_left, &relay_status),
|
||||
"relay event: LAN gateway disconnected (peer 1, Normal)"
|
||||
);
|
||||
assert!(!relay_status.gateway_connected());
|
||||
}
|
||||
|
||||
const fn mac(last_octet: u8) -> MacAddr {
|
||||
|
||||
@@ -196,14 +196,16 @@ impl TunnelStats {
|
||||
pub struct RelayDiagnostics {
|
||||
reachable: bool,
|
||||
route_pinned: bool,
|
||||
gateway_connected: bool,
|
||||
}
|
||||
|
||||
impl RelayDiagnostics {
|
||||
#[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 {
|
||||
reachable,
|
||||
route_pinned,
|
||||
gateway_connected,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +218,11 @@ impl RelayDiagnostics {
|
||||
pub const fn route_pinned(&self) -> bool {
|
||||
self.route_pinned
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn gateway_connected(&self) -> bool {
|
||||
self.gateway_connected
|
||||
}
|
||||
}
|
||||
|
||||
#[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 stats = TunnelStats::new(1, 2, 3, 4, 5, 6);
|
||||
let diagnostics = ClientDiagnostics::new(
|
||||
RelayDiagnostics::new(true, true),
|
||||
RelayDiagnostics::new(true, true, true),
|
||||
QuicDiagnostics::new(true, Some(1400)),
|
||||
TapDiagnostics::new(
|
||||
true,
|
||||
@@ -441,6 +448,7 @@ mod tests {
|
||||
|
||||
assert!(diagnostics.relay().reachable());
|
||||
assert!(diagnostics.relay().route_pinned());
|
||||
assert!(diagnostics.relay().gateway_connected());
|
||||
assert!(diagnostics.quic().datagram_supported());
|
||||
assert_eq!(diagnostics.quic().max_datagram_size(), Some(1400));
|
||||
assert!(diagnostics.tap().adapter_found());
|
||||
|
||||
Reference in New Issue
Block a user