add payload_len field to wire protocol header

Ethernet pads frames to 60 bytes minimum; without an explicit length
field the receiver cannot distinguish real payload from zero padding
(e.g. a 6-byte DISCOVER becomes 46 bytes after padding, failing the
'payload must be empty' check).

Header is now 8 bytes: [ver:1][type:1][session_id:4][payload_len:2]
(both multi-byte fields big-endian). The receiver slices exactly
payload_len bytes and ignores trailing padding.

Updated PROTOCOL.md, Rust frame.rs, and C# Frame.cs.
This commit is contained in:
2026-08-13 07:51:39 +00:00
parent dc5df5c073
commit f09028b135
3 changed files with 78 additions and 57 deletions
+34 -28
View File
@@ -3,6 +3,7 @@
pub const ETHERTYPE: u16 = 0x6969;
pub const ETH_HEADER_LEN: usize = 14;
pub const VERSION: u8 = 1;
pub const HEADER_LEN: usize = 8;
pub const MAX_PAYLOAD: usize = 1480;
pub const TYPE_DISCOVER: u8 = 0x01;
@@ -76,50 +77,50 @@ fn encode_entry(buf: &mut Vec<u8>, e: &UpstreamEntry) {
buf.extend_from_slice(&label_bytes[..label_len as usize]);
}
/// Build the 8-byte header + payload. The payload_len field records the
/// exact payload length so the receiver can ignore Ethernet padding.
fn build(type_byte: u8, session_id: u32, payload: Vec<u8>) -> Vec<u8> {
let len = payload.len() as u16;
let mut buf = Vec::with_capacity(HEADER_LEN + payload.len());
buf.push(VERSION);
buf.push(type_byte);
buf.extend_from_slice(&session_id.to_be_bytes());
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(&payload);
buf
}
impl Frame {
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
match self {
Frame::Discover => {
buf.extend_from_slice(&[VERSION, TYPE_DISCOVER, 0, 0, 0, 0]);
}
Frame::Discover => build(TYPE_DISCOVER, 0, Vec::new()),
Frame::Manifest(entries) => {
buf.extend_from_slice(&[VERSION, TYPE_MANIFEST, 0, 0, 0, 0]);
let mut payload = Vec::new();
for e in entries {
encode_entry(&mut buf, e);
encode_entry(&mut payload, e);
}
build(TYPE_MANIFEST, 0, payload)
}
Frame::Open { upstream_id } => {
buf.extend_from_slice(&[VERSION, TYPE_OPEN, 0, 0, 0, 0, *upstream_id]);
}
Frame::Open { upstream_id } => build(TYPE_OPEN, 0, vec![*upstream_id]),
Frame::OpenAck { session_id, upstream_id } => {
buf.extend_from_slice(&[VERSION, TYPE_OPEN_ACK]);
buf.extend_from_slice(&session_id.to_be_bytes());
buf.push(*upstream_id);
build(TYPE_OPEN_ACK, *session_id, vec![*upstream_id])
}
Frame::OpenNak { upstream_id, reason } => {
buf.extend_from_slice(&[VERSION, TYPE_OPEN_NAK, 0, 0, 0, 0]);
buf.push(*upstream_id);
buf.push(*reason);
}
Frame::Data { session_id, payload } => {
buf.extend_from_slice(&[VERSION, TYPE_DATA]);
buf.extend_from_slice(&session_id.to_be_bytes());
buf.extend_from_slice(payload);
build(TYPE_OPEN_NAK, 0, vec![*upstream_id, *reason])
}
Frame::Data { session_id, payload } => build(TYPE_DATA, *session_id, payload.clone()),
Frame::Close { session_id, reason } => {
buf.extend_from_slice(&[VERSION, TYPE_CLOSE]);
buf.extend_from_slice(&session_id.to_be_bytes());
if let Some(r) = reason {
buf.push(*r);
}
let p = match reason {
Some(r) => vec![*r],
None => Vec::new(),
};
build(TYPE_CLOSE, *session_id, p)
}
}
buf
}
pub fn parse(buf: &[u8]) -> Result<Frame, DecodeError> {
if buf.len() < 6 {
if buf.len() < HEADER_LEN {
return Err(DecodeError::Short);
}
let version = buf[0];
@@ -128,7 +129,12 @@ impl Frame {
}
let typ = buf[1];
let session_id = u32::from_be_bytes([buf[2], buf[3], buf[4], buf[5]]);
let payload = &buf[6..];
let payload_len = u16::from_be_bytes([buf[6], buf[7]]) as usize;
if buf.len() < HEADER_LEN + payload_len {
return Err(DecodeError::BadPayload("payload_len exceeds available data"));
}
// Slice exactly payload_len bytes, ignoring any trailing Ethernet padding.
let payload = &buf[HEADER_LEN..HEADER_LEN + payload_len];
match typ {
TYPE_DISCOVER => {
if !payload.is_empty() {