Two bugs when upstream closes:
1. Data loss: gatuna received CLOSE from gatunad and immediately
RST'd the socket to curl while the drain pump still had queued
payloads in _deliverChannel. Fix: OnRemoteClose() completes the
channel writer and cancels the read pump, but lets the drain pump
finish naturally. Drain pump no longer takes _cts.Token — it stops
on channel completion and closes the socket gracefully (FIN, OS
flushes in background).
2. Duplicate CLOSE: gatuna echoed CLOSE back to gatunad (server
already knows — it initiated). SendClose() now guarded by
_closeSent flag. OnRemoteClose() never sends CLOSE at all.
Server-side: remove session from store before sending CLOSE so the
retransmit timer stops before CLOSE is queued. Only log CLOSE if
the session actually existed (dedup).
Server (gatunad):
- New --verbose/-v flag: logs lifecycle events at info level
(DISCOVER, MANIFEST sent, OPEN, session established, CLOSE,
OPEN_NAK). Without the flag, errors only as before.
Client (gatuna):
- OPEN_NAK and session CLOSE now send TCP RST to the local app
instead of a graceful FIN. LingerOption(true, 0) causes Winsock
to emit RST on close. The local app (e.g. ssh) sees a broken
connection instead of a clean close, which is more honest about
what happened (upstream was unreachable).
Versioning scheme: MAJOR.MINOR.PATCH where MAJOR matches the wire
protocol version (no cross-major compat), MINOR is the main change
indicator, PATCH generally unused.
- gatunad: Cargo.toml version 2.0.0 (clap --version picks it up)
- gatuna: csproj Version 2.0.0, shown in form title + tray tooltip
- app.manifest: assemblyIdentity version 2.0.0.0
- Frame version rejection already in place (both sides reject != 2)
- README: document versioning scheme
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.
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.