feat(relay): log egress datagram budget skips

PLAN.md keeps MVP forwarding to one Ethernet frame per QUIC datagram with no
fragmentation. The relay already skipped targets whose negotiated datagram
budget could not carry an encoded forwarded frame, but that skip was silent.

Retain the no-fragmentation behavior and log the skipped egress target with the
room, ingress peer, target peer, encoded length, and target datagram budget.
Store the peer id in relay sessions so the diagnostic can identify the skipped
target directly.

Document the egress budget-skip diagnostic in the relay README section.

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

Refs: PLAN.md
This commit is contained in:
2026-05-21 22:13:18 +02:00
parent 2c946ce9c2
commit d2cf20f597
2 changed files with 42 additions and 0 deletions
+2
View File
@@ -131,6 +131,8 @@ Safety-policy rejects use the `filtered` action so they are distinguishable
from malformed/unknown-destination drops and rate limits.
Malformed peer datagrams log their per-peer count before the relay disconnects
peers that cross the malformed-datagram threshold.
Relay egress skips caused by a target peer's smaller datagram budget are logged
with the ingress peer, target peer, encoded length, and target budget.
Unknown unicast from a client is forwarded only to the gateway port; unknown
unicast from the gateway is dropped instead of flooded to every remote client.
When a peer joins or leaves, the relay sends a reliable lifecycle control event
+40
View File
@@ -56,6 +56,7 @@ impl PeerKey {
#[derive(Debug, Clone)]
struct PeerSession {
connection: quinn::Connection,
peer_id: u32,
max_datagram_size: usize,
latest_stats: Option<TunnelStats>,
}
@@ -299,6 +300,7 @@ async fn register_peer(
PeerKey::from_accepted(accepted),
PeerSession {
connection,
peer_id: accepted.peer.peer_id(),
max_datagram_size: accepted.max_datagram_size,
latest_stats: None,
},
@@ -429,6 +431,16 @@ async fn forward_peer_datagram(
for target in target_sessions {
if outgoing.len() > target.max_datagram_size {
eprintln!(
"{}",
egress_budget_skip_log_line(
&accepted.room,
accepted.peer.peer_id(),
target.peer_id,
outgoing.len(),
target.max_datagram_size,
)
);
continue;
}
@@ -590,6 +602,19 @@ fn malformed_datagram_log_line(
)
}
fn egress_budget_skip_log_line(
room: &RoomCode,
ingress_peer_id: u32,
target_peer_id: u32,
datagram_len: usize,
max_datagram_size: usize,
) -> String {
format!(
"relay egress skipped room={} peer_id={} target_peer_id={} len={} max_datagram_size={} reason=datagram_budget",
room, ingress_peer_id, target_peer_id, datagram_len, max_datagram_size
)
}
async fn collect_target_sessions(
sessions: &Arc<Mutex<HashMap<PeerKey, PeerSession>>>,
room: &RoomCode,
@@ -1030,6 +1055,21 @@ mod tests {
assert!(line.contains("disconnecting=true"));
}
#[test]
fn formats_egress_budget_skip_log_line() {
let room = RoomCode::new("TESTROOM").unwrap();
let line = egress_budget_skip_log_line(&room, 2, 7, 1401, 1400);
assert!(line.contains("relay egress skipped"));
assert!(line.contains("room=TESTROOM"));
assert!(line.contains("peer_id=2"));
assert!(line.contains("target_peer_id=7"));
assert!(line.contains("len=1401"));
assert!(line.contains("max_datagram_size=1400"));
assert!(line.contains("reason=datagram_budget"));
}
#[tokio::test]
async fn classifies_bad_peer_datagrams_as_malformed() {
let rooms = Arc::new(Mutex::new(RoomRegistry::default()));