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
This commit is contained in:
mfahampshire
2026-03-20 15:04:48 +00:00
parent df8e55960a
commit 754603f739
3 changed files with 80 additions and 10 deletions
+10 -7
View File
@@ -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<dyn std::error::Error + Send + Sync>;
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?;
+2 -3
View File
@@ -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 ---
+68
View File
@@ -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.