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
This commit is contained in:
2026-05-22 05:28:52 +02:00
parent 0a97b77ad9
commit da937a50c4
6 changed files with 47 additions and 11 deletions
+3 -2
View File
@@ -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,
+34 -3
View File
@@ -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<Self, ProtoError> {
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];