Proof that Rust compiled to wasm32-unknown-emscripten can make a real HTTP/3
(QUIC) request to the public internet using tokio sockets — running on Node.js.
HTTP/3 GET OK: cloudflare-quic.com (104.18.26.14:443) alpn=h3 rtt=40ms
status=200 OK body=125959B first_line="<!DOCTYPE html>"
This repository is a reproducible proof-of-concept. A Rust program is compiled to
wasm32-unknown-emscripten, and, via Guy Bedford's Emscripten, mio, and Tokio forks,
it uses async tokio UDP sockets to complete a
QUIC handshake and an HTTP/3 (ALPN h3) GET against cloudflare-quic.com over the
real internet (200 OK, ~126 KB of HTML), executed under Node.js.
There are two demos, proving the same result on the two major Rust QUIC stacks:
quinn(TLS via rustls + ring) — the original PoC (demos/quinn_h3/src/main.rs).quiche(Cloudflare's own QUIC/H3, TLS via BoringSSL through theboringcrate) — a second demo (demos/quiche/) that also does a full HTTP/3 GET. Cargo compiles the bundled BoringSSL to wasm. Seedocs/quiche-boringssl.md.
It builds on Guy Bedford's work bringing full POSIX
networking (epoll, async DNS, raw TCP/UDP sockets over node:net/node:dgram, and a
tokio reactor) to the wasm32-unknown-emscripten target.
Scope: the POC targets Node.js. Cloudflare Workers is the eventual home for the same crate, but is out of scope here — it needs
node:dgramin the Workers runtime, which is being added separately. Nothing in this POC depends on it.
h3 (HTTP/3) ──► quinn (QUIC/TLS 1.3)
│ quinn-udp Unix socket path
▼
tokio::net::UdpSocket ← standard Tokio API over Guy's mio backend
│ mio over the epoll reactor
▼
emscripten libc + JS FS ← epoll + async DNS + -sNODERAWSOCKETS (Guy's fork)
▼
node:net / node:dgram → the internet → QUIC endpoint
TLS 1.3 is provided by rustls + ring in the quinn demo; the quiche demo uses BoringSSL. Both build on Emscripten without assembly.
quinn is the de-facto pure-Rust QUIC implementation, is tokio-native, and uses
rustls — the exact TLS stack that already works on emscripten. A small Emscripten patch
implements the recvmmsg syscall used by quinn-udp's normal Unix backend. This keeps
Quinn and quinn-udp unmodified and needs no C crypto library.
The second demo (demos/quiche/) uses quiche, Cloudflare's own QUIC/HTTP-3 library,
which does TLS via BoringSSL (through the boring crate). BoringSSL was initially
assumed intractable on emscripten, but Cargo can compile it to wasm (OPENSSL_NO_ASM) and
run it on Node. The target configuration and upstream wasm FFI fix are documented in
docs/quiche-boringssl.md. The demo uses Cloudflare's
tokio-quiche adapter to drive quiche's sans-I/O state machines over the same Emscripten
tokio::net::UdpSocket. (s2n-quic pulls aws-lc-rs, which remains intractable on
Emscripten.)
| Path | What |
|---|---|
demos/quinn_h3/ |
The quinn demo. h3 + quinn using its normal Tokio runtime and quinn-udp Unix backend, doing an HTTP/3 GET to cloudflare-quic.com. |
demos/quiche/ |
The quiche demo. tokio-quiche + BoringSSL over the Emscripten tokio::net::UdpSocket. |
patches/ |
Reproducible Emscripten compatibility patches for Emscripten, wasm-bindgen, quanta, and tokio-quiche dependencies. |
cmake/boringssl-emscripten.cmake |
Selects BoringSSL's portable C implementation and loads the Emscripten CMake toolchain for Cargo's boring-sys build. |
scripts/ |
setup.sh (clone dependencies + apply patches + toolchain), run-quinn_h3.sh (quinn demo), run-quiche.sh (quiche demo). No user-supplied configuration is required. |
.github/workflows/ci.yml |
CI that provisions the toolchain and runs both demos on macOS, asserting the success sentinels. |
docs/branch-map.md |
Exact revisions and provisioning methods for nonstandard dependencies. |
AGENTS.md |
Orientation + hard-won build knowledge for contributors and coding agents. |
Run
scripts/setup.shbefore building. It provisions five pinned Guy Bedford forks plus pinned quanta and quiche checkouts under.work; Cargo fetches mio, socket2, quinn-udp, and registry dependencies. The run scripts set the target environment and execute the generated modules under Node.
Prereqs: git, a recent Rust toolchain with rustup, Node.js ≥ 24 (JSPI +
node:dgram bindSync/connectSync; Node 26 recommended), Python 3, cmake (for
BoringSSL), a C toolchain, and a system emscripten for its LLVM/binaryen backend plus a
libclang for bindgen — on macOS: brew install emscripten llvm cmake (setup.sh
auto-detects the Homebrew emscripten backend; the cf fork frontend is cloned for you).
# 1. One-time setup: clones pinned dependencies, applies the compatibility
# patches, builds the patched wasm-bindgen CLI, and configures emcc.
# Everything lands under ./.work (git-ignored).
./scripts/setup.sh
# 2. Build + run the quinn QUIC demo under Node.
./scripts/run-quinn_h3.shExpected tail:
QUIC handshake OK: alpn=h3; rtt=40ms
sending HTTP/3 GET https://cloudflare-quic.com/ ...
HTTP/3 GET OK: cloudflare-quic.com (...) alpn=h3 status=200 OK body=125959B first_line="<!DOCTYPE html>"
QUIC-H3-ON-EMSCRIPTEN-OK
To build BoringSSL through Cargo and run the quiche demo instead:
./scripts/run-quiche.shExpected tail:
QUIC handshake and HTTP/3 connection established
HTTP/3 GET OK (tokio-quiche): cloudflare-quic.com (...) status=200 server="cloudflare" body=125959B first_line="<!DOCTYPE html>"
QUIC-H3-QUICHE-ON-EMSCRIPTEN-OK
If you run behind a TLS-inspecting VPN/proxy with a private CA (e.g. corporate
Zero-Trust), strict certificate verification will fail (UnknownIssuer, or an
UnsupportedSignatureAlgorithm if the private CA uses a key ring doesn't implement,
such as ECDSA P-521). The QUIC transport is unaffected.
The quinn transport PoC uses a verifier that checks the server's handshake signature via
ring but accepts the certificate chain path, so it remains demonstrable through an
inspecting proxy. This is intentionally not production certificate verification.
- ✅ tokio async DNS +
TcpStreamon Node (emscripten reactor). - ✅ standard
tokio::net::UdpSocketover Guy's Emscripten mio backend. - ✅
quinnQUIC handshake tocloudflare-quic.comover the internet, on Node. - ✅ HTTP/3 GET over that connection (
h3+h3-quinn) — 200 OK, real HTML body. - ✅ BoringSSL compiled to wasm;
tokio-quicheGET on Node — 200 OK, same body (second demo). - ✅ CI (
.github/workflows/ci.yml) provisions the toolchain and runs both demos on macOS. - ✅ Tokio UDP and libc
in6_pktinfoare supplied by the pinned Guy Bedford branches. - ⬜ Upstream the remaining BoringSSL Emscripten build configuration.
- ⬜ Same crate on Cloudflare Workers, once the runtime exposes
node:dgram.
The heavy lifting — epoll, async DNS, raw sockets, and the tokio reactor on
wasm32-unknown-emscripten — is Guy Bedford's work in
his forks of emscripten, mio, tokio, wasm-bindgen, libc, and ring. See
docs/branch-map.md.
MIT. See LICENSE.