feat(client): log filtered relay-to-TAP frames
Windows MVP debugging needs more than aggregate drop counters when LAN traffic reaches the client but is kept out of the TAP adapter. A DHCP or discovery failure is much easier to diagnose when the client log says which relayed frame was filtered and why. Expose a client receive outcome that preserves the existing accepted-frame API while allowing the Windows frame pump to log filtered RelayToTap frames with the source peer and drop reason. Document the new log signal in the README and manual MVP test guide. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-core connects_to_relay_control_stream_as_client - cargo test -p lanparty-client-win formats_client_frame_log_lines - cargo test -p lanparty-client-core - cargo test -p lanparty-client-win - cargo test --workspace - cargo clippy -p lanparty-client-core --all-targets -- -D warnings - cargo clippy -p lanparty-client-win --all-targets -- -D warnings - cargo clippy --workspace --all-targets -- -D warnings - git diff --check - git diff --cached --check Windows-target check attempted: - cargo check -p lanparty-client-win --target x86_64-pc-windows-msvc The Windows-target check is still blocked on this Linux host before compiling lanparty-client-win because ring cannot find the MSVC lib.exe tool. Refs: MVP client diagnostics
This commit is contained in:
@@ -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
|
invalid-source frames, jumbo frames, and over-TAP-MTU frames stay out of the
|
||||||
Windows adapter even if they reached the client over QUIC.
|
Windows adapter even if they reached the client over QUIC.
|
||||||
Misdirected unicast frames not addressed to the client's virtual MAC are also
|
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
|
counted, skipped, and logged with the drop reason; accepted TAP-to-relay and
|
||||||
with direction, peer id, MACs, ethertype/length, frame length, action, and drop
|
relay-to-TAP frames are logged with direction, peer id, MACs, ethertype/length,
|
||||||
reason. TAP device read/write errors still stop the bridge.
|
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
|
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
|
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
|
and from the initial welcome, so later leave logs can identify a disconnected
|
||||||
|
|||||||
@@ -289,8 +289,14 @@ Broadcast sent toward LAN; waiting for LAN broadcast reply
|
|||||||
LAN broadcast received
|
LAN broadcast received
|
||||||
client frame direction=TapToRelay ... action=Forwarded drop_reason=-
|
client frame direction=TapToRelay ... action=Forwarded drop_reason=-
|
||||||
client frame direction=RelayToTap ... 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:
|
Drops that can be normal during testing:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -232,6 +232,19 @@ pub struct ReceivedEthernetFrame {
|
|||||||
payload: Bytes,
|
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 {
|
impl ReceivedEthernetFrame {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn source_peer_id(&self) -> u32 {
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ClientSendOutcome {
|
pub enum ClientSendOutcome {
|
||||||
Sent,
|
Sent,
|
||||||
@@ -295,6 +325,10 @@ impl ClientSession {
|
|||||||
self.relay_io().recv_ethernet().await
|
self.relay_io().recv_ethernet().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn recv_ethernet_outcome(&self) -> Result<ClientReceiveOutcome> {
|
||||||
|
self.relay_io().recv_ethernet_outcome().await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn recv_control_event(&self) -> Result<ControlMessage> {
|
pub async fn recv_control_event(&self) -> Result<ControlMessage> {
|
||||||
recv_control_event(&self.connection).await
|
recv_control_event(&self.connection).await
|
||||||
}
|
}
|
||||||
@@ -429,6 +463,15 @@ impl ClientRelayIo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
|
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
|
||||||
|
loop {
|
||||||
|
match self.recv_ethernet_outcome().await? {
|
||||||
|
ClientReceiveOutcome::Accepted(frame) => return Ok(frame),
|
||||||
|
ClientReceiveOutcome::Filtered(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recv_ethernet_outcome(&self) -> Result<ClientReceiveOutcome> {
|
||||||
loop {
|
loop {
|
||||||
let datagram = self.connection.read_datagram().await?;
|
let datagram = self.connection.read_datagram().await?;
|
||||||
self.stats.record_datagram_rx();
|
self.stats.record_datagram_rx();
|
||||||
@@ -453,26 +496,38 @@ impl ClientRelayIo {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.stats.record_ethernet_rx(ethernet_frame);
|
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();
|
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(
|
if ethernet_frame_exceeds_tap_mtu(
|
||||||
ethernet_frame,
|
ethernet_frame,
|
||||||
usize::from(self.welcome.effective_tap_mtu()),
|
usize::from(self.welcome.effective_tap_mtu()),
|
||||||
) {
|
) {
|
||||||
self.stats.record_dropped_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::TapMtuExceeded,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
if !is_accepted_relay_destination(ethernet_frame, self.virtual_mac) {
|
if !is_accepted_relay_destination(ethernet_frame, self.virtual_mac) {
|
||||||
self.stats.record_dropped_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::UnknownDestination,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(ReceivedEthernetFrame {
|
return Ok(ClientReceiveOutcome::Accepted(ReceivedEthernetFrame {
|
||||||
source_peer_id: header.peer_id(),
|
source_peer_id: header.peer_id(),
|
||||||
payload: Bytes::copy_from_slice(packet.payload()),
|
payload: Bytes::copy_from_slice(packet.payload()),
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,10 +998,44 @@ mod tests {
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
ClientSendOutcome::Sent
|
ClientSendOutcome::Sent
|
||||||
);
|
);
|
||||||
let received = tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet())
|
let filtered =
|
||||||
.await
|
tokio::time::timeout(Duration::from_secs(5), relay_io.recv_ethernet_outcome())
|
||||||
.unwrap()
|
.await
|
||||||
.unwrap();
|
.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.source_peer_id(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
received.payload(),
|
received.payload(),
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ use std::{sync::mpsc, thread, time::Duration};
|
|||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
#[cfg(windows)]
|
|
||||||
use lanparty_client_core::ClientRelayIo;
|
|
||||||
use lanparty_client_core::{
|
use lanparty_client_core::{
|
||||||
ClientIdentity, ClientIdentityStore, ClientSession, ClientSessionConfig, connect_client,
|
ClientIdentity, ClientIdentityStore, ClientSession, ClientSessionConfig, connect_client,
|
||||||
};
|
};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
use lanparty_client_core::{ClientReceiveOutcome, ClientRelayIo};
|
||||||
|
#[cfg(windows)]
|
||||||
use lanparty_client_route::{
|
use lanparty_client_route::{
|
||||||
IpInterfaceFamily, NetworkInterfaceIdentity, PinnedRelayRoute, RouteSnapshot,
|
IpInterfaceFamily, NetworkInterfaceIdentity, PinnedRelayRoute, RouteSnapshot,
|
||||||
ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu,
|
ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu,
|
||||||
@@ -1115,28 +1115,43 @@ async fn run_tap_frame_pump(relay_io: ClientRelayIo, tap: Arc<TapAdapter>) -> Re
|
|||||||
.context("TAP reader thread stopped without reporting an error")?;
|
.context("TAP reader thread stopped without reporting an error")?;
|
||||||
return Err::<(), _>(error).context("TAP reader thread stopped");
|
return Err::<(), _>(error).context("TAP reader thread stopped");
|
||||||
}
|
}
|
||||||
relay_frame = relay_io.recv_ethernet() => {
|
relay_frame = relay_io.recv_ethernet_outcome() => {
|
||||||
let relay_frame = relay_frame.context("failed to receive relay Ethernet frame")?;
|
match relay_frame.context("failed to receive relay Ethernet frame")? {
|
||||||
let source_peer_id = relay_frame.source_peer_id();
|
ClientReceiveOutcome::Accepted(relay_frame) => {
|
||||||
let tap = Arc::clone(&tap);
|
let source_peer_id = relay_frame.source_peer_id();
|
||||||
let payload = relay_frame.payload().to_vec();
|
let tap = Arc::clone(&tap);
|
||||||
let log_payload = payload.clone();
|
let payload = relay_frame.payload().to_vec();
|
||||||
tokio::task::spawn_blocking(move || {
|
let log_payload = payload.clone();
|
||||||
tap.write_ethernet_frame(&payload)
|
tokio::task::spawn_blocking(move || {
|
||||||
.context("failed to write relay Ethernet frame to TAP")
|
tap.write_ethernet_frame(&payload)
|
||||||
})
|
.context("failed to write relay Ethernet frame to TAP")
|
||||||
.await
|
})
|
||||||
.context("TAP writer task panicked")??;
|
.await
|
||||||
println!(
|
.context("TAP writer task panicked")??;
|
||||||
"{}",
|
println!(
|
||||||
client_frame_log_line(
|
"{}",
|
||||||
FrameDirection::RelayToTap,
|
client_frame_log_line(
|
||||||
Some(source_peer_id),
|
FrameDirection::RelayToTap,
|
||||||
&log_payload,
|
Some(source_peer_id),
|
||||||
FrameAction::Forwarded,
|
&log_payload,
|
||||||
None,
|
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"
|
"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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user