eb8994d1e1
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.
107 lines
3.1 KiB
C#
107 lines
3.1 KiB
C#
using SharpPcap;
|
|
using SharpPcap.LibPcap;
|
|
|
|
namespace gatuna;
|
|
|
|
sealed class TunnelLink : IDisposable
|
|
{
|
|
LibPcapLiveDevice _device;
|
|
readonly byte[] _ourMac = new byte[6];
|
|
|
|
public event Action<Frame, byte[]>? FrameReceived;
|
|
public event Action<string>? Log;
|
|
|
|
public TunnelLink(LibPcapLiveDevice device)
|
|
{
|
|
_device = device;
|
|
}
|
|
|
|
public static LibPcapLiveDevice[] ListDevices()
|
|
{
|
|
return [.. LibPcapLiveDeviceList.Instance
|
|
.Where(d => !d.Loopback && d.MacAddress != null)];
|
|
}
|
|
|
|
public void Open()
|
|
{
|
|
_device.Open(new DeviceConfiguration
|
|
{
|
|
Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness,
|
|
ReadTimeout = 1000,
|
|
});
|
|
_device.Filter = $"ether proto 0x{Proto.EtherType:X4}";
|
|
|
|
if (_device.MacAddress?.GetAddressBytes() is { Length: 6 } mac)
|
|
{
|
|
Buffer.BlockCopy(mac, 0, _ourMac, 0, 6);
|
|
}
|
|
|
|
_device.OnPacketArrival += OnPacketArrival;
|
|
_device.StartCapture();
|
|
Log?.Invoke($"capture started on {_device.Name}");
|
|
}
|
|
|
|
void OnPacketArrival(object? sender, PacketCapture capture)
|
|
{
|
|
var raw = capture.GetPacket();
|
|
var data = raw.Data;
|
|
if (data.Length < Proto.EthHeaderLen + 6)
|
|
return;
|
|
var et = (ushort)(data[12] << 8 | data[13]);
|
|
if (et != Proto.EtherType)
|
|
return;
|
|
// Skip our own outgoing frames (Npcap loops them back in promiscuous mode).
|
|
if (data[6] == _ourMac[0] && data[7] == _ourMac[1]
|
|
&& data[8] == _ourMac[2] && data[9] == _ourMac[3]
|
|
&& data[10] == _ourMac[4] && data[11] == _ourMac[5])
|
|
return;
|
|
var srcMac = new byte[6];
|
|
Buffer.BlockCopy(data, 6, srcMac, 0, 6);
|
|
var payload = data.AsSpan(Proto.EthHeaderLen);
|
|
if (FrameCodec.Parse(payload) is { } frame)
|
|
FrameReceived?.Invoke(frame, srcMac);
|
|
}
|
|
|
|
public void SendBroadcast(Frame frame)
|
|
{
|
|
SendRaw([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], FrameCodec.Encode(frame));
|
|
}
|
|
|
|
public void SendTo(byte[] dstMac, Frame frame)
|
|
{
|
|
SendRaw(dstMac, FrameCodec.Encode(frame));
|
|
}
|
|
|
|
/// Send a pre-encoded protocol payload (for retransmit, where we already
|
|
/// have the exact bytes and want to avoid re-encoding).
|
|
public void SendRaw(byte[] dstMac, byte[] payload)
|
|
{
|
|
var frame = new byte[Proto.EthHeaderLen + payload.Length];
|
|
Buffer.BlockCopy(dstMac, 0, frame, 0, 6);
|
|
Buffer.BlockCopy(_ourMac, 0, frame, 6, 6);
|
|
frame[12] = (byte)(Proto.EtherType >> 8);
|
|
frame[13] = (byte)(Proto.EtherType & 0xFF);
|
|
Buffer.BlockCopy(payload, 0, frame, 14, payload.Length);
|
|
try
|
|
{
|
|
_device.SendPacket(frame);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log?.Invoke($"send failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
if (_device.Started)
|
|
_device.StopCapture();
|
|
_device.OnPacketArrival -= OnPacketArrival;
|
|
_device.Close();
|
|
}
|
|
catch { }
|
|
}
|
|
}
|