feat(client): log relay lifecycle events

The Windows client now listens for relay control events while the TAP frame pump
is running and logs peer lifecycle updates as they arrive. Gateway joins get a
clear LAN-gateway message, client joins include their virtual MAC, and peer
leaves include the relay-provided reason.

The non-Windows placeholder path also listens for the same events while waiting
for Ctrl-C. That keeps lifecycle diagnostics visible in local relay/client smoke
tests even before the Windows TAP path can be exercised on a real machine.

Test Plan:
- cargo fmt --check
- cargo test -p lanparty-client-win
- cargo clippy -p lanparty-client-win --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check

Refs: PLAN.md
This commit is contained in:
2026-05-21 20:37:56 +02:00
parent 20bed4b45e
commit f4f617ea01
2 changed files with 76 additions and 4 deletions
+2
View File
@@ -165,3 +165,5 @@ policy on exit. Until automatic TAP MAC configuration is wired, startup fails
before bridging if the driver-reported MAC does not match the tunnel identity. before bridging if the driver-reported MAC does not match the tunnel identity.
It prints client diagnostics snapshots with relay reachability, route-pinning, It prints client diagnostics snapshots with relay reachability, route-pinning,
QUIC datagram budget, TAP status/IP, frame/datagram counters, and drops. QUIC datagram budget, TAP status/IP, frame/datagram counters, and drops.
Relay lifecycle events are logged as they arrive, including gateway joins and
peer leaves.
+74 -4
View File
@@ -22,7 +22,7 @@ use lanparty_client_route::{
}; };
#[cfg(windows)] #[cfg(windows)]
use lanparty_client_tap::TapAdapter; use lanparty_client_tap::TapAdapter;
use lanparty_ctrl::RoomCode; use lanparty_ctrl::{ControlMessage, Role, RoomCode};
use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics}; use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics};
use lanparty_proto::MacAddr; use lanparty_proto::MacAddr;
@@ -156,6 +156,8 @@ 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);
tokio::pin!(control_events);
let shutdown = tokio::signal::ctrl_c(); let shutdown = tokio::signal::ctrl_c();
tokio::pin!(shutdown); tokio::pin!(shutdown);
let mut relay_route_check = tokio::time::interval_at( let mut relay_route_check = tokio::time::interval_at(
@@ -172,6 +174,7 @@ async fn run_client(session: &ClientSession, relay_route_pin: &PinnedRelayRoute)
loop { loop {
tokio::select! { tokio::select! {
result = &mut frame_pump => return result, result = &mut frame_pump => return result,
result = &mut control_events => return result,
result = &mut shutdown => return result.context("failed to wait for Ctrl-C"), result = &mut shutdown => return result.context("failed to wait for Ctrl-C"),
_ = relay_route_check.tick() => { _ = relay_route_check.tick() => {
verify_relay_route_is_pinned( verify_relay_route_is_pinned(
@@ -201,9 +204,15 @@ async fn run_client(session: &ClientSession) -> Result<()> {
)); ));
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");
tokio::signal::ctrl_c() let control_events = run_control_event_log(session);
.await tokio::pin!(control_events);
.context("failed to wait for Ctrl-C") let shutdown = tokio::signal::ctrl_c();
tokio::pin!(shutdown);
tokio::select! {
result = &mut control_events => result,
result = &mut shutdown => result.context("failed to wait for Ctrl-C"),
}
} }
#[cfg(windows)] #[cfg(windows)]
@@ -406,6 +415,42 @@ 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<()> {
loop {
let event = session
.recv_control_event()
.await
.context("failed to receive relay control event")?;
println!("{}", format_control_event(&event));
}
}
fn format_control_event(event: &ControlMessage) -> String {
match event {
ControlMessage::PeerJoined(peer) if peer.role() == Role::Gateway => {
format!(
"relay event: LAN gateway connected as peer {}",
peer.peer_id()
)
}
ControlMessage::PeerJoined(peer) => {
let mac = peer
.mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "unknown".to_string());
format!(
"relay event: client peer {} joined with MAC {}",
peer.peer_id(),
mac
)
}
ControlMessage::PeerLeft { peer_id, reason } => {
format!("relay event: peer {peer_id} left ({reason:?})")
}
_ => format!("relay event: {event:?}"),
}
}
#[cfg(windows)] #[cfg(windows)]
fn tap_unicast_ip(identity: NetworkInterfaceIdentity) -> Option<IpAddr> { fn tap_unicast_ip(identity: NetworkInterfaceIdentity) -> Option<IpAddr> {
match lanparty_client_route::interface_unicast_addresses(identity) { match lanparty_client_route::interface_unicast_addresses(identity) {
@@ -635,6 +680,7 @@ fn open_tap_adapter(_session: &ClientSession) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use lanparty_ctrl::{DisconnectReason, PeerInfo};
use lanparty_obs::{QuicDiagnostics, TunnelStats}; use lanparty_obs::{QuicDiagnostics, TunnelStats};
#[test] #[test]
@@ -684,6 +730,30 @@ mod tests {
); );
} }
#[test]
fn formats_relay_lifecycle_events() {
let gateway = ControlMessage::PeerJoined(PeerInfo::new(1, Role::Gateway, None).unwrap());
let client =
ControlMessage::PeerJoined(PeerInfo::new(2, Role::Client, Some(mac(2))).unwrap());
let left = ControlMessage::PeerLeft {
peer_id: 2,
reason: DisconnectReason::Normal,
};
assert_eq!(
format_control_event(&gateway),
"relay event: LAN gateway connected as peer 1"
);
assert_eq!(
format_control_event(&client),
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
);
assert_eq!(
format_control_event(&left),
"relay event: peer 2 left (Normal)"
);
}
const fn mac(last_octet: u8) -> MacAddr { const fn mac(last_octet: u8) -> MacAddr {
MacAddr::new([0x02, 0, 0, 0, 0, last_octet]) MacAddr::new([0x02, 0, 0, 0, 0, last_octet])
} }