Engineering
Language Strategy¶
The goal is to support every device that exists — phones, desktops, IoT — from a single protocol implementation. The answer is one Rust core, thin bindings per platform. This page records why, including the constraints that make the obvious alternatives fail.
The hard fact that settles it¶
Browsers cannot open UDP sockets. No JavaScript API exists, and none is coming. So "hole punching in a browser tab" is not a thing — the only browser transports are WebRTC (which does ICE hole-punching inside a large opaque C++ stack) or WebTransport/WebSockets to a server. React Native has no UDP either without a native module. So the mobile JS path also means shipping compiled code.
Once you are shipping compiled code anyway, the argument for a JavaScript core collapses. And a compiled language is what you actually want for IoT and easy installation.
One sans-io core, many bindings¶
Write the protocol once in Rust as a sans-io state machine — no sockets, no
clock, no RNG inside it. Bytes and events in, bytes and events out. This is how
quinn, rustls, and quiche are built, and it is the property that makes the
port story work.
graph TD
CORE["🦀 Rust sans-io core<br/>protocol logic · no I/O · one wire format"]
CORE -->|wasm-bindgen| WASM["Browser (WASM)<br/>WebTransport / WebRTC"]
CORE -->|UniFFI| MOB["iOS / Android<br/>UDP + QUIC"]
CORE -->|napi-rs| NODE["Node / Electron<br/>UDP + QUIC"]
CORE -->|native crate| DESK["Desktop / CLI<br/>UDP + QUIC"]
CORE -->|no_std + C FFI| IOT["IoT / embedded<br/>whatever the board has"]
style CORE fill:#c1121f,color:#fff,stroke:#8c0c16
| Target | Binding | Transport underneath |
|---|---|---|
| Browser | wasm-bindgen → WASM |
WebTransport (QUIC/H3) or WebRTC |
| iOS / Android | UniFFI → Swift/Kotlin | real UDP + QUIC |
| Node / Electron | napi-rs |
real UDP + QUIC |
| Desktop / CLI | native crate | real UDP + QUIC |
| IoT / embedded | no_std + C FFI |
whatever the board has |
The core does not know which one it is in. Same crate, same tests, same wire format everywhere — that property is worth more than any individual platform decision.
The honest cost
WASM still cannot do UDP, so the browser build gets a degraded transport (relay or WebRTC). This is unavoidable in any language. Rust just means the protocol is not rewritten to accommodate it. (For Zajil's operator-controlled network this is a minor concern — see the threat model.)
Why sans-io is non-negotiable¶
The no-I/O constraint is not stylistic. It is what enables the whole testing standard:
- Deterministic simulation — the entire protocol runs against a simulated network with seeded loss, reordering, delay, and partition, because nothing below the transport adapter ever touches a real socket, clock, or RNG.
- 100% branch coverage — every state transition is reachable from a unit test without a network.
- Portability — a core that reaches for
std::netcannot be a WASM module or ano_stdembedded target.
Protocol logic is I/O-free and deterministic
No sockets, clocks, or randomness are reached for inside the protocol core.
They are passed in, so every state transition is testable without a network.
(This is also a project convention in CLAUDE.md.)
On implementing our own cryptography¶
Long-term, hand-implementing Ed25519 and experimenting with other primitives is a stated goal — this is a mastery project. But it is deferred. The skill being built first is orchestration: how identity, derivation, handshake, and revocation compose into a protocol that holds. Primitives are a solved problem with vetted crates; composition is where real systems fail (BEAST, Lucky13, KRACK — composition failures, not broken ciphers). We compose vetted primitives now, and revisit building our own once the core layers are solid.