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.
New frame types PING (0x0B) and PONG (0x0C), each carrying an 8-byte
nonce. Server echoes PING nonce verbatim in PONG. Client sends pings
at random 10-100ms intervals, correlates nonces to measure RTT.
Stats shown live: sent/recv counts, loss %, average latency, jitter
(mean absolute delta of consecutive RTTs). Test button toggles on/off.
Updated both Rust server (echo in main.rs) and C# client (PingTest.cs,
SessionManager routing, MainForm Test button + stats label).
Server reads its hostname via gethostname(2) and includes it as a
length-prefixed UTF-8 string at the start of the MANIFEST payload.
Client displays 'Server: <hostname> — <MAC>' in a label above the
upstream list. Both sides updated for the new MANIFEST format:
[hostname_len:1][hostname:N][entries...]
Protocol version unchanged (still 1); MANIFEST payload layout change
is backward-incompatible but both sides ship together.
Ethernet pads frames to 60 bytes minimum; without an explicit length
field the receiver cannot distinguish real payload from zero padding
(e.g. a 6-byte DISCOVER becomes 46 bytes after padding, failing the
'payload must be empty' check).
Header is now 8 bytes: [ver:1][type:1][session_id:4][payload_len:2]
(both multi-byte fields big-endian). The receiver slices exactly
payload_len bytes and ignores trailing padding.
Updated PROTOCOL.md, Rust frame.rs, and C# Frame.cs.
- io::last_os_error() -> io::Error::last_os_error()
- MacAddr(mac.0) -> MacAddr(mac.octets()) for pnet 0.35
- cast htons() result to c_int for libc::socket protocol arg
- mark read as mut in spawn_pump