Files
gatuna/PROTOCOL.md
T
mute 5bbda4c4c4 finalise docs: accurate header size, PING/PONG, build instructions, repo layout
README:
- Fix header size (8 bytes, not 6) and payload_len field
- Mark client as built (not 'planned')
- Add PING/PONG to protocol summary
- Add client usage section (Discover, mirror ports, Test button, tray)
- Add building from source section (Rust + .NET)
- Add Npcap prerequisite explanation
- Add Windows privilege/UAC note
- Add full repository layout tree

PROTOCOL:
- Fix MANIFEST payload description in frame types table (hostname + entries)
- Add PING/PONG flow diagram and semantics section
2026-08-13 08:40:02 +00:00

238 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | `hostname_len:1, hostname:N, entries...` |
| 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.
## Network test (PING/PONG)
```
client server
| |
| PING { nonce } |
|-------------------------------->|
| |
| PONG { nonce } |
|<--------------------------------|
| |
| (repeated at random intervals) |
| |
```
- The client sends `PING` frames at random 10100 ms intervals, each with a
unique `nonce`.
- The server echoes the nonce verbatim in a `PONG` frame.
- The client correlates `PONG` nonces with outstanding `PING` timestamps to
compute RTT, average latency, jitter (mean absolute delta of consecutive
RTTs), and drop rate (unanswered PINGs).
- PING/PONG frames use `session_id = 0`; they are independent of TCP sessions.