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:
+11
-5
@@ -11,7 +11,9 @@ discriminator; there is no magic number inside the payload.
|
|||||||
+---------------+---------------+-------------------------------+
|
+---------------+---------------+-------------------------------+
|
||||||
| version | type | session_id |
|
| 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.
|
- **type** (u8): frame type, see table below.
|
||||||
- **session_id** (u32, big-endian): `0` for non-session frames; the
|
- **session_id** (u32, big-endian): `0` for non-session frames; the
|
||||||
server-assigned ID for session-scoped frames.
|
server-assigned ID for session-scoped frames.
|
||||||
- **payload** (variable): type-dependent. No length field is carried — the
|
- **payload_len** (u16, big-endian): number of payload bytes that follow.
|
||||||
Ethernet frame length from the capture gives the payload extent.
|
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
|
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
|
the endpoints' TCP stacks; UDP (reserved) will rely on application-level
|
||||||
mechanisms.
|
mechanisms.
|
||||||
|
|
||||||
Maximum payload: 1500 (Ethernet MTU) − 14 (eth header) − 6 (our header) =
|
Maximum payload: 1500 (Ethernet MTU) − 8 (our header) = **1492 bytes**. In
|
||||||
**1480 bytes**. Larger payloads are not emitted in v1.
|
practice we cap at **1480** to stay conservative. Larger payloads are not
|
||||||
|
emitted in v1.
|
||||||
|
|
||||||
## Frame types
|
## Frame types
|
||||||
|
|
||||||
|
|||||||
+34
-28
@@ -3,6 +3,7 @@
|
|||||||
pub const ETHERTYPE: u16 = 0x6969;
|
pub const ETHERTYPE: u16 = 0x6969;
|
||||||
pub const ETH_HEADER_LEN: usize = 14;
|
pub const ETH_HEADER_LEN: usize = 14;
|
||||||
pub const VERSION: u8 = 1;
|
pub const VERSION: u8 = 1;
|
||||||
|
pub const HEADER_LEN: usize = 8;
|
||||||
pub const MAX_PAYLOAD: usize = 1480;
|
pub const MAX_PAYLOAD: usize = 1480;
|
||||||
|
|
||||||
pub const TYPE_DISCOVER: u8 = 0x01;
|
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]);
|
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 {
|
impl Frame {
|
||||||
pub fn encode(&self) -> Vec<u8> {
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
let mut buf = Vec::new();
|
|
||||||
match self {
|
match self {
|
||||||
Frame::Discover => {
|
Frame::Discover => build(TYPE_DISCOVER, 0, Vec::new()),
|
||||||
buf.extend_from_slice(&[VERSION, TYPE_DISCOVER, 0, 0, 0, 0]);
|
|
||||||
}
|
|
||||||
Frame::Manifest(entries) => {
|
Frame::Manifest(entries) => {
|
||||||
buf.extend_from_slice(&[VERSION, TYPE_MANIFEST, 0, 0, 0, 0]);
|
let mut payload = Vec::new();
|
||||||
for e in entries {
|
for e in entries {
|
||||||
encode_entry(&mut buf, e);
|
encode_entry(&mut payload, e);
|
||||||
}
|
}
|
||||||
|
build(TYPE_MANIFEST, 0, payload)
|
||||||
}
|
}
|
||||||
Frame::Open { upstream_id } => {
|
Frame::Open { upstream_id } => build(TYPE_OPEN, 0, vec![*upstream_id]),
|
||||||
buf.extend_from_slice(&[VERSION, TYPE_OPEN, 0, 0, 0, 0, *upstream_id]);
|
|
||||||
}
|
|
||||||
Frame::OpenAck { session_id, upstream_id } => {
|
Frame::OpenAck { session_id, upstream_id } => {
|
||||||
buf.extend_from_slice(&[VERSION, TYPE_OPEN_ACK]);
|
build(TYPE_OPEN_ACK, *session_id, vec![*upstream_id])
|
||||||
buf.extend_from_slice(&session_id.to_be_bytes());
|
|
||||||
buf.push(*upstream_id);
|
|
||||||
}
|
}
|
||||||
Frame::OpenNak { upstream_id, reason } => {
|
Frame::OpenNak { upstream_id, reason } => {
|
||||||
buf.extend_from_slice(&[VERSION, TYPE_OPEN_NAK, 0, 0, 0, 0]);
|
build(TYPE_OPEN_NAK, 0, vec![*upstream_id, *reason])
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
Frame::Data { session_id, payload } => build(TYPE_DATA, *session_id, payload.clone()),
|
||||||
Frame::Close { session_id, reason } => {
|
Frame::Close { session_id, reason } => {
|
||||||
buf.extend_from_slice(&[VERSION, TYPE_CLOSE]);
|
let p = match reason {
|
||||||
buf.extend_from_slice(&session_id.to_be_bytes());
|
Some(r) => vec![*r],
|
||||||
if let Some(r) = reason {
|
None => Vec::new(),
|
||||||
buf.push(*r);
|
};
|
||||||
|
build(TYPE_CLOSE, *session_id, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(buf: &[u8]) -> Result<Frame, DecodeError> {
|
pub fn parse(buf: &[u8]) -> Result<Frame, DecodeError> {
|
||||||
if buf.len() < 6 {
|
if buf.len() < HEADER_LEN {
|
||||||
return Err(DecodeError::Short);
|
return Err(DecodeError::Short);
|
||||||
}
|
}
|
||||||
let version = buf[0];
|
let version = buf[0];
|
||||||
@@ -128,7 +129,12 @@ impl Frame {
|
|||||||
}
|
}
|
||||||
let typ = buf[1];
|
let typ = buf[1];
|
||||||
let session_id = u32::from_be_bytes([buf[2], buf[3], buf[4], buf[5]]);
|
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 {
|
match typ {
|
||||||
TYPE_DISCOVER => {
|
TYPE_DISCOVER => {
|
||||||
if !payload.is_empty() {
|
if !payload.is_empty() {
|
||||||
|
|||||||
+33
-24
@@ -7,6 +7,7 @@ static class Proto
|
|||||||
public const ushort EtherType = 0x6969;
|
public const ushort EtherType = 0x6969;
|
||||||
public const int EthHeaderLen = 14;
|
public const int EthHeaderLen = 14;
|
||||||
public const byte Version = 1;
|
public const byte Version = 1;
|
||||||
|
public const int HeaderLen = 8;
|
||||||
public const int MaxPayload = 1480;
|
public const int MaxPayload = 1480;
|
||||||
|
|
||||||
public const byte TypeDiscover = 0x01;
|
public const byte TypeDiscover = 0x01;
|
||||||
@@ -49,34 +50,49 @@ abstract record Frame
|
|||||||
|
|
||||||
static class FrameCodec
|
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)
|
public static byte[] Encode(Frame frame)
|
||||||
{
|
{
|
||||||
return frame switch
|
return frame switch
|
||||||
{
|
{
|
||||||
Frame.Discover =>
|
Frame.Discover =>
|
||||||
Header(Proto.TypeDiscover, 0),
|
Build(Proto.TypeDiscover, 0, []),
|
||||||
Frame.Manifest manifest =>
|
Frame.Manifest manifest =>
|
||||||
BuildManifest(manifest.Entries),
|
Build(Proto.TypeManifest, 0, BuildManifestPayload(manifest.Entries)),
|
||||||
Frame.Open open =>
|
Frame.Open open =>
|
||||||
[.. Header(Proto.TypeOpen, 0), open.UpstreamId],
|
Build(Proto.TypeOpen, 0, [open.UpstreamId]),
|
||||||
Frame.OpenAck ack =>
|
Frame.OpenAck ack =>
|
||||||
[.. Header(Proto.TypeOpenAck, ack.SessionId), ack.UpstreamId],
|
Build(Proto.TypeOpenAck, ack.SessionId, [ack.UpstreamId]),
|
||||||
Frame.OpenNak nak =>
|
Frame.OpenNak nak =>
|
||||||
[.. Header(Proto.TypeOpenNak, 0), nak.UpstreamId, nak.Reason],
|
Build(Proto.TypeOpenNak, 0, [nak.UpstreamId, nak.Reason]),
|
||||||
Frame.Data data =>
|
Frame.Data data =>
|
||||||
[.. Header(Proto.TypeData, data.SessionId), .. data.Payload],
|
Build(Proto.TypeData, data.SessionId, data.Payload),
|
||||||
Frame.Close close =>
|
Frame.Close close =>
|
||||||
close.Reason.HasValue
|
Build(Proto.TypeClose, close.SessionId,
|
||||||
? [.. Header(Proto.TypeClose, close.SessionId), close.Reason.Value]
|
close.Reason.HasValue ? [close.Reason.Value] : []),
|
||||||
: Header(Proto.TypeClose, close.SessionId),
|
|
||||||
_ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"),
|
_ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static byte[] BuildManifest(UpstreamEntry[] entries)
|
static byte[] BuildManifestPayload(UpstreamEntry[] entries)
|
||||||
{
|
{
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
ms.Write(Header(Proto.TypeManifest, 0));
|
|
||||||
foreach (var e in entries)
|
foreach (var e in entries)
|
||||||
{
|
{
|
||||||
var labelBytes = Encoding.UTF8.GetBytes(e.Label ?? "");
|
var labelBytes = Encoding.UTF8.GetBytes(e.Label ?? "");
|
||||||
@@ -92,26 +108,19 @@ static class FrameCodec
|
|||||||
return ms.ToArray();
|
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<byte> buf)
|
public static Frame? Parse(ReadOnlySpan<byte> buf)
|
||||||
{
|
{
|
||||||
if (buf.Length < 6)
|
if (buf.Length < Proto.HeaderLen)
|
||||||
return null;
|
return null;
|
||||||
if (buf[0] != Proto.Version)
|
if (buf[0] != Proto.Version)
|
||||||
return null;
|
return null;
|
||||||
var type = buf[1];
|
var type = buf[1];
|
||||||
var sessionId = (uint)(buf[2] << 24 | buf[3] << 16 | buf[4] << 8 | buf[5]);
|
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
|
return type switch
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user