From da937a50c4b53a5cd3a7de04ec037e1ab400f842 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 05:28:52 +0200 Subject: [PATCH] fix(proto): reject reserved overlay flags The MVP overlay reserves its flags field for later features such as fragmentation or payload encryption, but version 1 does not define any flag semantics. Accepting nonzero flags would let unknown behavior silently traverse the relay and reach the tunnel endpoints. Make zero the only valid v1 flag value. Overlay encoding and decoding now reject reserved nonzero flags, production send paths use the explicit OVERLAY_FLAGS_NONE constant, and the relay emits forwarded datagrams with the same zero-flag policy instead of preserving peer-supplied bits. Document the reserved-flag rule in the protocol crate overview. Test Plan: - cargo test -p lanparty-proto overlay - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo fmt --check - git diff --check Refs: PLAN.md no-fragmentation MVP overlay format --- README.md | 2 ++ crates/lanparty-client-core/src/lib.rs | 4 +-- crates/lanparty-gateway/src/lib.rs | 4 +-- crates/lanparty-proto/src/lib.rs | 5 ++-- crates/lanparty-proto/src/overlay.rs | 37 +++++++++++++++++++++++--- crates/lanparty-relay/src/server.rs | 6 +++-- 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 11eba4e..eaa66e4 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Monorepo for a Layer 2 over QUIC LAN party bridge. Transport-agnostic tunnel contract shared by all binaries: - overlay datagram header encoding and decoding +- v1 overlay datagrams reject reserved nonzero flags until their semantics are + defined - negotiated QUIC datagram budget validation before send - Ethernet frame header parsing - MAC address parsing and identity validation diff --git a/crates/lanparty-client-core/src/lib.rs b/crates/lanparty-client-core/src/lib.rs index e1ceac6..13fcf90 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -27,7 +27,7 @@ use lanparty_ctrl::{ }; use lanparty_obs::{DropReason, QuicDiagnostics, TunnelStats}; use lanparty_proto::{ - EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram, + EthernetFrame, FrameType, MacAddr, OVERLAY_FLAGS_NONE, decode_datagram, encode_datagram, gateway_lan_safety_drop_reason, remote_client_safety_drop_reason, validate_datagram_budget, }; use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig}; @@ -401,7 +401,7 @@ impl ClientRelayIo { FrameType::Ethernet, self.welcome.room_id(), self.welcome.peer_id(), - 0, + OVERLAY_FLAGS_NONE, frame, ) .context("failed to encode client Ethernet datagram")?; diff --git a/crates/lanparty-gateway/src/lib.rs b/crates/lanparty-gateway/src/lib.rs index 5a980be..18835cc 100644 --- a/crates/lanparty-gateway/src/lib.rs +++ b/crates/lanparty-gateway/src/lib.rs @@ -32,7 +32,7 @@ use lanparty_obs::{DropReason, TunnelStats}; #[cfg(target_os = "linux")] use lanparty_obs::{FrameAction, FrameDirection, FrameLog}; use lanparty_proto::{ - EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram, + EthernetFrame, FrameType, MacAddr, OVERLAY_FLAGS_NONE, decode_datagram, encode_datagram, gateway_lan_safety_drop_reason, remote_client_safety_drop_reason, validate_datagram_budget, }; use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig}; @@ -466,7 +466,7 @@ fn send_gateway_ethernet( FrameType::Ethernet, welcome.room_id(), welcome.peer_id(), - 0, + OVERLAY_FLAGS_NONE, frame, ) .context("failed to encode gateway Ethernet datagram")?; diff --git a/crates/lanparty-proto/src/lib.rs b/crates/lanparty-proto/src/lib.rs index 64b8a80..7e0a95f 100644 --- a/crates/lanparty-proto/src/lib.rs +++ b/crates/lanparty-proto/src/lib.rs @@ -20,8 +20,9 @@ pub use mtu::{ max_tap_mtu_for_datagram, recommended_tap_mtu, }; pub use overlay::{ - FrameType, OVERLAY_HEADER_LEN, OVERLAY_MAGIC, OVERLAY_VERSION, OverlayHeader, OverlayPacket, - ProtoError, decode_datagram, encode_datagram, validate_datagram_budget, + FrameType, OVERLAY_FLAGS_NONE, OVERLAY_HEADER_LEN, OVERLAY_MAGIC, OVERLAY_VERSION, + OverlayHeader, OverlayPacket, ProtoError, decode_datagram, encode_datagram, + validate_datagram_budget, }; pub use safety::{ ETHERTYPE_8021AD, ETHERTYPE_8021Q, ETHERTYPE_EAPOL, ETHERTYPE_IPV4, ETHERTYPE_IPV6, diff --git a/crates/lanparty-proto/src/overlay.rs b/crates/lanparty-proto/src/overlay.rs index cf9d628..d70dbb9 100644 --- a/crates/lanparty-proto/src/overlay.rs +++ b/crates/lanparty-proto/src/overlay.rs @@ -3,6 +3,7 @@ use thiserror::Error; pub const OVERLAY_MAGIC: u32 = 0x534c_414e; // "SLAN" pub const OVERLAY_VERSION: u8 = 1; pub const OVERLAY_HEADER_LEN: usize = 22; +pub const OVERLAY_FLAGS_NONE: u16 = 0; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[repr(u8)] @@ -45,6 +46,7 @@ impl OverlayHeader { flags: u16, payload_len: usize, ) -> Result { + validate_overlay_flags(flags)?; let payload_len = u16::try_from(payload_len).map_err(|_| ProtoError::PayloadTooLarge { len: payload_len, max: u16::MAX as usize, @@ -115,11 +117,14 @@ impl OverlayHeader { return Err(ProtoError::UnsupportedVersion { actual: version }); } + let flags = u16::from_be_bytes(bytes[18..20].try_into().expect("flags slice length")); + validate_overlay_flags(flags)?; + Ok(Self { frame_type: FrameType::from_u8(bytes[5])?, room_id: u64::from_be_bytes(bytes[6..14].try_into().expect("room id slice length")), peer_id: u32::from_be_bytes(bytes[14..18].try_into().expect("peer id slice length")), - flags: u16::from_be_bytes(bytes[18..20].try_into().expect("flags slice length")), + flags, payload_len: u16::from_be_bytes( bytes[20..22].try_into().expect("payload len slice length"), ), @@ -168,6 +173,8 @@ pub enum ProtoError { UnsupportedVersion { actual: u8 }, #[error("unknown overlay frame type {0}")] UnknownFrameType(u8), + #[error("unsupported overlay flags 0x{actual:04x}")] + UnsupportedFlags { actual: u16 }, #[error("payload length {len} exceeds wire maximum {max}")] PayloadTooLarge { len: usize, max: usize }, #[error("encoded datagram length {len} exceeds negotiated QUIC datagram budget {max}")] @@ -178,6 +185,14 @@ pub enum ProtoError { EthernetFrameTooShort { actual: usize, minimum: usize }, } +fn validate_overlay_flags(flags: u16) -> Result<(), ProtoError> { + if flags != OVERLAY_FLAGS_NONE { + return Err(ProtoError::UnsupportedFlags { actual: flags }); + } + + Ok(()) +} + pub fn encode_datagram( frame_type: FrameType, room_id: u64, @@ -224,7 +239,7 @@ mod tests { FrameType::Ethernet, 0x0102_0304_0506_0708, 0x0a0b_0c0d, - 0x1001, + OVERLAY_FLAGS_NONE, &payload, ) .unwrap(); @@ -239,7 +254,7 @@ mod tests { assert_eq!(header.frame_type(), FrameType::Ethernet); assert_eq!(header.room_id(), 0x0102_0304_0506_0708); assert_eq!(header.peer_id(), 0x0a0b_0c0d); - assert_eq!(header.flags(), 0x1001); + assert_eq!(header.flags(), OVERLAY_FLAGS_NONE); assert_eq!(header.payload_len(), 4); assert_eq!(packet.payload(), payload); } @@ -288,6 +303,22 @@ mod tests { ); } + #[test] + fn rejects_reserved_overlay_flags() { + assert_eq!( + OverlayHeader::new(FrameType::Ethernet, 1, 2, 1, 0).unwrap_err(), + ProtoError::UnsupportedFlags { actual: 1 } + ); + + let mut datagram = encode_datagram(FrameType::Ethernet, 1, 2, 0, &[]).unwrap(); + datagram[18..20].copy_from_slice(&0x8000_u16.to_be_bytes()); + + assert_eq!( + decode_datagram(&datagram).unwrap_err(), + ProtoError::UnsupportedFlags { actual: 0x8000 } + ); + } + #[test] fn rejects_payloads_too_large_for_header() { let payload = vec![0; usize::from(u16::MAX) + 1]; diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index c702e96..9aa9ca3 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -8,7 +8,9 @@ use lanparty_ctrl::{ ServerWelcome, decode_control_frame, encode_control_message, }; use lanparty_obs::{DropReason, FrameDirection, FrameLog, TunnelStats}; -use lanparty_proto::{EthernetFrame, FrameType, decode_datagram, encode_datagram}; +use lanparty_proto::{ + EthernetFrame, FrameType, OVERLAY_FLAGS_NONE, decode_datagram, encode_datagram, +}; use quinn::crypto::rustls::QuicServerConfig; use quinn::{Endpoint, Incoming, RecvStream, SendStream, ServerConfig, TransportConfig}; use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; @@ -424,7 +426,7 @@ async fn forward_peer_datagram( FrameType::Ethernet, accepted.welcome.room_id(), accepted.peer.peer_id(), - header.flags(), + OVERLAY_FLAGS_NONE, packet.payload(), )?; let target_sessions = collect_target_sessions(sessions, &accepted.room, &target_peer_ids).await;