move win/gatuna-client to gatuna-win, rename project to gatuna
- Directory: win/gatuna-client/ -> gatuna-win/ - Project file: gatuna-client.csproj -> gatuna.csproj - Assembly name: gatuna-client -> gatuna - Root namespace: gatuna_client -> gatuna - All .cs files: namespace gatuna_client -> gatuna - app.manifest: assemblyIdentity name -> gatuna - README: updated all paths and references
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace gatuna;
|
||||
|
||||
sealed class SessionManager : IDisposable
|
||||
{
|
||||
TunnelLink? _link;
|
||||
readonly ConcurrentDictionary<uint, Session> _sessions = new();
|
||||
readonly ConcurrentDictionary<int, ListenerState> _listeners = new();
|
||||
|
||||
byte[]? _serverMac;
|
||||
string _serverHostname = "";
|
||||
UpstreamEntry[] _upstreams = [];
|
||||
PingTest? _pingTest;
|
||||
|
||||
// Serialized OPEN: only one outstanding at a time.
|
||||
readonly object _openLock = new();
|
||||
PendingOpen? _pending;
|
||||
readonly Queue<PendingOpen> _openQueue = new();
|
||||
|
||||
public event Action<string>? Log;
|
||||
public event Action<string, byte[], UpstreamEntry[]>? ManifestReceived;
|
||||
|
||||
public UpstreamEntry[] Upstreams => _upstreams;
|
||||
public byte[]? ServerMac => _serverMac;
|
||||
public string ServerHostname => _serverHostname;
|
||||
public TunnelLink? Link => _link;
|
||||
|
||||
public void AttachLink(TunnelLink link)
|
||||
{
|
||||
_link = link;
|
||||
link.FrameReceived += HandleFrame;
|
||||
}
|
||||
|
||||
public void DetachLink()
|
||||
{
|
||||
if (_link != null)
|
||||
_link.FrameReceived -= HandleFrame;
|
||||
_link = null;
|
||||
}
|
||||
|
||||
public void Discover()
|
||||
{
|
||||
if (_link == null) return;
|
||||
_link.SendBroadcast(new Frame.Discover());
|
||||
}
|
||||
|
||||
public PingTest? StartPing()
|
||||
{
|
||||
if (_link == null || _serverMac == null)
|
||||
return null;
|
||||
_pingTest?.Stop();
|
||||
_pingTest = new PingTest(_link, _serverMac);
|
||||
_pingTest.Start();
|
||||
return _pingTest;
|
||||
}
|
||||
|
||||
public void StopPing()
|
||||
{
|
||||
_pingTest?.Stop();
|
||||
_pingTest = null;
|
||||
}
|
||||
|
||||
public void HandleFrame(Frame frame, byte[] srcMac)
|
||||
{
|
||||
switch (frame)
|
||||
{
|
||||
case Frame.Manifest manifest:
|
||||
_serverMac = srcMac;
|
||||
_serverHostname = manifest.Hostname;
|
||||
_upstreams = manifest.Entries;
|
||||
Log?.Invoke($"manifest: {manifest.Entries.Length} upstreams from {manifest.Hostname} ({BitConverter.ToString(srcMac)})");
|
||||
ManifestReceived?.Invoke(manifest.Hostname, srcMac, manifest.Entries);
|
||||
break;
|
||||
|
||||
case Frame.OpenAck ack:
|
||||
HandleOpenAck(ack, srcMac);
|
||||
break;
|
||||
|
||||
case Frame.OpenNak nak:
|
||||
Log?.Invoke($"OPEN_NAK upstream {nak.UpstreamId} reason {nak.Reason}");
|
||||
lock (_openLock)
|
||||
{
|
||||
if (_pending != null)
|
||||
_pending.Client.Dispose();
|
||||
}
|
||||
ProcessQueue();
|
||||
break;
|
||||
|
||||
case Frame.Data data:
|
||||
if (_sessions.TryGetValue(data.SessionId, out var session))
|
||||
session.Deliver(data.Payload);
|
||||
else if (_link != null && _serverMac != null)
|
||||
_link.SendTo(_serverMac,
|
||||
new Frame.Close(data.SessionId, Proto.ReasonUnknownSession));
|
||||
break;
|
||||
|
||||
case Frame.Close close:
|
||||
if (_sessions.TryRemove(close.SessionId, out var s))
|
||||
s.Dispose();
|
||||
break;
|
||||
|
||||
case Frame.Pong pong:
|
||||
_pingTest?.HandlePong(pong.Nonce);
|
||||
break;
|
||||
|
||||
case Frame.Ping _:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute a deterministic mirror port from the server MAC and the
|
||||
/// upstream port. XOR the upstream port with (mac[0]<<8 | mac[5]),
|
||||
/// then ensure the result is outside the privileged range.
|
||||
/// </summary>
|
||||
static ushort ComputeMirrorPort(byte[] serverMac, ushort upstreamPort)
|
||||
{
|
||||
var k = (ushort)((serverMac[0] << 8) | serverMac[5]);
|
||||
var port = (ushort)(upstreamPort ^ k);
|
||||
if (port < 1024)
|
||||
port += 1024;
|
||||
return port;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a local TCP listener for the given upstream. Returns the mirror
|
||||
/// port, or 0 on failure.
|
||||
/// </summary>
|
||||
public int StartListener(UpstreamEntry upstream)
|
||||
{
|
||||
if (_serverMac == null)
|
||||
return 0;
|
||||
|
||||
var preferred = ComputeMirrorPort(_serverMac, upstream.Port);
|
||||
|
||||
// Try the deterministic port first; fall back to OS assignment.
|
||||
TcpListener listener;
|
||||
int port;
|
||||
try
|
||||
{
|
||||
listener = new TcpListener(IPAddress.Loopback, preferred);
|
||||
listener.Start();
|
||||
port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
}
|
||||
catch
|
||||
{
|
||||
listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
Log?.Invoke($"port {preferred} in use, fell back to {port}");
|
||||
}
|
||||
|
||||
var state = new ListenerState(listener, upstream);
|
||||
_listeners[port] = state;
|
||||
_ = AcceptLoop(state);
|
||||
Log?.Invoke($"listening 127.0.0.1:{port} -> upstream {upstream.Id} ({upstream.ProtoName}:{upstream.Port})");
|
||||
return port;
|
||||
}
|
||||
|
||||
public void StopListener(int port)
|
||||
{
|
||||
if (_listeners.TryRemove(port, out var state))
|
||||
{
|
||||
state.Listener.Stop();
|
||||
Log?.Invoke($"stopped listener port {port}");
|
||||
}
|
||||
}
|
||||
|
||||
async Task AcceptLoop(ListenerState state)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
TcpClient client;
|
||||
try
|
||||
{
|
||||
client = await state.Listener.AcceptTcpClientAsync();
|
||||
}
|
||||
catch { break; }
|
||||
EnqueueOpen(client, state.Upstream.Id);
|
||||
}
|
||||
}
|
||||
|
||||
void EnqueueOpen(TcpClient client, byte upstreamId)
|
||||
{
|
||||
lock (_openLock)
|
||||
{
|
||||
if (_pending == null)
|
||||
{
|
||||
_pending = new PendingOpen(client, upstreamId);
|
||||
SendOpen(_pending);
|
||||
}
|
||||
else
|
||||
{
|
||||
_openQueue.Enqueue(new PendingOpen(client, upstreamId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SendOpen(PendingOpen po)
|
||||
{
|
||||
if (_link == null || _serverMac == null)
|
||||
{
|
||||
Log?.Invoke("no server; cannot OPEN");
|
||||
po.Client.Dispose();
|
||||
return;
|
||||
}
|
||||
_link.SendTo(_serverMac, new Frame.Open(po.UpstreamId));
|
||||
}
|
||||
|
||||
void ProcessQueue()
|
||||
{
|
||||
lock (_openLock)
|
||||
{
|
||||
if (_openQueue.Count > 0)
|
||||
{
|
||||
_pending = _openQueue.Dequeue();
|
||||
SendOpen(_pending);
|
||||
}
|
||||
else
|
||||
{
|
||||
_pending = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HandleOpenAck(Frame.OpenAck ack, byte[] srcMac)
|
||||
{
|
||||
PendingOpen? po;
|
||||
lock (_openLock)
|
||||
po = _pending;
|
||||
|
||||
if (po == null || po.UpstreamId != ack.UpstreamId)
|
||||
{
|
||||
Log?.Invoke($"OPEN_ACK upstream {ack.UpstreamId} session {ack.SessionId} — no matching pending");
|
||||
return;
|
||||
}
|
||||
|
||||
var session = new Session(
|
||||
ack.SessionId, po.Client, srcMac, _link!,
|
||||
() => _sessions.TryRemove(ack.SessionId, out _),
|
||||
msg => Log?.Invoke(msg));
|
||||
_sessions[ack.SessionId] = session;
|
||||
session.Start();
|
||||
Log?.Invoke($"session {ack.SessionId} upstream {ack.UpstreamId} established");
|
||||
ProcessQueue();
|
||||
}
|
||||
|
||||
public void StopAll()
|
||||
{
|
||||
StopPing();
|
||||
foreach (var kv in _listeners)
|
||||
kv.Value.Listener.Stop();
|
||||
_listeners.Clear();
|
||||
foreach (var s in _sessions.Values)
|
||||
s.Dispose();
|
||||
_sessions.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DetachLink();
|
||||
StopAll();
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ListenerState(TcpListener listener, UpstreamEntry upstream)
|
||||
{
|
||||
public TcpListener Listener { get; } = listener;
|
||||
public UpstreamEntry Upstream { get; } = upstream;
|
||||
}
|
||||
|
||||
sealed class PendingOpen(TcpClient client, byte upstreamId)
|
||||
{
|
||||
public TcpClient Client { get; } = client;
|
||||
public byte UpstreamId { get; } = upstreamId;
|
||||
}
|
||||
|
||||
sealed class Session(
|
||||
uint sessionId,
|
||||
TcpClient client,
|
||||
byte[] serverMac,
|
||||
TunnelLink link,
|
||||
Action onClosed,
|
||||
Action<string>? log) : IDisposable
|
||||
{
|
||||
readonly CancellationTokenSource _cts = new();
|
||||
readonly Channel<byte[]> _incoming = Channel.CreateBounded<byte[]>(256);
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_ = PumpSocketToTunnel();
|
||||
_ = PumpTunnelToSocket();
|
||||
}
|
||||
|
||||
public void Deliver(byte[] payload)
|
||||
{
|
||||
if (!_incoming.Writer.TryWrite(payload))
|
||||
log?.Invoke($"session {sessionId}: incoming channel full");
|
||||
}
|
||||
|
||||
async Task PumpSocketToTunnel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = client.GetStream();
|
||||
var buf = new byte[Proto.MaxPayload];
|
||||
using var reg = _cts.Token.Register(() => client.Dispose());
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
var n = await stream.ReadAsync(buf, _cts.Token);
|
||||
if (n == 0) break;
|
||||
link.SendTo(serverMac, new Frame.Data(sessionId, buf[..n]));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
SendClose();
|
||||
onClosed();
|
||||
}
|
||||
|
||||
async Task PumpTunnelToSocket()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = client.GetStream();
|
||||
await foreach (var payload in _incoming.Reader.ReadAllAsync(_cts.Token))
|
||||
await stream.WriteAsync(payload, _cts.Token);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
void SendClose() => link.SendTo(serverMac, new Frame.Close(sessionId, null));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_incoming.Writer.TryComplete();
|
||||
SendClose();
|
||||
try { client.Dispose(); } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user