Files
nym/smolmix/core/src/ARCHITECTURE.md
T
mfahampshire 7ceaf9a40e Max/mixtcp (#6321)
* Add mixtcp crate 

Components:
- NymIprDevice: smoltcp::phy::Device impl using channel-based I/O
- NymIprBridge: async task bridging the device to IpMixStream
- create_device(): helper to set up the complete stack

* - Cleanup
- Add graceful shutdown
- Declutter logging - move a lot of bridge info! -> trace!
- Move rustls, nym-bin-common, bytes to dev-dependencies
- Extract TlsOverTcp to mod.rs
- Make timing more granular
- Update readme

* Add UDP example

* Add UDP example to readme

* rename mixtcp -> smolmix

* Add Tunnel API with TcpStream and UdpSocket over tokio-smoltcp

* Re-export Tunnel API and add init_logging convenience function

* Remove raw smoltcp path, flatten tunnel module

* Clean up bridge, device, and tunnel code

* Consolidate architecture docs, tidy examples and README

- Add src/ARCHITECTURE.md as single source of truth for architecture
- Include in docs.rs via doc = include_str!
- Strip duplicated diagrams from tunnel.rs, device.rs, README
- Extract tls_connector() helper in HTTPS example to match websocket example
- Use consistent 'smolmix' casing in README

* Update smolmix imports for ipr_wrapper API

- stream_wrapper::{IpMixStream, NetworkEnvironment} → ipr_wrapper::
- connect_tunnel() → check_connected()
- disconnect_stream() → disconnect()
- allocated_ips() returns &IpPair directly (no Option)

* Add Tunnel::new_with_ipr, re-export IpPair/Recipient, tidy examples

- Add Tunnel::new_with_ipr() for targeting a specific exit node
- Re-export IpPair and Recipient so users don't need direct deps
- Add DNS leak warning to WebSocket example
- Await hyper connection task in HTTPS example

* Restructure smolmix into multi-crate workspace

- Move core tunnel code to smolmix/core/- Rewrite examples for each crate with clearnet/mixnet comparisons

* Add workspace README with architecture overview

* Update nym-sdk README module descriptions

- Replace stale stream_wrapper description with ipr_wrapper + mixnet::stream
- Remove TODO comment

* Remove companion crates, scope to smolmix-core

* Comment out additional components on -core branch README.md

* Cargo.lock fix for compilation issue

* Downgrade accidentally bumped dependencies in Cargo lock + change
smolmix dependencies to import from workspace

* Fix workspace deps + move nym-bin-common to dev-deps

* PR review changes + fix Sink delegation

* Fix borked merge + update README.md

* Fix up stale docs + rewrite examples to use proper imports and timing
logs

* Update readmes + architecture file

* Impl Drop for BridgeShutdownHandle + update comment
2026-04-14 20:13:12 +00:00

4.5 KiB

Architecture

smolmix is a TCP/UDP tunnel over the Nym mixnet. It gives you standard TcpStream and UdpSocket types that work transparently with the async Rust ecosystem (tokio-rustls, hyper, tokio-tungstenite, etc.) while routing all traffic through the mixnet for metadata privacy.

Stack

┌─────────────────────────────────────────────────────────────────┐
│  User code                                                      │
│  tunnel.tcp_connect() → TcpStream (AsyncRead + AsyncWrite)      │
│  tunnel.udp_socket()  → UdpSocket (send_to / recv_from)         │
├─────────────────────────────────────────────────────────────────┤
│  tokio-smoltcp::Net                                             │
│  Owns the smoltcp Interface + SocketSet + async poll loop.      │
│  Manages TCP state machines, retransmits, port allocation.      │
├─────────────────────────────────────────────────────────────────┤
│  NymAsyncDevice  (device.rs)                                    │
│  Stream + Sink adapter for raw IP packets over mpsc channels.   │
├─────────────────────────────────────────────────────────────────┤
│  NymIprBridge  (bridge.rs)                                      │
│  Background task shuttling packets between channels and the     │
│  mixnet. Bundles outgoing packets with MultiIpPacketCodec       │
│  (required by the IPR protocol).                                │
├─────────────────────────────────────────────────────────────────┤
│  IpMixStream → MixnetClient → Nym mixnet → IPR exit node        │
└─────────────────────────────────────────────────────────────────┘

Data flow

outgoing: smoltcp → NymAsyncDevice (Sink) → channel → NymIprBridge → IpMixStream → mixnet
incoming: mixnet → IpMixStream → NymIprBridge → channel → NymAsyncDevice (Stream) → smoltcp

tokio-smoltcp handles all the hard parts (smoltcp polling, TCP state machines, port allocation, waker management). We just give it a device that produces and consumes raw IP packets — NymAsyncDevice wraps the mpsc channel ends in the Stream/Sink traits that tokio-smoltcp requires.

Key design decisions

  • Single async device adapter. All traffic flows through one NymAsyncDevice. If you need a new transport type (e.g. ICMP), add a method to Tunnel rather than introducing a separate device — the device and bridge don't need to change. smoltcp already supports ICMP sockets; you'd enable the socket-icmp feature in Cargo.toml, add a method like Tunnel::icmp_socket() that calls the appropriate Net method, and expose the socket type via a re-export in lib.rs.

  • Tokio-only. The bridge, SDK (IpMixStream, MixnetClient), and shutdown signaling are tokio-based. The data-plane channels use futures::channel::mpsc because UnboundedSender implements Sink — required by tokio-smoltcp's AsyncDevice trait. An earlier version had a sync smoltcp Device adapter for use without tokio-smoltcp, but it still required a tokio runtime underneath (for the bridge and SDK), so it provided no real runtime independence — just duplicated the bridging logic. If alternative-runtime support is ever needed, it would require swapping out the bridge, SDK, and channel layers — a separate crate, not a feature flag on this one.

  • Unbounded channels. The channels between the device and bridge are unbounded. Backpressure is handled at the mixnet layer (IPR protocol), not at the channel level. If that assumption changes, consider bounded channels with a drop policy.

  • Medium::Ip (no Ethernet framing). Raw IP packets go in and out, matching what the IPR protocol expects.