Learn/irohesp32rusty_esp_irohpeer-to-peerquicmeshrust

iroh on ESP32: How rusty_esp_iroh Meshes Your Home Devices

rusty_esp_iroh runs an iroh peer-to-peer node on an ESP32: dialed by public key, streaming media, adopted by its owner and updated with signed images, no cloud.

Signed by M·
Brick-built neighborhood at dusk where every rooftop ESP brick holds a glowing orb and thin golden threads link them into one mesh
Can an ESP32 run iroh?

Yes, on the std ESP-IDF track with enough memory and at least 8 MB of flash. rusty_esp_iroh runs an iroh node on a Seeed XIAO ESP32-S3 Sense: up 3.2 seconds after boot, streaming a camera with 721 packets in 60 seconds and none lost. Bare-metal no_std chips join through a bridge instead.

Yes, an ESP32 can run iroh, and rusty_esp_iroh is the package that does it for the Janus family. It puts a real peer-to-peer node on the chip: QUIC with pure-Rust TLS, a signed capability manifest, media streaming, owner adoption and signed over-the-air updates, on a part with half a megabyte of internal memory. On a Seeed XIAO ESP32-S3 Sense the node is up 3.2 seconds after boot and streamed a camera for sixty seconds with 721 packets, none lost and none out of order, reproduced three times from cold boots.

This field note covers how iroh on ESP32 works, which boards can carry it, what the Janus protocols are, and what is still waiting for hardware. The code is at github.com/Remade-With-Rust/rusty_esp_iroh, and iroh itself is from n0.

iroh on ESP32, and the memory tiers

Why iroh, and why only on ESP-IDF

Most device SDKs assume the device talks to someone's cloud. iroh flips that: every endpoint is addressed by its public key, and peers find each other directly, with relays and hole punching when they need help. For a household that wants no vendor server in the loop, that is the right shape. The mesh is the cloud.

iroh needs an async runtime and TLS, so it only runs on the std track, Rust on ESP-IDF. rusty_esp_iroh uses single-threaded tokio and rustls with n0's RustCrypto provider, and it refuses ring and aws-lc, which carry platform assembly that will not build for Xtensa. On the bare-metal no_std track, asking for the iroh node is a compile error by design, with a message pointing at the bridge. A chip without an operating system reaches the mesh through a neighbour, not by pretending to run a node.

The board taught three lessons no laptop could, each now a rule with a test: the compiler crashed on the TLS library at two optimisation settings, a leftover bus-driver check aborted boot at 1.8 seconds, and the async runtime needed an eventfd driver registered before it could start.

PSRAM and no-PSRAM tiers

iroh on ESP32 comes in two tiers, following what n0 proved in their own examples:

  • PSRAM: boards: XIAO ESP32-S3 Sense, WROVER, C61 with PSRAM, P4; reach: relay and discovery; a short ticket can work from anywhere
  • no PSRAM: boards: C6, S3 without PSRAM, original ESP32; reach: LAN-direct; long tickets carrying the address; relay off

Flash is the other limit. The S3 LAN-tier image is 4,660,544 bytes, and turning on relays adds 106,240 more. The C6 LAN-tier image is 4,575,232 bytes. None of these fit a 4 MB part, so 8 MB of flash is the practical floor, and cheap 4 MB boards like the ESP32-CAM join through the bridge. The choosing an ESP32 board guide covers the trade-offs.

Honest status: the XIAO ESP32-S3 Sense has run the node on silicon, on a Wi-Fi network the board hosts itself. The C6 firmware builds but has not run, and the relay path compiles but no relay has been dialed from a board.

The Janus protocols on the mesh

janus/rpc/1 and janus/media/1

An iroh connection is opened for a named protocol, an ALPN. A Janus node speaks a small, fixed set:

  • janus/echo/1: liveness, bytes back.
  • janus/rpc/1: manifest, telemetry, capability calls and adoption. Plain postcard frames with a length prefix, one request per stream, and every request carries the caller's mID assertion. Without one, the answer is Unauthorized; from a stranger, Denied.
  • janus/media/1: camera and audio packets. Small packets ride QUIC datagrams; a frame too big for a datagram rides as one stream, so the receiver gets whole frames or nothing.
  • janus/ota/1: signed updates, kept apart so rpc stays one frame per call.
  • mata-oem-sidecar/rpc/1: the home computer's existing JSON protocol, so a device appears in the owner's app with no change at the other end.

Measured on the XIAO: ten connections of ten, 520 ms median. That is a full encrypted handshake from a fresh process with the chip doing the cryptography, not a round-trip time. Media is not free: one subscription costs 19,088 bytes of internal memory, so the S3 is capped at two live subscribers.

