* Mod gitignore + license trimming + comment trimming
* Big rewrite
* SURB inputs + DNS button in internal-dev
* Make ipr addr optional
* Accidentatly omitted files from rewrite commit
* Makefile + readme
* Comment rewrite
* Optimisation comment
* Replace manual waker map with
smoltcp built-ins + adaptive poll
* Comments
* Extract socket creation helpers into stream.rs
* Cleanup comments
* Comment
* Comment notes and restrict ciphersuites wrt rustls-rustcrypto
* Dep. hack fix for demo + add clearnet fetch() for contrast
* Stripped down devtester
* Fix Clippy arg (fatfingered deletion)
* CodeRabbit catches
* Cargofmt
* Review nits: bridge logs, fetch early-return, static port counter, copyright years, README + Cargo + headless.js tidying
* PHONY + taskset override, switch internal-dev/tests to pnpm, fix wasm-pack out-dir
* Gate codec tests behind the codec feature for no-default-features builds
* IPv6 addr/route on smoltcp iface + configurable DNS resolvers via TunnelOpts
* DNS GUI inputs, close stale WS on reconnect, worker init guards + ws-send warning, Playwright listener cleanup, pnpm-lock in internal-dev
* Fix lp -> lp-data after rebase
* Revert nym-lp/nym-lp-data feature-gating left over from rebase
* Lift getrandom wasm_js cfg to workspace .cargo/config.toml so cargo check -p smolmix-wasm works from any CWD
* temp will amend git message
* Auto-discover IPR when none specified + 'Use random IPR' checkbox in internal-dev
* smolmix_tracker + State machine + ready_tunnel gate + getTunnelState JS surface
* Mirror red display() entries to console.error
* Add left out package-lock
* Reactor clock + yield_now + atomic seq + gateway-storage errors
* setupMixTunnel gate + MTU 1980 + http::Uri cleanup
* Review pass + fix test + clippy
* restore axum 0.8 bump from borked earlier merge
* Feature gating (dns/fetch/socket) + TunnelOptsBuilder + pnpm bypass
* Cont. with review comments
* tokio Nofity reactor wakes + cancellation + setup polishing
* Notify wakes + inner pattern + close_notify + util
* Tunable tunnelopts
* Fix tired commit
* CI prep
* Lint + Clippy
* coderabbit u32 fix
* nits + runtime debugging + expose in internal-dev
* remove redudant default-features
* Remove more redundant default-features
8.0 KiB
smolmix-wasm
Drop-in browser networking over the Nym mixnet. Routes HTTP and WebSocket traffic through a mixnet tunnel, giving web applications network-level privacy without changing application code.
Public API
Three WASM exports that mirror the browser's native networking surface:
| Browser API | smolmix export | Description |
|---|---|---|
fetch() |
mixFetch(url, init) |
HTTP/HTTPS request-response |
new WebSocket() |
mixSocket(url, protocols, onEvent) |
WebSocket (WS/WSS) |
| (no direct browser equivalent) | mixResolve(hostname) |
DNS-only hostname lookup over UDP/IPR (no TCP/TLS) |
Arch
WasmTunnel
+---------- tunnel.rs -----------+
| |
| Owns: smoltcp stack, Nym |
| client, connection pool, |
| DNS cache, origin locks |
+--------------------------------+
| |
TCP/UDP sockets |
(futures::io) |
| |
v v
+-----------+ +-----------+ +-----------+
| Reactor | | Bridge | | Nym Client|
| reactor.rs| | bridge.rs | | (base |
| | | | | client) |
+-----------+ +-----------+ +-----------+
| | |
v v |
+-----------+ +-------+ |
| smoltcp | | IPR | |
| Interface | |ipr.rs | |
+-----------+ +-------+ |
| | |
v | |
+-----------+ | |
| Device |<----+ |
| device.rs | | |
| (virtual | v |
| NIC) | LP frames |
+-----------+ + SURBs |
rx[] / tx[] | |
+--------->------+
mixnet
Component walkthrough
- Device (
device.rs) - the virtual network interface card - Reactor (
reactor.rs) - the smoltcp poll loop - Bridge (
bridge.rs) - shuttles packets between the device and the mixnet - IPR (
ipr.rs) - IP Packet Router protocol layer - WasmTcpStream / WasmUdpSocket / PooledConn (
stream.rs) -futures::io::AsyncRead + AsyncWriteadapters over smoltcp sockets - WASM exports (
lib.rs,mixfetch.rs,mixsocket.rs) - the surface JS calls into
Tuning
The JS setupMixTunnel(opts) shape accepts the following optional fields for
timeouts, buffer sizes, and protocol limits. All have sensible defaults; only
override when you have a concrete reason.
| Field | Default | Notes |
|---|---|---|
connectTimeoutMs |
60000 |
IPR connect handshake timeout |
dnsTimeoutMs |
30000 |
DNS query timeout (per primary/fallback attempt) |
tcpKeepaliveMs |
10000 |
TCP keepalive probe interval |
tcpBufferSize |
65535 |
Per-TCP-stream RX/TX buffer; capped at u16::MAX |
maxRedirects |
5 |
mixFetch redirect chain depth before bail |
On the Rust side these live in TunnelOpts::tuning: TuningOpts. The builder
exposes them flat (.connect_timeout(d), .tcp_buffer_size(n), etc.) so
callers don't see the grouping.
Feature flags
The crate is split into three user-facing cargo features matching the JS entry points. Default builds enable all three; downstream TS SDK packages can opt into a subset to drop the corresponding implementation + native deps from the wasm binary.
| Feature | JS export | Pulls |
|---|---|---|
dns |
mixResolve |
(nothing extra; DNS resolver is always compiled) |
fetch |
mixFetch |
rustls TLS stack + hyper HTTP/1.1 client |
socket |
mixSocket |
rustls TLS stack + async-tungstenite |
Build a dns-only client:
cargo build --target wasm32-unknown-unknown --no-default-features --features dns
Build a fetch-only client (no WebSocket, no mixResolve JS export):
cargo build --target wasm32-unknown-unknown --no-default-features --features fetch
fetch and socket share the TLS stack (rustls + rustls-rustcrypto + webpki-roots);
enabling both is roughly the same wasm size as either alone plus the hyper +
async-tungstenite specifics.
Debug logging
debug_log! and debug_error! (in util.rs) wrap nym_wasm_utils::console_log! /
console_error! behind the debug cargo feature. Tunnel start/shutdown and the
IPR connect handshake stay unconditional; everything else is silent in release.
make build-debug enables the feature automatically (it builds with
--features debug). make build-release-opt leaves it off, so release
artefacts ship no verbose logging.
Cryptography
TLS terminates inside the WASM client, so we need a pure-Rust rustls
crypto provider. rustls-rustcrypto as
the only viable option: the underlying RustCrypto AEADs were
audited by NCC Group in 2020
with no findings, while the rustls integration glue is 0.0.2-alpha.
src/tls.rs restricts negotiation to AEAD-only suites with forward-secret
key exchange.
Build
make build # plain release wasm-pack build
make build-debug # dev profile, verbose console logs on
make build-release-opt # release + wasm-opt -Oz
make dev # build-debug then start internal-dev webpack
Summary diagram
JS caller
|
+---------+---------+--------------+
v v v
mixFetch mixSocket mixResolve
(mixfetch.rs) (mixsocket.rs) (mixdns.rs)
| | |
v v v
fetch::fetch fetch::new_ dns::resolve
connection + (dns.rs)
async_tungst.
\ | /
\ v /
'-> WasmTcpStream / WasmUdpSocket (stream.rs)
|
v smoltcp socket buffer
+-------- smoltcp::Interface::poll() (reactor.rs)
|
v IP packet
WasmDevice.tx_queue (device.rs)
|
v drained 5ms
bridge::start_bridge (bridge.rs)
|
v
ipr::send_ip_packet (ipr.rs)
|
v LP-framed DataRequest
ClientInput::send (upstream, nym-wasm-client-core)
|
v Sphinx-packed
JSWebsocket::new -> WebSocket::open -> web_sys::WebSocket::new
(common/wasm/utils/src/websocket/mod.rs:58)
|
v
Single wss:// to chosen gateway
(Separately, at startup + on TopologyRefresher tick:)
nym_http_api_client::ClientBuilder
-> reqwest -> web_sys::fetch
(common/client-core/src/init/helpers.rs:155)
|
v
HTTPS GET https://validator.nymtech.net/...
Everything else (TLS handshakes, HTTP/1.1 requests, WebSocket frames in
mixSocket) is content travelling inside that single gateway WSS as
Sphinx-packed bytes.