7ceaf9a40e
* Add mixtcp crate
Components:
- NymIprDevice: smoltcp::phy::Device impl using channel-based I/O
- NymIprBridge: async task bridging the device to IpMixStream
- create_device(): helper to set up the complete stack
* - Cleanup
- Add graceful shutdown
- Declutter logging - move a lot of bridge info! -> trace!
- Move rustls, nym-bin-common, bytes to dev-dependencies
- Extract TlsOverTcp to mod.rs
- Make timing more granular
- Update readme
* Add UDP example
* Add UDP example to readme
* rename mixtcp -> smolmix
* Add Tunnel API with TcpStream and UdpSocket over tokio-smoltcp
* Re-export Tunnel API and add init_logging convenience function
* Remove raw smoltcp path, flatten tunnel module
* Clean up bridge, device, and tunnel code
* 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
* Update smolmix imports for ipr_wrapper API
- stream_wrapper::{IpMixStream, NetworkEnvironment} → ipr_wrapper::
- connect_tunnel() → check_connected()
- disconnect_stream() → disconnect()
- allocated_ips() returns &IpPair directly (no Option)
* Add Tunnel::new_with_ipr, re-export IpPair/Recipient, tidy examples
- Add Tunnel::new_with_ipr() for targeting a specific exit node
- Re-export IpPair and Recipient so users don't need direct deps
- Add DNS leak warning to WebSocket example
- Await hyper connection task in HTTPS example
* Restructure smolmix into multi-crate workspace
- Move core tunnel code to smolmix/core/- Rewrite examples for each crate with clearnet/mixnet comparisons
* Add workspace README with architecture overview
* Update nym-sdk README module descriptions
- Replace stale stream_wrapper description with ipr_wrapper + mixnet::stream
- Remove TODO comment
* Remove companion crates, scope to smolmix-core
* Comment out additional components on -core branch README.md
* Cargo.lock fix for compilation issue
* Downgrade accidentally bumped dependencies in Cargo lock + change
smolmix dependencies to import from workspace
* Fix workspace deps + move nym-bin-common to dev-deps
* PR review changes + fix Sink delegation
* Fix borked merge + update README.md
* Fix up stale docs + rewrite examples to use proper imports and timing
logs
* Update readmes + architecture file
* Impl Drop for BridgeShutdownHandle + update comment
132 lines
4.6 KiB
Rust
132 lines
4.6 KiB
Rust
//! 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.
|
|
//!
|
|
//! Run with:
|
|
//! cargo run -p smolmix --example tcp
|
|
//! cargo run -p smolmix --example tcp -- --ipr <IPR_ADDRESS>
|
|
|
|
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::Tunnel;
|
|
use tokio_rustls::TlsConnector;
|
|
use tracing::info;
|
|
|
|
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
|
|
|
const HOST: &str = "cloudflare.com";
|
|
const PATH: &str = "/cdn-cgi/trace";
|
|
|
|
fn tls_connector() -> 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();
|
|
TlsConnector::from(Arc::new(config))
|
|
}
|
|
|
|
#[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");
|
|
|
|
// 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: smolmix TCP -> tokio-rustls -> hyper
|
|
let args: Vec<String> = std::env::args().collect();
|
|
let ipr_addr = args
|
|
.iter()
|
|
.position(|a| a == "--ipr")
|
|
.and_then(|i| args.get(i + 1));
|
|
|
|
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?;
|
|
|
|
// Phase 1: Setup (TCP + TLS + HTTP handshakes)
|
|
let setup_start = tokio::time::Instant::now();
|
|
|
|
info!("TCP connecting to 1.1.1.1:443 via mixnet...");
|
|
let tcp = tunnel.tcp_connect("1.1.1.1:443".parse()?).await?;
|
|
info!("TCP connected ({:?})", setup_start.elapsed());
|
|
|
|
info!("TLS handshake...");
|
|
let connector = tls_connector();
|
|
let domain = ServerName::try_from(HOST)?.to_owned();
|
|
let tls = connector.connect(domain, tcp).await?;
|
|
info!("TLS established ({:?})", setup_start.elapsed());
|
|
|
|
info!("HTTP/1.1 handshake...");
|
|
let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(tls)).await?;
|
|
tokio::spawn(conn);
|
|
|
|
let setup_duration = setup_start.elapsed();
|
|
info!("Setup complete ({:?})", setup_duration);
|
|
|
|
// Phase 2: Request/response
|
|
let request_start = tokio::time::Instant::now();
|
|
|
|
info!("Sending GET {PATH}...");
|
|
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 request_duration = request_start.elapsed();
|
|
info!(
|
|
"Response: {} ({} bytes, {:?})",
|
|
mixnet_status,
|
|
body_bytes.len(),
|
|
request_duration
|
|
);
|
|
|
|
// Results
|
|
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: {} in {:?}", clearnet_status, clearnet_duration);
|
|
info!(
|
|
"Mixnet: {} (setup {:?} + request {:?} = {:?})",
|
|
mixnet_status,
|
|
setup_duration,
|
|
request_duration,
|
|
setup_duration + request_duration
|
|
);
|
|
info!("Clearnet IP: {}", clearnet_ip.unwrap_or("?"));
|
|
info!("Mixnet IP: {}", mixnet_ip.unwrap_or("?"));
|
|
|
|
let total = setup_duration + request_duration;
|
|
let slowdown = total.as_millis() as f64 / clearnet_duration.as_millis().max(1) as f64;
|
|
info!(
|
|
"Slowdown: {slowdown:.1}x (setup: {:.1}x, request: {:.1}x)",
|
|
setup_duration.as_millis() as f64 / clearnet_duration.as_millis().max(1) as f64,
|
|
request_duration.as_millis() as f64 / clearnet_duration.as_millis().max(1) as f64
|
|
);
|
|
|
|
tunnel.shutdown().await;
|
|
Ok(())
|
|
}
|