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:
@@ -167,8 +167,9 @@ cargo run -p lanparty-gateway -- \
|
|||||||
The gateway connects to the relay as `role = gateway`, completes the
|
The gateway connects to the relay as `role = gateway`, completes the
|
||||||
control-stream hello/welcome handshake, opens an AF_PACKET socket on the LAN
|
control-stream hello/welcome handshake, opens an AF_PACKET socket on the LAN
|
||||||
interface with promiscuous packet membership, and bridges Ethernet frames
|
interface with promiscuous packet membership, and bridges Ethernet frames
|
||||||
between the relay and wired LAN until shutdown. Sends fail before fragmentation
|
between the relay and wired LAN until shutdown. It never fragments Ethernet
|
||||||
when an encoded Ethernet datagram exceeds the negotiated QUIC datagram budget.
|
frames; LAN frames whose encoded datagrams exceed the negotiated QUIC budget are
|
||||||
|
counted, dropped, and logged instead of stopping the bridge.
|
||||||
`--relay` accepts a DNS name or socket address; bare hosts default to UDP/443.
|
`--relay` accepts a DNS name or socket address; bare hosts default to UDP/443.
|
||||||
It tracks remote-client source MACs seen from relay traffic and periodically
|
It tracks remote-client source MACs seen from relay traffic and periodically
|
||||||
emits small CAM refresh frames so the physical switch keeps those MACs
|
emits small CAM refresh frames so the physical switch keeps those MACs
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use lanparty_ctrl::{
|
|||||||
decode_control_frame, encode_control_message,
|
decode_control_frame, encode_control_message,
|
||||||
};
|
};
|
||||||
use lanparty_net::RelayEndpoint;
|
use lanparty_net::RelayEndpoint;
|
||||||
use lanparty_obs::TunnelStats;
|
use lanparty_obs::{DropReason, TunnelStats};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use lanparty_obs::{FrameAction, FrameDirection, FrameLog};
|
use lanparty_obs::{FrameAction, FrameDirection, FrameLog};
|
||||||
use lanparty_proto::{
|
use lanparty_proto::{
|
||||||
@@ -235,13 +235,21 @@ impl GatewayConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> {
|
pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> {
|
||||||
send_gateway_ethernet(
|
match send_gateway_ethernet(
|
||||||
&self.connection,
|
&self.connection,
|
||||||
&self.welcome,
|
&self.welcome,
|
||||||
self.quic_max_datagram_size,
|
self.quic_max_datagram_size,
|
||||||
&self.stats,
|
&self.stats,
|
||||||
frame,
|
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> {
|
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
|
||||||
@@ -305,13 +313,17 @@ impl GatewayConnection {
|
|||||||
}
|
}
|
||||||
lan_frame = read_lan_ethernet(&packet_socket) => {
|
lan_frame = read_lan_ethernet(&packet_socket) => {
|
||||||
let lan_frame = lan_frame?;
|
let lan_frame = lan_frame?;
|
||||||
send_gateway_ethernet(
|
let outcome = send_gateway_ethernet(
|
||||||
&connection,
|
&connection,
|
||||||
&welcome,
|
&welcome,
|
||||||
quic_max_datagram_size,
|
quic_max_datagram_size,
|
||||||
&stats,
|
&stats,
|
||||||
&lan_frame,
|
&lan_frame,
|
||||||
)?;
|
)?;
|
||||||
|
let (action, drop_reason) = match outcome {
|
||||||
|
GatewaySendOutcome::Sent => (FrameAction::Forwarded, None),
|
||||||
|
GatewaySendOutcome::Dropped(reason) => (FrameAction::Dropped, Some(reason)),
|
||||||
|
};
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
gateway_frame_log_line(
|
gateway_frame_log_line(
|
||||||
@@ -319,8 +331,8 @@ impl GatewayConnection {
|
|||||||
FrameDirection::LanToRemote,
|
FrameDirection::LanToRemote,
|
||||||
Some(welcome.peer_id()),
|
Some(welcome.peer_id()),
|
||||||
&lan_frame,
|
&lan_frame,
|
||||||
FrameAction::Forwarded,
|
action,
|
||||||
None,
|
drop_reason,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -385,7 +397,7 @@ fn send_gateway_ethernet(
|
|||||||
quic_max_datagram_size: u16,
|
quic_max_datagram_size: u16,
|
||||||
stats: &GatewayTunnelStats,
|
stats: &GatewayTunnelStats,
|
||||||
frame: &[u8],
|
frame: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<GatewaySendOutcome> {
|
||||||
let ethernet_frame = match EthernetFrame::parse(frame) {
|
let ethernet_frame = match EthernetFrame::parse(frame) {
|
||||||
Ok(frame) => frame,
|
Ok(frame) => frame,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -401,11 +413,9 @@ fn send_gateway_ethernet(
|
|||||||
frame,
|
frame,
|
||||||
)
|
)
|
||||||
.context("failed to encode gateway Ethernet datagram")?;
|
.context("failed to encode gateway Ethernet datagram")?;
|
||||||
if let Err(error) =
|
if validate_datagram_budget(datagram.len(), usize::from(quic_max_datagram_size)).is_err() {
|
||||||
validate_datagram_budget(datagram.len(), usize::from(quic_max_datagram_size))
|
|
||||||
{
|
|
||||||
stats.record_dropped_frame();
|
stats.record_dropped_frame();
|
||||||
return Err(error).context("gateway Ethernet datagram exceeds negotiated QUIC budget");
|
return Ok(GatewaySendOutcome::Dropped(DropReason::DatagramBudget));
|
||||||
}
|
}
|
||||||
|
|
||||||
connection
|
connection
|
||||||
@@ -413,7 +423,13 @@ fn send_gateway_ethernet(
|
|||||||
.context("failed to send gateway Ethernet datagram")?;
|
.context("failed to send gateway Ethernet datagram")?;
|
||||||
stats.record_ethernet_tx(ethernet_frame);
|
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(
|
async fn recv_gateway_ethernet(
|
||||||
@@ -961,7 +977,7 @@ mod tests {
|
|||||||
let ControlMessage::Stats(stats) = stats_message else {
|
let ControlMessage::Stats(stats) = stats_message else {
|
||||||
panic!("expected gateway stats event");
|
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();
|
stats_received_tx.send(()).unwrap();
|
||||||
|
|
||||||
let mut disconnect_recv = connection.accept_uni().await.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_eq!(received.payload(), ethernet_frame(b"from relay").as_slice());
|
||||||
|
|
||||||
assert!(gateway.send_ethernet(&[0; 4]).is_err());
|
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();
|
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();
|
gateway.send_stats_snapshot().await.unwrap();
|
||||||
tokio::time::timeout(Duration::from_secs(5), stats_received_rx)
|
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>) {
|
fn test_server_config() -> (ServerConfig, CertificateDer<'static>) {
|
||||||
let certified_key =
|
let certified_key =
|
||||||
rcgen::generate_simple_self_signed(vec!["lanparty-relay.local".into()]).unwrap();
|
rcgen::generate_simple_self_signed(vec!["lanparty-relay.local".into()]).unwrap();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub enum DropReason {
|
|||||||
ControlPlaneEtherType,
|
ControlPlaneEtherType,
|
||||||
DhcpServerReply,
|
DhcpServerReply,
|
||||||
Ipv6RouterAdvertisement,
|
Ipv6RouterAdvertisement,
|
||||||
|
DatagramBudget,
|
||||||
UnknownDestination,
|
UnknownDestination,
|
||||||
RateLimit,
|
RateLimit,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user