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 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 { public static byte[] Encode(Frame frame) { return frame switch { Frame.Discover => Header(Proto.TypeDiscover, 0), Frame.Manifest manifest => BuildManifest(manifest.Entries), Frame.Open open => [.. Header(Proto.TypeOpen, 0), open.UpstreamId], Frame.OpenAck ack => [.. Header(Proto.TypeOpenAck, ack.SessionId), ack.UpstreamId], Frame.OpenNak nak => [.. Header(Proto.TypeOpenNak, 0), nak.UpstreamId, nak.Reason], Frame.Data data => [.. Header(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), _ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"), }; } static byte[] BuildManifest(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 ?? ""); 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(); } 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) 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..]; 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 payload) { var entries = new List(); 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()); } }