carry transport proto end-to-end through OPEN/OPEN_ACK

OPEN and OPEN_ACK now carry proto:1 alongside upstream_id:1. The
session is tagged with its transport proto and reliability semantics
switch on the specific proto:

- TCP (proto=1): full L2 reliability (seq, ack, retransmit, in-order)
- UDP (proto=2, reserved): best-effort (no retransmit, no ordering,
  no pure ACKs — seq/ack_seq fields present but ignored)

This is architecturally correct: instead of a generic 'reliable:bool'
flag, each proto gets the semantics it needs. Today only TCP exists so
every session is reliable, but the extension point is clean for when
stateless protos are added.

Updated: PROTOCOL.md, Rust frame.rs/session.rs/main.rs,
C# Frame.cs/SessionManager.cs.
This commit is contained in:
2026-08-13 09:15:15 +00:00
parent eb8994d1e1
commit 7fa50dd4e4
6 changed files with 129 additions and 68 deletions
+39 -19
View File
@@ -180,22 +180,22 @@ sealed class SessionManager : IDisposable
client = await state.Listener.AcceptTcpClientAsync();
}
catch { break; }
EnqueueOpen(client, state.Upstream.Id);
EnqueueOpen(client, state.Upstream.Id, state.Upstream.Protocol);
}
}
void EnqueueOpen(TcpClient client, byte upstreamId)
void EnqueueOpen(TcpClient client, byte upstreamId, byte proto)
{
lock (_openLock)
{
if (_pending == null)
{
_pending = new PendingOpen(client, upstreamId);
_pending = new PendingOpen(client, upstreamId, proto);
SendOpen(_pending);
}
else
{
_openQueue.Enqueue(new PendingOpen(client, upstreamId));
_openQueue.Enqueue(new PendingOpen(client, upstreamId, proto));
}
}
}
@@ -208,7 +208,7 @@ sealed class SessionManager : IDisposable
po.Client.Dispose();
return;
}
_link.SendTo(_serverMac, new Frame.Open(po.UpstreamId));
_link.SendTo(_serverMac, new Frame.Open(po.UpstreamId, po.Proto));
}
void ProcessQueue()
@@ -240,7 +240,7 @@ sealed class SessionManager : IDisposable
}
var session = new Session(
ack.SessionId, po.Client, srcMac, _link!,
ack.SessionId, ack.Proto, po.Client, srcMac, _link!,
() => _sessions.TryRemove(ack.SessionId, out _),
msg => Log?.Invoke(msg));
_sessions[ack.SessionId] = session;
@@ -273,10 +273,11 @@ sealed class ListenerState(TcpListener listener, UpstreamEntry upstream)
public UpstreamEntry Upstream { get; } = upstream;
}
sealed class PendingOpen(TcpClient client, byte upstreamId)
sealed class PendingOpen(TcpClient client, byte upstreamId, byte proto)
{
public TcpClient Client { get; } = client;
public byte UpstreamId { get; } = upstreamId;
public byte Proto { get; } = proto;
}
/// L2 reliability: sender-side state.
@@ -380,6 +381,7 @@ class RecvState
sealed class Session(
uint sessionId,
byte proto,
TcpClient client,
byte[] serverMac,
TunnelLink link,
@@ -387,6 +389,7 @@ sealed class Session(
Action<string>? log) : IDisposable
{
readonly CancellationTokenSource _cts = new();
readonly bool _isTcp = proto == Proto.ProtoTcp;
readonly SendState _send = new();
readonly RecvState _recv = new();
readonly object _sendLock = new();
@@ -396,13 +399,22 @@ sealed class Session(
{
_ = PumpSocketToTunnel();
_ = PumpTunnelToSocket();
_ = RetransmitTimer();
if (_isTcp)
_ = RetransmitTimer();
}
/// Handle a DATA frame from the tunnel: process ack, deliver in-order.
/// Handle a DATA frame from the tunnel.
public void HandleData(Frame.Data data)
{
// Process ack_seq to advance send window.
if (!_isTcp)
{
// Best-effort: deliver directly, ignore seq/ack.
if (data.Payload.Length > 0)
_deliverChannel.Writer.TryWrite(data.Payload);
return;
}
// TCP: process ack_seq to advance send window.
lock (_sendLock)
_send.ProcessAck(data.AckSeq);
@@ -438,20 +450,27 @@ sealed class Session(
var n = await stream.ReadAsync(buf, _cts.Token);
if (n == 0) break;
uint seq;
uint ackSeq;
lock (_sendLock)
uint seq = 0, ackSeq = 0;
byte[]? frameBytes = null;
if (_isTcp)
{
seq = _send.NextSeq();
lock (_recvLock)
ackSeq = _recv.CurrentAckSeq;
lock (_sendLock)
{
seq = _send.NextSeq();
lock (_recvLock)
ackSeq = _recv.CurrentAckSeq;
}
}
var frame = new Frame.Data(sessionId, seq, ackSeq, buf[..n]);
var frameBytes = FrameCodec.Encode(frame);
lock (_sendLock)
_send.RecordSent(seq, frameBytes);
if (_isTcp)
{
frameBytes = FrameCodec.Encode(frame);
lock (_sendLock)
_send.RecordSent(seq, frameBytes);
}
link.SendTo(serverMac, frame);
}
@@ -508,6 +527,7 @@ sealed class Session(
void SendPureAck()
{
if (!_isTcp) return;
uint seq, ackSeq;
lock (_sendLock)
{