Update license + add old tutorial code as examples
This commit is contained in:
Generated
+1
@@ -11167,6 +11167,7 @@ dependencies = [
|
||||
name = "smolmix"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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 <IPR_ADDRESS>
|
||||
//! ```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 <IPR_ADDRESS>
|
||||
//! ```
|
||||
|
||||
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<String> = 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}...");
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
|
||||
//! 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 <IPR_ADDRESS>
|
||||
//! ```
|
||||
|
||||
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<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
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<String> = 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::<Bytes>::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<Ipv4Addr, BoxError> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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 <IPR_ADDRESS>
|
||||
//! ```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 <IPR_ADDRESS>
|
||||
//! ```
|
||||
|
||||
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<String> = 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?;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
|
||||
//! 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 <IPR_ADDRESS>
|
||||
//! ```
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use hickory_proto::op::{Message, Query};
|
||||
use hickory_proto::rr::{Name, RData, RecordType};
|
||||
use smolmix::Tunnel;
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
/// 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<String> = 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<Ipv4Addr, BoxError> {
|
||||
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)
|
||||
}
|
||||
@@ -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 <IPR_ADDRESS>
|
||||
//! ## 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 <IPR_ADDRESS>
|
||||
//! ```
|
||||
|
||||
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<String> = 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?;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
//! Async device adapter for tokio-smoltcp.
|
||||
//!
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
#![doc = include_str!("ARCHITECTURE.md")]
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
//! High-level tunnel providing TCP and UDP sockets over the Nym mixnet.
|
||||
//!
|
||||
|
||||
Reference in New Issue
Block a user