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

215 lines
7.9 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 | `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.