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;