diff --git a/README.md b/README.md index 1f64a16..2a87768 100644 --- a/README.md +++ b/README.md @@ -285,9 +285,10 @@ also safety-checked before TAP writes, so switch-control traffic, invalid-source frames, jumbo frames, and over-TAP-MTU frames stay out of the Windows adapter even if they reached the client over QUIC. Misdirected unicast frames not addressed to the client's virtual MAC are also -counted and skipped; accepted TAP-to-relay and relay-to-TAP frames are logged -with direction, peer id, MACs, ethertype/length, frame length, action, and drop -reason. TAP device read/write errors still stop the bridge. +counted, skipped, and logged with the drop reason; accepted TAP-to-relay and +relay-to-TAP frames are logged with direction, peer id, MACs, ethertype/length, +frame length, action, and drop reason. TAP device read/write errors still stop +the bridge. 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 and from the initial welcome, so later leave logs can identify a disconnected diff --git a/TESTING.md b/TESTING.md index 613e89d..f8a99b8 100644 --- a/TESTING.md +++ b/TESTING.md @@ -289,8 +289,14 @@ Broadcast sent toward LAN; waiting for LAN broadcast reply LAN broadcast received client frame direction=TapToRelay ... action=Forwarded drop_reason=- client frame direction=RelayToTap ... action=Forwarded drop_reason=- +client frame direction=RelayToTap ... action=Filtered drop_reason=UnknownDestination ``` +A filtered `RelayToTap` line means the client received a relay frame but kept it +out of the TAP adapter. Occasional unrelated unicast can be normal; repeated +filtered DHCP, broadcast, or LAN-game traffic is worth investigating with the +drop reason. + Drops that can be normal during testing: ```text diff --git a/crates/lanparty-client-core/src/lib.rs b/crates/lanparty-client-core/src/lib.rs index 9b4a0a2..edde7e1 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -232,6 +232,19 @@ pub struct ReceivedEthernetFrame { payload: Bytes, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FilteredRelayEthernetFrame { + source_peer_id: u32, + payload: Bytes, + drop_reason: DropReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ClientReceiveOutcome { + Accepted(ReceivedEthernetFrame), + Filtered(FilteredRelayEthernetFrame), +} + impl ReceivedEthernetFrame { #[must_use] pub const fn source_peer_id(&self) -> u32 { @@ -244,6 +257,23 @@ impl ReceivedEthernetFrame { } } +impl FilteredRelayEthernetFrame { + #[must_use] + pub const fn source_peer_id(&self) -> u32 { + self.source_peer_id + } + + #[must_use] + pub fn payload(&self) -> &[u8] { + &self.payload + } + + #[must_use] + pub const fn drop_reason(&self) -> DropReason { + self.drop_reason + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientSendOutcome { Sent, @@ -295,6 +325,10 @@ impl ClientSession { self.relay_io().recv_ethernet().await } + pub async fn recv_ethernet_outcome(&self) -> Result { + self.relay_io().recv_ethernet_outcome().await + } + pub async fn recv_control_event(&self) -> Result { recv_control_event(&self.connection).await } @@ -429,6 +463,15 @@ impl ClientRelayIo { } pub async fn recv_ethernet(&self) -> Result { + loop { + match self.recv_ethernet_outcome().await? { + ClientReceiveOutcome::Accepted(frame) => return Ok(frame), + ClientReceiveOutcome::Filtered(_) => {} + } + } + } + + pub async fn recv_ethernet_outcome(&self) -> Result { loop { let datagram = self.connection.read_datagram().await?; self.stats.record_datagram_rx(); @@ -453,26 +496,38 @@ impl ClientRelayIo { }; self.stats.record_ethernet_rx(ethernet_frame); - if gateway_lan_safety_drop_reason(ethernet_frame).is_some() { + if let Some(drop_reason) = gateway_lan_safety_drop_reason(ethernet_frame) { self.stats.record_dropped_frame(); - continue; + return Ok(ClientReceiveOutcome::Filtered(FilteredRelayEthernetFrame { + source_peer_id: header.peer_id(), + payload: Bytes::copy_from_slice(packet.payload()), + drop_reason: DropReason::from(drop_reason), + })); } if ethernet_frame_exceeds_tap_mtu( ethernet_frame, usize::from(self.welcome.effective_tap_mtu()), ) { self.stats.record_dropped_frame(); - continue; + return Ok(ClientReceiveOutcome::Filtered(FilteredRelayEthernetFrame { + source_peer_id: header.peer_id(), + payload: Bytes::copy_from_slice(packet.payload()), + drop_reason: DropReason::TapMtuExceeded, + })); } if !is_accepted_relay_destination(ethernet_frame, self.virtual_mac) { self.stats.record_dropped_frame(); - continue; + return Ok(ClientReceiveOutcome::Filtered(FilteredRelayEthernetFrame { + source_peer_id: header.peer_id(), + payload: Bytes::copy_from_slice(packet.payload()), + drop_reason: DropReason::UnknownDestination, + })); } - return Ok(ReceivedEthernetFrame { + return Ok(ClientReceiveOutcome::Accepted(ReceivedEthernetFrame { source_peer_id: header.peer_id(), payload: Bytes::copy_from_slice(packet.payload()), - }); + })); } } @@ -943,10 +998,44 @@ mod tests { .unwrap(), ClientSendOutcome::Sent ); - let received = tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet()) - .await - .unwrap() - .unwrap(); + let filtered = + tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet_outcome()) + .await + .unwrap() + .unwrap(); + let ClientReceiveOutcome::Filtered(filtered) = filtered else { + panic!("expected filtered relay frame"); + }; + assert_eq!(filtered.source_peer_id(), 1); + assert_eq!(filtered.drop_reason(), DropReason::ControlPlaneEtherType); + assert_eq!( + filtered.payload(), + control_plane_ethernet_frame().as_slice() + ); + + let filtered = + tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet_outcome()) + .await + .unwrap() + .unwrap(); + let ClientReceiveOutcome::Filtered(filtered) = filtered else { + panic!("expected filtered misdirected relay frame"); + }; + assert_eq!(filtered.source_peer_id(), 1); + assert_eq!(filtered.drop_reason(), DropReason::UnknownDestination); + assert_eq!( + filtered.payload(), + misdirected_unicast_ethernet_frame().as_slice() + ); + + let received = + tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet_outcome()) + .await + .unwrap() + .unwrap(); + let ClientReceiveOutcome::Accepted(received) = received else { + panic!("expected accepted relay frame"); + }; assert_eq!(received.source_peer_id(), 1); assert_eq!( received.payload(), diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index f7da10e..9bb8680 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -8,12 +8,12 @@ use std::{sync::mpsc, thread, time::Duration}; use anyhow::{Context, Result, bail}; use clap::Parser; -#[cfg(windows)] -use lanparty_client_core::ClientRelayIo; use lanparty_client_core::{ ClientIdentity, ClientIdentityStore, ClientSession, ClientSessionConfig, connect_client, }; #[cfg(windows)] +use lanparty_client_core::{ClientReceiveOutcome, ClientRelayIo}; +#[cfg(windows)] use lanparty_client_route::{ IpInterfaceFamily, NetworkInterfaceIdentity, PinnedRelayRoute, RouteSnapshot, ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu, @@ -1115,28 +1115,43 @@ async fn run_tap_frame_pump(relay_io: ClientRelayIo, tap: Arc) -> Re .context("TAP reader thread stopped without reporting an error")?; return Err::<(), _>(error).context("TAP reader thread stopped"); } - relay_frame = relay_io.recv_ethernet() => { - let relay_frame = relay_frame.context("failed to receive relay Ethernet frame")?; - let source_peer_id = relay_frame.source_peer_id(); - let tap = Arc::clone(&tap); - let payload = relay_frame.payload().to_vec(); - let log_payload = payload.clone(); - tokio::task::spawn_blocking(move || { - tap.write_ethernet_frame(&payload) - .context("failed to write relay Ethernet frame to TAP") - }) - .await - .context("TAP writer task panicked")??; - println!( - "{}", - client_frame_log_line( - FrameDirection::RelayToTap, - Some(source_peer_id), - &log_payload, - FrameAction::Forwarded, - None, - ) - ); + relay_frame = relay_io.recv_ethernet_outcome() => { + match relay_frame.context("failed to receive relay Ethernet frame")? { + ClientReceiveOutcome::Accepted(relay_frame) => { + let source_peer_id = relay_frame.source_peer_id(); + let tap = Arc::clone(&tap); + let payload = relay_frame.payload().to_vec(); + let log_payload = payload.clone(); + tokio::task::spawn_blocking(move || { + tap.write_ethernet_frame(&payload) + .context("failed to write relay Ethernet frame to TAP") + }) + .await + .context("TAP writer task panicked")??; + println!( + "{}", + client_frame_log_line( + FrameDirection::RelayToTap, + Some(source_peer_id), + &log_payload, + FrameAction::Forwarded, + None, + ) + ); + } + ClientReceiveOutcome::Filtered(relay_frame) => { + eprintln!( + "{}", + client_frame_log_line( + FrameDirection::RelayToTap, + Some(relay_frame.source_peer_id()), + relay_frame.payload(), + FrameAction::Filtered, + Some(relay_frame.drop_reason()), + ) + ); + } + } } } } @@ -1300,6 +1315,16 @@ mod tests { ), "client frame direction=TapToRelay peer_id=2 src=- dst=- ethertype_or_len=- len=4 action=Dropped drop_reason=Malformed" ); + assert_eq!( + client_frame_log_line( + FrameDirection::RelayToTap, + Some(1), + &frame, + FrameAction::Filtered, + Some(DropReason::UnknownDestination), + ), + "client frame direction=RelayToTap peer_id=1 src=02:00:00:00:00:01 dst=02:00:00:00:00:02 ethertype_or_len=0x0800 len=21 action=Filtered drop_reason=UnknownDestination" + ); } #[test]