Files
gatuna/PROTOCOL.md
T
mute f09028b135 add payload_len field to wire protocol header
Ethernet pads frames to 60 bytes minimum; without an explicit length
field the receiver cannot distinguish real payload from zero padding
(e.g. a 6-byte DISCOVER becomes 46 bytes after padding, failing the
'payload must be empty' check).

Header is now 8 bytes: [ver:1][type:1][session_id:4][payload_len:2]
(both multi-byte fields big-endian). The receiver slices exactly
payload_len bytes and ignores trailing padding.

Updated PROTOCOL.md, Rust frame.rs, and C# Frame.cs.
2026-08-13 07:51:39 +00:00

191 lines
6.8 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` |
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
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
+---------------+---------------+-------------------------------+
| id | proto | port (big-endian) |
+---------------+---------------+-------------------------------+
| label_len | label (UTF-8, label_len 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 the 5-byte fixed prefix, then `label_len` bytes, and repeat 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.
## 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.