From f09028b135379b79283b5f9dcb6d46954744832c Mon Sep 17 00:00:00 2001 From: Mute Date: Thu, 13 Aug 2026 07:51:39 +0000 Subject: [PATCH] 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. --- PROTOCOL.md | 16 +++++++--- gatunad/src/frame.rs | 62 +++++++++++++++++++++----------------- win/gatuna-client/Frame.cs | 57 ++++++++++++++++++++--------------- 3 files changed, 78 insertions(+), 57 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 5748e1b..3982592 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -11,7 +11,9 @@ discriminator; there is no magic number inside the payload. +---------------+---------------+-------------------------------+ | version | type | session_id | +---------------+---------------+-------------------------------+ -| payload ... | +| payload_len (big-endian) | payload ... | ++-------------------------------+ + +| | +---------------------------------------------------------------+ ``` @@ -19,15 +21,19 @@ discriminator; there is no magic number inside the payload. - **type** (u8): frame type, see table below. - **session_id** (u32, big-endian): `0` for non-session frames; the server-assigned ID for session-scoped frames. -- **payload** (variable): type-dependent. No length field is carried — the - Ethernet frame length from the capture gives the payload extent. +- **payload_len** (u16, big-endian): number of payload bytes that follow. + The receiver reads exactly this many bytes and ignores any trailing + Ethernet padding (frames under 60 bytes are zero-padded by the NIC to + meet the minimum Ethernet frame size). +- **payload** (`payload_len` bytes): type-dependent. No CRC, no retransmit, no ordering at this layer. TCP reliability is handled by the endpoints' TCP stacks; UDP (reserved) will rely on application-level mechanisms. -Maximum payload: 1500 (Ethernet MTU) − 14 (eth header) − 6 (our header) = -**1480 bytes**. Larger payloads are not emitted in v1. +Maximum payload: 1500 (Ethernet MTU) − 8 (our header) = **1492 bytes**. In +practice we cap at **1480** to stay conservative. Larger payloads are not +emitted in v1. ## Frame types diff --git a/gatunad/src/frame.rs b/gatunad/src/frame.rs index 13d5e88..5ab5c95 100644 --- a/gatunad/src/frame.rs +++ b/gatunad/src/frame.rs @@ -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, 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) -> Vec { + 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 { - 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 { - 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() { diff --git a/win/gatuna-client/Frame.cs b/win/gatuna-client/Frame.cs index 53d02cb..b0890b4 100644 --- a/win/gatuna-client/Frame.cs +++ b/win/gatuna-client/Frame.cs @@ -7,6 +7,7 @@ static class Proto public const ushort EtherType = 0x6969; public const int EthHeaderLen = 14; public const byte Version = 1; + public const int HeaderLen = 8; public const int MaxPayload = 1480; public const byte TypeDiscover = 0x01; @@ -49,34 +50,49 @@ abstract record Frame static class FrameCodec { + /// Build the 8-byte header + payload. payload_len records the exact + /// payload length so the receiver can ignore Ethernet padding. + static byte[] Build(byte type, uint sessionId, byte[] payload) + { + var buf = new byte[Proto.HeaderLen + payload.Length]; + buf[0] = Proto.Version; + buf[1] = type; + buf[2] = (byte)(sessionId >> 24); + buf[3] = (byte)(sessionId >> 16); + buf[4] = (byte)(sessionId >> 8); + buf[5] = (byte)(sessionId & 0xFF); + buf[6] = (byte)(payload.Length >> 8); + buf[7] = (byte)(payload.Length & 0xFF); + Buffer.BlockCopy(payload, 0, buf, Proto.HeaderLen, payload.Length); + return buf; + } + public static byte[] Encode(Frame frame) { return frame switch { Frame.Discover => - Header(Proto.TypeDiscover, 0), + Build(Proto.TypeDiscover, 0, []), Frame.Manifest manifest => - BuildManifest(manifest.Entries), + Build(Proto.TypeManifest, 0, BuildManifestPayload(manifest.Entries)), Frame.Open open => - [.. Header(Proto.TypeOpen, 0), open.UpstreamId], + Build(Proto.TypeOpen, 0, [open.UpstreamId]), Frame.OpenAck ack => - [.. Header(Proto.TypeOpenAck, ack.SessionId), ack.UpstreamId], + Build(Proto.TypeOpenAck, ack.SessionId, [ack.UpstreamId]), Frame.OpenNak nak => - [.. Header(Proto.TypeOpenNak, 0), nak.UpstreamId, nak.Reason], + Build(Proto.TypeOpenNak, 0, [nak.UpstreamId, nak.Reason]), Frame.Data data => - [.. Header(Proto.TypeData, data.SessionId), .. data.Payload], + Build(Proto.TypeData, data.SessionId, data.Payload), Frame.Close close => - close.Reason.HasValue - ? [.. Header(Proto.TypeClose, close.SessionId), close.Reason.Value] - : Header(Proto.TypeClose, close.SessionId), + Build(Proto.TypeClose, close.SessionId, + close.Reason.HasValue ? [close.Reason.Value] : []), _ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"), }; } - static byte[] BuildManifest(UpstreamEntry[] entries) + static byte[] BuildManifestPayload(UpstreamEntry[] entries) { using var ms = new MemoryStream(); - ms.Write(Header(Proto.TypeManifest, 0)); foreach (var e in entries) { var labelBytes = Encoding.UTF8.GetBytes(e.Label ?? ""); @@ -92,26 +108,19 @@ static class FrameCodec return ms.ToArray(); } - static byte[] Header(byte type, uint sessionId) - { - return [ - Proto.Version, type, - (byte)(sessionId >> 24), - (byte)(sessionId >> 16), - (byte)(sessionId >> 8), - (byte)(sessionId & 0xFF), - ]; - } - public static Frame? Parse(ReadOnlySpan buf) { - if (buf.Length < 6) + if (buf.Length < Proto.HeaderLen) return null; if (buf[0] != Proto.Version) return null; var type = buf[1]; var sessionId = (uint)(buf[2] << 24 | buf[3] << 16 | buf[4] << 8 | buf[5]); - var payload = buf[6..]; + var payloadLen = (ushort)(buf[6] << 8 | buf[7]); + if (buf.Length < Proto.HeaderLen + payloadLen) + return null; + // Slice exactly payloadLen bytes, ignoring any trailing Ethernet padding. + var payload = buf.Slice(Proto.HeaderLen, payloadLen); return type switch {