Remove companion crates, scope to smolmix-core
This commit is contained in:
+3
-147
@@ -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<Uri>` | `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<TcpStream>
|
||||
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<Uri>` 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<T>` (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<TcpStream> }
|
||||
→ TokioIo<MaybeTlsStream> (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<TcpStream>` 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<T>` — 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<TcpStream> (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 -- <MULTIADDR from listener>
|
||||
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
|
||||
|
||||
@@ -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 }
|
||||
@@ -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<SocketAddr>`
|
||||
- **`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 <IPR_ADDRESS>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix-dns = { workspace = true }
|
||||
```
|
||||
|
||||
This crate depends on `smolmix` (for the `Tunnel` type), `hickory-proto`, and `hickory-resolver`.
|
||||
@@ -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 <IPR_ADDRESS>
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use hickory_resolver::TokioResolver;
|
||||
use smolmix::{Recipient, Tunnel};
|
||||
use smolmix_dns::Resolver;
|
||||
use tracing::info;
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
#[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<Ipv4Addr> = 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<String> = 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<Ipv4Addr> = 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(())
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<GenericConnector<SmolmixRuntimeProvider>>;
|
||||
|
||||
/// 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<Vec<SocketAddr>> {
|
||||
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<Vec<SocketAddr>> {
|
||||
let r = resolver(tunnel);
|
||||
r.resolve(host, port).await
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<io::Result<(usize, SocketAddr)>> {
|
||||
self.0.poll_recv_from(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_send_to(
|
||||
&self,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
target: SocketAddr,
|
||||
) -> Poll<io::Result<usize>> {
|
||||
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<tokio_smoltcp::TcpStream>;
|
||||
|
||||
fn create_handle(&self) -> Self::Handle {
|
||||
self.handle.clone()
|
||||
}
|
||||
|
||||
fn connect_tcp(
|
||||
&self,
|
||||
server_addr: SocketAddr,
|
||||
_bind_addr: Option<SocketAddr>,
|
||||
_timeout: Option<std::time::Duration>,
|
||||
) -> Pin<Box<dyn Send + Future<Output = io::Result<Self::Tcp>>>> {
|
||||
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<Box<dyn Send + Future<Output = io::Result<Self::Udp>>>> {
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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::<Bytes>::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<Bytes>`, 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<Bytes>`)
|
||||
- **`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<Bytes>` (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<SmolmixConnector, Full<Bytes>> =
|
||||
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 <IPR_ADDRESS>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix-hyper = { workspace = true }
|
||||
```
|
||||
|
||||
This crate depends on `smolmix` and `smolmix-dns` (DNS resolution goes through the tunnel automatically).
|
||||
@@ -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 <IPR_ADDRESS>
|
||||
|
||||
use smolmix::{Recipient, Tunnel};
|
||||
use smolmix_hyper::{BodyExt, Client, EmptyBody, Request};
|
||||
use tracing::info;
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
#[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<String> = 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::<bytes::Bytes>::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(())
|
||||
}
|
||||
@@ -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<Bytes>` — for POST you build a custom client).
|
||||
//!
|
||||
//! Run with:
|
||||
//! cargo run -p smolmix-hyper --example post
|
||||
//! cargo run -p smolmix-hyper --example post -- --ipr <IPR_ADDRESS>
|
||||
|
||||
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<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
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<String> = 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<Bytes> body
|
||||
let connector = SmolmixConnector::new(&tunnel);
|
||||
let client: Client<SmolmixConnector, Full<Bytes>> =
|
||||
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(())
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
//! [`tower::Service<Uri>`] 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<Uri>`] 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<Uri> for SmolmixConnector {
|
||||
type Response = TokioIo<MaybeTlsStream>;
|
||||
type Error = io::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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::<Bytes>::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<SmolmixConnector, Empty<Bytes>>;
|
||||
|
||||
/// 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<Bytes>`], 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)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<tokio_smoltcp::TcpStream> },
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for MaybeTlsStream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
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<io::Result<usize>> {
|
||||
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<io::Result<()>> {
|
||||
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<io::Result<()>> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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::<libp2p::Multiaddr>()?)?;
|
||||
```
|
||||
|
||||
## 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/<addr>/tcp/<port>` | Direct TCP connect through tunnel |
|
||||
| `/ip6/<addr>/tcp/<port>` | Direct TCP connect through tunnel |
|
||||
| `/dns4/<host>/tcp/<port>` | DNS resolved through tunnel, then TCP connect |
|
||||
| `/dns/<host>/tcp/<port>` | DNS resolved through tunnel, then TCP connect |
|
||||
|
||||
Trailing components (like `/p2p/<peer_id>`) 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/<YOUR_IP>/tcp/<PORT>/p2p/<PEER_ID>
|
||||
cargo run -p smolmix-libp2p --example ping -- --ipr <IPR_ADDRESS> /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).
|
||||
@@ -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 -- <MULTIADDR from listener output>
|
||||
|
||||
use futures::StreamExt;
|
||||
use libp2p::swarm::SwarmEvent;
|
||||
use libp2p::{noise, ping, yamux, SwarmBuilder};
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
#[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}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 -- <MULTIADDR from listener>
|
||||
//! cargo run -p smolmix-libp2p --example ping -- --ipr <IPR_ADDRESS> <MULTIADDR>
|
||||
|
||||
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<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), BoxError> {
|
||||
smolmix::init_logging();
|
||||
|
||||
let args: Vec<String> = 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 <IPR_ADDRESS>] <MULTIADDR>\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(())
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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/<addr>/tcp/<port>`
|
||||
//! - `/ip6/<addr>/tcp/<port>`
|
||||
//! - `/dns4/<host>/tcp/<port>` (resolved through the tunnel)
|
||||
//! - `/dns/<host>/tcp/<port>` (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<DialTarget> {
|
||||
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<tokio_smoltcp::TcpStream>;
|
||||
type Error = io::Error;
|
||||
type ListenerUpgrade = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
type Dial = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
|
||||
fn listen_on(
|
||||
&mut self,
|
||||
_id: ListenerId,
|
||||
addr: Multiaddr,
|
||||
) -> Result<(), TransportError<Self::Error>> {
|
||||
Err(TransportError::MultiaddrNotSupported(addr))
|
||||
}
|
||||
|
||||
fn remove_listener(&mut self, _id: ListenerId) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn dial(
|
||||
&mut self,
|
||||
addr: Multiaddr,
|
||||
_opts: DialOpts,
|
||||
) -> Result<Self::Dial, TransportError<Self::Error>> {
|
||||
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<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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`.
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<ClientConfig>`)
|
||||
/// 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<TlsStream<TcpStream>> {
|
||||
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<TlsStream<TcpStream>> {
|
||||
let domain = ServerName::try_from(hostname.to_owned())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||
tls.connect(domain, tcp).await
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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<Message> + Stream<Item = Message>`
|
||||
- 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<TlsStream<TcpStream>>`
|
||||
- **`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 <IPR_ADDRESS>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix-tungstenite = { workspace = true }
|
||||
```
|
||||
|
||||
This crate depends on `smolmix` and `smolmix-dns` (DNS resolution goes through the tunnel automatically).
|
||||
@@ -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 <IPR_ADDRESS>
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use smolmix::{Recipient, Tunnel};
|
||||
use smolmix_tungstenite::Message;
|
||||
use tracing::info;
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
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<String> = 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(())
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<smolmix_tls::TlsStream<tokio_smoltcp::TcpStream>>;
|
||||
|
||||
/// 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<R>(
|
||||
tunnel: &Tunnel,
|
||||
request: R,
|
||||
) -> Result<(WsStream, WsResponse<Option<Vec<u8>>>), 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<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WsError> for Error {
|
||||
fn from(e: WsError) -> Self {
|
||||
Error::WebSocket(e)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user