feat(relay): log Ethernet forwarding decisions

Phase 1 needs noisy frame diagnostics while the tunnel is being proven on real
LANs. The relay already had forwarding/drop decisions, but the runtime did not
emit the MAC-level fields needed to understand what happened to each frame.

Print one relay ingress log line for every accepted Ethernet datagram after the
room registry decides whether to forward or drop it. The line includes room,
peer id, source/destination MACs, ethertype or length, frame length, action,
drop reason, and target count using the shared diagnostics vocabulary.

This keeps logging simple stdout text for now. A later product slice can route
the same `lanparty-obs` fields through tracing or JSON logs without changing the
forwarding rules.

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

Refs: PLAN.md logging diagnostics
This commit is contained in:
2026-05-21 18:37:51 +02:00
parent a3d24a1173
commit c07e49581c
2 changed files with 87 additions and 3 deletions
+85 -2
View File
@@ -7,14 +7,15 @@ use lanparty_ctrl::{
MAX_CONTROL_MESSAGE_LEN, PeerInfo, RELAY_ALPN, Reject, RejectReason, Role, RoomCode,
ServerWelcome, decode_control_frame, encode_control_message,
};
use lanparty_proto::{FrameType, decode_datagram, encode_datagram};
use lanparty_obs::{FrameDirection, FrameLog};
use lanparty_proto::{EthernetFrame, FrameType, decode_datagram, encode_datagram};
use quinn::crypto::rustls::QuicServerConfig;
use quinn::{Endpoint, Incoming, SendStream, ServerConfig, TransportConfig};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::collections::HashMap;
use tokio::sync::Mutex;
use crate::{RelayConfig, RoomRegistry};
use crate::{ForwardingDecision, RelayConfig, RoomRegistry};
const DATAGRAM_BUFFER_BYTES: usize = 4 * 1024 * 1024;
const MAX_CONTROL_FRAME_LEN: usize = CONTROL_LENGTH_PREFIX_LEN + MAX_CONTROL_MESSAGE_LEN;
@@ -278,6 +279,15 @@ async fn forward_peer_datagram(
accepted.peer.peer_id(),
packet.payload(),
)?;
println!(
"{}",
relay_frame_log_line(
&accepted.room,
accepted.peer.peer_id(),
packet.payload(),
&decision
)
);
let target_peer_ids = decision.targets().to_vec();
if target_peer_ids.is_empty() {
return Ok(());
@@ -312,6 +322,58 @@ async fn forward_peer_datagram(
Ok(())
}
fn relay_frame_log_line(
room: &RoomCode,
ingress_peer_id: u32,
frame_bytes: &[u8],
decision: &ForwardingDecision,
) -> String {
let log = match EthernetFrame::parse(frame_bytes) {
Ok(frame) => FrameLog::from_ethernet(
FrameDirection::RelayIngress,
Some(ingress_peer_id),
decision.action(),
decision.drop_reason(),
frame,
),
Err(_) => FrameLog::malformed(
FrameDirection::RelayIngress,
Some(ingress_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 drop_reason = log
.drop_reason()
.map(|reason| format!("{reason:?}"))
.unwrap_or_else(|| "-".to_owned());
format!(
"relay frame room={} direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={} targets={}",
room,
log.direction(),
log.peer_id().unwrap_or(ingress_peer_id),
source_mac,
destination_mac,
ethertype_or_len,
log.frame_len(),
log.action(),
drop_reason,
decision.targets().len()
)
}
async fn collect_target_sessions(
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
room: &RoomCode,
@@ -592,6 +654,27 @@ mod tests {
std::fs::remove_file(cert_path).unwrap();
}
#[test]
fn formats_relay_forwarding_log_line() {
let decision = ForwardingDecision::forwarded(vec![2, 3]);
let line = relay_frame_log_line(
&RoomCode::new("TESTROOM").unwrap(),
1,
&ethernet_frame(client_mac(2), client_mac(1)),
&decision,
);
assert!(line.contains("room=TESTROOM"));
assert!(line.contains("direction=RelayIngress"));
assert!(line.contains("peer_id=1"));
assert!(line.contains("src=02:00:00:00:00:01"));
assert!(line.contains("dst=02:00:00:00:00:02"));
assert!(line.contains("ethertype_or_len=0x0800"));
assert!(line.contains("action=Forwarded"));
assert!(line.contains("drop_reason=-"));
assert!(line.contains("targets=2"));
}
#[tokio::test]
async fn forwards_ethernet_datagrams_between_joined_peers() {
let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM);