Files
gatuna/PROTOCOL.md
T
mute 279af33fd8 add network test feature (PING/PONG with latency stats)
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).
2026-08-13 08:28:59 +00:00

7.9 KiB
Raw Blame History

gatuna wire protocol

All frames ride Ethernet with ethertype 0x6969. The ethertype is the sole discriminator; there is no magic number inside the payload.

Frame layout

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------+---------------+-------------------------------+
| version       | type          | session_id                    |
+---------------+---------------+-------------------------------+
| payload_len (big-endian)      | payload ...                   |
+-------------------------------+                               +
|                                                               |
+---------------------------------------------------------------+
  • version (u8): protocol version. Currently 1.
  • type (u8): frame type, see table below.
  • session_id (u32, big-endian): 0 for non-session frames; the server-assigned ID for session-scoped frames.
  • payload_len (u16, big-endian): number of payload bytes that follow. The receiver reads exactly this many bytes and ignores any trailing Ethernet padding (frames under 60 bytes are zero-padded by the NIC to meet the minimum Ethernet frame size).
  • payload (payload_len bytes): type-dependent.

No CRC, no retransmit, no ordering at this layer. TCP reliability is handled by the endpoints' TCP stacks; UDP (reserved) will rely on application-level mechanisms.

Maximum payload: 1500 (Ethernet MTU) 8 (our header) = 1492 bytes. In practice we cap at 1480 to stay conservative. Larger payloads are not emitted in v1.

Frame types

Type Name Direction session_id Payload
0x01 DISCOVER C → broadcast 0 empty
0x02 MANIFEST S → C 0 id:1, proto:1, port:2 × N
0x03 OPEN C → S 0 upstream_id:1
0x04 OPEN_ACK S → C assigned upstream_id:1
0x05 OPEN_NAK S → C 0 upstream_id:1, reason:1
0x06 DATA both session raw bytes (≤1480)
0x07 CLOSE both session optional reason:1
0x0B PING C → S 0 nonce:8
0x0C PONG S → C 0 nonce:8 (echoed)

Reserved (unimplemented in v1; parse returns Err, encode unimplemented):

Type Name
0x08 UDP_OPEN
0x09 UDP_DATA
0x0A UDP_CLOSE

Payload field encodings

MANIFEST payload

A hostname prefix followed by variable-length entries, parsed sequentially until the payload is consumed.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------+-----------------------------------------------+
| hostname_len  | hostname (UTF-8, hostname_len bytes) ...       |
+---------------+-----------------------------------------------+
| id            | proto         | port (big-endian)             |
+---------------+---------------+-------------------------------+
| label_len     | label (UTF-8, label_len bytes) ...           |
+---------------+-----------------------------------------------+
| ... repeated ...                                             |
+---------------------------------------------------------------+
  • hostname_len (u8): length in bytes of the server's hostname. 0 is valid (unknown hostname).
  • hostname (hostname_len bytes, UTF-8): the server's hostname, read via gethostname(2) at startup. Maximum 255 bytes.
  • id (u8): upstream identifier (1-based positional index from gatunad cmdline).
  • proto (u8): 1 = TCP, 2 = UDP (reserved; not emitted in v1).
  • port (u16, big-endian): the real port on the server's 127.0.0.1.
  • label_len (u8): length in bytes of the label that follows. 0 means no label.
  • label (label_len bytes, UTF-8): human-readable name for the upstream, taken from the PORT[:label] cmdline argument. Maximum 255 bytes.

To parse: read hostname_len, then hostname_len bytes of hostname, then read 5-byte fixed entry prefixes + label_len bytes of label each, repeating until the payload is exhausted. The number of entries is not carried explicitly.

OPEN payload

+---------------+
| upstream_id   |
+---------------+
  • upstream_id (u8): which MANIFEST entry to open.

OPEN_ACK payload

+---------------+
| upstream_id   |
+---------------+
  • upstream_id (u8): echoes the requested upstream. The session is identified by the session_id field in the header, not the payload.

OPEN_NAK payload

+---------------+---------------+
| upstream_id   | reason        |
+---------------+---------------+
  • upstream_id (u8): echoes the requested upstream.
  • reason (u8): see reason codes.

DATA payload

Raw application bytes. Up to 1480 bytes per frame. The session_id header field identifies which session the bytes belong to.

CLOSE payload

+---------------+
| reason?       |
+---------------+
  • reason (u8, optional): present iff payload length ≥ 1. See reason codes.

PING / PONG payload

+                                                               +
| nonce (big-endian, 8 bytes)                                   |
+                                                               +
  • nonce (u64, big-endian): arbitrary value chosen by the client. The server echoes it verbatim in the PONG reply. Used to correlate RTT measurements.

Reason codes

Value Meaning
0 unspecified
1 unknown_upstream
2 connect_failed
3 oversize
4 unknown_session

Discovery flow

client                            server
  |                                 |
  | DISCOVER (dst = broadcast)      |
  |-------------------------------->|
  |                                 |
  |        MANIFEST (unicast)       |
  |<--------------------------------|
  |                                 |

The server learns the client's MAC from the DISCOVER frame's source address and unicasts the MANIFEST back. The server never speaks unsolicited.

TCP session lifecycle

client                            server
  |                                 |
  | OPEN { upstream_id }            |
  |-------------------------------->|
  |                                 | TcpStream::connect(127.0.0.1:port)
  |                                 |
  |   OPEN_ACK { session_id }       |
  |<--------------------------------|   (on success)
  |        OR                       |
  |   OPEN_NAK { reason }           |
  |<--------------------------------|   (on failure)
  |                                 |
  | DATA { session_id, bytes }      |
  |<------------------------------->| DATA { session_id, bytes }
  |                                 |
  | CLOSE { session_id, reason? }   |
  |<------------------------------->|   (on EOF, RST, or error)
  |                                 |
  • session_id is allocated by the server as a monotonically increasing u32 (starting at 1) from an atomic counter. Collision by wraparound is ignored.
  • Either side may send CLOSE. The side receiving CLOSE tears down its half and stops emitting frames for that session.
  • The server's socket→tunnel pump reads TcpStream in 1480-byte chunks and emits one DATA frame per chunk. On read returning 0 (FIN) or an error, it emits CLOSE and exits.
  • The server's tunnel→socket path writes DATA payloads to the TcpStream with write_all. On error it emits CLOSE and drops the session.