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.
|
|
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
|
## Frame layout (non-DATA frames)
|
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
|
|
|
|
|
|
+---------------+---------------+-------------------------------+
|
|
|
|
|
|
| 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
|
|
|
|
+---------------------------------------------------------------+
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
|
- **version** (u8): protocol version. Currently `2`.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
- **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
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
|
## DATA frame layout
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
|
DATA frames carry two additional fields after the common header for L2-level
|
|
|
|
|
|
reliability:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
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=2 | type=0x06 | session_id |
|
|
|
|
|
|
+---------------+---------------+-------------------------------+
|
|
|
|
|
|
| payload_len (big-endian) | seq (big-endian) |
|
|
|
|
|
|
+-------------------------------+-------------------------------+
|
|
|
|
|
|
| ack_seq (big-endian) | payload ... |
|
|
|
|
|
|
+-------------------------------+ +
|
|
|
|
|
|
| |
|
|
|
|
|
|
+---------------------------------------------------------------+
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **seq** (u32, big-endian): monotonically increasing per-session sequence
|
|
|
|
|
|
number. Wraps at 2^32 (same as TCP). Identifies this DATA frame's position
|
|
|
|
|
|
in the byte stream.
|
|
|
|
|
|
- **ack_seq** (u32, big-endian): cumulative acknowledgment — the highest
|
|
|
|
|
|
contiguous `seq` that the sender of this frame has delivered to its local
|
|
|
|
|
|
TCP socket. The receiver uses this to advance its retransmit window.
|
|
|
|
|
|
- **payload** (`payload_len` bytes): raw application bytes (0–1480). A
|
|
|
|
|
|
`payload_len = 0` DATA frame is a **pure ACK** — it carries no data, just
|
|
|
|
|
|
an acknowledgment. This mirrors TCP's empty-segment ACK.
|
|
|
|
|
|
|
|
|
|
|
|
Maximum payload: 1500 (Ethernet MTU) − 8 (common header) − 8 (seq + ack_seq)
|
|
|
|
|
|
= **1484 bytes**. In practice we cap at **1480** to stay conservative.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
## Frame types
|
|
|
|
|
|
|
|
|
|
|
|
| Type | Name | Direction | session_id | Payload |
|
|
|
|
|
|
|------|-------------|---------------|------------|----------------------------------|
|
|
|
|
|
|
| 0x01 | DISCOVER | C → broadcast | 0 | empty |
|
2026-08-13 08:40:02 +00:00
|
|
|
|
| 0x02 | MANIFEST | S → C | 0 | `hostname_len:1, hostname:N, entries...` |
|
2026-08-13 09:15:15 +00:00
|
|
|
|
| 0x03 | OPEN | C → S | 0 | `upstream_id:1, proto:1` |
|
|
|
|
|
|
| 0x04 | OPEN_ACK | S → C | assigned | `upstream_id:1, proto:1` |
|
2026-08-12 18:20:51 +00:00
|
|
|
|
| 0x05 | OPEN_NAK | S → C | 0 | `upstream_id:1, reason:1` |
|
2026-08-13 09:08:05 +00:00
|
|
|
|
| 0x06 | DATA | both | session | `seq:4, ack_seq:4, raw bytes` |
|
2026-08-12 18:20:51 +00:00
|
|
|
|
| 0x07 | CLOSE | both | session | optional `reason:1` |
|
2026-08-13 08:28:59 +00:00
|
|
|
|
| 0x0B | PING | C → S | 0 | `nonce:8` |
|
|
|
|
|
|
| 0x0C | PONG | S → C | 0 | `nonce:8` (echoed) |
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
|
Reserved (unimplemented; parse returns Err, encode unimplemented):
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
| Type | Name |
|
|
|
|
|
|
|------|-------------|
|
|
|
|
|
|
| 0x08 | UDP_OPEN |
|
|
|
|
|
|
| 0x09 | UDP_DATA |
|
|
|
|
|
|
| 0x0A | UDP_CLOSE |
|
|
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
|
## L2 reliability
|
|
|
|
|
|
|
2026-08-13 09:15:15 +00:00
|
|
|
|
The reliability semantics of a session depend on its transport proto,
|
|
|
|
|
|
which is carried end-to-end through OPEN/OPEN_ACK:
|
|
|
|
|
|
|
|
|
|
|
|
- **TCP (`proto = 1`):** DATA frames are delivered reliably and in-order via
|
|
|
|
|
|
a TCP-like sequence + cumulative ACK + retransmit mechanism. This ensures
|
|
|
|
|
|
that a dropped Ethernet frame does not permanently corrupt a TCP session
|
|
|
|
|
|
(which would otherwise happen because the local kernel TCP stack ACKs data
|
|
|
|
|
|
before it is chunked into DATA frames).
|
|
|
|
|
|
- **UDP (`proto = 2`, reserved):** DATA frames are delivered best-effort. The
|
|
|
|
|
|
`seq` and `ack_seq` fields are present in the frame layout for uniformity
|
|
|
|
|
|
but are ignored — no retransmit, no in-order buffering, no pure ACKs. Drops
|
|
|
|
|
|
are tolerated because stateless protocols either don't care or handle
|
|
|
|
|
|
recovery at the application layer.
|
|
|
|
|
|
|
|
|
|
|
|
The following sections describe the TCP reliability mechanism.
|
2026-08-13 09:08:05 +00:00
|
|
|
|
|
|
|
|
|
|
### Sender state (per session)
|
|
|
|
|
|
|
|
|
|
|
|
- `send_seq`: next seq to assign (starts at 0, increments per DATA frame).
|
|
|
|
|
|
- `retransmit_buffer`: map of `seq → (payload, timestamp)`, holding all sent
|
|
|
|
|
|
but unacked frames.
|
|
|
|
|
|
- `acked_seq`: highest seq acknowledged by the peer (initially `None`).
|
|
|
|
|
|
|
|
|
|
|
|
On sending DATA:
|
|
|
|
|
|
1. Assign `seq = send_seq; send_seq += 1`.
|
|
|
|
|
|
2. Set `ack_seq` to the highest contiguous seq we have received from the peer
|
|
|
|
|
|
(our receive side's `deliver_seq`).
|
|
|
|
|
|
3. Store `(payload, now)` in `retransmit_buffer[seq]`.
|
|
|
|
|
|
4. Transmit the frame.
|
|
|
|
|
|
|
|
|
|
|
|
On receiving an `ack_seq` in any DATA frame (including pure ACKs):
|
|
|
|
|
|
1. Advance `acked_seq` to `max(acked_seq, ack_seq)`.
|
|
|
|
|
|
2. Remove all entries from `retransmit_buffer` with `seq <= ack_seq`.
|
|
|
|
|
|
|
|
|
|
|
|
Retransmit timer (per session, checked periodically):
|
|
|
|
|
|
1. For each entry in `retransmit_buffer` older than `RETRANSMIT_TIMEOUT`
|
|
|
|
|
|
(default 5 ms), retransmit the frame and reset its timestamp.
|
|
|
|
|
|
2. If any entry has been retransmitted more than `MAX_RETRIES` times (default
|
|
|
|
|
|
10), send `CLOSE` and tear down the session.
|
|
|
|
|
|
|
|
|
|
|
|
### Receiver state (per session)
|
|
|
|
|
|
|
|
|
|
|
|
- `expected_seq`: next seq expected (starts at 0).
|
|
|
|
|
|
- `receive_buffer`: map of `seq → payload`, holding out-of-order frames.
|
|
|
|
|
|
- `deliver_seq`: highest seq delivered to the local TCP socket (starts at
|
|
|
|
|
|
`None`; reported as `ack_seq` in outgoing DATA frames).
|
|
|
|
|
|
|
|
|
|
|
|
On receiving DATA with `seq`:
|
|
|
|
|
|
1. If `seq < expected_seq`: duplicate (already delivered). Discard the payload,
|
|
|
|
|
|
but still process the `ack_seq` field to advance the send window. Send a
|
|
|
|
|
|
pure ACK so the sender can converge.
|
|
|
|
|
|
2. If `seq == expected_seq`: deliver payload to the TCP socket. Increment
|
|
|
|
|
|
`expected_seq`. Then check `receive_buffer` for the next contiguous seq and
|
|
|
|
|
|
deliver those too (drain the buffer in order). Update `deliver_seq`.
|
|
|
|
|
|
3. If `seq > expected_seq`: store in `receive_buffer[seq]`. Do not deliver yet.
|
|
|
|
|
|
Send a pure ACK (re-ACKing `deliver_seq`) to trigger retransmit of the gap.
|
|
|
|
|
|
|
|
|
|
|
|
### Pure ACK frames
|
|
|
|
|
|
|
|
|
|
|
|
When a side needs to ACK but has no data to send, it sends a DATA frame with
|
|
|
|
|
|
`payload_len = 0`. The `seq` field is set to `send_seq` (consuming a seq
|
|
|
|
|
|
number, same as TCP's empty segment) and `ack_seq` carries the cumulative
|
|
|
|
|
|
acknowledgment. The receiver processes the `ack_seq` and discards the empty
|
|
|
|
|
|
payload without delivering to the TCP socket.
|
|
|
|
|
|
|
|
|
|
|
|
### Acknowledgment timing
|
|
|
|
|
|
|
|
|
|
|
|
- When data is flowing in both directions, each DATA frame carries the latest
|
|
|
|
|
|
`ack_seq` — no separate ACK frames needed.
|
|
|
|
|
|
- When data is one-directional, the receiver sends a pure ACK after each DATA
|
|
|
|
|
|
frame (or after a small batch, implementation-defined).
|
|
|
|
|
|
- On receiving a duplicate or out-of-order frame, the receiver immediately
|
|
|
|
|
|
sends a pure ACK to help the sender converge.
|
|
|
|
|
|
|
|
|
|
|
|
### Why this is not the Two Generals Problem
|
|
|
|
|
|
|
|
|
|
|
|
We do not need mutual consensus. We need one-sided reliable delivery: the
|
|
|
|
|
|
sender retransmits until it gets an ACK. If the ACK is lost, the sender
|
|
|
|
|
|
retransmits the data; the receiver sees a duplicate, discards it, and re-ACKs.
|
|
|
|
|
|
This converges in O(1) round trips. The only unsolvable case — the last frame
|
|
|
|
|
|
before a permanent link death — is handled by `MAX_RETRIES` → `CLOSE`, which is
|
|
|
|
|
|
correct: a dead link should kill the session.
|
|
|
|
|
|
|
2026-08-12 18:20:51 +00:00
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-08-13 09:15:15 +00:00
|
|
|
|
+---------------+---------------+
|
|
|
|
|
|
| upstream_id | proto |
|
|
|
|
|
|
+---------------+---------------+
|
2026-08-12 18:20:51 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **upstream_id** (u8): which MANIFEST entry to open.
|
2026-08-13 09:15:15 +00:00
|
|
|
|
- **proto** (u8): the transport protocol of the upstream (`1 = TCP`,
|
|
|
|
|
|
`2 = UDP`). Carried end-to-end so both sides know which reliability
|
|
|
|
|
|
semantics apply to the session. Must match the proto advertised in the
|
|
|
|
|
|
MANIFEST for that upstream.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
### OPEN_ACK payload
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-08-13 09:15:15 +00:00
|
|
|
|
+---------------+---------------+
|
|
|
|
|
|
| upstream_id | proto |
|
|
|
|
|
|
+---------------+---------------+
|
2026-08-12 18:20:51 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-13 09:15:15 +00:00
|
|
|
|
- **upstream_id** (u8): echoes the requested upstream.
|
|
|
|
|
|
- **proto** (u8): echoes the requested proto. The session is identified by
|
|
|
|
|
|
the `session_id` field in the header, not the payload.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
### OPEN_NAK payload
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
+---------------+---------------+
|
|
|
|
|
|
| upstream_id | reason |
|
|
|
|
|
|
+---------------+---------------+
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **upstream_id** (u8): echoes the requested upstream.
|
|
|
|
|
|
- **reason** (u8): see reason codes.
|
|
|
|
|
|
|
|
|
|
|
|
### DATA payload
|
|
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
|
`[ seq:4 ][ ack_seq:4 ][ raw bytes ]` — the `seq` and `ack_seq` fields are
|
|
|
|
|
|
part of the DATA frame's extended header (between `payload_len` and the
|
|
|
|
|
|
payload). The `payload_len` field counts only the raw bytes, not the seq/ack
|
|
|
|
|
|
fields. Up to 1480 bytes of application data per frame.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
### CLOSE payload
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
+---------------+
|
|
|
|
|
|
| reason? |
|
|
|
|
|
|
+---------------+
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **reason** (u8, optional): present iff payload length ≥ 1. See reason codes.
|
|
|
|
|
|
|
2026-08-13 08:28:59 +00:00
|
|
|
|
### 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.
|
|
|
|
|
|
|
2026-08-12 18:20:51 +00:00
|
|
|
|
## Reason codes
|
|
|
|
|
|
|
|
|
|
|
|
| Value | Meaning |
|
|
|
|
|
|
|-------|-------------------|
|
|
|
|
|
|
| 0 | unspecified |
|
|
|
|
|
|
| 1 | unknown_upstream |
|
|
|
|
|
|
| 2 | connect_failed |
|
|
|
|
|
|
| 3 | oversize |
|
|
|
|
|
|
| 4 | unknown_session |
|
2026-08-13 09:08:05 +00:00
|
|
|
|
| 5 | max_retries |
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
## 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)
|
|
|
|
|
|
| |
|
2026-08-13 09:08:05 +00:00
|
|
|
|
| DATA { seq, ack_seq, bytes } |
|
|
|
|
|
|
|<------------------------------->| DATA { seq, ack_seq, bytes }
|
2026-08-12 18:20:51 +00:00
|
|
|
|
| |
|
|
|
|
|
|
| CLOSE { session_id, reason? } |
|
2026-08-13 09:08:05 +00:00
|
|
|
|
|<------------------------------->| (on EOF, RST, or max_retries)
|
2026-08-12 18:20:51 +00:00
|
|
|
|
| |
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- `session_id` is allocated by the server as a monotonically increasing u32
|
|
|
|
|
|
(starting at 1) from an atomic counter. Collision by wraparound is ignored.
|
2026-08-13 09:08:05 +00:00
|
|
|
|
- `seq` starts at 0 on both sides of each session and increments per DATA
|
|
|
|
|
|
frame (including pure ACKs).
|
2026-08-12 18:20:51 +00:00
|
|
|
|
- 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
|
2026-08-13 09:08:05 +00:00
|
|
|
|
emits one DATA frame per chunk. On `read` returning 0 (FIN) or an error,
|
2026-08-12 18:20:51 +00:00
|
|
|
|
it emits `CLOSE` and exits.
|
2026-08-13 09:08:05 +00:00
|
|
|
|
- The server's tunnel→socket path delivers DATA payloads in seq order to the
|
|
|
|
|
|
`TcpStream` with `write_all`. On error it emits `CLOSE` and drops the session.
|
|
|
|
|
|
- A DATA frame is retransmitted if no ACK is received within 5 ms. After 10
|
|
|
|
|
|
failed retransmits, the session is closed with `reason = max_retries`.
|
2026-08-13 08:40:02 +00:00
|
|
|
|
|
|
|
|
|
|
## Network test (PING/PONG)
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
client server
|
|
|
|
|
|
| |
|
|
|
|
|
|
| PING { nonce } |
|
|
|
|
|
|
|-------------------------------->|
|
|
|
|
|
|
| |
|
|
|
|
|
|
| PONG { nonce } |
|
|
|
|
|
|
|<--------------------------------|
|
|
|
|
|
|
| |
|
|
|
|
|
|
| (repeated at random intervals) |
|
|
|
|
|
|
| |
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- The client sends `PING` frames at random 10–100 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.
|
2026-08-13 09:08:05 +00:00
|
|
|
|
|
|
|
|
|
|
## Version compatibility
|
|
|
|
|
|
|
|
|
|
|
|
- Version 1: no `seq`/`ack_seq` in DATA frames, no L2 reliability. Dropped
|
|
|
|
|
|
DATA frames kill the TCP session.
|
|
|
|
|
|
- Version 2: DATA frames carry `seq` + `ack_seq`, L2 retransmit. Dropped
|
|
|
|
|
|
frames are recovered.
|
|
|
|
|
|
|
|
|
|
|
|
Version 2 is the current version. A receiver that sees `version != 2` rejects
|
|
|
|
|
|
the frame. Both sides of a session must speak the same version; there is no
|
|
|
|
|
|
negotiation.
|