add L2 reliability: seq + cumulative ACK + retransmit (protocol v2)
DATA frames now carry seq:4 and ack_seq:4 in a 16-byte extended header. Both sides maintain per-session send/recv state: Sender: - Monotonic seq counter, retransmit buffer (seq -> frame bytes) - Retransmit timer: 5ms timeout, 10 max retries -> CLOSE - Window advances on cumulative ACK Receiver: - In-order delivery to TCP socket (expected_seq) - Out-of-order buffering (SortedList by seq) - Duplicate detection (seq < expected -> discard + re-ACK) - Pure ACK frames (empty-payload DATA) for duplicate/OOO responses This prevents lost Ethernet frames from permanently corrupting TCP sessions, which was the key v1 limitation. The local kernel TCP stack ACKs data before we chunk it into DATA frames; without L2 reliability a dropped frame creates an unrecoverable gap. Version bumped to 2. Both sides must speak v2; no negotiation. Updated: PROTOCOL.md (full v2 spec), README.md, Rust frame.rs/ session.rs/main.rs, C# Frame.cs/SessionManager.cs/TunnelLink.cs.
This commit is contained in:
@@ -92,7 +92,7 @@ sealed class SessionManager : IDisposable
|
||||
|
||||
case Frame.Data data:
|
||||
if (_sessions.TryGetValue(data.SessionId, out var session))
|
||||
session.Deliver(data.Payload);
|
||||
session.HandleData(data);
|
||||
else if (_link != null && _serverMac != null)
|
||||
_link.SendTo(_serverMac,
|
||||
new Frame.Close(data.SessionId, Proto.ReasonUnknownSession));
|
||||
@@ -279,6 +279,105 @@ sealed class PendingOpen(TcpClient client, byte upstreamId)
|
||||
public byte UpstreamId { get; } = upstreamId;
|
||||
}
|
||||
|
||||
/// L2 reliability: sender-side state.
|
||||
class SendState
|
||||
{
|
||||
public uint SendSeq;
|
||||
public uint AckedSeq;
|
||||
// seq -> (frame_bytes, send_time_ticks, retry_count)
|
||||
public readonly SortedList<uint, (byte[], long, uint)> RetransmitBuffer = new();
|
||||
|
||||
public uint NextSeq()
|
||||
{
|
||||
var s = SendSeq;
|
||||
SendSeq++;
|
||||
return s;
|
||||
}
|
||||
|
||||
public void RecordSent(uint seq, byte[] frameBytes)
|
||||
{
|
||||
RetransmitBuffer[seq] = (frameBytes, Environment.TickCount64, 0);
|
||||
}
|
||||
|
||||
public void ProcessAck(uint ackSeq)
|
||||
{
|
||||
var toRemove = RetransmitBuffer.Keys
|
||||
.Where(k => k <= ackSeq)
|
||||
.ToList();
|
||||
foreach (var k in toRemove)
|
||||
RetransmitBuffer.Remove(k);
|
||||
if (ackSeq > AckedSeq)
|
||||
AckedSeq = ackSeq;
|
||||
}
|
||||
|
||||
/// Returns frames to retransmit and whether the session should close.
|
||||
public (List<byte[]> resend, bool shouldClose) CheckRetransmit()
|
||||
{
|
||||
var resend = new List<byte[]>();
|
||||
var shouldClose = false;
|
||||
var now = Environment.TickCount64;
|
||||
const int timeoutMs = 5;
|
||||
const uint maxRetries = 10;
|
||||
|
||||
foreach (var kv in RetransmitBuffer.ToList())
|
||||
{
|
||||
if (now - kv.Value.Item2 > timeoutMs)
|
||||
{
|
||||
if (kv.Value.Item3 >= maxRetries)
|
||||
{
|
||||
shouldClose = true;
|
||||
break;
|
||||
}
|
||||
RetransmitBuffer[kv.Key] = (kv.Value.Item1, now, kv.Value.Item3 + 1);
|
||||
resend.Add(kv.Value.Item1);
|
||||
}
|
||||
}
|
||||
return (resend, shouldClose);
|
||||
}
|
||||
}
|
||||
|
||||
/// L2 reliability: receiver-side state.
|
||||
class RecvState
|
||||
{
|
||||
public uint ExpectedSeq;
|
||||
public uint DeliverSeq;
|
||||
// seq -> payload (out-of-order buffer)
|
||||
public readonly SortedList<uint, byte[]> ReceiveBuffer = new();
|
||||
|
||||
/// Process an incoming DATA frame. Returns (payloads to deliver, needAck).
|
||||
public (List<byte[]> deliver, bool needAck) ProcessData(uint seq, byte[] payload)
|
||||
{
|
||||
if (seq < ExpectedSeq)
|
||||
{
|
||||
// Duplicate.
|
||||
return ([], true);
|
||||
}
|
||||
if (seq == ExpectedSeq)
|
||||
{
|
||||
// In-order: deliver and drain buffer.
|
||||
var deliver = new List<byte[]> { payload };
|
||||
ExpectedSeq++;
|
||||
DeliverSeq = ExpectedSeq - 1;
|
||||
while (ReceiveBuffer.Remove(ExpectedSeq, out var buffered))
|
||||
{
|
||||
deliver.Add(buffered);
|
||||
ExpectedSeq++;
|
||||
DeliverSeq = ExpectedSeq - 1;
|
||||
}
|
||||
return (deliver, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Out-of-order: buffer.
|
||||
ReceiveBuffer[seq] = payload;
|
||||
return ([], true);
|
||||
}
|
||||
}
|
||||
|
||||
/// The ack_seq to report in outgoing DATA frames.
|
||||
public uint CurrentAckSeq => ExpectedSeq == 0 ? uint.MaxValue : ExpectedSeq - 1;
|
||||
}
|
||||
|
||||
sealed class Session(
|
||||
uint sessionId,
|
||||
TcpClient client,
|
||||
@@ -288,20 +387,45 @@ sealed class Session(
|
||||
Action<string>? log) : IDisposable
|
||||
{
|
||||
readonly CancellationTokenSource _cts = new();
|
||||
readonly Channel<byte[]> _incoming = Channel.CreateBounded<byte[]>(256);
|
||||
readonly SendState _send = new();
|
||||
readonly RecvState _recv = new();
|
||||
readonly object _sendLock = new();
|
||||
readonly object _recvLock = new();
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_ = PumpSocketToTunnel();
|
||||
_ = PumpTunnelToSocket();
|
||||
_ = RetransmitTimer();
|
||||
}
|
||||
|
||||
public void Deliver(byte[] payload)
|
||||
/// Handle a DATA frame from the tunnel: process ack, deliver in-order.
|
||||
public void HandleData(Frame.Data data)
|
||||
{
|
||||
if (!_incoming.Writer.TryWrite(payload))
|
||||
log?.Invoke($"session {sessionId}: incoming channel full");
|
||||
// Process ack_seq to advance send window.
|
||||
lock (_sendLock)
|
||||
_send.ProcessAck(data.AckSeq);
|
||||
|
||||
// Process seq for in-order delivery.
|
||||
List<byte[]> deliver;
|
||||
bool needAck;
|
||||
lock (_recvLock)
|
||||
(deliver, needAck) = _recv.ProcessData(data.Seq, data.Payload);
|
||||
|
||||
// Deliver to the TCP socket via the channel.
|
||||
foreach (var chunk in deliver)
|
||||
{
|
||||
if (!_deliverChannel.Writer.TryWrite(chunk))
|
||||
log?.Invoke($"session {sessionId}: deliver channel full");
|
||||
}
|
||||
|
||||
// Send pure ACK if duplicate or out-of-order.
|
||||
if (needAck)
|
||||
SendPureAck();
|
||||
}
|
||||
|
||||
readonly Channel<byte[]> _deliverChannel = Channel.CreateBounded<byte[]>(256);
|
||||
|
||||
async Task PumpSocketToTunnel()
|
||||
{
|
||||
try
|
||||
@@ -313,7 +437,23 @@ sealed class Session(
|
||||
{
|
||||
var n = await stream.ReadAsync(buf, _cts.Token);
|
||||
if (n == 0) break;
|
||||
link.SendTo(serverMac, new Frame.Data(sessionId, buf[..n]));
|
||||
|
||||
uint seq;
|
||||
uint ackSeq;
|
||||
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);
|
||||
|
||||
link.SendTo(serverMac, frame);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
@@ -326,18 +466,65 @@ sealed class Session(
|
||||
try
|
||||
{
|
||||
var stream = client.GetStream();
|
||||
await foreach (var payload in _incoming.Reader.ReadAllAsync(_cts.Token))
|
||||
await foreach (var payload in _deliverChannel.Reader.ReadAllAsync(_cts.Token))
|
||||
await stream.WriteAsync(payload, _cts.Token);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
async Task RetransmitTimer()
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1));
|
||||
try
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
await timer.WaitForNextTickAsync(_cts.Token);
|
||||
|
||||
List<byte[]>? resend = null;
|
||||
bool shouldClose = false;
|
||||
lock (_sendLock)
|
||||
(resend, shouldClose) = _send.CheckRetransmit();
|
||||
|
||||
if (resend != null)
|
||||
{
|
||||
foreach (var frameBytes in resend)
|
||||
{
|
||||
// Send raw bytes directly (already encoded).
|
||||
link.SendRaw(serverMac, frameBytes);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldClose)
|
||||
{
|
||||
link.SendTo(serverMac, new Frame.Close(sessionId, Proto.ReasonMaxRetries));
|
||||
onClosed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
void SendPureAck()
|
||||
{
|
||||
uint seq, ackSeq;
|
||||
lock (_sendLock)
|
||||
{
|
||||
seq = _send.NextSeq();
|
||||
lock (_recvLock)
|
||||
ackSeq = _recv.CurrentAckSeq;
|
||||
}
|
||||
// Pure ACK: empty payload. Not stored in retransmit buffer.
|
||||
link.SendTo(serverMac, new Frame.Data(sessionId, seq, ackSeq, []));
|
||||
}
|
||||
|
||||
void SendClose() => link.SendTo(serverMac, new Frame.Close(sessionId, null));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_incoming.Writer.TryComplete();
|
||||
_deliverChannel.Writer.TryComplete();
|
||||
SendClose();
|
||||
try { client.Dispose(); } catch { }
|
||||
_cts.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user