fix(gateway): capture whole LAN frames before budget checks

The gateway AF_PACKET read path used the standard 1514 byte Ethernet frame
length as its receive buffer. VLAN-tagged or jumbo LAN frames could therefore
be truncated before the bridge reached the encoded-datagram budget check, so
logs and drop accounting saw a corrupted shorter frame.

Use an overlay payload-sized capture buffer instead. This lets the Linux
gateway observe the whole frame that the kernel reports, then leave the
existing Ethernet parsing and negotiated QUIC datagram budget checks to decide
whether the frame can cross the tunnel. The bridge still never fragments
Ethernet frames.

Document the behavior in the gateway README section and add a compile-time
guard so the capture buffer stays above the standard Ethernet frame size.

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

Refs: PLAN.md
This commit is contained in:
2026-05-21 22:40:16 +02:00
parent d8f281e1cd
commit 022e74d62b
2 changed files with 14 additions and 6 deletions
+5 -3
View File
@@ -167,9 +167,11 @@ 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. It never fragments Ethernet between the relay and wired LAN until shutdown. It captures whole LAN frames up
frames; LAN frames whose encoded datagrams exceed the negotiated QUIC budget are to the overlay payload-length ceiling before deciding whether they fit the
counted, dropped, and logged instead of stopping the bridge. tunnel. It never fragments Ethernet 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
+9 -3
View File
@@ -32,8 +32,7 @@ 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::{
EthernetFrame, FrameType, MAX_STANDARD_ETHERNET_FRAME_LEN, MacAddr, decode_datagram, EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram, validate_datagram_budget,
encode_datagram, validate_datagram_budget,
}; };
use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig}; use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig};
use rustls::pki_types::CertificateDer; use rustls::pki_types::CertificateDer;
@@ -56,6 +55,13 @@ const CAM_REFRESH_ETHERTYPE: u16 = 0x88b5;
const CAM_REFRESH_PAYLOAD: &[u8] = b"lanparty-cam-refresh"; const CAM_REFRESH_PAYLOAD: &[u8] = b"lanparty-cam-refresh";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
const MIN_ETHERNET_FRAME_WITHOUT_FCS: usize = 60; const MIN_ETHERNET_FRAME_WITHOUT_FCS: usize = 60;
#[cfg(target_os = "linux")]
const LAN_CAPTURE_BUFFER_LEN: usize = u16::MAX as usize;
#[cfg(target_os = "linux")]
const _: () = assert!(
LAN_CAPTURE_BUFFER_LEN
> lanparty_proto::MAX_STANDARD_ETHERNET_FRAME_LEN + lanparty_proto::ETHERNET_HEADER_LEN
);
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command( #[command(
@@ -658,7 +664,7 @@ fn gateway_frame_log_line(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
async fn read_lan_ethernet(packet_socket: &AsyncFd<PacketSocket>) -> Result<Bytes> { async fn read_lan_ethernet(packet_socket: &AsyncFd<PacketSocket>) -> Result<Bytes> {
loop { loop {
let mut buffer = vec![0; MAX_STANDARD_ETHERNET_FRAME_LEN]; let mut buffer = vec![0; LAN_CAPTURE_BUFFER_LEN];
let mut guard = packet_socket let mut guard = packet_socket
.readable() .readable()
.await .await