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).
- Restore default FormBorderStyle (resizable with min/max/close buttons)
- Minimize button hides to tray (WindowState=Minimized -> Hide())
- Close button and tray Exit both call Shutdown() then close normally
- Tray Show/DoubleClick restores window from tray
mirror = (upstream_port ^ (mac[0]<<8 | mac[5])); clamped to >=1024.
Same server+upstream always yields the same local port so the user
knows where to connect without checking the UI each time. Falls back
to OS-assigned port if the deterministic one is already in use.
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.
Npcap in promiscuous mode loops back our own sent frames to the
capture callback. Without filtering, every DATA frame the client
sent was also processed as an incoming frame, duplicating the data
stream. This corrupted SSH sessions (Bad packet length 0x5353482D
= 'SSH-' — the version banner received twice).
Fix: compare source MAC in captured frames against our own MAC and
skip matches. Mirrors the PACKET_OUTGOING check on the Rust side.
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