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 _sessions = new(); readonly ConcurrentDictionary _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 _openQueue = new(); public event Action? Log; public event Action? 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; } } /// /// 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. /// 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; } /// /// Start a local TCP listener for the given upstream. Returns the mirror /// port, or 0 on failure. /// 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? log) : IDisposable { readonly CancellationTokenSource _cts = new(); readonly Channel _incoming = Channel.CreateBounded(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(); } }