diff --git a/README.md b/README.md index 0c88a19..75fa954 100644 --- a/README.md +++ b/README.md @@ -167,8 +167,9 @@ cargo run -p lanparty-gateway -- \ The gateway connects to the relay as `role = gateway`, completes the control-stream hello/welcome handshake, opens an AF_PACKET socket on the LAN interface with promiscuous packet membership, and bridges Ethernet frames -between the relay and wired LAN until shutdown. Sends fail before fragmentation -when an encoded Ethernet datagram exceeds the negotiated QUIC datagram budget. +between the relay and wired LAN until shutdown. 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. It tracks remote-client source MACs seen from relay traffic and periodically emits small CAM refresh frames so the physical switch keeps those MACs diff --git a/crates/lanparty-gateway/src/lib.rs b/crates/lanparty-gateway/src/lib.rs index f67e585..17d9896 100644 --- a/crates/lanparty-gateway/src/lib.rs +++ b/crates/lanparty-gateway/src/lib.rs @@ -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 { @@ -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 { 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(); diff --git a/crates/lanparty-obs/src/lib.rs b/crates/lanparty-obs/src/lib.rs index 6f15472..65d62b1 100644 --- a/crates/lanparty-obs/src/lib.rs +++ b/crates/lanparty-obs/src/lib.rs @@ -36,6 +36,7 @@ pub enum DropReason { ControlPlaneEtherType, DhcpServerReply, Ipv6RouterAdvertisement, + DatagramBudget, UnknownDestination, RateLimit, }