2026-08-12 18:20:51 +00:00
|
|
|
|
# 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 |
|
|
|
|
|
|
+---------------+---------------+-------------------------------+
|
2026-08-13 07:51:39 +00:00
|
|
|
|
| payload_len (big-endian) | payload ... |
|
|
|
|
|
|
+-------------------------------+ +
|
|
|
|
|
|
| |
|
2026-08-12 18:20:51 +00:00
|
|
|
|
+---------------------------------------------------------------+
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **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.
|
2026-08-13 07:51:39 +00:00
|
|
|
|
- **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.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
2026-08-13 07:51:39 +00:00
|
|
|
|
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.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
## 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` |
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-08-13 08:08:05 +00:00
|
|
|
|
A hostname prefix followed by variable-length entries, parsed sequentially
|
|
|
|
|
|
until the payload is consumed.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
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
|
2026-08-13 08:08:05 +00:00
|
|
|
|
+---------------+-----------------------------------------------+
|
|
|
|
|
|
| hostname_len | hostname (UTF-8, hostname_len bytes) ... |
|
|
|
|
|
|
+---------------+-----------------------------------------------+
|
2026-08-12 18:20:51 +00:00
|
|
|
|
| id | proto | port (big-endian) |
|
|
|
|
|
|
+---------------+---------------+-------------------------------+
|
|
|
|
|
|
| label_len | label (UTF-8, label_len bytes) ... |
|
|
|
|
|
|
+---------------+-----------------------------------------------+
|
2026-08-13 08:08:05 +00:00
|
|
|
|
| ... repeated ... |
|
|
|
|
|
|
+---------------------------------------------------------------+
|
2026-08-12 18:20:51 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-13 08:08:05 +00:00
|
|
|
|
- **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.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
- **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.
|
|
|
|
|
|
|
2026-08-13 08:08:05 +00:00
|
|
|
|
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
|
2026-08-12 18:20:51 +00:00
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|