Files
nym/smolmix/examples/tunnel_https.rs
T
mfahampshire 754603f739 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
2026-04-29 10:28:04 +01:00

100 lines
3.6 KiB
Rust

//! HTTPS GET through the Nym mixnet using the Tunnel API.
//!
//! Fetches Cloudflare's `/cdn-cgi/trace` endpoint over clearnet (reqwest) and
//! through the mixnet (hyper over tokio-rustls over smolmix), then compares
//! responses and timing.
//!
//! Run with:
//! cargo run --example tunnel_https
use std::sync::Arc;
use http_body_util::BodyExt;
use hyper::body::Bytes;
use hyper::Request;
use hyper_util::rt::TokioIo;
use rustls::pki_types::ServerName;
use smolmix::{NetworkEnvironment, Tunnel};
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();
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
let host = "cloudflare.com";
let path = "/cdn-cgi/trace";
// --- Clearnet baseline via reqwest ---
info!("Fetching via clearnet...");
let clearnet_start = tokio::time::Instant::now();
let clearnet_resp = reqwest::get(format!("https://{host}{path}")).await?;
let clearnet_status = clearnet_resp.status();
let clearnet_body = clearnet_resp.text().await?;
let clearnet_duration = clearnet_start.elapsed();
info!("Clearnet: {} in {:?}", clearnet_status, clearnet_duration);
// --- Mixnet via tunnel + tokio-rustls + hyper ---
let tunnel = Tunnel::new(NetworkEnvironment::Mainnet).await?;
let mixnet_start = tokio::time::Instant::now();
let tcp = tunnel.tcp_connect("1.1.1.1:443".parse()?).await?;
let connector = tls_connector();
let domain = ServerName::try_from(host)?.to_owned();
let tls = connector.connect(domain, tcp).await?;
// Hand the TLS stream to hyper for proper HTTP/1.1 handling.
let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(tls)).await?;
tokio::spawn(conn);
let req = Request::get(path)
.header("Host", host)
.body(http_body_util::Empty::<Bytes>::new())?;
let resp = sender.send_request(req).await?;
let mixnet_status = resp.status();
let body_bytes = resp.into_body().collect().await?.to_bytes();
let mixnet_body = String::from_utf8_lossy(&body_bytes);
let mixnet_duration = mixnet_start.elapsed();
// --- Compare ---
info!("=== Results ===");
info!("Clearnet: {} in {:?}", clearnet_status, clearnet_duration);
info!("Mixnet: {} in {:?}", mixnet_status, mixnet_duration);
info!("Status match: {}", clearnet_status == mixnet_status);
// Both should contain the same diagnostic fields.
let fields = ["fl=", "visit_scheme=https", "uag="];
for field in fields {
let clearnet_has = clearnet_body.contains(field);
let mixnet_has = mixnet_body.contains(field);
info!(" {field:<25} clearnet={clearnet_has} mixnet={mixnet_has}");
}
// The IP should differ (mixnet uses an exit node).
let clearnet_ip = clearnet_body.lines().find(|l| l.starts_with("ip="));
let mixnet_ip = mixnet_body.lines().find(|l| l.starts_with("ip="));
info!(" Clearnet IP: {}", clearnet_ip.unwrap_or("?"));
info!(" Mixnet IP: {}", mixnet_ip.unwrap_or("?"));
let slowdown = mixnet_duration.as_millis() as f64 / clearnet_duration.as_millis().max(1) as f64;
info!(" Slowdown: {slowdown:.1}x");
tunnel.shutdown().await;
Ok(())
}