# 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 ... | +---------------------------------------------------------------+ ``` - **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** (variable): type-dependent. No length field is carried — the Ethernet frame length from the capture gives the payload extent. 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) − 14 (eth header) − 6 (our header) = **1480 bytes**. 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.