Files
gatuna/win/gatuna-client/Frame.cs
T

166 lines
5.8 KiB
C#
Raw Normal View History

namespace gatuna_client;
using System.Text;
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;
public const byte TypeManifest = 0x02;
public const byte TypeOpen = 0x03;
public const byte TypeOpenAck = 0x04;
public const byte TypeOpenNak = 0x05;
public const byte TypeData = 0x06;
public const byte TypeClose = 0x07;
public const byte ProtoTcp = 1;
public const byte ProtoUdp = 2;
public const byte ReasonUnspecified = 0;
public const byte ReasonUnknownUpstream = 1;
public const byte ReasonConnectFailed = 2;
public const byte ReasonOversize = 3;
public const byte ReasonUnknownSession = 4;
}
readonly record struct UpstreamEntry(
byte Id,
byte Protocol,
ushort Port,
string? Label)
{
public string ProtoName => Protocol == Proto.ProtoTcp ? "tcp" : "udp";
}
abstract record Frame
{
internal record Discover : Frame;
internal record Manifest(UpstreamEntry[] Entries) : Frame;
internal record Open(byte UpstreamId) : Frame;
internal record OpenAck(uint SessionId, byte UpstreamId) : Frame;
internal record OpenNak(byte UpstreamId, byte Reason) : Frame;
internal record Data(uint SessionId, byte[] Payload) : Frame;
internal record Close(uint SessionId, byte? Reason) : 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 =>
Build(Proto.TypeDiscover, 0, []),
Frame.Manifest manifest =>
Build(Proto.TypeManifest, 0, BuildManifestPayload(manifest.Entries)),
Frame.Open open =>
Build(Proto.TypeOpen, 0, [open.UpstreamId]),
Frame.OpenAck ack =>
Build(Proto.TypeOpenAck, ack.SessionId, [ack.UpstreamId]),
Frame.OpenNak nak =>
Build(Proto.TypeOpenNak, 0, [nak.UpstreamId, nak.Reason]),
Frame.Data data =>
Build(Proto.TypeData, data.SessionId, data.Payload),
Frame.Close close =>
Build(Proto.TypeClose, close.SessionId,
close.Reason.HasValue ? [close.Reason.Value] : []),
_ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"),
};
}
static byte[] BuildManifestPayload(UpstreamEntry[] entries)
{
using var ms = new MemoryStream();
foreach (var e in entries)
{
var labelBytes = Encoding.UTF8.GetBytes(e.Label ?? "");
var labelLen = (byte)Math.Min(labelBytes.Length, 255);
ms.WriteByte(e.Id);
ms.WriteByte(e.Protocol);
ms.WriteByte((byte)(e.Port >> 8));
ms.WriteByte((byte)(e.Port & 0xFF));
ms.WriteByte(labelLen);
if (labelLen > 0)
ms.Write(labelBytes, 0, labelLen);
}
return ms.ToArray();
}
public static Frame? Parse(ReadOnlySpan<byte> buf)
{
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 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
{
Proto.TypeManifest => ParseManifest(payload),
Proto.TypeOpenAck when payload.Length == 1 =>
new Frame.OpenAck(sessionId, payload[0]),
Proto.TypeOpenNak when payload.Length == 2 =>
new Frame.OpenNak(payload[0], payload[1]),
Proto.TypeData when payload.Length <= Proto.MaxPayload =>
new Frame.Data(sessionId, payload.ToArray()),
Proto.TypeClose when payload.Length is 0 or 1 =>
new Frame.Close(sessionId, payload.Length == 1 ? payload[0] : null),
Proto.TypeDiscover when payload.Length == 0 =>
new Frame.Discover(),
_ => null,
};
}
static Frame.Manifest? ParseManifest(ReadOnlySpan<byte> payload)
{
var entries = new List<UpstreamEntry>();
int i = 0;
while (i < payload.Length)
{
if (i + 5 > payload.Length)
return null;
var id = payload[i];
var proto = payload[i + 1];
var port = (ushort)(payload[i + 2] << 8 | payload[i + 3]);
var labelLen = payload[i + 4];
i += 5;
if (i + labelLen > payload.Length)
return null;
string? label = labelLen == 0
? null
: Encoding.UTF8.GetString(payload[i..(i + labelLen)]);
i += labelLen;
entries.Add(new UpstreamEntry(id, proto, port, label));
}
return new Frame.Manifest(entries.ToArray());
}
}