smolmix
smolmix is a TCP/UDP tunnel over the Nym mixnet. It uses a userspace network stack smoltcp (opens in a new tab) to provide TcpStream and UdpSocket types that work with the async tokio (opens in a new tab) Rust ecosystem e.g. tokio-rustls (opens in a new tab), hyper (opens in a new tab), tokio-tungstenite (opens in a new tab), etc.
The TcpStream type implements tokio's AsyncRead/AsyncWrite traits and UdpSocket provides send_to/recv_from for datagrams.
┌──────────────────────────────────────────────────────────────┐
│ Your application (TLS, HTTP, WebSocket, DNS, etc.) │
│ └─ smolmix::TcpStream / UdpSocket │
│ └─ smoltcp (userspace TCP/IP) │
│ └─ Nym mixnet → IPR exit gateway → internet │
└──────────────────────────────────────────────────────────────┘Traffic exits the mixnet at an IPR (Internet Packet Router) exit gateway. The exit IP is the gateway's, not yours.
Runtime and platform support
Other runtimes
smolmix currently requires tokio. The internal pipeline is tokio-based: the bridge task that shuttles packets to the mixnet, the Nym SDK's MixnetClient, and the tokio-smoltcp (opens in a new tab) reactor that drives the userspace TCP/IP stack all run on the tokio runtime.
This means smolmix is not directly compatible with alternative async runtimes like smol (opens in a new tab) or async-std (opens in a new tab). If you need to use smolmix from another runtime, the async-compat (opens in a new tab) crate can bridge the gap.
Non-native smolmix
A WASM version of smolmix is planned for a future release.
Installation
Add smolmix to your Cargo.toml:
[dependencies]
smolmix = "1.21.0"
nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }tokio is a transitive dependency of smolmix, but you need to enable rt-multi-thread (the default runtime spun up by #[tokio::main]) and macros (for the #[tokio::main] attribute itself).
nym-bin-common is optional but recommended: it sets up tracing (opens in a new tab) logging so you can see mixnet connection progress.
Minimum Rust version: 1.87+
From Git
For unreleased changes, import directly from the repository:
smolmix = { git = "https://github.com/nymtech/nym", branch = "develop" }When to use smolmix
| smolmix | Stream module | mixFetch | SOCKS client | |
|---|---|---|---|---|
| Layer | Transport (TCP/UDP) | Message (multiplexed streams) | HTTP | TCP (SOCKS proxy) |
| Controls both sides? | No (proxy mode) | Yes (E2E) | No (proxy mode) | No (proxy mode) |
| API | TcpStream, UdpSocket | AsyncRead + AsyncWrite | fetch() replacement | SOCKS4/5 protocol |
| Composability | Full: TLS, HTTP, WebSocket, DNS, etc. stack on top | Byte streams only | HTTP(S) only | Application-dependent |
| Best for | Reaching external services from Rust with standard networking | Peer-to-peer / E2E protocols between Nym clients | Browser HTTP requests | Legacy apps with SOCKS support |
Security model
Traffic is Sphinx-encrypted inside the mixnet. Past the Exit Gateway, it travels over the public internet to the destination, the same as any other server-initiated connection. Protect the payload at the application layer with TLS (rustls (opens in a new tab)), Noise Protocol (snow (opens in a new tab)), or equivalent, as you would on a direct connection.
What's protected
| Segment | Mixnet encryption | What's visible |
|---|---|---|
| Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination |
| Inside the mixnet (entry + 3 mix layers + exit) | Sphinx (layered) | Each node only knows prev/next hop |
| Exit gateway (IPR) | Sphinx removed, raw IP packet exposed | IPR sees destination IP + port. Payload depends on your application layer (see below). |
| IPR → remote host | None (Sphinx is mixnet-only) | Remote host sees IPR's IP, not yours |
The Sphinx encryption is the mixnet transport layer: it protects packets as they traverse the mix nodes. At the exit gateway, the Sphinx layers are stripped and the original IP packet is forwarded to the destination. This is analogous to how a Tor exit node or VPN endpoint unwraps its tunnel.
What's inside that IP packet is up to you. If you connect with TLS (as in the TCP example (opens in a new tab)), the IPR sees encrypted TLS ciphertext going to a destination IP: it knows where but not what. If you send plaintext HTTP, the IPR can read the full request and response.
Trust boundaries
- You trust the mixnet to provide unlinkability between sender and receiver. This is enforced cryptographically by the Sphinx packet format and mixing.
- You trust the IPR exit gateway in the same way you trust a VPN exit or Tor exit node: it can inspect your raw IP packets. The difference is that the IPR doesn't know who is sending the traffic (the mixnet hides your identity).
- Application-layer encryption closes the gap. TLS, Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the IPR. It can see destination IP and port, but not payload content.
Comparison with other privacy tools
| smolmix | Tor | VPN | |
|---|---|---|---|
| Exit node sees traffic? | Yes (encrypt it) | Yes (encrypt it) | Yes (encrypt it) |
| Exit node knows sender? | No (mixnet hides identity) | No (onion routing) | Yes (VPN provider knows) |
| Timing analysis resistance | Strong (mixing, cover traffic) | Weak (low-latency) | None |
| UDP support | Yes | No (TCP only) | Yes |
Examples
Runnable examples in smolmix/core/examples/ (opens in a new tab). Each is self-contained; read the //! doc comments at the top of each file for a walkthrough.
cargo run -p smolmix --example <name>All examples accept --ipr <ADDRESS> to target a specific exit node (pass a Recipient address to Tunnel::builder().ipr_address()).
| Example | Source | What it demonstrates |
|---|---|---|
| UDP | udp.rs (opens in a new tab) | DNS lookup via hickory-proto (opens in a new tab), sending a raw UDP query to 1.1.1.1:53 through the mixnet. Runs a clearnet hickory-resolver (opens in a new tab) lookup alongside it to compare resolved IPs and latency |
| TCP | tcp.rs (opens in a new tab) | HTTPS request via hyper (opens in a new tab) + tokio-rustls (opens in a new tab). Fetches Cloudflare's /cdn-cgi/trace to show that the exit IP differs from clearnet |
| WebSocket | websocket.rs (opens in a new tab) | WebSocket echo against ws.postman-echo.com via tokio-tungstenite (opens in a new tab) + tokio-rustls (opens in a new tab). Runs the same stack over clearnet first, so the only thing that changes between runs is the underlying TCP stream |
| TCP download | tcp_download.rs (opens in a new tab) | DNS-over-mixnet + multi-request HTTP/1.1 download over a single keep-alive connection, the full real-world pattern |
Architecture
The internal stack (smoltcp reactor, device adapter, bridge task, packet flow) is documented in ARCHITECTURE.md (opens in a new tab). This is also the crate-level documentation on docs.rs.
API reference
Full API documentation is available on docs.rs/smolmix (opens in a new tab).