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
+2 -1
View File
@@ -77,7 +77,8 @@ replies with `welcome` or `reject`, and forwards live Ethernet QUIC datagrams
between accepted peers in the same room. It currently uses a generated between accepted peers in the same room. It currently uses a generated
self-signed development certificate; `--dev-cert-der-out` writes that self-signed development certificate; `--dev-cert-der-out` writes that
certificate so the gateway and client can pin it in development. Production certificate so the gateway and client can pin it in development. Production
certificate handling remains future work. certificate handling remains future work. Ethernet forwarding decisions are
logged with room, peer, MAC, ethertype, action, drop reason, and target count.
## Gateway ## Gateway
+85 -2
View File
@@ -7,14 +7,15 @@ use lanparty_ctrl::{
MAX_CONTROL_MESSAGE_LEN, PeerInfo, RELAY_ALPN, Reject, RejectReason, Role, RoomCode, MAX_CONTROL_MESSAGE_LEN, PeerInfo, RELAY_ALPN, Reject, RejectReason, Role, RoomCode,
ServerWelcome, decode_control_frame, encode_control_message, 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::crypto::rustls::QuicServerConfig;
use quinn::{Endpoint, Incoming, SendStream, ServerConfig, TransportConfig}; use quinn::{Endpoint, Incoming, SendStream, ServerConfig, TransportConfig};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::collections::HashMap; use std::collections::HashMap;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{RelayConfig, RoomRegistry}; use crate::{ForwardingDecision, RelayConfig, RoomRegistry};
const DATAGRAM_BUFFER_BYTES: usize = 4 * 1024 * 1024; const DATAGRAM_BUFFER_BYTES: usize = 4 * 1024 * 1024;
const MAX_CONTROL_FRAME_LEN: usize = CONTROL_LENGTH_PREFIX_LEN + MAX_CONTROL_MESSAGE_LEN; 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(), accepted.peer.peer_id(),
packet.payload(), packet.payload(),
)?; )?;
println!(
"{}",
relay_frame_log_line(
&accepted.room,
accepted.peer.peer_id(),
packet.payload(),
&decision
)
);
let target_peer_ids = decision.targets().to_vec(); let target_peer_ids = decision.targets().to_vec();
if target_peer_ids.is_empty() { if target_peer_ids.is_empty() {
return Ok(()); return Ok(());
@@ -312,6 +322,58 @@ async fn forward_peer_datagram(
Ok(()) 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( async fn collect_target_sessions(
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>, sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
room: &RoomCode, room: &RoomCode,
@@ -592,6 +654,27 @@ mod tests {
std::fs::remove_file(cert_path).unwrap(); 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] #[tokio::test]
async fn forwards_ethernet_datagrams_between_joined_peers() { async fn forwards_ethernet_datagrams_between_joined_peers() {
let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM); let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM);