From 144617f5e1dba87b0f9506d14fb24671db576c7f Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Sun, 26 Apr 2026 16:35:41 +0100 Subject: [PATCH] Update license + add old tutorial code as examples --- Cargo.lock | 1 + smolmix/core/Cargo.toml | 1 + smolmix/core/examples/tcp.rs | 48 +++++-- smolmix/core/examples/tcp_download.rs | 186 ++++++++++++++++++++++++++ smolmix/core/examples/udp.rs | 37 ++++- smolmix/core/examples/udp_multi.rs | 182 +++++++++++++++++++++++++ smolmix/core/examples/websocket.rs | 39 ++++-- smolmix/core/src/device.rs | 1 - smolmix/core/src/error.rs | 1 - smolmix/core/src/lib.rs | 1 - smolmix/core/src/tunnel.rs | 1 - 11 files changed, 464 insertions(+), 34 deletions(-) create mode 100644 smolmix/core/examples/tcp_download.rs create mode 100644 smolmix/core/examples/udp_multi.rs diff --git a/Cargo.lock b/Cargo.lock index 970150dff0..863f51d8f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11167,6 +11167,7 @@ dependencies = [ name = "smolmix" version = "0.0.1" dependencies = [ + "chrono", "futures", "hickory-proto", "hickory-resolver", diff --git a/smolmix/core/Cargo.toml b/smolmix/core/Cargo.toml index 4bc26bfb13..a33265b542 100644 --- a/smolmix/core/Cargo.toml +++ b/smolmix/core/Cargo.toml @@ -42,3 +42,4 @@ hyper = { workspace = true, features = ["client", "http1"] } hyper-util = { workspace = true, features = ["tokio"] } http-body-util = { workspace = true } reqwest = { workspace = true, features = ["rustls"] } +chrono = { workspace = true } diff --git a/smolmix/core/examples/tcp.rs b/smolmix/core/examples/tcp.rs index 54b31c7885..ad187f35e4 100644 --- a/smolmix/core/examples/tcp.rs +++ b/smolmix/core/examples/tcp.rs @@ -1,16 +1,34 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 //! HTTPS request through the Nym mixnet. //! -//! Fetches Cloudflare's `/cdn-cgi/trace` diagnostic endpoint over clearnet -//! (reqwest) and through the mixnet (hyper over tokio-rustls over smolmix), -//! then compares the responses. The exit IP should differ — the mixnet path -//! exits through an IPR gateway. +//! Fetches Cloudflare's `/cdn-cgi/trace` over clearnet (reqwest) and through +//! the mixnet (hyper over tokio-rustls over smolmix), then compares the exit +//! IPs. The mixnet path should show a different IP — traffic exits through +//! an IPR (Internet Packet Router) gateway, not your machine. //! -//! Run with: -//! cargo run -p smolmix --example tcp -//! cargo run -p smolmix --example tcp -- --ipr +//! ```text +//! hyper (HTTP/1.1 client) +//! └─ tokio-rustls (TLS encryption) +//! └─ smolmix::TcpStream (TCP over mixnet) +//! └─ smoltcp (userspace TCP/IP) +//! └─ Nym mixnet → IPR exit gateway → internet +//! ``` +//! +//! ## What this demonstrates +//! +//! - Creating a [`Tunnel`] and connecting TCP through the mixnet +//! - Layering TLS ([`tokio_rustls`]) on a [`smolmix::TcpStream`] — it +//! implements `AsyncRead + AsyncWrite`, so standard crates work unchanged +//! - Using [`hyper`]'s HTTP/1.1 client over a custom transport via +//! [`TokioIo`](hyper_util::rt::TokioIo) +//! - The exit IP differs from clearnet — the remote server sees the IPR +//! gateway's IP, not yours +//! +//! ```sh +//! cargo run -p smolmix --example tcp +//! cargo run -p smolmix --example tcp -- --ipr +//! ``` use std::sync::Arc; @@ -53,7 +71,10 @@ async fn main() -> Result<(), BoxError> { let clearnet_duration = clearnet_start.elapsed(); info!("Clearnet: {} in {:?}", clearnet_status, clearnet_duration); - // Mixnet: smolmix TCP -> tokio-rustls -> hyper + // -- Mixnet path -- + // Create a tunnel, then stack the same TLS + HTTP layers on top. + // The only difference: smolmix::TcpStream instead of tokio::net::TcpStream. + let args: Vec = std::env::args().collect(); let ipr_addr = args .iter() @@ -66,7 +87,10 @@ async fn main() -> Result<(), BoxError> { } let tunnel = builder.build().await?; - // Phase 1: Setup (TCP + TLS + HTTP handshakes) + // Stage 1: TCP + TLS + HTTP handshakes through the mixnet. + // tcp_connect() returns a TcpStream that implements AsyncRead + AsyncWrite. + // tokio-rustls accepts it directly — no adapters or trait shims needed. + // TokioIo then bridges hyper's I/O traits with tokio's. let setup_start = tokio::time::Instant::now(); info!("TCP connecting to 1.1.1.1:443 via mixnet..."); @@ -86,7 +110,9 @@ async fn main() -> Result<(), BoxError> { let setup_duration = setup_start.elapsed(); info!("Setup complete ({:?})", setup_duration); - // Phase 2: Request/response + // Stage 2: Send request and read response. + // From here the code is identical to any hyper client — the mixnet + // transport is invisible to higher layers. let request_start = tokio::time::Instant::now(); info!("Sending GET {PATH}..."); diff --git a/smolmix/core/examples/tcp_download.rs b/smolmix/core/examples/tcp_download.rs new file mode 100644 index 0000000000..e3f9685ed5 --- /dev/null +++ b/smolmix/core/examples/tcp_download.rs @@ -0,0 +1,186 @@ +// Copyright 2024-2026 - Nym Technologies SA + +//! Multi-request HTTPS download through the Nym mixnet. +//! +//! Resolves a hostname via mixnet UDP DNS, then makes multiple HTTP/1.1 +//! requests over a single keep-alive TCP+TLS connection — all routed +//! through the mixnet. Demonstrates DNS resolution, connection reuse, +//! and progress feedback. +//! +//! ```text +//! hyper (HTTP/1.1 client, keep-alive) +//! └─ tokio-rustls (TLS encryption) +//! └─ smolmix::TcpStream (TCP over mixnet) +//! └─ smoltcp (userspace TCP/IP) +//! └─ Nym mixnet → IPR exit gateway → internet +//! ``` +//! +//! ## What this demonstrates +//! +//! - DNS resolution via mixnet UDP (avoids clearnet DNS leaks) +//! - TCP + TLS connection to the resolved IP +//! - HTTP/1.1 keep-alive: multiple requests over one mixnet connection +//! - Progress spinner during downloads +//! +//! Compare with `tcp.rs` which does a single request with clearnet comparison. +//! +//! ```sh +//! cargo run -p smolmix --example tcp_download +//! cargo run -p smolmix --example tcp_download -- --ipr +//! ``` + +use std::net::Ipv4Addr; +use std::sync::Arc; + +use hickory_proto::op::{Message, Query}; +use hickory_proto::rr::{Name, RData, RecordType}; +use http_body_util::{BodyExt, Empty}; +use hyper::body::Bytes; +use hyper::client::conn::http1; +use hyper_util::rt::TokioIo; +use rustls::pki_types::ServerName; +use smolmix::Tunnel; + +type BoxError = Box; + +const HOST: &str = "httpbin.org"; + +/// Sizes (in bytes) to download sequentially over one connection. +const SIZES: &[usize] = &[100, 1_000, 10_000]; + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + nym_bin_common::logging::setup_tracing_logger(); + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rustls crypto provider"); + + let args: Vec = std::env::args().collect(); + let ipr_addr = args + .iter() + .position(|a| a == "--ipr") + .and_then(|i| args.get(i + 1)); + + // Create the tunnel + let mut builder = Tunnel::builder(); + if let Some(addr) = ipr_addr { + builder = builder.ipr_address(addr.parse().expect("invalid IPR address")); + } + let tunnel = builder.build().await?; + println!( + "Tunnel ready — allocated IP: {}", + tunnel.allocated_ips().ipv4 + ); + + // DNS resolution via mixnet UDP + // We use hickory-proto to send a raw DNS query through the tunnel's + // UdpSocket, so even the DNS lookup is hidden from your ISP. + let ip = resolve_dns(&tunnel, HOST).await?; + println!("Resolved {HOST} → {ip} (via mixnet DNS)"); + + // TCP + TLS through the mixnet + println!("Connecting to {HOST}:443..."); + let tcp = tunnel.tcp_connect((ip, 443).into()).await?; + println!("TCP connected to {ip}:443 via mixnet"); + + 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 = tokio_rustls::TlsConnector::from(Arc::new(tls_config)); + let domain = ServerName::try_from(HOST)?.to_owned(); + let tls = connector.connect(domain, tcp).await?; + println!("TLS established with {HOST}"); + + // HTTP/1.1 connection (reused for all requests) + let io = TokioIo::new(tls); + let (mut sender, conn) = http1::handshake(io).await?; + tokio::spawn(conn); + + // Multiple requests over the same connection + let total = SIZES.len(); + println!("\nSending {total} requests over one connection...\n"); + let overall = std::time::Instant::now(); + let mut total_bytes = 0usize; + + for (i, &size) in SIZES.iter().enumerate() { + let seq = i + 1; + let start = std::time::Instant::now(); + + let req = hyper::Request::get(format!("/bytes/{size}")) + .header("Host", HOST) + .body(Empty::::new())?; + + let spinner = tokio::spawn(async move { + let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + let mut i = 0; + loop { + eprint!( + "\r [{seq}/{total}] GET /bytes/{size:<5} {}", + frames[i % frames.len()] + ); + i += 1; + tokio::time::sleep(std::time::Duration::from_millis(80)).await; + } + }); + + let resp = sender.send_request(req).await?; + let status = resp.status(); + let body = resp.into_body().collect().await?.to_bytes(); + let elapsed = start.elapsed(); + spinner.abort(); + + let speed = body.len() as f64 / elapsed.as_secs_f64(); + eprintln!( + "\r [{seq}/{total}] GET /bytes/{size:<5} → {status} {} in {elapsed:.1?} ({}/s) ", + format_bytes(body.len() as u64), + format_bytes(speed as u64), + ); + total_bytes += body.len(); + } + + let elapsed = overall.elapsed(); + println!( + "\nDone! {} in {total} requests over {elapsed:.1?}", + format_bytes(total_bytes as u64), + ); + + tunnel.shutdown().await; + Ok(()) +} + +/// Resolve a hostname to an IPv4 address via mixnet UDP DNS. +async fn resolve_dns(tunnel: &Tunnel, host: &str) -> Result { + let mut query = Message::new(); + query.set_recursion_desired(true); + query.add_query(Query::query(Name::from_ascii(host)?, RecordType::A)); + let query_bytes = query.to_vec()?; + + let udp = tunnel.udp_socket().await?; + udp.send_to(&query_bytes, "1.1.1.1:53".parse()?).await?; + + let mut buf = vec![0u8; 1500]; + let (n, _) = udp.recv_from(&mut buf).await?; + + let response = Message::from_vec(&buf[..n])?; + let ip = response + .answers() + .iter() + .find_map(|r| match r.data() { + RData::A(a) => Some(a.0), + _ => None, + }) + .ok_or("no A record in DNS response")?; + Ok(ip) +} + +fn format_bytes(n: u64) -> String { + if n >= 1_000_000 { + format!("{:.1} MB", n as f64 / 1_000_000.0) + } else if n >= 1_000 { + format!("{:.1} KB", n as f64 / 1_000.0) + } else { + format!("{n} B") + } +} diff --git a/smolmix/core/examples/udp.rs b/smolmix/core/examples/udp.rs index f934fad365..6dbca57572 100644 --- a/smolmix/core/examples/udp.rs +++ b/smolmix/core/examples/udp.rs @@ -1,12 +1,31 @@ //! DNS lookup through the Nym mixnet. //! //! Resolves `example.com` via clearnet (hickory-resolver) and via the mixnet -//! (hickory-proto UDP query to Cloudflare 1.1.1.1), then compares resolved -//! IPs and timing. +//! (hickory-proto raw UDP query to Cloudflare `1.1.1.1`), then compares +//! resolved IPs and timing. //! -//! Run with: -//! cargo run -p smolmix --example udp -//! cargo run -p smolmix --example udp -- --ipr +//! ```text +//! DNS query / response (application-layer UDP) +//! └─ smolmix::UdpSocket (UDP over mixnet) +//! └─ smoltcp (userspace IP stack) +//! └─ Nym mixnet → IPR exit gateway → internet +//! ``` +//! +//! ## What this demonstrates +//! +//! - Creating a [`Tunnel`] and using its [`UdpSocket`](smolmix::UdpSocket) +//! - The `send_to` / `recv_from` API matches [`tokio::net::UdpSocket`] +//! - Constructing a raw DNS query with [`hickory_proto`] and parsing the +//! response — standard crates work unchanged over smolmix UDP +//! - The DNS server sees the IPR gateway's IP, not yours +//! +//! For a more complete UDP example (multiple lookups + NTP time sync), see +//! the test tutorial in `test-tutorials/smolmix-udp/`. +//! +//! ```sh +//! cargo run -p smolmix --example udp +//! cargo run -p smolmix --example udp -- --ipr +//! ``` use std::net::Ipv4Addr; @@ -39,7 +58,10 @@ async fn main() -> Result<(), BoxError> { let clearnet_duration = clearnet_start.elapsed(); info!("Clearnet: {:?} in {:?}", clearnet_ips, clearnet_duration); - // Mixnet: hickory-proto query over smolmix UDP + // We use hickory-proto (not hickory-resolver) because we want to send + // the raw UDP query through the tunnel ourselves, rather than relying + // on the system resolver which would go over clearnet. + let args: Vec = std::env::args().collect(); let ipr_addr = args .iter() @@ -59,7 +81,8 @@ async fn main() -> Result<(), BoxError> { query.add_query(Query::query(Name::from_ascii(domain)?, RecordType::A)); let query_bytes = query.to_vec()?; - // UDP is connectionless — no setup phase, just send/recv + // Send the DNS query through the mixnet. + // UDP is connectionless — no handshake, just send_to / recv_from. info!("Sending DNS query via mixnet..."); let mixnet_start = tokio::time::Instant::now(); udp.send_to(&query_bytes, "1.1.1.1:53".parse()?).await?; diff --git a/smolmix/core/examples/udp_multi.rs b/smolmix/core/examples/udp_multi.rs new file mode 100644 index 0000000000..24b9e9cc80 --- /dev/null +++ b/smolmix/core/examples/udp_multi.rs @@ -0,0 +1,182 @@ +// Copyright 2024-2026 - Nym Technologies SA + +//! Multiple DNS lookups + NTP time sync through the Nym mixnet. +//! +//! Resolves several hostnames and syncs the clock via NTP — all over +//! mixnet UDP. Demonstrates timeout handling, socket reuse, and raw +//! protocol construction over smolmix's `UdpSocket`. +//! +//! ```text +//! DNS / NTP (application-layer UDP protocols) +//! └─ smolmix::UdpSocket (UDP over mixnet) +//! └─ smoltcp (userspace IP stack) +//! └─ Nym mixnet → IPR exit gateway → internet +//! ``` +//! +//! ## What this demonstrates +//! +//! - Multiple DNS queries over a single `UdpSocket` +//! - Timeout handling with `tokio::time::timeout` (essential for UDP) +//! - NTP time sync via a raw 48-byte UDP packet +//! - Converting NTP epoch (1900) to Unix epoch (1970) +//! +//! Compare with `udp.rs` which does a single DNS lookup with clearnet comparison. +//! +//! ```sh +//! cargo run -p smolmix --example udp_multi +//! cargo run -p smolmix --example udp_multi -- --ipr +//! ``` + +use std::net::Ipv4Addr; + +use hickory_proto::op::{Message, Query}; +use hickory_proto::rr::{Name, RData, RecordType}; +use smolmix::Tunnel; + +type BoxError = Box; + +/// Hostnames to resolve via mixnet DNS. +const DNS_TARGETS: &[&str] = &["example.com", "cloudflare.com", "nymtech.net"]; + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + nym_bin_common::logging::setup_tracing_logger(); + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rustls crypto provider"); + + let args: Vec = std::env::args().collect(); + let ipr_addr = args + .iter() + .position(|a| a == "--ipr") + .and_then(|i| args.get(i + 1)); + + // Stage 1: Create the tunnel + let mut builder = Tunnel::builder(); + if let Some(addr) = ipr_addr { + builder = builder.ipr_address(addr.parse().expect("invalid IPR address")); + } + let tunnel = builder.build().await?; + println!( + "Tunnel ready — allocated IP: {}", + tunnel.allocated_ips().ipv4 + ); + + // Stage 2: Multiple DNS lookups over one UdpSocket + // Each query goes to Cloudflare DNS (1.1.1.1:53) through the mixnet. + // The DNS server sees the IPR exit gateway's IP, not yours. + println!("\nPrivate DNS Lookups (via mixnet UDP)\n"); + + let udp = tunnel.udp_socket().await?; + + for host in DNS_TARGETS { + let start = std::time::Instant::now(); + + let mut query = Message::new(); + query.set_recursion_desired(true); + query.add_query(Query::query(Name::from_ascii(host)?, RecordType::A)); + let query_bytes = query.to_vec()?; + + udp.send_to(&query_bytes, "1.1.1.1:53".parse()?).await?; + + let mut buf = vec![0u8; 1500]; + let result = + tokio::time::timeout(std::time::Duration::from_secs(15), udp.recv_from(&mut buf)).await; + + match result { + Ok(Ok((n, _))) => { + let rtt = start.elapsed(); + let response = Message::from_vec(&buf[..n])?; + let ips: Vec<_> = response + .answers() + .iter() + .filter_map(|r| match r.data() { + RData::A(a) => Some(a.0.to_string()), + _ => None, + }) + .collect(); + println!("{host:<16} → {} (rtt: {rtt:.1?})", ips.join(", ")); + } + Ok(Err(e)) => println!("{host:<16} → ERROR: {e}"), + Err(_) => println!("{host:<16} → TIMEOUT"), + } + } + + // Stage 3: NTP time sync via mixnet UDP + // NTP uses a simple 48-byte request/response over UDP port 123. + // We first resolve pool.ntp.org via the mixnet, then send the NTP request. + println!("\nNTP Time Sync (via mixnet UDP)\n"); + + let ntp_ip = resolve_dns(&tunnel, "pool.ntp.org").await?; + println!("Resolved pool.ntp.org → {ntp_ip}"); + + // NTP request: 48 bytes, LI=0 Version=4 Mode=3 (client) + let mut ntp_req = [0u8; 48]; + ntp_req[0] = 0x23; + + let ntp_udp = tunnel.udp_socket().await?; + let start = std::time::Instant::now(); + let ntp_dest: std::net::SocketAddr = (ntp_ip, 123).into(); + ntp_udp.send_to(&ntp_req, ntp_dest).await?; + + let mut buf = [0u8; 48]; + let result = tokio::time::timeout( + std::time::Duration::from_secs(30), + ntp_udp.recv_from(&mut buf), + ) + .await; + + match result { + Ok(Ok((n, _))) if n >= 48 => { + let rtt = start.elapsed(); + + // Transmit timestamp at bytes 40..48 (seconds since 1900-01-01) + let secs = u32::from_be_bytes([buf[40], buf[41], buf[42], buf[43]]); + let frac = u32::from_be_bytes([buf[44], buf[45], buf[46], buf[47]]); + + // NTP epoch (1900) → Unix epoch (1970) + // Valid for Era 0 (until 2036-02-07); Era 1 wraps secs to 0. + const NTP_TO_UNIX: u64 = 2_208_988_800; + let unix_secs = secs as u64 - NTP_TO_UNIX; + let millis = (frac as u64 * 1000) >> 32; + + let dt = + chrono::DateTime::from_timestamp(unix_secs as i64, (millis * 1_000_000) as u32) + .expect("valid timestamp"); + println!("NTP response in {rtt:.1?}"); + println!("Unix timestamp: {unix_secs}.{millis:03}"); + println!("UTC: {}", dt.format("%Y-%m-%d %H:%M:%S%.3f UTC")); + } + Ok(Ok((n, _))) => println!("Short response: {n} bytes (expected 48)"), + Ok(Err(e)) => println!("ERROR: {e}"), + Err(_) => println!("TIMEOUT (30s)"), + } + + tunnel.shutdown().await; + Ok(()) +} + +/// Resolve a hostname to an IPv4 address via mixnet UDP DNS. +async fn resolve_dns(tunnel: &Tunnel, host: &str) -> Result { + let mut query = Message::new(); + query.set_recursion_desired(true); + query.add_query(Query::query(Name::from_ascii(host)?, RecordType::A)); + let query_bytes = query.to_vec()?; + + let udp = tunnel.udp_socket().await?; + udp.send_to(&query_bytes, "1.1.1.1:53".parse()?).await?; + + let mut buf = vec![0u8; 1500]; + let (n, _) = udp.recv_from(&mut buf).await?; + + let response = Message::from_vec(&buf[..n])?; + let ip = response + .answers() + .iter() + .find_map(|r| match r.data() { + RData::A(a) => Some(a.0), + _ => None, + }) + .ok_or("no A record in DNS response")?; + Ok(ip) +} diff --git a/smolmix/core/examples/websocket.rs b/smolmix/core/examples/websocket.rs index 449aaa30b9..f62bba4b96 100644 --- a/smolmix/core/examples/websocket.rs +++ b/smolmix/core/examples/websocket.rs @@ -1,11 +1,8 @@ //! WebSocket echo over the Nym mixnet. //! -//! Demonstrates stacking tokio-tungstenite on top of tokio-rustls on top of -//! smolmix TcpStream. Sends a message to a public echo server via clearnet -//! and via the mixnet, then compares responses and timing. -//! -//! The clearnet and mixnet paths use the *exact same* TLS + WebSocket stack — -//! only the underlying TCP transport differs: +//! Sends a message to a public echo server via clearnet and via the mixnet, +//! then compares responses and timing. The clearnet and mixnet paths use the +//! *exact same* TLS + WebSocket stack — only the TCP transport differs. //! //! ```text //! tokio-tungstenite (WebSocket framing) @@ -14,9 +11,19 @@ //! └─ smolmix::TcpStream (mixnet) //! ``` //! -//! Run with: -//! cargo run -p smolmix --example websocket -//! cargo run -p smolmix --example websocket -- --ipr +//! ## What this demonstrates +//! +//! - Composability: [`tokio_tungstenite::client_async`] accepts any +//! `AsyncRead + AsyncWrite` stream — it doesn't know or care that +//! TLS is backed by the mixnet rather than a kernel TCP socket +//! - The same `tls_connector()` and WebSocket upgrade code works for both +//! clearnet and mixnet — you only swap the underlying TCP stream +//! - The echo server sees the IPR gateway's IP, not yours +//! +//! ```sh +//! cargo run -p smolmix --example websocket +//! cargo run -p smolmix --example websocket -- --ipr +//! ``` use std::sync::Arc; @@ -75,7 +82,11 @@ async fn main() -> Result<(), BoxError> { info!("Clearnet: \"{clearnet_text}\" in {clearnet_duration:?}"); - // Mixnet: smolmix TCP -> rustls -> tungstenite (same stack) + // -- Mixnet path -- + // Exact same stack as clearnet, but over smolmix::TcpStream. + // This is the key composability point: swap the TCP transport + // and everything above it works unchanged. + let args: Vec = std::env::args().collect(); let ipr_addr = args .iter() @@ -89,7 +100,11 @@ async fn main() -> Result<(), BoxError> { let tunnel = builder.build().await?; info!("Allocated IP: {}", tunnel.allocated_ips().ipv4); - // Phase 1: Setup (TCP + TLS + WebSocket handshakes) + // Stage 1: TCP + TLS + WebSocket handshakes through the mixnet. + // Each layer only knows about the one directly below it: + // tungstenite thinks it's talking to a normal TLS stream + // rustls thinks it's talking to a normal TCP stream + // smolmix handles the mixnet routing transparently let setup_start = tokio::time::Instant::now(); info!("TCP connecting via mixnet..."); @@ -107,7 +122,7 @@ async fn main() -> Result<(), BoxError> { let setup_duration = setup_start.elapsed(); info!("Setup complete ({:?})", setup_duration); - // Phase 2: Echo request/response + // Stage 2: Send a message and verify the echo. let request_start = tokio::time::Instant::now(); mixnet_ws.send(Message::Text(ECHO_MSG.into())).await?; diff --git a/smolmix/core/src/device.rs b/smolmix/core/src/device.rs index 7258355003..1b564a5ab0 100644 --- a/smolmix/core/src/device.rs +++ b/smolmix/core/src/device.rs @@ -1,5 +1,4 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only //! Async device adapter for tokio-smoltcp. //! diff --git a/smolmix/core/src/error.rs b/smolmix/core/src/error.rs index 84ab5744fd..f901823e1a 100644 --- a/smolmix/core/src/error.rs +++ b/smolmix/core/src/error.rs @@ -1,5 +1,4 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only use thiserror::Error; diff --git a/smolmix/core/src/lib.rs b/smolmix/core/src/lib.rs index c46a3d310e..f28b53104f 100644 --- a/smolmix/core/src/lib.rs +++ b/smolmix/core/src/lib.rs @@ -1,5 +1,4 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only #![doc = include_str!("ARCHITECTURE.md")] diff --git a/smolmix/core/src/tunnel.rs b/smolmix/core/src/tunnel.rs index 6e2e33617b..0d70a6931d 100644 --- a/smolmix/core/src/tunnel.rs +++ b/smolmix/core/src/tunnel.rs @@ -1,5 +1,4 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only //! High-level tunnel providing TCP and UDP sockets over the Nym mixnet. //!