Clients
Reference clients:
| Client | Status | Source |
|---|---|---|
| Rust CLI | shipped | src/client.rs |
| iOS/iPadOS | shipped | ios/Zuko/ |
| Web | experimental | /web/ / web/ |
| Android | planned | — |
The web client is published with the docs. It uses browser Iroh over relays and stores claimed host tickets in IndexedDB. See Targets for its security boundary and known gaps.
Implementing a client
Read protocol.md. Checklist:
- Claim ticket via
zuko/handoff/1:- derive handoff key from code;
- dial throwaway endpoint;
- read
<label>\n<ticket>; - derive host-scoped token;
- send
AUTHORIZEbefore closing handoff connection.
- Persist a client secret. Derive non-zero token from
(client secret, host id). - Dial host ticket with ALPN
zuko/2. open_bi; first frame must beATTACH(token, cols, rows, pixels).- Pump length-prefixed frames:
- stdin/terminal bytes →
DATA; - remote
DATA→ terminal emulator; - size changes →
RESIZE; - optional control stream for
RESIZE/PING/PONG.
- stdin/terminal bytes →
- Store
ATTACHEDtoken. Reuse it for short reconnects.
Operational details:
- First frame is mandatory; Iroh exposes streams to the peer after initiator data.
- Clamp terminal cells to at least
1x1. - Serialise writes; frame interleaving corrupts the stream.
- Bound outbound queues. Dropping impatient input is better than unbounded memory.
- Forward terminal bytes verbatim. Local Ctrl-C handling should be an explicit escape hatch only.
- Clean EOF means shell exit. Redial transient link errors.
Mobile clients should call the Rust FFI derive_handoff_key(code) instead of
reimplementing Argon2id. See src/ffi.rs and ios/Zuko/.
Reference code:
- Rust framing/session:
src/wire.rs,src/client.rs,src/handoff.rs - Swift framing/session:
ios/ZukoWire/,ios/Zuko/Zuko/Net/