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:
+33
-24
@@ -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<byte> 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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user