fix(gateway): drop over-budget LAN frames
The gateway may see full-sized frames from the physical LAN even when the room uses a smaller tunnel MTU. With no overlay fragmentation, those frames cannot be encoded into the negotiated QUIC datagram budget. The live bridge previously propagated that budget error and stopped, which made one oversized LAN frame a fatal condition. Teach the gateway send path to return a send outcome. Normal direct callers still get an error for over-budget sends, but the Linux bridge loop now records, logs, and drops those frames with a DatagramBudget drop reason before continuing with later traffic. Malformed local Ethernet still remains an error because that indicates a broken local boundary rather than ordinary LAN traffic. The gateway stats test now covers the extra drop, and the frame-log test covers the new drop reason. README now documents that over-budget LAN frames are counted, dropped, and logged instead of fragmented or killing the bridge. Test Plan: - cargo fmt --check - cargo test -p lanparty-obs -p lanparty-gateway - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check - git diff --cached --check Refs: PLAN.md No fragmentation for MVP
This commit is contained in:
@@ -28,7 +28,7 @@ use lanparty_ctrl::{
|
||||
decode_control_frame, encode_control_message,
|
||||
};
|
||||
use lanparty_net::RelayEndpoint;
|
||||
use lanparty_obs::TunnelStats;
|
||||
use lanparty_obs::{DropReason, TunnelStats};
|
||||
#[cfg(target_os = "linux")]
|
||||
use lanparty_obs::{FrameAction, FrameDirection, FrameLog};
|
||||
use lanparty_proto::{
|
||||
@@ -235,13 +235,21 @@ impl GatewayConnection {
|
||||
}
|
||||
|
||||
pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> {
|
||||
send_gateway_ethernet(
|
||||
match send_gateway_ethernet(
|
||||
&self.connection,
|
||||
&self.welcome,
|
||||
self.quic_max_datagram_size,
|
||||
&self.stats,
|
||||
frame,
|
||||
)
|
||||
)? {
|
||||
GatewaySendOutcome::Sent => Ok(()),
|
||||
GatewaySendOutcome::Dropped(DropReason::DatagramBudget) => {
|
||||
bail!("gateway Ethernet datagram exceeds negotiated QUIC budget")
|
||||
}
|
||||
GatewaySendOutcome::Dropped(drop_reason) => {
|
||||
bail!("gateway Ethernet frame was dropped: {drop_reason:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
|
||||
@@ -305,13 +313,17 @@ impl GatewayConnection {
|
||||
}
|
||||
lan_frame = read_lan_ethernet(&packet_socket) => {
|
||||
let lan_frame = lan_frame?;
|
||||
send_gateway_ethernet(
|
||||
let outcome = send_gateway_ethernet(
|
||||
&connection,
|
||||
&welcome,
|
||||
quic_max_datagram_size,
|
||||
&stats,
|
||||
&lan_frame,
|
||||
)?;
|
||||
let (action, drop_reason) = match outcome {
|
||||
GatewaySendOutcome::Sent => (FrameAction::Forwarded, None),
|
||||
GatewaySendOutcome::Dropped(reason) => (FrameAction::Dropped, Some(reason)),
|
||||
};
|
||||
println!(
|
||||
"{}",
|
||||
gateway_frame_log_line(
|
||||
@@ -319,8 +331,8 @@ impl GatewayConnection {
|
||||
FrameDirection::LanToRemote,
|
||||
Some(welcome.peer_id()),
|
||||
&lan_frame,
|
||||
FrameAction::Forwarded,
|
||||
None,
|
||||
action,
|
||||
drop_reason,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -385,7 +397,7 @@ fn send_gateway_ethernet(
|
||||
quic_max_datagram_size: u16,
|
||||
stats: &GatewayTunnelStats,
|
||||
frame: &[u8],
|
||||
) -> Result<()> {
|
||||
) -> Result<GatewaySendOutcome> {
|
||||
let ethernet_frame = match EthernetFrame::parse(frame) {
|
||||
Ok(frame) => frame,
|
||||
Err(error) => {
|
||||
@@ -401,11 +413,9 @@ fn send_gateway_ethernet(
|
||||
frame,
|
||||
)
|
||||
.context("failed to encode gateway Ethernet datagram")?;
|
||||
if let Err(error) =
|
||||
validate_datagram_budget(datagram.len(), usize::from(quic_max_datagram_size))
|
||||
{
|
||||
if validate_datagram_budget(datagram.len(), usize::from(quic_max_datagram_size)).is_err() {
|
||||
stats.record_dropped_frame();
|
||||
return Err(error).context("gateway Ethernet datagram exceeds negotiated QUIC budget");
|
||||
return Ok(GatewaySendOutcome::Dropped(DropReason::DatagramBudget));
|
||||
}
|
||||
|
||||
connection
|
||||
@@ -413,7 +423,13 @@ fn send_gateway_ethernet(
|
||||
.context("failed to send gateway Ethernet datagram")?;
|
||||
stats.record_ethernet_tx(ethernet_frame);
|
||||
|
||||
Ok(())
|
||||
Ok(GatewaySendOutcome::Sent)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GatewaySendOutcome {
|
||||
Sent,
|
||||
Dropped(DropReason),
|
||||
}
|
||||
|
||||
async fn recv_gateway_ethernet(
|
||||
@@ -961,7 +977,7 @@ mod tests {
|
||||
let ControlMessage::Stats(stats) = stats_message else {
|
||||
panic!("expected gateway stats event");
|
||||
};
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 1, 1));
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 2, 1));
|
||||
stats_received_tx.send(()).unwrap();
|
||||
|
||||
let mut disconnect_recv = connection.accept_uni().await.unwrap();
|
||||
@@ -1017,8 +1033,14 @@ mod tests {
|
||||
assert_eq!(received.payload(), ethernet_frame(b"from relay").as_slice());
|
||||
|
||||
assert!(gateway.send_ethernet(&[0; 4]).is_err());
|
||||
let oversized_payload = vec![0; usize::from(gateway.quic_max_datagram_size())];
|
||||
assert!(
|
||||
gateway
|
||||
.send_ethernet(ðernet_frame(&oversized_payload))
|
||||
.is_err()
|
||||
);
|
||||
let stats = gateway.stats_snapshot();
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 1, 1));
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 2, 1));
|
||||
|
||||
gateway.send_stats_snapshot().await.unwrap();
|
||||
tokio::time::timeout(Duration::from_secs(5), stats_received_rx)
|
||||
@@ -1141,6 +1163,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn formats_gateway_datagram_budget_drops() {
|
||||
let line = gateway_frame_log_line(
|
||||
"eth0",
|
||||
FrameDirection::LanToRemote,
|
||||
Some(1),
|
||||
ðernet_frame(b"oversized"),
|
||||
FrameAction::Dropped,
|
||||
Some(DropReason::DatagramBudget),
|
||||
);
|
||||
|
||||
assert!(line.contains("direction=LanToRemote"));
|
||||
assert!(line.contains("action=Dropped"));
|
||||
assert!(line.contains("drop_reason=DatagramBudget"));
|
||||
}
|
||||
|
||||
fn test_server_config() -> (ServerConfig, CertificateDer<'static>) {
|
||||
let certified_key =
|
||||
rcgen::generate_simple_self_signed(vec!["lanparty-relay.local".into()]).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user