Wire protocol
Transport
- Iroh QUIC endpoint ticket (
endpointa…). - Session ALPN:
zuko/2. - Handoff ALPN:
zuko/handoff/1. - First bidi stream is data. Optional second bidi stream is control.
Frame format
[type: u8][len: u16 BE][payload: len bytes]
len excludes the 3-byte header. Max payload: 65535 bytes. Receivers must
accumulate and parse greedily; frames can split/coalesce across QUIC reads.
Frame types
| Type | Name | Direction | Payload |
|---|---|---|---|
0x00 | DATA | both | terminal bytes |
0x01 | RESIZE | client → host | cols:u16 rows:u16 pixel_width:u16 pixel_height:u16 |
0x04 | PING | both | nonce:u64 |
0x05 | PONG | both | nonce:u64 |
0x06 | ATTACH | client → host | token:16 bytes + resize payload |
0x07 | ATTACHED | host → client | token:16 bytes |
0x08 | AUTHORIZE | client → handoff host | token:16 bytes + UTF-8 label |
0x09 | ERROR | host → client | code:u8 + UTF-8 message |
Unknown types are ignored.
ERROR is fatal. A client must show the message and stop reconnecting. Defined
codes are 0x01 (authorization failure; pair again) and 0x02 (protocol
violation).
Session handshake
- Client dials host ticket on
zuko/2. - Client opens data bidi stream.
- First frame must be
ATTACHwith a non-zero host-scoped token and current terminal size. - Host checks
authorized_clientsfor the token. - Host creates or reattaches the PTY keyed by that token.
- Host sends
ATTACHED(token).
The token identifies both an authorized client and that client’s in-memory PTY lease. A second connection with the same token takes over the same PTY.
RESIZE is valid only after ATTACH. Cell dimensions are clamped to at least
1x1. Pixel dimensions may be zero.
Pump
- Keystrokes/stdin:
DATAclient → host. - PTY output:
DATAhost → client. - Size changes:
RESIZEon control stream when available, otherwise data stream. PINGreplies withPONGcarrying the same nonce.
Shell EOF closes the stream and kills the PTY. Network/client drop detaches the PTY for 5 minutes; output while detached is discarded.
Compatibility
- The ALPN is the incompatible-version boundary. Current peers support only
zuko/2; there is no version negotiation or v1 fallback. - New optional frame types may be added to
zuko/2; receivers ignore unknown types. - Existing frame meanings and required handshake order must not change within
zuko/2. - A deliberate host rejection uses
ERRORand must not enter a retry loop. - Malformed required frames or an unexpected stream close are fatal to that connection.
Ticket handoff
Purpose: let a client learn the host ticket and register its future ATTACH
token without putting the ticket on argv/stdin/stdout.
Possession of the short code while zuko share is active grants enrollment. A
share accepts one claim by default; --count can explicitly allow more. Treat
the code as temporary sensitive data and do not leave an unlimited share
(--timeout 0) unattended.
Host (zuko share):
- Generate memorable code.
- Derive throwaway Iroh secret:
Argon2id(normalized_code, salt="zuko-share-handoff-v1") -> 32-byte seed. - Bind endpoint on
zuko/handoff/1. - Open uni stream and write:
<label>\n<ticket> - Wait briefly for client
AUTHORIZEuni stream. - Save token + label to
authorized_clients.
Client (zuko claim / zuko <code>):
- Derive same throwaway endpoint id from code.
- Dial
zuko/handoff/1, retrying until timeout. - Read label + ticket.
- Parse ticket host id.
- Derive stable host-scoped token from local client secret + host id.
- Open uni stream, send
AUTHORIZE(token, client_label). - Save ticket locally and optionally connect.
Token derivation
Rust CLI:
SHA256("zuko-session-token-v1" || client_key_bytes || host_id_bytes)[0..16]
The browser client uses the same derivation with its IndexedDB client key.
iOS:
SHA256("zuko-ios-session-token-v1" || keychain_seed || host_id_string)[0..16]
Tokens must be non-zero.
Security notes
- A shell connection requires host dial information plus a token in the host’s authorized-client list.
- Host key stays on host at
~/.config/zuko/key. - Host admits only authorised client tokens.
- Iroh provides transport encryption; relays see encrypted traffic.
- Rotate host trust with
zuko reset, restart, then re-pair clients.
Reference implementations: src/wire.rs, src/client.rs, src/host.rs,
src/handoff.rs, ios/ZukoWire/, ios/Zuko/Zuko/Net/, and
web/wasm/src/lib.rs.