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
This commit is contained in:
2026-05-21 20:06:41 +02:00
parent 587b0516cd
commit aa9105541f
4 changed files with 97 additions and 2 deletions
+1
View File
@@ -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
+92 -1
View File
@@ -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<u32>,
frame_bytes: &[u8],
action: FrameAction,
drop_reason: Option<lanparty_obs::DropReason>,
) -> 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<PacketSocket>) -> Result<Bytes> {
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),
&ethernet_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();