diff --git a/smolmix/examples/tunnel_https.rs b/smolmix/examples/tunnel_https.rs index 2a373001c5..fd80126095 100644 --- a/smolmix/examples/tunnel_https.rs +++ b/smolmix/examples/tunnel_https.rs @@ -15,11 +15,19 @@ use hyper::Request; use hyper_util::rt::TokioIo; use rustls::pki_types::ServerName; use smolmix::{NetworkEnvironment, Tunnel}; -use tokio_rustls::TlsConnector; use tracing::info; type BoxError = Box; +fn tls_connector() -> tokio_rustls::TlsConnector { + let mut root_store = rustls::RootCertStore::empty(); + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + tokio_rustls::TlsConnector::from(Arc::new(config)) +} + #[tokio::main] async fn main() -> Result<(), BoxError> { smolmix::init_logging(); @@ -45,12 +53,7 @@ async fn main() -> Result<(), BoxError> { let mixnet_start = tokio::time::Instant::now(); let tcp = tunnel.tcp_connect("1.1.1.1:443".parse()?).await?; - let mut root_store = rustls::RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let tls_config = rustls::ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - let connector = TlsConnector::from(Arc::new(tls_config)); + let connector = tls_connector(); let domain = ServerName::try_from(host)?.to_owned(); let tls = connector.connect(domain, tcp).await?; diff --git a/smolmix/examples/tunnel_websocket.rs b/smolmix/examples/tunnel_websocket.rs index 587ab2d0c4..ba6230b87b 100644 --- a/smolmix/examples/tunnel_websocket.rs +++ b/smolmix/examples/tunnel_websocket.rs @@ -31,8 +31,7 @@ const WS_HOST: &str = "ws.postman-echo.com"; const WS_PATH: &str = "/raw"; const ECHO_MSG: &str = "Hello from the Nym mixnet!"; -/// Build a reusable TLS config with the system root certificates. -fn tls_config() -> tokio_rustls::TlsConnector { +fn tls_connector() -> tokio_rustls::TlsConnector { let mut root_store = rustls::RootCertStore::empty(); root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); let config = rustls::ClientConfig::builder() @@ -55,7 +54,7 @@ async fn main() -> Result<(), BoxError> { .ok_or("DNS resolution failed")?; info!("Resolved {WS_HOST} -> {addr}"); - let connector = tls_config(); + let connector = tls_connector(); let domain = ServerName::try_from(WS_HOST)?.to_owned(); // --- Clearnet baseline: tokio TCP → rustls → tungstenite --- diff --git a/smolmix/src/ARCHITECTURE.md b/smolmix/src/ARCHITECTURE.md new file mode 100644 index 0000000000..e76b9b13af --- /dev/null +++ b/smolmix/src/ARCHITECTURE.md @@ -0,0 +1,68 @@ +# 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 + +```text +┌─────────────────────────────────────────────────────────────────┐ +│ 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 + +```text +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 channels + are all tokio-based. 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.