The host client and the bridge for no_std nodes

rusty_esp_iroh-host is both the node and the client. The same std code runs on ESP-IDF and on a laptop, which is why most behaviour was proven on the host first:

use rusty_esp_iroh_host::{Node, NodeConfig, NodeIdentity};

let identity = NodeIdentity::load_or_create(&mut kv, &mut rng, "janus")?;
let node = Node::bind_with(identity, kv, &manifest, Some(factory), config, extras).await?;
println!("{}", node.ticket_text());   // hand this to a subscriber

The client can fetch a device's manifest and verify its signature under the device's DID before trusting any capability it claims.

rusty_esp_iroh-bridge is for chips that cannot run iroh. A std process on a Pi, a P4 or a laptop holds one iroh endpoint that represents many radio neighbours over ESP-NOW, LoRa or BLE. It relays each neighbour's own signed manifest verbatim, so the home computer verifies the neighbour itself; the bridge holds sessions, never keys. It answers only DIDs on its roster, and an empty roster refuses everyone. Raw Wi-Fi channel state for rusty_esp_sense rides this path too. All of this is tested host to host with simulated neighbours; the radios have not been run.

Keys, updates, and what is still open

Keeping the endpoint secret in NVS

iroh wants an ed25519 endpoint key. The Janus family's identity is a P-256 did:mata, so the device signs a small Binding that ties its iroh endpoint to its DID. The identity side is covered in the rusty_esp_mid field note.

Where the key lives matters. In September two hand-written mesh firmwares were found keeping it in the default nvs partition, the one espino provision rewrites when you change Wi-Fi. Changing your password would have minted a new device and dropped its owner. Both now end the flash with a dedicated partition, matching the espino convention:

identity, data, nvs, 0x7fd000, 0x3000

On the XIAO, the endpoint id stayed the same across a whole-image reflash and across a signed update. The storage layer refuses a plaintext partition unless a loudly named development feature is on; development boards run with it, and the ledger says so.

Adoption, signed updates, and open defects

Adoption ran on silicon: the owner adopted the device, a stranger presenting the owner's own record was refused, an older roster version was refused, and the owner then read private telemetry. The owner in that test was a deterministic bench key, not a person.

A signed update also ran on silicon: a maker-signed 4,031,312-byte image over the board's own Wi-Fi, written at 58.8 KB/s into the other slot, committed, booted, and kept after a hard reset, in 72.79 seconds end to end. A bad signature and an unvalidated image rolling back are tested on the host only.

One open defect is written down rather than hidden: after adoption the device answers "paired" when asked, but keeps advertising itself as open on the local network, because that record is composed once at boot. For how all of this fits a home with no vendor cloud, see smart home without the cloud and the Connect vision. rusty_esp_iroh is part of Remade With Rust from Mata Network.

FAQ

Quick answers for builders evaluating this technology.

Which ESP32 boards can run iroh with rusty_esp_iroh?

Boards with PSRAM (such as the XIAO ESP32-S3 Sense, WROVER, C61 with PSRAM, and the P4) form the relay tier. Boards without PSRAM, like the C6, form a LAN-direct tier. 8 MB of flash is the practical floor. Only the XIAO ESP32-S3 Sense has run the node on silicon so far; the C6 build compiles.

What are the Janus ALPNs?

They are the protocols a Janus node speaks over iroh: janus/echo/1 for liveness, janus/rpc/1 for manifest, telemetry, capability calls and adoption, janus/media/1 for camera and audio packets, janus/ota/1 for signed updates, and mata-oem-sidecar/rpc/1 so the home computer's existing app sees the device.

Does rusty_esp_iroh need a cloud server?

No. Devices are dialed by public key. On the LAN tier the device is reached directly with a long ticket that carries its address. The PSRAM tier can also use iroh relays for reach from other networks; that relay path builds but has not been dialed on a board yet.

How does a no_std ESP32 join the iroh mesh?

Through rusty_esp_iroh-bridge, a std process on a Pi, P4 or laptop that fronts radio neighbours (ESP-NOW, LoRa, BLE) as mesh presences. It relays each neighbour's own signed manifest, holds sessions but never keys, and answers only DIDs on its roster. It is tested host to host, not yet over real radios.

Where does the device keep its iroh key?

In a dedicated identity NVS partition at the end of flash, separate from the nvs partition that Wi-Fi provisioning rewrites. The iroh endpoint key is bound to the device's P-256 did:mata by a signed Binding, and it has stayed the same across reflashes and a signed update.