From 4eca683665a0fc2ce15a0bbaf08f36110cdea597 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Tue, 31 Mar 2026 14:15:33 +0100 Subject: [PATCH] Remove companion crates, scope to smolmix-core --- smolmix/README.md | 150 +---------------------- smolmix/dns/Cargo.toml | 17 --- smolmix/dns/README.md | 52 -------- smolmix/dns/examples/resolve.rs | 85 ------------- smolmix/dns/src/lib.rs | 109 ----------------- smolmix/dns/src/provider.rs | 96 --------------- smolmix/hyper/Cargo.toml | 25 ---- smolmix/hyper/README.md | 72 ----------- smolmix/hyper/examples/get.rs | 85 ------------- smolmix/hyper/examples/post.rs | 98 --------------- smolmix/hyper/src/connector.rs | 86 ------------- smolmix/hyper/src/lib.rs | 84 ------------- smolmix/hyper/src/tls_stream.rs | 67 ----------- smolmix/libp2p/Cargo.toml | 20 --- smolmix/libp2p/README.md | 79 ------------ smolmix/libp2p/examples/listener.rs | 60 --------- smolmix/libp2p/examples/ping.rs | 106 ---------------- smolmix/libp2p/src/lib.rs | 174 --------------------------- smolmix/tls/Cargo.toml | 11 -- smolmix/tls/README.md | 42 ------- smolmix/tls/src/lib.rs | 62 ---------- smolmix/tungstenite/Cargo.toml | 18 --- smolmix/tungstenite/README.md | 55 --------- smolmix/tungstenite/examples/echo.rs | 75 ------------ smolmix/tungstenite/src/lib.rs | 142 ---------------------- 25 files changed, 3 insertions(+), 1867 deletions(-) delete mode 100644 smolmix/dns/Cargo.toml delete mode 100644 smolmix/dns/README.md delete mode 100644 smolmix/dns/examples/resolve.rs delete mode 100644 smolmix/dns/src/lib.rs delete mode 100644 smolmix/dns/src/provider.rs delete mode 100644 smolmix/hyper/Cargo.toml delete mode 100644 smolmix/hyper/README.md delete mode 100644 smolmix/hyper/examples/get.rs delete mode 100644 smolmix/hyper/examples/post.rs delete mode 100644 smolmix/hyper/src/connector.rs delete mode 100644 smolmix/hyper/src/lib.rs delete mode 100644 smolmix/hyper/src/tls_stream.rs delete mode 100644 smolmix/libp2p/Cargo.toml delete mode 100644 smolmix/libp2p/README.md delete mode 100644 smolmix/libp2p/examples/listener.rs delete mode 100644 smolmix/libp2p/examples/ping.rs delete mode 100644 smolmix/libp2p/src/lib.rs delete mode 100644 smolmix/tls/Cargo.toml delete mode 100644 smolmix/tls/README.md delete mode 100644 smolmix/tls/src/lib.rs delete mode 100644 smolmix/tungstenite/Cargo.toml delete mode 100644 smolmix/tungstenite/README.md delete mode 100644 smolmix/tungstenite/examples/echo.rs delete mode 100644 smolmix/tungstenite/src/lib.rs diff --git a/smolmix/README.md b/smolmix/README.md index 0cb2e9bdb2..57205311d7 100644 --- a/smolmix/README.md +++ b/smolmix/README.md @@ -35,116 +35,6 @@ over a TCP stream works over smolmix — with zero adaptation. └──────────────────────────────────────────────────────────────────┘ ``` -Concrete example: the Nym SDK's libp2p integration (`libp2p_shared/`) is -~2400 lines — custom `Connection` (StreamMuxer), custom `Substream` -(AsyncRead/Write), nonce-based ordering, and a custom handshake. With -smolmix, the same integration is ~175 lines: parse a multiaddr, resolve DNS -through the tunnel, and call `tunnel.tcp_connect()`. libp2p's built-in noise -and yamux work natively because they only need `AsyncRead + AsyncWrite`. - -Each companion crate is a thin wrapper — typically under 200 lines — that -adapts an existing library to use tunnel I/O instead of OS sockets. The key -insight is that `tokio_smoltcp::TcpStream` implements tokio's `AsyncRead + -AsyncWrite`, so the only work needed is bridging to whatever I/O trait the -target library expects: - -| Crate | Lines | Trait implemented | I/O bridge | -|-------|------:|-------------------|------------| -| `smolmix-dns` | ~110 | hickory `RuntimeProvider` | `AsyncIoTokioAsStd` (tokio→futures) | -| `smolmix-tls` | ~60 | *(none — factory only)* | direct (tokio-rustls takes `AsyncRead/Write`) | -| `smolmix-hyper` | ~170 | `tower::Service` | `TokioIo` + `pin_project!` enum | -| `smolmix-tungstenite` | ~140 | *(none — composition)* | direct (tungstenite takes `AsyncRead/Write`) | -| `smolmix-libp2p` | ~175 | libp2p `Transport` | `Compat` (tokio→futures) | - -### smolmix-dns - -hickory-resolver's extension point is the `RuntimeProvider` trait — it -controls how the resolver creates TCP connections and UDP sockets. -`SmolmixRuntimeProvider` implements this trait, routing all I/O through the -tunnel: - -```text -RuntimeProvider::connect_tcp() → tunnel.tcp_connect() → AsyncIoTokioAsStd -RuntimeProvider::bind_udp() → tunnel.udp_socket() → SmolmixUdpSocket (newtype) -``` - -hickory expects `futures_io::AsyncRead/Write` for TCP, not tokio's version. -`AsyncIoTokioAsStd` (from hickory-proto) adapts between them — and because -hickory's `DnsTcpStream` has a blanket impl for any `futures_io::AsyncRead + -AsyncWrite`, the wrapped stream satisfies it automatically. Zero glue code. - -For UDP, `SmolmixUdpSocket` is a thin newtype over `tokio_smoltcp::UdpSocket` -that implements hickory's `DnsUdpSocket` — just delegates `poll_recv_from` -and `poll_send_to`, whose signatures already match. - -### smolmix-tls - -No trait adaptation needed. `tokio-rustls` works directly with anything that -implements tokio's `AsyncRead + AsyncWrite` — which `TcpStream` does. This -crate is pure configuration: build a `ClientConfig` with webpki roots, wrap -it in a `TlsConnector`, expose `connect()` and `connect_with()`. - -Re-exports `TlsStream` and `TlsConnector` so downstream crates don't need -`tokio-rustls` in their Cargo.toml. - -### smolmix-hyper - -hyper-util's `Client` uses the `tower::Service` trait to open -connections. `SmolmixConnector` implements this: given a URI, it resolves the -hostname, connects TCP, and optionally wraps in TLS. - -The interesting part is the return type. hyper needs a stream that implements -`hyper::rt::Read + Write + Connection`. `TokioIo` (from hyper-util) -provides this, but it needs a single type — not "sometimes TLS, sometimes -plain". `MaybeTlsStream` solves this with a two-variant enum and -`pin_project_lite` for safe pin projection through `AsyncRead`/`AsyncWrite`: - -```text -SmolmixConnector::call(uri) - → resolve + tcp_connect + optional TLS - → MaybeTlsStream::Plain { TcpStream } - or MaybeTlsStream::Tls { TlsStream } - → TokioIo (implements hyper's Read/Write/Connection) -``` - -### smolmix-tungstenite - -No new types or trait impls — pure function composition. -`tokio_tungstenite::client_async()` takes any `AsyncRead + AsyncWrite` -stream, and `TlsStream` qualifies. The `connect()` function -chains four steps: - -```text -connect(tunnel, request) - → smolmix_dns::resolve(host, port) DNS through tunnel - → tunnel.tcp_connect(addr) TCP through mixnet - → smolmix_tls::connect(tcp, host) TLS handshake - → client_async(request, tls_stream) WebSocket upgrade -``` - -### smolmix-libp2p - -libp2p's extension point is the `Transport` trait. `SmolmixTransport` -implements it for dial-only connections (no inbound — that would require IPR -listener support). - -libp2p uses `futures_io::AsyncRead/Write`, not tokio's. The bridge is -`tokio_util::compat::Compat` — called via `.compat()` on the TcpStream. -This is zero-cost trait delegation (no buffering, no copying): - -```text -SmolmixTransport::dial(multiaddr) - → parse /ip4/.../tcp/... or /dns4/.../tcp/... - → smolmix_dns::resolve() if hostname - → tunnel.tcp_connect(addr) - → tcp.compat() → Compat (futures_io AsyncRead/Write) -``` - -libp2p's built-in upgrade pipeline — noise (encryption) → yamux -(multiplexing) — works over any `futures_io::AsyncRead + AsyncWrite`, so -the standard `.upgrade(V1).authenticate(noise).multiplex(yamux)` chain -works without modification. - ## Quick start ```rust @@ -162,46 +52,12 @@ let udp = tunnel.udp_socket().await?; udp.send_to(&packet, "1.1.1.1:53".parse()?).await?; ``` -## Crates - -| Crate | What it provides | -|-------|-----------------| -| [`smolmix`](core/) | `Tunnel`, `TcpStream`, `UdpSocket` | -| [`smolmix-dns`](dns/) | `Resolver` newtype wrapping hickory-resolver | -| [`smolmix-tls`](tls/) | Shared `TlsConnector` and `connect()` with webpki roots | -| [`smolmix-hyper`](hyper/) | `Client` newtype wrapping hyper-util | -| [`smolmix-tungstenite`](tungstenite/) | `connect()` for WebSocket over TLS | -| [`smolmix-libp2p`](libp2p/) | `SmolmixTransport` implementing libp2p `Transport` | - -`smolmix-hyper` and `smolmix-tungstenite` depend on `smolmix-dns` (hostname -resolution through the tunnel) and `smolmix-tls` (shared TLS setup). - -```toml -[dependencies] -smolmix = { workspace = true } -smolmix-dns = { workspace = true } # DNS resolution -smolmix-tls = { workspace = true } # TLS (used by hyper + tungstenite) -smolmix-hyper = { workspace = true } # HTTP client -smolmix-tungstenite = { workspace = true } # WebSocket client -smolmix-libp2p = { workspace = true } # libp2p transport -``` - ## Examples -Each crate has its own examples with clearnet-vs-mixnet comparisons: - ```sh -cargo run -p smolmix --example tcp # raw TCP -cargo run -p smolmix --example udp # raw UDP -cargo run -p smolmix --example websocket # WebSocket over TLS -cargo run -p smolmix-dns --example resolve # DNS resolution -cargo run -p smolmix-hyper --example get # HTTPS GET -cargo run -p smolmix-hyper --example post # HTTP POST -cargo run -p smolmix-tungstenite --example echo # WebSocket echo - -# libp2p: start the clearnet listener, then dial through the mixnet -cargo run -p smolmix-libp2p --example listener -cargo run -p smolmix-libp2p --example ping -- +cargo run -p smolmix --example tcp # raw TCP +cargo run -p smolmix --example udp # raw UDP +cargo run -p smolmix --example websocket # WebSocket over TLS ``` ## Architecture diff --git a/smolmix/dns/Cargo.toml b/smolmix/dns/Cargo.toml deleted file mode 100644 index 9d9a26f1d8..0000000000 --- a/smolmix/dns/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "smolmix-dns" -version = "0.0.1" -edition = "2021" -license.workspace = true - -[dependencies] -smolmix = { workspace = true } -hickory-proto = { workspace = true, features = ["tokio"] } -hickory-resolver = { workspace = true, features = ["tokio"] } -tokio-smoltcp = "0.5" - -[dev-dependencies] -# Clearnet baseline in example -hickory-resolver = { workspace = true, features = ["tokio", "system-config"] } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } -tracing = { workspace = true } diff --git a/smolmix/dns/README.md b/smolmix/dns/README.md deleted file mode 100644 index a530192d50..0000000000 --- a/smolmix/dns/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# smolmix-dns - -DNS resolution through the Nym mixnet. Wraps [hickory-resolver](https://docs.rs/hickory-resolver) with a `Resolver` newtype that routes all DNS queries through a smolmix `Tunnel`, preventing hostname leaks to the local network. - -## Quick start - -```rust -use smolmix_dns::Resolver; - -let tunnel = smolmix::Tunnel::new().await?; -let resolver = Resolver::new(&tunnel); - -// Full hickory-resolver API via Deref: -let lookup = resolver.lookup_ip("example.com").await?; -for ip in lookup.iter() { println!("{ip}"); } - -// Convenience one-shot: -let addrs = resolver.resolve("example.com", 443).await?; -``` - -## API - -- **`Resolver::new(&tunnel)`** — Cloudflare upstream (`1.1.1.1`) -- **`Resolver::with_config(&tunnel, config)`** — custom upstream DNS -- **`Resolver::resolve(&self, host, port)`** — convenience one-shot returning `Vec` -- **`Deref` to hickory's `Resolver`** — full `lookup_ip()`, `lookup()`, etc. -- **`resolve(&tunnel, host, port)`** — free function for quick one-shots -- **`resolver(&tunnel)`** — free function returning a `Resolver` - -### Re-exports - -Commonly-used hickory types are re-exported so you don't need `hickory-resolver` in your `Cargo.toml`: - -- `ResolverConfig`, `LookupIp`, `ResolveError` - -## Example - -Clearnet-vs-mixnet DNS comparison with timing: - -```sh -cargo run -p smolmix-dns --example resolve -cargo run -p smolmix-dns --example resolve -- --ipr -``` - -## Dependencies - -```toml -[dependencies] -smolmix-dns = { workspace = true } -``` - -This crate depends on `smolmix` (for the `Tunnel` type), `hickory-proto`, and `hickory-resolver`. diff --git a/smolmix/dns/examples/resolve.rs b/smolmix/dns/examples/resolve.rs deleted file mode 100644 index 0ca5fa82e9..0000000000 --- a/smolmix/dns/examples/resolve.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! DNS resolution: clearnet vs Nym mixnet comparison. -//! -//! Resolves a hostname via clearnet (hickory-resolver) and via the mixnet -//! (smolmix-dns), then compares resolved IPs and timing. -//! -//! Run with: -//! cargo run -p smolmix-dns --example resolve -//! cargo run -p smolmix-dns --example resolve -- --ipr - -use std::net::Ipv4Addr; - -use hickory_resolver::TokioResolver; -use smolmix::{Recipient, Tunnel}; -use smolmix_dns::Resolver; -use tracing::info; - -type BoxError = Box; - -#[tokio::main] -async fn main() -> Result<(), BoxError> { - smolmix::init_logging(); - - let domain = "example.com"; - - // --- Clearnet baseline via hickory-resolver --- - info!("Clearnet DNS lookup for '{domain}'..."); - let clearnet_resolver = TokioResolver::builder_tokio()?.build(); - let clearnet_start = tokio::time::Instant::now(); - let lookup = clearnet_resolver.lookup_ip(domain).await?; - let clearnet_ips: Vec = lookup - .iter() - .filter_map(|ip| match ip { - std::net::IpAddr::V4(v4) => Some(v4), - _ => None, - }) - .collect(); - let clearnet_duration = clearnet_start.elapsed(); - info!("Clearnet: {:?} in {:?}", clearnet_ips, clearnet_duration); - - // --- Mixnet via smolmix-dns --- - let args: Vec = std::env::args().collect(); - let ipr_addr = args - .iter() - .position(|a| a == "--ipr") - .and_then(|i| args.get(i + 1)); - - let tunnel = if let Some(addr) = ipr_addr { - let recipient: Recipient = addr.parse().expect("invalid IPR address"); - Tunnel::new_with_ipr(recipient).await? - } else { - Tunnel::new().await? - }; - - let resolver = Resolver::new(&tunnel); - - // Full hickory API via Deref: - let mixnet_start = tokio::time::Instant::now(); - let lookup = resolver.lookup_ip(domain).await?; - let mixnet_ips: Vec = lookup - .iter() - .filter_map(|ip| match ip { - std::net::IpAddr::V4(v4) => Some(v4), - _ => None, - }) - .collect(); - let mixnet_duration = mixnet_start.elapsed(); - - // Convenience method for a second lookup: - let addrs = resolver.resolve("nymtech.net", 443).await?; - info!("Resolved nymtech.net:443 → {addrs:?}"); - - // --- Compare --- - info!("=== Results ==="); - info!("Clearnet: {:?} ({:?})", clearnet_ips, clearnet_duration); - info!("Mixnet: {:?} ({:?})", mixnet_ips, mixnet_duration); - - let ips_match = !mixnet_ips.is_empty() && mixnet_ips.iter().all(|ip| clearnet_ips.contains(ip)); - info!("IPs match: {ips_match}"); - - 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(()) -} diff --git a/smolmix/dns/src/lib.rs b/smolmix/dns/src/lib.rs deleted file mode 100644 index 979db915e1..0000000000 --- a/smolmix/dns/src/lib.rs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only - -//! DNS resolution through the Nym mixnet. -//! -//! This crate wraps [hickory-resolver] with a newtype [`Resolver`] that routes -//! all DNS traffic through a smolmix [`Tunnel`], ensuring hostnames are resolved -//! via the mixnet rather than leaking queries to the local network. -//! -//! # Quick start -//! -//! ```ignore -//! use smolmix_dns::Resolver; -//! -//! let tunnel = smolmix::Tunnel::new().await?; -//! let resolver = Resolver::new(&tunnel); -//! -//! // Full hickory API via Deref: -//! let lookup = resolver.lookup_ip("example.com").await?; -//! for ip in lookup.iter() { println!("{ip}"); } -//! -//! // Convenience one-shot: -//! let addrs = resolver.resolve("example.com", 443).await?; -//! ``` - -mod provider; - -use std::io; -use std::net::SocketAddr; -use std::ops::Deref; - -use hickory_resolver::name_server::GenericConnector; - -use hickory_proto::runtime::TokioHandle; -use smolmix::Tunnel; - -// Re-exports so users don't need hickory-resolver in their Cargo.toml -pub use hickory_resolver::config::ResolverConfig; -pub use hickory_resolver::lookup_ip::LookupIp; -pub use hickory_resolver::ResolveError; -pub use provider::SmolmixRuntimeProvider; - -/// Inner resolver type alias for readability. -type HickoryResolver = hickory_resolver::Resolver>; - -/// A DNS resolver that routes all queries through a smolmix [`Tunnel`]. -/// -/// Wraps a hickory-resolver `Resolver` and exposes its full API via [`Deref`]. -/// All DNS traffic (both TCP and UDP) travels through the mixnet. -pub struct Resolver { - inner: HickoryResolver, -} - -impl Resolver { - /// Create a resolver using Cloudflare (`1.1.1.1`) as upstream DNS. - pub fn new(tunnel: &Tunnel) -> Self { - Self::with_config(tunnel, ResolverConfig::cloudflare()) - } - - /// Create a resolver with a custom upstream DNS configuration. - pub fn with_config(tunnel: &Tunnel, config: ResolverConfig) -> Self { - let provider = SmolmixRuntimeProvider { - tunnel: tunnel.clone(), - handle: TokioHandle::default(), - }; - let connector = GenericConnector::new(provider); - Self { - inner: hickory_resolver::Resolver::builder_with_config(config, connector).build(), - } - } - - /// Resolve a hostname to socket addresses through the tunnel. - /// - /// Convenience method for one-shot lookups. Returns all resolved addresses - /// paired with the given `port`. - pub async fn resolve(&self, host: &str, port: u16) -> io::Result> { - let lookup = self - .inner - .lookup_ip(host) - .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - Ok(lookup.iter().map(|ip| SocketAddr::new(ip, port)).collect()) - } -} - -impl Deref for Resolver { - type Target = HickoryResolver; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -/// Create a hickory [`Resolver`] that routes all DNS through the tunnel. -/// -/// Uses Cloudflare (`1.1.1.1`) as the upstream DNS server. Equivalent to -/// [`Resolver::new()`]. -pub fn resolver(tunnel: &Tunnel) -> Resolver { - Resolver::new(tunnel) -} - -/// Resolve a hostname through the tunnel. -/// -/// Convenience wrapper for one-shot lookups. Equivalent to -/// `Resolver::new(tunnel).resolve(host, port)`. -pub async fn resolve(tunnel: &Tunnel, host: &str, port: u16) -> io::Result> { - let r = resolver(tunnel); - r.resolve(host, port).await -} diff --git a/smolmix/dns/src/provider.rs b/smolmix/dns/src/provider.rs deleted file mode 100644 index 60b0997203..0000000000 --- a/smolmix/dns/src/provider.rs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only - -//! hickory-resolver [`RuntimeProvider`] routing all DNS I/O through a [`Tunnel`]. - -use std::future::Future; -use std::io; -use std::net::SocketAddr; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use hickory_proto::runtime::iocompat::AsyncIoTokioAsStd; -use hickory_proto::runtime::{RuntimeProvider, TokioHandle, TokioTime}; -use hickory_proto::udp::DnsUdpSocket; - -use smolmix::Tunnel; - -/// UDP socket wrapper routing DNS queries through the tunnel. -/// -/// Thin newtype around [`tokio_smoltcp::UdpSocket`] implementing hickory's -/// [`DnsUdpSocket`] trait. The poll methods delegate directly since the -/// signatures are identical. -pub struct SmolmixUdpSocket(tokio_smoltcp::UdpSocket); - -impl DnsUdpSocket for SmolmixUdpSocket { - type Time = TokioTime; - - fn poll_recv_from( - &self, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - self.0.poll_recv_from(cx, buf) - } - - fn poll_send_to( - &self, - cx: &mut Context<'_>, - buf: &[u8], - target: SocketAddr, - ) -> Poll> { - self.0.poll_send_to(cx, buf, target) - } -} - -/// Runtime provider that routes all DNS I/O through a [`Tunnel`]. -/// -/// Implements hickory's [`RuntimeProvider`] so the resolver sends TCP and UDP DNS -/// traffic over the mixnet instead of the local network. -#[derive(Clone)] -pub struct SmolmixRuntimeProvider { - pub(crate) tunnel: Tunnel, - pub(crate) handle: TokioHandle, -} - -impl RuntimeProvider for SmolmixRuntimeProvider { - type Handle = TokioHandle; - type Timer = TokioTime; - type Udp = SmolmixUdpSocket; - type Tcp = AsyncIoTokioAsStd; - - fn create_handle(&self) -> Self::Handle { - self.handle.clone() - } - - fn connect_tcp( - &self, - server_addr: SocketAddr, - _bind_addr: Option, - _timeout: Option, - ) -> Pin>>> { - let tunnel = self.tunnel.clone(); - Box::pin(async move { - let tcp = tunnel - .tcp_connect(server_addr) - .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - Ok(AsyncIoTokioAsStd(tcp)) - }) - } - - fn bind_udp( - &self, - _local_addr: SocketAddr, - _server_addr: SocketAddr, - ) -> Pin>>> { - let tunnel = self.tunnel.clone(); - Box::pin(async move { - let udp = tunnel - .udp_socket() - .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - Ok(SmolmixUdpSocket(udp)) - }) - } -} diff --git a/smolmix/hyper/Cargo.toml b/smolmix/hyper/Cargo.toml deleted file mode 100644 index a3f401dde0..0000000000 --- a/smolmix/hyper/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "smolmix-hyper" -version = "0.0.1" -edition = "2021" -license.workspace = true - -[dependencies] -smolmix = { workspace = true } -smolmix-dns = { workspace = true } -bytes = { workspace = true } -hyper = { workspace = true, features = ["client", "http1"] } -hyper-util = { workspace = true, features = ["tokio", "client-legacy"] } -http-body-util = { workspace = true } -smolmix-tls = { workspace = true } -tokio-smoltcp = "0.5" -pin-project-lite = "0.2" -tower = { workspace = true } -tokio = { workspace = true } - -[dev-dependencies] -# Clearnet baseline in example -reqwest = { workspace = true } -rustls = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } -tracing = { workspace = true } diff --git a/smolmix/hyper/README.md b/smolmix/hyper/README.md deleted file mode 100644 index c238374e10..0000000000 --- a/smolmix/hyper/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# smolmix-hyper - -HTTP client routing all traffic through the Nym mixnet. Wraps [hyper-util](https://docs.rs/hyper-util)'s `Client` with a newtype that handles DNS resolution, TCP connections, and TLS — all through a smolmix `Tunnel`. - -## Quick start - -```rust -use smolmix_hyper::{Client, Request, EmptyBody, BodyExt}; -use bytes::Bytes; - -let tunnel = smolmix::Tunnel::new().await?; -let client = Client::new(&tunnel); - -let req = Request::get("https://example.com") - .header("Host", "example.com") - .body(EmptyBody::::new())?; -let resp = client.request(req).await?; -let body = resp.into_body().collect().await?.to_bytes(); -``` - -## API - -- **`Client::new(&tunnel)`** — create an HTTP client (body type: `Empty`, suitable for GET) -- **`Deref` to hyper-util's `Client`** — full `request()`, `get()`, etc. -- **`SmolmixConnector::new(&tunnel)`** — for custom body types (e.g. POST with `Full`) -- **`client(&tunnel)`** — free function returning a `Client` - -### Re-exports - -Commonly-used types are re-exported so you don't need `hyper`, `http-body-util`, or `bytes` in your `Cargo.toml`: - -- `Request`, `Response`, `StatusCode`, `Uri` -- `BodyExt`, `EmptyBody` (alias for `http_body_util::Empty`) -- `bytes` (the crate, for `Bytes`) - -### POST and custom body types - -The `Client` newtype is typed for `Empty` (GET requests). For POST, use `SmolmixConnector` directly: - -```rust -use smolmix_hyper::SmolmixConnector; -use hyper_util::client::legacy::Client; -use http_body_util::Full; - -let connector = SmolmixConnector::new(&tunnel); -let client: Client> = - Client::builder(TokioExecutor::new()).build(connector); - -let req = Request::post("https://httpbin.org/post") - .header("Content-Type", "application/json") - .body(Full::new(Bytes::from(r#"{"key": "value"}"#)))?; -let resp = client.request(req).await?; -``` - -## Examples - -Clearnet-vs-mixnet HTTP comparisons with timing: - -```sh -cargo run -p smolmix-hyper --example get # HTTPS GET -cargo run -p smolmix-hyper --example post # HTTP POST with JSON body -cargo run -p smolmix-hyper --example get -- --ipr -``` - -## Dependencies - -```toml -[dependencies] -smolmix-hyper = { workspace = true } -``` - -This crate depends on `smolmix` and `smolmix-dns` (DNS resolution goes through the tunnel automatically). diff --git a/smolmix/hyper/examples/get.rs b/smolmix/hyper/examples/get.rs deleted file mode 100644 index 5e67c09500..0000000000 --- a/smolmix/hyper/examples/get.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! HTTPS GET: clearnet vs Nym mixnet comparison. -//! -//! Fetches Cloudflare's `/cdn-cgi/trace` endpoint over clearnet (reqwest) and -//! through the mixnet (smolmix-hyper), then compares responses and timing. -//! -//! Run with: -//! cargo run -p smolmix-hyper --example get -//! cargo run -p smolmix-hyper --example get -- --ipr - -use smolmix::{Recipient, Tunnel}; -use smolmix_hyper::{BodyExt, Client, EmptyBody, Request}; -use tracing::info; - -type BoxError = Box; - -#[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 smolmix-hyper --- - let args: Vec = std::env::args().collect(); - let ipr_addr = args - .iter() - .position(|a| a == "--ipr") - .and_then(|i| args.get(i + 1)); - - let tunnel = if let Some(addr) = ipr_addr { - let recipient: Recipient = addr.parse().expect("invalid IPR address"); - Tunnel::new_with_ipr(recipient).await? - } else { - Tunnel::new().await? - }; - - let client = Client::new(&tunnel); - let mixnet_start = tokio::time::Instant::now(); - - let req = Request::get(format!("https://{host}{path}")) - .header("Host", host) - .body(EmptyBody::::new())?; - let resp = client.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); - - 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}"); - } - - 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(()) -} diff --git a/smolmix/hyper/examples/post.rs b/smolmix/hyper/examples/post.rs deleted file mode 100644 index 5e6fedd213..0000000000 --- a/smolmix/hyper/examples/post.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! HTTP POST: clearnet vs Nym mixnet comparison. -//! -//! Sends a POST request with a JSON body to httpbin.org via clearnet (reqwest) -//! and through the mixnet (smolmix-hyper with SmolmixConnector), then compares -//! responses and timing. -//! -//! Demonstrates using `SmolmixConnector` directly for requests that carry a body -//! (the `Client` newtype uses `Empty` — for POST you build a custom client). -//! -//! Run with: -//! cargo run -p smolmix-hyper --example post -//! cargo run -p smolmix-hyper --example post -- --ipr - -use bytes::Bytes; -use http_body_util::{BodyExt, Full}; -use hyper::Request; -use hyper_util::client::legacy::Client; -use hyper_util::rt::TokioExecutor; -use smolmix::{Recipient, Tunnel}; -use smolmix_hyper::SmolmixConnector; -use tracing::info; - -type BoxError = Box; - -const URL: &str = "https://httpbin.org/post"; -const JSON_BODY: &str = r#"{"message": "hello from the Nym mixnet!"}"#; - -#[tokio::main] -async fn main() -> Result<(), BoxError> { - smolmix::init_logging(); - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install rustls crypto provider"); - - // --- Clearnet baseline via reqwest --- - info!("POST via clearnet..."); - let clearnet_start = tokio::time::Instant::now(); - let clearnet_resp = reqwest::Client::new() - .post(URL) - .header("Content-Type", "application/json") - .body(JSON_BODY) - .send() - .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 smolmix-hyper --- - let args: Vec = std::env::args().collect(); - let ipr_addr = args - .iter() - .position(|a| a == "--ipr") - .and_then(|i| args.get(i + 1)); - - let tunnel = if let Some(addr) = ipr_addr { - let recipient: Recipient = addr.parse().expect("invalid IPR address"); - Tunnel::new_with_ipr(recipient).await? - } else { - Tunnel::new().await? - }; - - // For POST requests, use SmolmixConnector directly with a Full body - let connector = SmolmixConnector::new(&tunnel); - let client: Client> = - Client::builder(TokioExecutor::new()).build(connector); - - info!("POST via mixnet..."); - let mixnet_start = tokio::time::Instant::now(); - let req = Request::post(URL) - .header("Host", "httpbin.org") - .header("Content-Type", "application/json") - .body(Full::new(Bytes::from(JSON_BODY)))?; - let resp = client.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); - - // Check that the echo'd body contains our message - let clearnet_has_msg = clearnet_body.contains("hello from the Nym mixnet!"); - let mixnet_has_msg = mixnet_body.contains("hello from the Nym mixnet!"); - info!("Body echo clearnet: {clearnet_has_msg}"); - info!("Body echo mixnet: {mixnet_has_msg}"); - - 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(()) -} diff --git a/smolmix/hyper/src/connector.rs b/smolmix/hyper/src/connector.rs deleted file mode 100644 index 0bc240eaa5..0000000000 --- a/smolmix/hyper/src/connector.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only - -//! [`tower::Service`] connector routing TCP + TLS through a [`Tunnel`]. - -use std::future::Future; -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use hyper::Uri; -use hyper_util::rt::TokioIo; -use tower::Service; - -use smolmix::Tunnel; -use smolmix_tls::TlsConnector; - -use crate::tls_stream::MaybeTlsStream; - -/// A hyper connector that routes TCP (and optionally TLS) through a [`Tunnel`]. -/// -/// Implements [`tower::Service`] so it plugs directly into hyper-util's `Client`. -/// DNS resolution also goes through the tunnel, preventing hostname leaks. -#[derive(Clone)] -pub struct SmolmixConnector { - tunnel: Tunnel, - tls: TlsConnector, -} - -impl SmolmixConnector { - /// Create a new connector for the given tunnel. - /// - /// Sets up a TLS connector with the standard webpki root certificates. - pub fn new(tunnel: &Tunnel) -> Self { - Self { - tunnel: tunnel.clone(), - tls: smolmix_tls::connector(), - } - } -} - -impl Service for SmolmixConnector { - type Response = TokioIo; - type Error = io::Error; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, uri: Uri) -> Self::Future { - let tunnel = self.tunnel.clone(); - let tls = self.tls.clone(); - - Box::pin(async move { - let scheme = uri.scheme_str().unwrap_or("https"); - let host = uri - .host() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "URI missing host"))? - .to_owned(); - let port = uri - .port_u16() - .unwrap_or(if scheme == "https" { 443 } else { 80 }); - - let addrs = smolmix_dns::resolve(&tunnel, &host, port).await?; - let addr = addrs - .into_iter() - .next() - .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, "no addresses"))?; - - let tcp = tunnel - .tcp_connect(addr) - .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - let stream = if scheme == "https" { - let tls_stream = smolmix_tls::connect_with(&tls, tcp, &host).await?; - MaybeTlsStream::Tls { inner: tls_stream } - } else { - MaybeTlsStream::Plain { inner: tcp } - }; - - Ok(TokioIo::new(stream)) - }) - } -} diff --git a/smolmix/hyper/src/lib.rs b/smolmix/hyper/src/lib.rs deleted file mode 100644 index 554b61a569..0000000000 --- a/smolmix/hyper/src/lib.rs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only - -//! HTTP client routing all traffic through the Nym mixnet. -//! -//! This crate wraps [hyper-util]'s `Client` with a newtype [`Client`] that -//! routes DNS, TCP, and TLS through a smolmix [`Tunnel`]. It feels like a -//! drop-in replacement for `hyper_util::client::legacy::Client`. -//! -//! # Quick start -//! -//! ```ignore -//! use smolmix_hyper::{Client, Request, EmptyBody, BodyExt}; -//! use bytes::Bytes; -//! -//! let tunnel = smolmix::Tunnel::new().await?; -//! let client = Client::new(&tunnel); -//! -//! let req = Request::get("https://example.com") -//! .header("Host", "example.com") -//! .body(EmptyBody::::new())?; -//! let resp = client.request(req).await?; -//! let body = resp.into_body().collect().await?.to_bytes(); -//! ``` - -mod connector; -pub mod tls_stream; - -use std::ops::Deref; - -use bytes::Bytes; -use http_body_util::Empty; -use hyper_util::client::legacy; -use hyper_util::rt::TokioExecutor; - -use smolmix::Tunnel; - -// Re-exports so users don't need hyper/http-body-util/bytes in their Cargo.toml -pub use bytes; -pub use http_body_util::{BodyExt, Empty as EmptyBody}; -pub use hyper::{Request, Response, StatusCode, Uri}; - -pub use connector::SmolmixConnector; -pub use tls_stream::MaybeTlsStream; - -/// Inner hyper-util client type alias for readability. -type HyperClient = legacy::Client>; - -/// An HTTP client that routes all traffic through a smolmix [`Tunnel`]. -/// -/// Wraps a hyper-util `Client` and exposes its full API via [`Deref`]. DNS -/// resolution, TCP connections, and TLS all travel through the mixnet. -/// -/// The body type is [`Empty`], suitable for GET requests. For requests -/// that carry a body, construct a [`SmolmixConnector`] directly and pass it -/// to [`hyper_util::client::legacy::Client::builder`]. -pub struct Client { - inner: HyperClient, -} - -impl Client { - /// Create a new HTTP client for the given tunnel. - pub fn new(tunnel: &Tunnel) -> Self { - let connector = SmolmixConnector::new(tunnel); - Self { - inner: legacy::Client::builder(TokioExecutor::new()).build(connector), - } - } -} - -impl Deref for Client { - type Target = HyperClient; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -/// Create a hyper-util [`Client`] that routes all traffic through the tunnel. -/// -/// Equivalent to [`Client::new()`]. -pub fn client(tunnel: &Tunnel) -> Client { - Client::new(tunnel) -} diff --git a/smolmix/hyper/src/tls_stream.rs b/smolmix/hyper/src/tls_stream.rs deleted file mode 100644 index 8d3ae44ecc..0000000000 --- a/smolmix/hyper/src/tls_stream.rs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only - -//! TLS stream abstraction for plain and encrypted connections. - -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use hyper_util::client::legacy::connect::{Connected, Connection}; -use pin_project_lite::pin_project; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - -pin_project! { - /// A stream that may or may not be wrapped in TLS. - #[project = MaybeTlsProj] - pub enum MaybeTlsStream { - Plain { #[pin] inner: tokio_smoltcp::TcpStream }, - Tls { #[pin] inner: smolmix_tls::TlsStream }, - } -} - -impl AsyncRead for MaybeTlsStream { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - match self.project() { - MaybeTlsProj::Plain { inner } => inner.poll_read(cx, buf), - MaybeTlsProj::Tls { inner } => inner.poll_read(cx, buf), - } - } -} - -impl AsyncWrite for MaybeTlsStream { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - match self.project() { - MaybeTlsProj::Plain { inner } => inner.poll_write(cx, buf), - MaybeTlsProj::Tls { inner } => inner.poll_write(cx, buf), - } - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.project() { - MaybeTlsProj::Plain { inner } => inner.poll_flush(cx), - MaybeTlsProj::Tls { inner } => inner.poll_flush(cx), - } - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.project() { - MaybeTlsProj::Plain { inner } => inner.poll_shutdown(cx), - MaybeTlsProj::Tls { inner } => inner.poll_shutdown(cx), - } - } -} - -impl Connection for MaybeTlsStream { - fn connected(&self) -> Connected { - Connected::new() - } -} diff --git a/smolmix/libp2p/Cargo.toml b/smolmix/libp2p/Cargo.toml deleted file mode 100644 index 91264d5637..0000000000 --- a/smolmix/libp2p/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "smolmix-libp2p" -version = "0.0.1" -edition = "2021" -license.workspace = true - -[dependencies] -smolmix = { workspace = true } -smolmix-dns = { workspace = true } -libp2p = { version = "0.56", features = ["tokio", "noise", "yamux", "ping", "macros"] } -tokio-smoltcp = "0.5" -tokio-util = { workspace = true, features = ["compat"] } -tracing = { workspace = true } - -[dev-dependencies] -# tcp feature only needed for the clearnet listener example -libp2p = { version = "0.56", features = ["tcp"] } -futures = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } -tracing-subscriber = { workspace = true } diff --git a/smolmix/libp2p/README.md b/smolmix/libp2p/README.md deleted file mode 100644 index b2f37c4be5..0000000000 --- a/smolmix/libp2p/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# smolmix-libp2p - -libp2p `Transport` implementation that routes connections through the Nym mixnet. Because smolmix provides real TCP streams (via a user-space smoltcp stack), libp2p's standard noise encryption and yamux multiplexing work out of the box -- no custom `Connection`, `Substream`, or message ordering needed. - -## Quick start - -```rust -use libp2p::{noise, yamux, SwarmBuilder}; -use libp2p::core::upgrade::Version; -use libp2p::core::Transport; -use smolmix_libp2p::SmolmixTransport; - -let tunnel = smolmix::Tunnel::new().await?; - -let mut swarm = SwarmBuilder::with_new_identity() - .with_tokio() - .with_other_transport(|keypair| { - SmolmixTransport::new(&tunnel) - .upgrade(Version::V1) - .authenticate(noise::Config::new(keypair).expect("noise config")) - .multiplex(yamux::Config::default()) - .boxed() - })? - .with_behaviour(|_| libp2p::ping::Behaviour::default())? - .build(); - -swarm.dial("/ip4/1.2.3.4/tcp/12345".parse::()?)?; -``` - -## API - -- **`SmolmixTransport::new(&tunnel)`** -- create a transport backed by a smolmix `Tunnel` - -### Transport trait - -- **`dial(addr, opts)`** -- parse multiaddr, resolve DNS if needed, TCP connect through the tunnel -- **`listen_on(id, addr)`** -- returns `MultiaddrNotSupported` (dial-only, no inbound) -- **`poll(cx)`** -- always `Pending` (no listener events) - -### Supported multiaddrs - -| Multiaddr | Behaviour | -|---|---| -| `/ip4//tcp/` | Direct TCP connect through tunnel | -| `/ip6//tcp/` | Direct TCP connect through tunnel | -| `/dns4//tcp/` | DNS resolved through tunnel, then TCP connect | -| `/dns//tcp/` | DNS resolved through tunnel, then TCP connect | - -Trailing components (like `/p2p/`) are ignored by the transport and handled by the swarm. - -## Examples - -Two examples demonstrate the full workflow — a clearnet listener and a mixnet dialer: - -```sh -# Terminal 1: start a standard libp2p node (clearnet TCP, noise + yamux) -cargo run -p smolmix-libp2p --example listener -# → prints: /ip4/127.0.0.1/tcp/12345/p2p/12D3KooW... - -# Terminal 2: dial it through the Nym mixnet -cargo run -p smolmix-libp2p --example ping -- /ip4//tcp//p2p/ -cargo run -p smolmix-libp2p --example ping -- --ipr /ip4/.../tcp/.../p2p/... -``` - -The listener sees a connection arriving from the exit gateway's IP — it has no idea the dialer is on the mixnet. - -## Limitations - -- **Dial-only** -- listening requires IPR listener support (future work) -- **No TLS in transport** -- libp2p uses noise for encryption; TLS would be redundant - -## Dependencies - -```toml -[dependencies] -smolmix-libp2p = { workspace = true } -``` - -This crate depends on `smolmix`, `smolmix-dns`, `libp2p` (0.56), and `tokio-util` (for the `Compat` bridge between tokio and futures I/O traits). diff --git a/smolmix/libp2p/examples/listener.rs b/smolmix/libp2p/examples/listener.rs deleted file mode 100644 index 05a2d57bf1..0000000000 --- a/smolmix/libp2p/examples/listener.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Clearnet libp2p listener that accepts pings. -//! -//! Run this first, then pass the printed address to the `ping` example which -//! dials through the Nym mixnet. -//! -//! Run with: -//! cargo run -p smolmix-libp2p --example listener -//! -//! Then in another terminal: -//! cargo run -p smolmix-libp2p --example ping -- - -use futures::StreamExt; -use libp2p::swarm::SwarmEvent; -use libp2p::{noise, ping, yamux, SwarmBuilder}; - -type BoxError = Box; - -#[tokio::main] -async fn main() -> Result<(), BoxError> { - tracing_subscriber::fmt().with_env_filter("info").init(); - - let mut swarm = SwarmBuilder::with_new_identity() - .with_tokio() - .with_tcp( - Default::default(), - noise::Config::new, - yamux::Config::default, - )? - .with_behaviour(|_| ping::Behaviour::default())? - .build(); - - let local_peer_id = *swarm.local_peer_id(); - swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; - - println!("Peer ID: {local_peer_id}"); - println!("Waiting for listen address..."); - - loop { - match swarm.select_next_some().await { - SwarmEvent::NewListenAddr { address, .. } => { - let full_addr = format!("{address}/p2p/{local_peer_id}"); - println!(); - println!("Listening! Pass this address to the ping example:"); - println!(" {full_addr}"); - println!(); - } - SwarmEvent::ConnectionEstablished { peer_id, .. } => { - println!("Connection established from {peer_id}"); - } - SwarmEvent::Behaviour(ping::Event { peer, result, .. }) => match result { - Ok(rtt) => println!("Ping from {peer}: {rtt:?}"), - Err(e) => println!("Ping error from {peer}: {e}"), - }, - SwarmEvent::ConnectionClosed { peer_id, .. } => { - println!("Connection closed: {peer_id}"); - } - _ => {} - } - } -} diff --git a/smolmix/libp2p/examples/ping.rs b/smolmix/libp2p/examples/ping.rs deleted file mode 100644 index da705821c5..0000000000 --- a/smolmix/libp2p/examples/ping.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! libp2p ping through the Nym mixnet. -//! -//! Dials a libp2p peer through the mixnet and exchanges pings. Start the -//! `listener` example first to get an address, then pass it here. -//! -//! Run with: -//! # Terminal 1: start the clearnet listener -//! cargo run -p smolmix-libp2p --example listener -//! -//! # Terminal 2: dial through the mixnet -//! cargo run -p smolmix-libp2p --example ping -- -//! cargo run -p smolmix-libp2p --example ping -- --ipr - -use futures::StreamExt; -use libp2p::core::upgrade::Version; -use libp2p::core::Transport; -use libp2p::swarm::SwarmEvent; -use libp2p::{noise, ping, yamux, Multiaddr, SwarmBuilder}; -use smolmix::{Recipient, Tunnel}; -use smolmix_libp2p::SmolmixTransport; -use tracing::info; - -type BoxError = Box; - -#[tokio::main] -async fn main() -> Result<(), BoxError> { - smolmix::init_logging(); - - let args: Vec = std::env::args().collect(); - - // Parse optional --ipr flag - let ipr_pos = args.iter().position(|a| a == "--ipr"); - let ipr_addr = ipr_pos.and_then(|i| args.get(i + 1)); - - // The multiaddr is the last positional argument (skip --ipr and its value) - let multiaddr_str = args - .iter() - .enumerate() - .filter(|(i, _)| *i != 0 && Some(*i) != ipr_pos && Some(*i) != ipr_pos.map(|p| p + 1)) - .map(|(_, s)| s) - .last() - .expect( - "Usage: ping [--ipr ] \n\ - \n\ - Start the listener first:\n\ - \x20 cargo run -p smolmix-libp2p --example listener", - ); - let remote: Multiaddr = multiaddr_str.parse()?; - - // Create the tunnel - let tunnel = if let Some(addr) = ipr_addr { - let recipient: Recipient = addr.parse().expect("invalid IPR address"); - Tunnel::new_with_ipr(recipient).await? - } else { - Tunnel::new().await? - }; - - // Build the swarm with our mixnet transport - let mut swarm = SwarmBuilder::with_new_identity() - .with_tokio() - .with_other_transport(|keypair| { - SmolmixTransport::new(&tunnel) - .upgrade(Version::V1) - .authenticate(noise::Config::new(keypair).expect("noise config")) - .multiplex(yamux::Config::default()) - .boxed() - })? - .with_behaviour(|_| ping::Behaviour::default())? - .build(); - - info!("Local peer ID: {}", swarm.local_peer_id()); - info!("Dialing {remote} through the Nym mixnet..."); - swarm.dial(remote)?; - - // Drive the swarm and print ping results - let mut pings = 0u32; - loop { - match swarm.select_next_some().await { - SwarmEvent::Behaviour(ping::Event { peer, result, .. }) => match result { - Ok(rtt) => { - pings += 1; - info!("Ping #{pings} to {peer}: {rtt:?}"); - if pings >= 5 { - info!("Done — {pings} pings completed."); - break; - } - } - Err(e) => { - info!("Ping error from {peer}: {e}"); - break; - } - }, - SwarmEvent::ConnectionEstablished { peer_id, .. } => { - info!("Connected to {peer_id}"); - } - SwarmEvent::OutgoingConnectionError { error, .. } => { - info!("Connection error: {error}"); - break; - } - _ => {} - } - } - - tunnel.shutdown().await; - Ok(()) -} diff --git a/smolmix/libp2p/src/lib.rs b/smolmix/libp2p/src/lib.rs deleted file mode 100644 index e349422681..0000000000 --- a/smolmix/libp2p/src/lib.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only - -//! libp2p [`Transport`] that routes connections through the Nym mixnet. -//! -//! [`SmolmixTransport`] implements the libp2p [`Transport`] trait by parsing -//! multiaddrs, optionally resolving DNS through the tunnel, and opening TCP -//! connections via smolmix. Because smolmix provides real TCP streams (via a -//! user-space smoltcp stack), libp2p's standard noise encryption and yamux -//! multiplexing work out of the box. -//! -//! # Supported multiaddrs -//! -//! - `/ip4//tcp/` -//! - `/ip6//tcp/` -//! - `/dns4//tcp/` (resolved through the tunnel) -//! - `/dns//tcp/` (resolved through the tunnel) -//! -//! # Quick start -//! -//! ```ignore -//! use libp2p::{noise, yamux, SwarmBuilder}; -//! use libp2p::core::{upgrade::Version, Transport}; -//! use smolmix_libp2p::SmolmixTransport; -//! -//! let tunnel = smolmix::Tunnel::new().await?; -//! -//! let swarm = SwarmBuilder::with_new_identity() -//! .with_tokio() -//! .with_other_transport(|keypair| { -//! SmolmixTransport::new(&tunnel) -//! .upgrade(Version::V1) -//! .authenticate(noise::Config::new(keypair).expect("noise config")) -//! .multiplex(yamux::Config::default()) -//! .boxed() -//! })? -//! .with_behaviour(|_| libp2p::ping::Behaviour::default())? -//! .build(); -//! ``` -//! -//! # Limitations -//! -//! - **Dial-only**: listening is not supported (no inbound connections through -//! the mixnet). `listen_on()` returns [`TransportError::MultiaddrNotSupported`]. -//! - **No TLS in transport**: libp2p uses noise for encryption, so adding TLS -//! would be redundant. - -use std::future::Future; -use std::io; -use std::net::SocketAddr; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use libp2p::core::multiaddr::Protocol; -use libp2p::core::transport::{DialOpts, ListenerId, TransportError, TransportEvent}; -use libp2p::core::Transport; -use libp2p::Multiaddr; -use tokio_util::compat::{Compat, TokioAsyncReadCompatExt}; -use tracing::debug; - -use smolmix::Tunnel; - -/// A libp2p transport that routes TCP connections through a smolmix [`Tunnel`]. -/// -/// Supports dialing peers by IP or hostname (DNS resolved through the tunnel). -/// Does not support listening — the mixnet doesn't provide inbound TCP yet. -pub struct SmolmixTransport { - tunnel: Tunnel, -} - -impl SmolmixTransport { - /// Create a new transport backed by the given tunnel. - pub fn new(tunnel: &Tunnel) -> Self { - Self { - tunnel: tunnel.clone(), - } - } -} - -/// Parsed dial target from a multiaddr. -enum DialTarget { - Ip(SocketAddr), - Dns { host: String, port: u16 }, -} - -/// Extract a dial target from a multiaddr. -/// -/// Accepts `/ip4/.../tcp/N`, `/ip6/.../tcp/N`, `/dns4/.../tcp/N`, and -/// `/dns/.../tcp/N`. Trailing components (like `/p2p/...`) are ignored. -fn parse_multiaddr(addr: &Multiaddr) -> Option { - let mut iter = addr.iter(); - match iter.next()? { - Protocol::Ip4(ip) => { - if let Some(Protocol::Tcp(port)) = iter.next() { - Some(DialTarget::Ip(SocketAddr::new(ip.into(), port))) - } else { - None - } - } - Protocol::Ip6(ip) => { - if let Some(Protocol::Tcp(port)) = iter.next() { - Some(DialTarget::Ip(SocketAddr::new(ip.into(), port))) - } else { - None - } - } - Protocol::Dns4(host) | Protocol::Dns(host) => { - if let Some(Protocol::Tcp(port)) = iter.next() { - Some(DialTarget::Dns { - host: host.to_string(), - port, - }) - } else { - None - } - } - _ => None, - } -} - -impl Transport for SmolmixTransport { - type Output = Compat; - type Error = io::Error; - type ListenerUpgrade = Pin> + Send>>; - type Dial = Pin> + Send>>; - - fn listen_on( - &mut self, - _id: ListenerId, - addr: Multiaddr, - ) -> Result<(), TransportError> { - Err(TransportError::MultiaddrNotSupported(addr)) - } - - fn remove_listener(&mut self, _id: ListenerId) -> bool { - false - } - - fn dial( - &mut self, - addr: Multiaddr, - _opts: DialOpts, - ) -> Result> { - let target = parse_multiaddr(&addr).ok_or(TransportError::MultiaddrNotSupported(addr))?; - let tunnel = self.tunnel.clone(); - - Ok(Box::pin(async move { - let socket_addr = match target { - DialTarget::Ip(addr) => addr, - DialTarget::Dns { host, port } => { - debug!(host = %host, port, "resolving DNS through tunnel"); - let addrs = smolmix_dns::resolve(&tunnel, &host, port).await?; - addrs.into_iter().next().ok_or_else(|| { - io::Error::new(io::ErrorKind::AddrNotAvailable, "DNS returned no addresses") - })? - } - }; - - debug!(addr = %socket_addr, "connecting TCP through tunnel"); - let tcp = tunnel - .tcp_connect(socket_addr) - .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - Ok(tcp.compat()) - })) - } - - fn poll( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Pending - } -} diff --git a/smolmix/tls/Cargo.toml b/smolmix/tls/Cargo.toml deleted file mode 100644 index c8b58f280a..0000000000 --- a/smolmix/tls/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "smolmix-tls" -version = "0.0.1" -edition = "2021" -license.workspace = true - -[dependencies] -tokio-smoltcp = "0.5" -tokio-rustls = "0.26" -rustls = { workspace = true } -webpki-roots = { workspace = true } diff --git a/smolmix/tls/README.md b/smolmix/tls/README.md deleted file mode 100644 index 3029dd666b..0000000000 --- a/smolmix/tls/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# smolmix-tls - -Shared TLS setup for smolmix tunneled connections. Provides a pre-configured `TlsConnector` with webpki root certificates and convenience functions for TLS over `TcpStream`, used internally by `smolmix-hyper` and `smolmix-tungstenite`. - -## Quick start - -```rust -use smolmix_tls::{connect, connector, connect_with}; - -let tunnel = smolmix::Tunnel::new().await?; -let tcp = tunnel.tcp_connect("93.184.216.34:443".parse()?).await?; - -// One-shot: TLS handshake over an existing TCP stream. -let tls_stream = connect(tcp, "example.com").await?; - -// Reusable: create a connector once, use for many connections. -let tls = connector(); -let stream1 = connect_with(&tls, tcp1, "a.example.com").await?; -let stream2 = connect_with(&tls, tcp2, "b.example.com").await?; -``` - -## API - -- **`connector()`** -- create a reusable `TlsConnector` with webpki root certificates (clones cheaply via `Arc`) -- **`connect(tcp, hostname)`** -- one-shot TLS handshake (creates a fresh connector internally) -- **`connect_with(&connector, tcp, hostname)`** -- TLS handshake with a pre-built connector (avoids rebuilding the root store) - -### Re-exports - -Commonly-used types are re-exported so you don't need `tokio-rustls` in your `Cargo.toml`: - -- `TlsStream` -- the connected TLS stream type (`tokio_rustls::client::TlsStream`) -- `TlsConnector` -- the connector type (`tokio_rustls::TlsConnector`) - -## Dependencies - -```toml -[dependencies] -smolmix-tls = { workspace = true } -``` - -This crate depends on `tokio-rustls`, `rustls`, and `webpki-roots`. diff --git a/smolmix/tls/src/lib.rs b/smolmix/tls/src/lib.rs deleted file mode 100644 index f90afe57ed..0000000000 --- a/smolmix/tls/src/lib.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only - -//! Shared TLS setup for smolmix tunneled connections. -//! -//! Provides a pre-configured [`TlsConnector`] with webpki root certificates -//! and convenience functions for TLS over [`TcpStream`]. -//! -//! # Quick start -//! -//! ```ignore -//! // One-shot: TLS handshake over an existing TCP stream. -//! let tls_stream = smolmix_tls::connect(tcp, "example.com").await?; -//! -//! // Reusable: create a connector once, use it for many connections. -//! let tls = smolmix_tls::connector(); -//! let stream1 = smolmix_tls::connect_with(&tls, tcp1, "a.com").await?; -//! let stream2 = smolmix_tls::connect_with(&tls, tcp2, "b.com").await?; -//! ``` - -use std::io; -use std::sync::Arc; - -use rustls::pki_types::ServerName; -use tokio_smoltcp::TcpStream; - -pub use tokio_rustls::client::TlsStream; -pub use tokio_rustls::TlsConnector; - -/// Create a [`TlsConnector`] configured with the standard webpki root certificates. -/// -/// The returned connector can be cloned cheaply (it wraps an `Arc`) -/// and reused across many connections. -pub fn 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)) -} - -/// Perform a TLS handshake over an existing TCP stream. -/// -/// Creates a fresh [`TlsConnector`] with webpki roots. For repeated connections, -/// prefer [`connect_with()`] to avoid rebuilding the root store each time. -pub async fn connect(tcp: TcpStream, hostname: &str) -> io::Result> { - connect_with(&connector(), tcp, hostname).await -} - -/// Perform a TLS handshake using a pre-built connector. -/// -/// Extracts the SNI hostname from `hostname` and connects. -pub async fn connect_with( - tls: &TlsConnector, - tcp: TcpStream, - hostname: &str, -) -> io::Result> { - let domain = ServerName::try_from(hostname.to_owned()) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; - tls.connect(domain, tcp).await -} diff --git a/smolmix/tungstenite/Cargo.toml b/smolmix/tungstenite/Cargo.toml deleted file mode 100644 index 2e3a000ab6..0000000000 --- a/smolmix/tungstenite/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "smolmix-tungstenite" -version = "0.0.1" -edition = "2021" -license.workspace = true - -[dependencies] -smolmix = { workspace = true } -smolmix-dns = { workspace = true } -smolmix-tls = { workspace = true } -tokio-smoltcp = "0.5" -tokio-tungstenite = { workspace = true } - -[dev-dependencies] -futures = { workspace = true } -rustls = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time", "net"] } -tracing = { workspace = true } diff --git a/smolmix/tungstenite/README.md b/smolmix/tungstenite/README.md deleted file mode 100644 index 9c9a8134e5..0000000000 --- a/smolmix/tungstenite/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# smolmix-tungstenite - -WebSocket connections through the Nym mixnet. Wraps [tokio-tungstenite](https://docs.rs/tokio-tungstenite) with a `connect()` function that handles DNS resolution, TCP, TLS, and the WebSocket upgrade — all through a smolmix `Tunnel`. - -## Quick start - -```rust -use smolmix_tungstenite::{connect, Message}; -use futures::{SinkExt, StreamExt}; - -let tunnel = smolmix::Tunnel::new().await?; -let (mut ws, _resp) = connect(&tunnel, "wss://echo.websocket.org").await?; - -ws.send(Message::Text("hello".into())).await?; -let reply = ws.next().await.ok_or("no reply")??; -println!("{}", reply.into_text()?); -ws.close(None).await?; -``` - -## API - -- **`connect(&tunnel, url)`** — connect to a `wss://` WebSocket server through the mixnet - - Handles DNS, TCP, TLS, and WebSocket upgrade in one call - - Returns `(WsStream, Response)` — the stream implements `Sink + Stream` - - Only `wss://` is supported (use raw `TcpStream` for unencrypted WebSocket) - -### Re-exports - -Commonly-used types are re-exported so you don't need `tokio-tungstenite` or `tungstenite` in your `Cargo.toml`: - -- `Message` — the WebSocket message type -- `tungstenite` — the underlying tungstenite crate (for `Error`, `protocol::WebSocketConfig`, etc.) - -### Types - -- **`WsStream`** — type alias for `WebSocketStream>` -- **`Error`** — wraps `io::Error` and `tungstenite::Error` - -## Example - -Clearnet-vs-mixnet WebSocket echo comparison with timing: - -```sh -cargo run -p smolmix-tungstenite --example echo -cargo run -p smolmix-tungstenite --example echo -- --ipr -``` - -## Dependencies - -```toml -[dependencies] -smolmix-tungstenite = { workspace = true } -``` - -This crate depends on `smolmix` and `smolmix-dns` (DNS resolution goes through the tunnel automatically). diff --git a/smolmix/tungstenite/examples/echo.rs b/smolmix/tungstenite/examples/echo.rs deleted file mode 100644 index 991289438a..0000000000 --- a/smolmix/tungstenite/examples/echo.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! WebSocket echo: clearnet vs Nym mixnet comparison. -//! -//! Sends a message to a public echo server over clearnet (tokio-tungstenite -//! directly) and through the mixnet (smolmix-tungstenite), then compares -//! responses and timing. -//! -//! Run with: -//! cargo run -p smolmix-tungstenite --example echo -//! cargo run -p smolmix-tungstenite --example echo -- --ipr - -use futures::{SinkExt, StreamExt}; -use smolmix::{Recipient, Tunnel}; -use smolmix_tungstenite::Message; -use tracing::info; - -type BoxError = Box; - -const WS_URL: &str = "wss://ws.postman-echo.com/raw"; -const ECHO_MSG: &str = "Hello from the Nym mixnet!"; - -#[tokio::main] -async fn main() -> Result<(), BoxError> { - smolmix::init_logging(); - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install rustls crypto provider"); - - // --- Clearnet baseline via tokio-tungstenite --- - info!("Connecting via clearnet..."); - let clearnet_start = tokio::time::Instant::now(); - - let (mut clearnet_ws, _) = tokio_tungstenite::connect_async(WS_URL).await?; - clearnet_ws.send(Message::Text(ECHO_MSG.into())).await?; - let clearnet_reply = clearnet_ws.next().await.ok_or("no clearnet reply")??; - let clearnet_duration = clearnet_start.elapsed(); - let clearnet_text = clearnet_reply.into_text()?; - clearnet_ws.close(None).await?; - - info!("Clearnet: \"{clearnet_text}\" in {clearnet_duration:?}"); - - // --- Mixnet via smolmix-tungstenite --- - let args: Vec = std::env::args().collect(); - let ipr_addr = args - .iter() - .position(|a| a == "--ipr") - .and_then(|i| args.get(i + 1)); - - let tunnel = if let Some(addr) = ipr_addr { - let recipient: Recipient = addr.parse().expect("invalid IPR address"); - Tunnel::new_with_ipr(recipient).await? - } else { - Tunnel::new().await? - }; - - let mixnet_start = tokio::time::Instant::now(); - let (mut mixnet_ws, _) = smolmix_tungstenite::connect(&tunnel, WS_URL).await?; - mixnet_ws.send(Message::Text(ECHO_MSG.into())).await?; - let mixnet_reply = mixnet_ws.next().await.ok_or("no mixnet reply")??; - let mixnet_duration = mixnet_start.elapsed(); - let mixnet_text = mixnet_reply.into_text()?; - mixnet_ws.close(None).await?; - - // --- Compare --- - info!("=== Results ==="); - info!("Clearnet: \"{clearnet_text}\" in {clearnet_duration:?}"); - info!("Mixnet: \"{mixnet_text}\" in {mixnet_duration:?}"); - info!("Clearnet echo match: {}", clearnet_text == ECHO_MSG); - info!("Mixnet echo match: {}", mixnet_text == ECHO_MSG); - - 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(()) -} diff --git a/smolmix/tungstenite/src/lib.rs b/smolmix/tungstenite/src/lib.rs deleted file mode 100644 index eeb014a214..0000000000 --- a/smolmix/tungstenite/src/lib.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only - -//! WebSocket connections through the Nym mixnet. -//! -//! This crate wraps [tokio-tungstenite] with a convenience [`connect()`] -//! function that handles DNS resolution, TCP, TLS, and the WebSocket upgrade -//! — all through a smolmix [`Tunnel`]. -//! -//! # Quick start -//! -//! ```ignore -//! use smolmix_tungstenite::connect; -//! use futures::{SinkExt, StreamExt}; -//! -//! let tunnel = smolmix::Tunnel::new().await?; -//! let (mut ws, _resp) = connect(&tunnel, "wss://echo.websocket.org").await?; -//! -//! ws.send(Message::Text("hello".into())).await?; -//! let reply = ws.next().await.ok_or("no reply")??; -//! ``` - -use std::io; - -use tokio_tungstenite::tungstenite::client::IntoClientRequest; -use tokio_tungstenite::tungstenite::error::Error as WsError; -use tokio_tungstenite::tungstenite::http::Response as WsResponse; -use tokio_tungstenite::WebSocketStream; - -use smolmix::Tunnel; - -// Re-exports so users don't need tokio-tungstenite or tungstenite in their Cargo.toml -pub use tokio_tungstenite::tungstenite; -pub use tokio_tungstenite::tungstenite::Message; - -/// The WebSocket stream type returned by [`connect()`]. -/// -/// This is a `WebSocketStream` over a TLS-wrapped TCP connection through the -/// tunnel. Use it with `futures::SinkExt` and `futures::StreamExt`. -pub type WsStream = WebSocketStream>; - -/// Connect to a WebSocket server through the tunnel. -/// -/// Handles DNS resolution, TCP connection, TLS handshake, and WebSocket -/// upgrade — all through the mixnet. Only `wss://` URLs are supported. -/// -/// Returns the WebSocket stream and the HTTP upgrade response. -pub async fn connect( - tunnel: &Tunnel, - request: R, -) -> Result<(WsStream, WsResponse>>), Error> -where - R: IntoClientRequest + Unpin, -{ - let request = request.into_client_request().map_err(Error::WebSocket)?; - - let uri = request.uri(); - let scheme = uri.scheme_str().unwrap_or("wss"); - if scheme != "wss" { - return Err(Error::Io(io::Error::new( - io::ErrorKind::InvalidInput, - "only wss:// URLs are supported (use raw TCP for unencrypted WebSocket)", - ))); - } - - let host = uri - .host() - .ok_or_else(|| { - Error::Io(io::Error::new( - io::ErrorKind::InvalidInput, - "URI missing host", - )) - })? - .to_owned(); - let port = uri.port_u16().unwrap_or(443); - - // DNS through the tunnel - let addrs = smolmix_dns::resolve(tunnel, &host, port) - .await - .map_err(Error::Io)?; - let addr = addrs.into_iter().next().ok_or_else(|| { - Error::Io(io::Error::new( - io::ErrorKind::AddrNotAvailable, - "no addresses", - )) - })?; - - // TCP through the tunnel - let tcp = tunnel - .tcp_connect(addr) - .await - .map_err(|e| Error::Io(io::Error::new(io::ErrorKind::Other, e)))?; - - // TLS handshake - let tls_stream = smolmix_tls::connect(tcp, &host).await.map_err(Error::Io)?; - - // WebSocket upgrade - let (ws, resp) = tokio_tungstenite::client_async(request, tls_stream) - .await - .map_err(Error::WebSocket)?; - - Ok((ws, resp)) -} - -/// Error type for WebSocket connections through the tunnel. -#[derive(Debug)] -pub enum Error { - /// I/O error (DNS, TCP, or TLS). - Io(io::Error), - /// WebSocket protocol error. - WebSocket(WsError), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Io(e) => write!(f, "I/O error: {e}"), - Error::WebSocket(e) => write!(f, "WebSocket error: {e}"), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Io(e) => Some(e), - Error::WebSocket(e) => Some(e), - } - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(e) - } -} - -impl From for Error { - fn from(e: WsError) -> Self { - Error::WebSocket(e) - } -}