From aa9105541fefdaefd614087b5c4bbd78151b8314 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 20:06:41 +0200 Subject: [PATCH] feat(gateway): log bridged Ethernet frames The gateway bridge now emits structured frame log lines for successful LAN to relay and relay to LAN forwarding. Logs include the physical interface, direction, peer id when one is known, MACs, ethertype or length field, frame length, action, and drop reason. This uses the shared `lanparty-obs` frame vocabulary instead of adding a second ad hoc diagnostics model to the gateway. The log line stays local to the gateway because the relay still owns its own room/target-specific formatting. Test Plan: - cargo fmt --check - cargo test -p lanparty-gateway - cargo clippy -p lanparty-gateway --all-targets -- -D warnings - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md --- Cargo.lock | 1 + README.md | 4 +- crates/lanparty-gateway/Cargo.toml | 1 + crates/lanparty-gateway/src/lib.rs | 93 +++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fd01cc..7da62d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -496,6 +496,7 @@ dependencies = [ "bytes", "clap", "lanparty-ctrl", + "lanparty-obs", "lanparty-proto", "libc", "quinn", diff --git a/README.md b/README.md index 85f30bc..6a1eac8 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,9 @@ control-stream hello/welcome handshake, opens an AF_PACKET socket on the LAN interface with promiscuous packet membership, and bridges Ethernet frames between the relay and wired LAN until shutdown. It tracks remote-client source MACs seen from relay traffic and periodically emits small CAM refresh frames so -the physical switch keeps those MACs associated with the gateway port. +the physical switch keeps those MACs associated with the gateway port. Gateway +frame logs include direction, peer id when present, MACs, ethertype/length, +frame length, action, and drop reason. ## Windows Client diff --git a/crates/lanparty-gateway/Cargo.toml b/crates/lanparty-gateway/Cargo.toml index a99fa8f..195db59 100644 --- a/crates/lanparty-gateway/Cargo.toml +++ b/crates/lanparty-gateway/Cargo.toml @@ -8,6 +8,7 @@ anyhow.workspace = true bytes.workspace = true clap.workspace = true lanparty-ctrl = { path = "../lanparty-ctrl" } +lanparty-obs = { path = "../lanparty-obs" } lanparty-proto = { path = "../lanparty-proto" } libc.workspace = true quinn.workspace = true diff --git a/crates/lanparty-gateway/src/lib.rs b/crates/lanparty-gateway/src/lib.rs index 3973232..32a0449 100644 --- a/crates/lanparty-gateway/src/lib.rs +++ b/crates/lanparty-gateway/src/lib.rs @@ -22,6 +22,8 @@ use lanparty_ctrl::{ CONTROL_LENGTH_PREFIX_LEN, ControlMessage, EndpointHello, MAX_CONTROL_MESSAGE_LEN, RELAY_ALPN, RoomCode, ServerWelcome, decode_control_frame, encode_control_message, }; +#[cfg(target_os = "linux")] +use lanparty_obs::{FrameAction, FrameDirection, FrameLog}; use lanparty_proto::{ EthernetFrame, FrameType, MAX_STANDARD_ETHERNET_FRAME_LEN, MacAddr, decode_datagram, encode_datagram, @@ -244,12 +246,35 @@ impl GatewayConnection { return Ok(()); } lan_frame = read_lan_ethernet(&packet_socket) => { - send_gateway_ethernet(&connection, &welcome, &lan_frame?)?; + let lan_frame = lan_frame?; + send_gateway_ethernet(&connection, &welcome, &lan_frame)?; + println!( + "{}", + gateway_frame_log_line( + packet_socket.get_ref().interface(), + FrameDirection::LanToRemote, + Some(welcome.peer_id()), + &lan_frame, + FrameAction::Forwarded, + None, + ) + ); } relay_frame = recv_gateway_ethernet(&connection, &welcome) => { let relay_frame = relay_frame?; cam_refresh.observe_remote_frame(relay_frame.payload())?; write_lan_ethernet(&packet_socket, relay_frame.payload()).await?; + println!( + "{}", + gateway_frame_log_line( + packet_socket.get_ref().interface(), + FrameDirection::RemoteToLan, + Some(relay_frame.source_peer_id()), + relay_frame.payload(), + FrameAction::Forwarded, + None, + ) + ); } _ = cam_refresh_tick.tick() => { for frame in cam_refresh.refresh_frames() { @@ -315,6 +340,54 @@ async fn recv_gateway_ethernet( } } +#[cfg(target_os = "linux")] +fn gateway_frame_log_line( + interface: &str, + direction: FrameDirection, + peer_id: Option, + frame_bytes: &[u8], + action: FrameAction, + drop_reason: Option, +) -> String { + let log = match EthernetFrame::parse(frame_bytes) { + Ok(frame) => FrameLog::from_ethernet(direction, peer_id, action, drop_reason, frame), + Err(_) => FrameLog::malformed(direction, peer_id, frame_bytes.len()), + }; + let source_mac = log + .source_mac() + .map(|mac| mac.to_string()) + .unwrap_or_else(|| "-".to_owned()); + let destination_mac = log + .destination_mac() + .map(|mac| mac.to_string()) + .unwrap_or_else(|| "-".to_owned()); + let ethertype_or_len = log + .ethertype_or_len() + .map(|value| format!("0x{value:04x}")) + .unwrap_or_else(|| "-".to_owned()); + let peer_id = log + .peer_id() + .map(|peer_id| peer_id.to_string()) + .unwrap_or_else(|| "-".to_owned()); + let drop_reason = log + .drop_reason() + .map(|reason| format!("{reason:?}")) + .unwrap_or_else(|| "-".to_owned()); + + format!( + "gateway frame interface={} direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={}", + interface, + log.direction(), + peer_id, + source_mac, + destination_mac, + ethertype_or_len, + log.frame_len(), + log.action(), + drop_reason, + ) +} + #[cfg(target_os = "linux")] async fn read_lan_ethernet(packet_socket: &AsyncFd) -> Result { loop { @@ -656,6 +729,24 @@ mod tests { assert_eq!(refresh_frame.destination(), gateway_mac); } + #[cfg(target_os = "linux")] + #[test] + fn formats_gateway_frame_log_lines() { + let line = gateway_frame_log_line( + "eth0", + FrameDirection::RemoteToLan, + Some(7), + ðernet_frame(b"payload"), + FrameAction::Forwarded, + None, + ); + + assert_eq!( + line, + "gateway frame interface=eth0 direction=RemoteToLan peer_id=7 src=02:00:00:00:00:01 dst=02:00:00:00:00:02 ethertype_or_len=0x0800 len=21 action=Forwarded drop_reason=-" + ); + } + fn test_server_config() -> (ServerConfig, CertificateDer<'static>) { let certified_key = rcgen::generate_simple_self_signed(vec!["lanparty-relay.local".into()]).unwrap();