25f00b0deb
Raw-Ethernet tunnel over ethertype 0x6969 to bypass WFP killswitches. Server (Rust, AF_PACKET + classic BPF) relays TCP to 127.0.0.1 services. Client (.NET 8 WinForms, SharpPcap/Npcap) discovers upstreams and exposes local loopback listeners. Wire protocol: 6-byte header, 7 frame types, MANIFEST with labeled upstreams. UDP types reserved, unimplemented.
157 lines
5.2 KiB
C#
157 lines
5.2 KiB
C#
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<byte> 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<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());
|
|
}
|
|
}
|