2026-08-13 08:43:11 +00:00
|
|
|
namespace gatuna;
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
|
|
|
|
static class Proto
|
|
|
|
|
{
|
|
|
|
|
public const ushort EtherType = 0x6969;
|
|
|
|
|
public const int EthHeaderLen = 14;
|
2026-08-13 09:08:05 +00:00
|
|
|
public const byte Version = 2;
|
2026-08-13 07:51:39 +00:00
|
|
|
public const int HeaderLen = 8;
|
2026-08-13 09:08:05 +00:00
|
|
|
public const int DataHeaderLen = 16; // 8 common + 4 seq + 4 ack_seq
|
2026-08-12 18:20:51 +00:00
|
|
|
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;
|
2026-08-13 08:28:59 +00:00
|
|
|
public const byte TypePing = 0x0B;
|
|
|
|
|
public const byte TypePong = 0x0C;
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
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;
|
2026-08-13 09:08:05 +00:00
|
|
|
public const byte ReasonMaxRetries = 5;
|
2026-08-12 18:20:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-08-13 08:08:05 +00:00
|
|
|
internal record Manifest(string Hostname, UpstreamEntry[] Entries) : Frame;
|
2026-08-13 09:15:15 +00:00
|
|
|
internal record Open(byte UpstreamId, byte Proto) : Frame;
|
|
|
|
|
internal record OpenAck(uint SessionId, byte UpstreamId, byte Proto) : Frame;
|
2026-08-12 18:20:51 +00:00
|
|
|
internal record OpenNak(byte UpstreamId, byte Reason) : Frame;
|
2026-08-13 09:08:05 +00:00
|
|
|
internal record Data(uint SessionId, uint Seq, uint AckSeq, byte[] Payload) : Frame;
|
2026-08-12 18:20:51 +00:00
|
|
|
internal record Close(uint SessionId, byte? Reason) : Frame;
|
2026-08-13 08:28:59 +00:00
|
|
|
internal record Ping(ulong Nonce) : Frame;
|
|
|
|
|
internal record Pong(ulong Nonce) : Frame;
|
2026-08-12 18:20:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static class FrameCodec
|
|
|
|
|
{
|
2026-08-13 09:08:05 +00:00
|
|
|
/// Build a non-DATA frame: 8-byte common header + payload.
|
2026-08-13 07:51:39 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
/// Build a DATA frame: 8-byte common header + seq + ack_seq + payload.
|
|
|
|
|
/// payload_len counts only the raw bytes, not seq/ack_seq.
|
|
|
|
|
static byte[] BuildData(uint sessionId, uint seq, uint ackSeq, byte[] payload)
|
|
|
|
|
{
|
|
|
|
|
var buf = new byte[Proto.DataHeaderLen + payload.Length];
|
|
|
|
|
buf[0] = Proto.Version;
|
|
|
|
|
buf[1] = Proto.TypeData;
|
|
|
|
|
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);
|
|
|
|
|
buf[8] = (byte)(seq >> 24);
|
|
|
|
|
buf[9] = (byte)(seq >> 16);
|
|
|
|
|
buf[10] = (byte)(seq >> 8);
|
|
|
|
|
buf[11] = (byte)(seq & 0xFF);
|
|
|
|
|
buf[12] = (byte)(ackSeq >> 24);
|
|
|
|
|
buf[13] = (byte)(ackSeq >> 16);
|
|
|
|
|
buf[14] = (byte)(ackSeq >> 8);
|
|
|
|
|
buf[15] = (byte)(ackSeq & 0xFF);
|
|
|
|
|
Buffer.BlockCopy(payload, 0, buf, Proto.DataHeaderLen, payload.Length);
|
|
|
|
|
return buf;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 18:20:51 +00:00
|
|
|
public static byte[] Encode(Frame frame)
|
|
|
|
|
{
|
|
|
|
|
return frame switch
|
|
|
|
|
{
|
|
|
|
|
Frame.Discover =>
|
2026-08-13 07:51:39 +00:00
|
|
|
Build(Proto.TypeDiscover, 0, []),
|
2026-08-12 18:20:51 +00:00
|
|
|
Frame.Manifest manifest =>
|
2026-08-13 08:08:05 +00:00
|
|
|
Build(Proto.TypeManifest, 0, BuildManifestPayload(manifest.Hostname, manifest.Entries)),
|
2026-08-12 18:20:51 +00:00
|
|
|
Frame.Open open =>
|
2026-08-13 09:15:15 +00:00
|
|
|
Build(Proto.TypeOpen, 0, [open.UpstreamId, open.Proto]),
|
2026-08-12 18:20:51 +00:00
|
|
|
Frame.OpenAck ack =>
|
2026-08-13 09:15:15 +00:00
|
|
|
Build(Proto.TypeOpenAck, ack.SessionId, [ack.UpstreamId, ack.Proto]),
|
2026-08-12 18:20:51 +00:00
|
|
|
Frame.OpenNak nak =>
|
2026-08-13 07:51:39 +00:00
|
|
|
Build(Proto.TypeOpenNak, 0, [nak.UpstreamId, nak.Reason]),
|
2026-08-12 18:20:51 +00:00
|
|
|
Frame.Data data =>
|
2026-08-13 09:08:05 +00:00
|
|
|
BuildData(data.SessionId, data.Seq, data.AckSeq, data.Payload),
|
2026-08-12 18:20:51 +00:00
|
|
|
Frame.Close close =>
|
2026-08-13 07:51:39 +00:00
|
|
|
Build(Proto.TypeClose, close.SessionId,
|
|
|
|
|
close.Reason.HasValue ? [close.Reason.Value] : []),
|
2026-08-13 08:28:59 +00:00
|
|
|
Frame.Ping ping =>
|
|
|
|
|
Build(Proto.TypePing, 0, EncodeNonce(ping.Nonce)),
|
|
|
|
|
Frame.Pong pong =>
|
|
|
|
|
Build(Proto.TypePong, 0, EncodeNonce(pong.Nonce)),
|
2026-08-12 18:20:51 +00:00
|
|
|
_ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 08:08:05 +00:00
|
|
|
static byte[] BuildManifestPayload(string hostname, UpstreamEntry[] entries)
|
2026-08-12 18:20:51 +00:00
|
|
|
{
|
|
|
|
|
using var ms = new MemoryStream();
|
2026-08-13 08:08:05 +00:00
|
|
|
var hnBytes = Encoding.UTF8.GetBytes(hostname);
|
|
|
|
|
var hnLen = (byte)Math.Min(hnBytes.Length, 255);
|
|
|
|
|
ms.WriteByte(hnLen);
|
|
|
|
|
if (hnLen > 0)
|
|
|
|
|
ms.Write(hnBytes, 0, hnLen);
|
2026-08-12 18:20:51 +00:00
|
|
|
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)
|
|
|
|
|
{
|
2026-08-13 07:51:39 +00:00
|
|
|
if (buf.Length < Proto.HeaderLen)
|
2026-08-12 18:20:51 +00:00
|
|
|
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]);
|
2026-08-13 07:51:39 +00:00
|
|
|
var payloadLen = (ushort)(buf[6] << 8 | buf[7]);
|
2026-08-13 09:08:05 +00:00
|
|
|
|
|
|
|
|
// DATA frames have seq + ack_seq after the common header.
|
|
|
|
|
if (type == Proto.TypeData)
|
|
|
|
|
{
|
|
|
|
|
if (buf.Length < Proto.DataHeaderLen + payloadLen)
|
|
|
|
|
return null;
|
|
|
|
|
var seq = (uint)(buf[8] << 24 | buf[9] << 16 | buf[10] << 8 | buf[11]);
|
|
|
|
|
var ackSeq = (uint)(buf[12] << 24 | buf[13] << 16 | buf[14] << 8 | buf[15]);
|
|
|
|
|
var payload = buf.Slice(Proto.DataHeaderLen, payloadLen);
|
|
|
|
|
if (payload.Length > Proto.MaxPayload)
|
|
|
|
|
return null;
|
|
|
|
|
return new Frame.Data(sessionId, seq, ackSeq, payload.ToArray());
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 07:51:39 +00:00
|
|
|
if (buf.Length < Proto.HeaderLen + payloadLen)
|
|
|
|
|
return null;
|
2026-08-13 09:08:05 +00:00
|
|
|
var payload2 = buf.Slice(Proto.HeaderLen, payloadLen);
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
return type switch
|
|
|
|
|
{
|
2026-08-13 09:08:05 +00:00
|
|
|
Proto.TypeManifest => ParseManifest(payload2),
|
2026-08-13 09:15:15 +00:00
|
|
|
Proto.TypeOpenAck when payload2.Length == 2 =>
|
|
|
|
|
new Frame.OpenAck(sessionId, payload2[0], payload2[1]),
|
2026-08-13 09:08:05 +00:00
|
|
|
Proto.TypeOpenNak when payload2.Length == 2 =>
|
|
|
|
|
new Frame.OpenNak(payload2[0], payload2[1]),
|
|
|
|
|
Proto.TypeClose when payload2.Length is 0 or 1 =>
|
|
|
|
|
new Frame.Close(sessionId, payload2.Length == 1 ? payload2[0] : null),
|
|
|
|
|
Proto.TypePong when payload2.Length == 8 =>
|
|
|
|
|
new Frame.Pong(ParseNonce(payload2)),
|
|
|
|
|
Proto.TypePing when payload2.Length == 8 =>
|
|
|
|
|
new Frame.Ping(ParseNonce(payload2)),
|
|
|
|
|
Proto.TypeDiscover when payload2.Length == 0 =>
|
2026-08-12 18:20:51 +00:00
|
|
|
new Frame.Discover(),
|
|
|
|
|
_ => null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 08:28:59 +00:00
|
|
|
static ulong ParseNonce(ReadOnlySpan<byte> payload)
|
|
|
|
|
{
|
|
|
|
|
ulong nonce = 0;
|
|
|
|
|
for (int i = 0; i < 8; i++)
|
|
|
|
|
nonce = (nonce << 8) | payload[i];
|
|
|
|
|
return nonce;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static byte[] EncodeNonce(ulong nonce)
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
(byte)(nonce >> 56), (byte)(nonce >> 48),
|
|
|
|
|
(byte)(nonce >> 40), (byte)(nonce >> 32),
|
|
|
|
|
(byte)(nonce >> 24), (byte)(nonce >> 16),
|
|
|
|
|
(byte)(nonce >> 8), (byte)(nonce & 0xFF),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 18:20:51 +00:00
|
|
|
static Frame.Manifest? ParseManifest(ReadOnlySpan<byte> payload)
|
|
|
|
|
{
|
2026-08-13 08:08:05 +00:00
|
|
|
if (payload.Length < 1)
|
|
|
|
|
return null;
|
|
|
|
|
var hnLen = payload[0];
|
|
|
|
|
if (1 + hnLen > payload.Length)
|
|
|
|
|
return null;
|
|
|
|
|
var hostname = hnLen == 0
|
|
|
|
|
? ""
|
|
|
|
|
: Encoding.UTF8.GetString(payload[1..(1 + hnLen)]);
|
|
|
|
|
var rest = payload[(1 + hnLen)..];
|
|
|
|
|
|
2026-08-12 18:20:51 +00:00
|
|
|
var entries = new List<UpstreamEntry>();
|
|
|
|
|
int i = 0;
|
2026-08-13 08:08:05 +00:00
|
|
|
while (i < rest.Length)
|
2026-08-12 18:20:51 +00:00
|
|
|
{
|
2026-08-13 08:08:05 +00:00
|
|
|
if (i + 5 > rest.Length)
|
2026-08-12 18:20:51 +00:00
|
|
|
return null;
|
2026-08-13 08:08:05 +00:00
|
|
|
var id = rest[i];
|
|
|
|
|
var proto = rest[i + 1];
|
|
|
|
|
var port = (ushort)(rest[i + 2] << 8 | rest[i + 3]);
|
|
|
|
|
var labelLen = rest[i + 4];
|
2026-08-12 18:20:51 +00:00
|
|
|
i += 5;
|
2026-08-13 08:08:05 +00:00
|
|
|
if (i + labelLen > rest.Length)
|
2026-08-12 18:20:51 +00:00
|
|
|
return null;
|
|
|
|
|
string? label = labelLen == 0
|
|
|
|
|
? null
|
2026-08-13 08:08:05 +00:00
|
|
|
: Encoding.UTF8.GetString(rest[i..(i + labelLen)]);
|
2026-08-12 18:20:51 +00:00
|
|
|
i += labelLen;
|
|
|
|
|
entries.Add(new UpstreamEntry(id, proto, port, label));
|
|
|
|
|
}
|
2026-08-13 08:08:05 +00:00
|
|
|
return new Frame.Manifest(hostname, entries.ToArray());
|
2026-08-12 18:20:51 +00:00
|
|
|
}
|
|
|
|
|
}
|