You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Driving goal: crate-level compatibility with iroh's protocol ecosystem
(iroh-blobs, iroh-gossip, ...) — stock crates running on our endpoint — not
just wire compatibility. Protocol crates bind concrete iroh::Endpoint / endpoint::Connection / tokio io types (iroh = "1.0" non-optional in
iroh-blobs; ProtocolHandler::accept(Connection)); there is no trait seam
to substitute an endpoint through. Consequences:
Wire compat (the current README design) lets us talk to blobs/gossip
nodes only by porting each protocol's wire logic — a permanent
parallel-maintenance treadmill that drifts from the ecosystem it chases.
An API-compatible iroh facade over the WIT surface is a whole-crate
fork; strictly worse than upstream + patches.
So ecosystem compat is achievable only by running the upstream crate
in-guest. The question becomes where that is feasible and what it costs
the architecture.
All findings below verified 2026-08-05 against n0-computer/iroh v1.0.3
(tokio 1.52.3, rustc 1.97.0), iroh-blobs v0.103.0, iroh-gossip v0.101.0.
File paths are upstream paths unless marked local. Compile claims are cargo check only — nothing linked or run.
Rationale check: README "Port the protocol, not the crate"
README claim
Status at v1.0.3
"quinn assumes OS sockets"
No longer true at the API boundary. unstable-custom-transports ships poll-based CustomTransport/CustomEndpoint/CustomSender (iroh/src/socket/transports/custom.rs), Builder::add_custom_transport + path_selector, clear_ip_transports()/clear_relay_transports(). TransportAddr::{Ip,Relay,Custom} is first-class; custom paths participate in multipath selection; discovery records round-trip custom addrs (iroh-dns/src/endpoint_info.rs:492). Feature is explicitly "may change without notice".
"tokio's WASI support is partial"
Materially weakened by tokio-rs/mio#1931 (merged 2026-02-24: wasip2 via wasi-libc poll(2), single-thread Waker) + tokio-rs/tokio#7933 (wasip2 networking, v1.51.0+), both gated behind --cfg tokio_unstable (stabilization: tokio-rs/tokio#4827). See empirical status below.
"existing browser build targets wasm-bindgen, not components"
Still true; wasm_browser cfg = wasm && os=unknown (iroh/build.rs:7), so wasip2 takes the native branch. The browser branch hard-requires JS (wasm_bindgen_futures::spawn_local, iroh/src/runtime.rs). Not componentizable; irrelevant to fix.
What upstream provides vs what stays coupled (v1.0.3)
Pluggable today: custom transports (above); rustls CryptoProvider is
builder-injectable (iroh/src/endpoint.rs:149) — the polymorph-tls
provider slots into the crypto split's in-guest half as-is; AddressLookup
is a public trait (custom publishers avoid the pkarr signing path).
Still coupled:
Identity is concrete.SecretKey (seed in linear memory) is wired
through the cert resolver (iroh/src/tls/resolver.rs:12), the relay
handshake challenge, and pkarr record signing. rustls's SigningKey/Signer machinery is right there; an Arc<dyn SigningKey>
refactor in iroh + iroh-relay is small and PR-able, but without it the
upstream path violates the non-extractable-identity ruling.
Runtime is pub(crate) and cfg-hardwired two ways
(iroh/src/runtime.rs): tokio (TaskTracker + noq::TokioRuntime
timers) vs wasm_bindgen_futures + web timers. With custom transports
only, tokio-as-I/O disappears and the residue is exactly two seams —
spawn (noq driver tasks: endpoint driver, per-connection driver, path
filter; plus iroh's actor mesh) and timers (noq new_timer, n0_future::time = tokio::time on native) — plus executor-agnostic tokio::sync/tokio-util and tokio io traits on streams (public API: tokio::* is in allowed external types). Upstream motion covers time
only (Allow host-provided QUIC time sources n0-computer/iroh#4459); a spawn-side sibling plus a relay-dialer
seam would complete it.
Relay client is compile-time split (native tokio-websockets/hyper vs
browser ws_stream_wasm); no dialer trait. Upstream peers are reachable
only through real Ip/Relay transports (routing is by addr variant),
so a port needs the internal relay transport working, not a
relay-as-custom-transport workaround (workable outbound, but inbound
reachability and internal lookup-driven dialing fight it).
noq-proto note (relevant to Direct UDP as an upgrade target #12 independently of this issue): n0_nat_traversal.rs, address_discovery.rs (QAD), and multipath live
at the sans-I/O proto layer of the noq fork — inheritable by the local
core as a dependency, without the tokio-coupled socket layer.
Without tokio_unstable: dies at tokio's guard (iroh's native branch
demands net,fs,io-std; iroh's own src has zero tokio::net/tokio::fs
uses — trimming is a one-line iroh PR).
With --cfg tokio_unstable: tokio + mio compile; ring,
tokio-websockets, hickory-resolver, hyper, reqwest all pass. Remaining
failures — exactly three:
noq-udp: platform imp module selection matches nothing for wasi
(posix_minimal.rs exists in-tree; needs selection or a std fallback);
netdev (via netwatch): IFF_* libc flags — needs the stub netwatch
already has for wasm_browser;
iroh-dns: one line, hickory read_system_conf() cfg'd out on wasi
(iroh-dns/src/dns.rs:774).
Interpretation: on socket-bearing hosts, tokio-as-executor becomes coherent
inside a component (current-thread rt parking on poll(2) → wasi-libc → wasi:io/poll). What looked like a family-wide port campaign is now ≈3
small patches + an iroh cfg/feature PR. Caveats: tokio_unstable carries
no semver guarantee; open wasip2 bugs (tokio-rs/tokio#8082, tokio-rs/tokio#7936).
Browser leg
Two vehicles, different gaps:
Upstream wasm_browser build + WebRTC custom transport (wasm-bindgen):
compiles today with unstable-custom-transports (checked, exit 0), and has
no executor problem — the JS event loop is the single wake path. Missing:
a signaling story (WebRTC needs SDP/ICE exchange; the natural shape is our
upgrade model — dial via relay, signal over an ALPN protocol, migrate — so
"custom transport only" is a fiction; relay stays for bootstrap/fallback);
a public mid-connection addr-injection API (mechanism exists — resolve_remote merges per-remote and open_path_on_all_conns
(iroh/src/socket/remote_map/remote_state.rs:309) reaches live
connections — but only insert_relay is public, and Custom-addr
end-to-end is unverified); Send + Sync trait bounds vs !Send web-sys
types (send_wrapper/channel confinement); identity in linear memory; no
per-transport MTU clamp (only GSO segment count). And it is not a
component: no WIT surface, no composition, JS host only — a legitimate
upstream contribution, not this repository's vehicle.
wasip2 component under jco: CM-async import completions (waitables)
cannot wake tokio parked in poll(2) — wasi-libc's poll sees fds only, and
p2 has no pollable→fd bridge, so polymorph:webrtc events go unnoticed
between timer ticks (bounded polling again, now under iroh's ~15-task
actor mesh). Browsers have no wasi:sockets, so relay/DNS/pkarr compile
but are dead at runtime; signaling must ride polymorph:websocket. Plus the
known jco CM-async scheduler defects (#10, #9, #6) that already block our
much less demanding pump. tokio-rs/mio#1931's own text defers unification
to p3 wasi-libc (pipes, threads); p3 CM-async is where waitables become
the single wake path.
Ecosystem crates measured
More portable than iroh itself; n0 engineered the family to the same
two-branch pattern, both crates CI wasm32:
iroh-gossip v0.101: protocol core has zero target cfg; net (the
iroh binding) and rpc are features; common tokio {io-util,sync}.
iroh-blobs v0.103: common tokio sync only; only target-split deps
are chrono/getrandom; fs-store (redb + reflink-copy + bao-tree/fs) and rpc (noq + irpc) are opt-out features; wasm_browser cfg confined to store/mem.rs + store/util.rs. MemStore portable by construction;
FsStore plausible on wasip2 (redb is std::fs-based, no mmap) —
unverified; absent on browser.
Long tail unverified: irpc noq_endpoint_setup, iroh-tickets, iroh-io,
iroh-util.
Composition-model consequence
#3's "protocols as plain consumer components composed via wac plug" is
crate-incompatible by construction — stock protocol crates cannot consume
a WIT endpoint import. The ecosystem-compatible shape is the fat
guest: upstream iroh + stock protocol crates linked into one wasip2
guest, WIT exported at the application boundary (blobs/gossip interfaces
and/or the endpoint surface), transports/crypto still injected where the
invariants demand. WIT composition remains for protocols written natively
as components. Both models coexist; "stock ecosystem crate as a
separately-composed component" cannot exist without ports.
Blocked on the CM-async→executor wake path, same as bare iroh; p3 is the durable fix; protocol crates add no new blockers
Browser via upstream wasm-bindgen
Works today upstream; not a component — n0's lane
The ruling this forces
Ecosystem-compat-now collides with one-implementation-across-targets:
(a) Declare crate-level ecosystem compat an optional capability of
socket-bearing targets (the portability ladder's last rung — declared,
gated), with p3 named as the convergence point where the browser joins.
Browser keeps the purpose-built endpoint — which is also the
WebRTC-direct story upstream lacks. Recommended.
(b) Dual endpoint implementations behind one WIT surface,
conformance-gated — the shape the README's central ruling exists to
forbid.
(c) Defer ecosystem compat wholesale to p3 — cedes a year-plus for
no architectural gain over (a).
Resolving this issue means amending the README ruling by PR, not sliding
into an implementation.
Next steps if (a) is adopted
Fat-guest spike (own issue): iroh + blobs MemStore on wasip2 under
wasmtime, the 3 local patches, fetch a blob from an upstream native
node; record binary size + handshake numbers (the budget policy will
demand them).
Upstream patches, each independently justified: noq-udp wasi impl
selection; netwatch/netdev wasi stub; iroh-dns one-liner; signer
abstraction (Arc<dyn SigningKey> in tls/resolver + relay handshake)
for the identity ruling.
Direct UDP as an upgrade target #12 tie-in regardless of outcome: evaluate switching the local core to
noq-proto to inherit n0_nat_traversal/QAD/multipath sans-I/O.
Driving goal: crate-level compatibility with iroh's protocol ecosystem
(iroh-blobs, iroh-gossip, ...) — stock crates running on our endpoint — not
just wire compatibility. Protocol crates bind concrete
iroh::Endpoint/endpoint::Connection/ tokio io types (iroh = "1.0"non-optional iniroh-blobs;
ProtocolHandler::accept(Connection)); there is no trait seamto substitute an endpoint through. Consequences:
nodes only by porting each protocol's wire logic — a permanent
parallel-maintenance treadmill that drifts from the ecosystem it chases.
irohfacade over the WIT surface is a whole-cratefork; strictly worse than upstream + patches.
in-guest. The question becomes where that is feasible and what it costs
the architecture.
All findings below verified 2026-08-05 against n0-computer/iroh v1.0.3
(tokio 1.52.3, rustc 1.97.0), iroh-blobs v0.103.0, iroh-gossip v0.101.0.
File paths are upstream paths unless marked local. Compile claims are
cargo checkonly — nothing linked or run.Rationale check: README "Port the protocol, not the crate"
unstable-custom-transportsships poll-basedCustomTransport/CustomEndpoint/CustomSender(iroh/src/socket/transports/custom.rs),Builder::add_custom_transport+path_selector,clear_ip_transports()/clear_relay_transports().TransportAddr::{Ip,Relay,Custom}is first-class; custom paths participate in multipath selection; discovery records round-trip custom addrs (iroh-dns/src/endpoint_info.rs:492). Feature is explicitly "may change without notice".--cfg tokio_unstable(stabilization: tokio-rs/tokio#4827). See empirical status below.wasm_browsercfg =wasm && os=unknown(iroh/build.rs:7), so wasip2 takes the native branch. The browser branch hard-requires JS (wasm_bindgen_futures::spawn_local,iroh/src/runtime.rs). Not componentizable; irrelevant to fix.What upstream provides vs what stays coupled (v1.0.3)
Pluggable today: custom transports (above); rustls
CryptoProviderisbuilder-injectable (
iroh/src/endpoint.rs:149) — thepolymorph-tlsprovider slots into the crypto split's in-guest half as-is;
AddressLookupis a public trait (custom publishers avoid the pkarr signing path).
Still coupled:
SecretKey(seed in linear memory) is wiredthrough the cert resolver (
iroh/src/tls/resolver.rs:12), the relayhandshake challenge, and pkarr record signing. rustls's
SigningKey/Signermachinery is right there; anArc<dyn SigningKey>refactor in iroh + iroh-relay is small and PR-able, but without it the
upstream path violates the non-extractable-identity ruling.
pub(crate)and cfg-hardwired two ways(
iroh/src/runtime.rs): tokio (TaskTracker +noq::TokioRuntimetimers) vs
wasm_bindgen_futures+ web timers. With custom transportsonly, tokio-as-I/O disappears and the residue is exactly two seams —
spawn (noq driver tasks: endpoint driver, per-connection driver, path
filter; plus iroh's actor mesh) and timers (noq
new_timer,n0_future::time=tokio::timeon native) — plus executor-agnostictokio::sync/tokio-utiland tokio io traits on streams (public API:tokio::*is in allowed external types). Upstream motion covers timeonly (Allow host-provided QUIC time sources n0-computer/iroh#4459); a spawn-side sibling plus a relay-dialer
seam would complete it.
browser ws_stream_wasm); no dialer trait. Upstream peers are reachable
only through real
Ip/Relaytransports (routing is by addr variant),so a port needs the internal relay transport working, not a
relay-as-custom-transport workaround (workable outbound, but inbound
reachability and internal lookup-driven dialing fight it).
n0_nat_traversal.rs,address_discovery.rs(QAD), and multipath liveat the sans-I/O proto layer of the noq fork — inheritable by the local
core as a dependency, without the tokio-coupled socket layer.
Empirical wasip2 compile status
cargo check -p iroh --no-default-features --features tls-ring,unstable-custom-transports --target wasm32-wasip2:tokio_unstable: dies at tokio's guard (iroh's native branchdemands
net,fs,io-std; iroh's own src has zerotokio::net/tokio::fsuses — trimming is a one-line iroh PR).
--cfg tokio_unstable: tokio + mio compile; ring,tokio-websockets, hickory-resolver, hyper, reqwest all pass. Remaining
failures — exactly three:
noq-udp: platformimpmodule selection matches nothing for wasi(
posix_minimal.rsexists in-tree; needs selection or a std fallback);netdev(via netwatch):IFF_*libc flags — needs the stub netwatchalready has for
wasm_browser;iroh-dns: one line, hickoryread_system_conf()cfg'd out on wasi(
iroh-dns/src/dns.rs:774).Interpretation: on socket-bearing hosts, tokio-as-executor becomes coherent
inside a component (current-thread rt parking on poll(2) → wasi-libc →
wasi:io/poll). What looked like a family-wide port campaign is now ≈3small patches + an iroh cfg/feature PR. Caveats:
tokio_unstablecarriesno semver guarantee; open wasip2 bugs (tokio-rs/tokio#8082,
tokio-rs/tokio#7936).
Browser leg
Two vehicles, different gaps:
Upstream
wasm_browserbuild + WebRTC custom transport (wasm-bindgen):compiles today with
unstable-custom-transports(checked, exit 0), and hasno executor problem — the JS event loop is the single wake path. Missing:
a signaling story (WebRTC needs SDP/ICE exchange; the natural shape is our
upgrade model — dial via relay, signal over an ALPN protocol, migrate — so
"custom transport only" is a fiction; relay stays for bootstrap/fallback);
a public mid-connection addr-injection API (mechanism exists —
resolve_remotemerges per-remote andopen_path_on_all_conns(
iroh/src/socket/remote_map/remote_state.rs:309) reaches liveconnections — but only
insert_relayis public, and Custom-addrend-to-end is unverified);
Send + Synctrait bounds vs!Sendweb-systypes (send_wrapper/channel confinement); identity in linear memory; no
per-transport MTU clamp (only GSO segment count). And it is not a
component: no WIT surface, no composition, JS host only — a legitimate
upstream contribution, not this repository's vehicle.
wasip2 component under jco: CM-async import completions (waitables)
cannot wake tokio parked in poll(2) — wasi-libc's poll sees fds only, and
p2 has no pollable→fd bridge, so polymorph:webrtc events go unnoticed
between timer ticks (bounded polling again, now under iroh's ~15-task
actor mesh). Browsers have no
wasi:sockets, so relay/DNS/pkarr compilebut are dead at runtime; signaling must ride polymorph:websocket. Plus the
known jco CM-async scheduler defects (#10, #9, #6) that already block our
much less demanding pump. tokio-rs/mio#1931's own text defers unification
to p3 wasi-libc (pipes, threads); p3 CM-async is where waitables become
the single wake path.
Ecosystem crates measured
More portable than iroh itself; n0 engineered the family to the same
two-branch pattern, both crates CI wasm32:
net(theiroh binding) and
rpcare features; common tokio{io-util,sync}.synconly; only target-split depsare chrono/getrandom;
fs-store(redb + reflink-copy + bao-tree/fs) andrpc(noq + irpc) are opt-out features;wasm_browsercfg confined tostore/mem.rs+store/util.rs. MemStore portable by construction;FsStore plausible on wasip2 (redb is std::fs-based, no mmap) —
unverified; absent on browser.
noq_endpoint_setup, iroh-tickets, iroh-io,iroh-util.
Composition-model consequence
#3's "protocols as plain consumer components composed via
wac plug" iscrate-incompatible by construction — stock protocol crates cannot consume
a WIT endpoint import. The ecosystem-compatible shape is the fat
guest: upstream iroh + stock protocol crates linked into one wasip2
guest, WIT exported at the application boundary (blobs/gossip interfaces
and/or the endpoint surface), transports/crypto still injected where the
invariants demand. WIT composition remains for protocols written natively
as components. Both models coexist; "stock ecosystem crate as a
separately-composed component" cannot exist without ports.
Per-target status under the ecosystem goal
tokio_unstable, blobs MemStore (FsStore worth one experiment)The ruling this forces
Ecosystem-compat-now collides with one-implementation-across-targets:
socket-bearing targets (the portability ladder's last rung — declared,
gated), with p3 named as the convergence point where the browser joins.
Browser keeps the purpose-built endpoint — which is also the
WebRTC-direct story upstream lacks. Recommended.
conformance-gated — the shape the README's central ruling exists to
forbid.
no architectural gain over (a).
Resolving this issue means amending the README ruling by PR, not sliding
into an implementation.
Next steps if (a) is adopted
wasmtime, the 3 local patches, fetch a blob from an upstream native
node; record binary size + handshake numbers (the budget policy will
demand them).
selection; netwatch/netdev wasi stub; iroh-dns one-liner; signer
abstraction (
Arc<dyn SigningKey>in tls/resolver + relay handshake)for the identity ruling.
noq-proto to inherit n0_nat_traversal/QAD/multipath sans-I/O.
unstable-custom-transportsstabilization; upstream runtime injection(Allow host-provided QUIC time sources n0-computer/iroh#4459 + a spawn-side sibling); a relay-dialer or
HTTP-client seam; wasi-libc p3 (pipes/threads) + jco CM-async fixes
(jco: scheduler stops delivering waitable events once a detached task holds in-flight imports across export calls #10). Two or more firing flips the browser-leg calculus.