diff --git a/README.md b/README.md index 5bcc429..1147dd1 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 7d36ee5..a914ab6 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -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 { 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(value: Option) -> 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, +} + +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 { diff --git a/crates/lanparty-obs/src/lib.rs b/crates/lanparty-obs/src/lib.rs index b3740a0..e9e1b75 100644 --- a/crates/lanparty-obs/src/lib.rs +++ b/crates/lanparty-obs/src/lib.rs @@ -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());