diff --git a/smolmix/core/src/bridge.rs b/smolmix/core/src/bridge.rs index 1cc0949db3..1f113d46c6 100644 --- a/smolmix/core/src/bridge.rs +++ b/smolmix/core/src/bridge.rs @@ -1,5 +1,4 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only use crate::error::SmolmixError; use futures::channel::mpsc; diff --git a/smolmix/core/src/error.rs b/smolmix/core/src/error.rs index f901823e1a..45ddf665b3 100644 --- a/smolmix/core/src/error.rs +++ b/smolmix/core/src/error.rs @@ -4,15 +4,25 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum SmolmixError { + /// The internal channel between the bridge and the network device was closed. + /// + /// This typically means the bridge task has exited — either because the tunnel + /// was shut down or the mixnet connection was lost. #[error("Channel closed")] ChannelClosed, + /// The IPR handshake has not completed. + /// + /// Returned from [`Tunnel::from_stream`](crate::Tunnel::from_stream) when + /// the provided `IpMixStream` is not in a connected state. #[error("Not connected to IPR")] NotConnected, + /// An error from the Nym SDK (mixnet client, gateway connection, etc.). #[error("Nym SDK error: {0}")] NymSdk(#[from] nym_sdk::Error), + /// An I/O error from the underlying TCP/UDP socket operations. #[error("IO error: {0}")] Io(#[from] std::io::Error), } diff --git a/smolmix/core/src/lib.rs b/smolmix/core/src/lib.rs index f28b53104f..1bca5cd492 100644 --- a/smolmix/core/src/lib.rs +++ b/smolmix/core/src/lib.rs @@ -1,5 +1,8 @@ // Copyright 2024-2026 - Nym Technologies SA +//! TCP/UDP tunnel over the Nym mixnet — standard `TcpStream` and `UdpSocket` +//! types that route all traffic through a mixnet for metadata privacy. + #![doc = include_str!("ARCHITECTURE.md")] mod bridge; @@ -35,12 +38,7 @@ pub use tunnel::Recipient; /// ``` pub use tunnel::TcpStream; -/// A mixnet tunnel providing TCP and UDP socket access. pub use tunnel::Tunnel; - -/// Builder for configuring and creating a [`Tunnel`]. -/// -/// See [`Tunnel::builder()`] for usage. pub use tunnel::TunnelBuilder; /// A UDP socket routed through the mixnet. Supports `send_to` / `recv_from`. diff --git a/smolmix/core/src/tunnel.rs b/smolmix/core/src/tunnel.rs index 0d70a6931d..eb008f18d0 100644 --- a/smolmix/core/src/tunnel.rs +++ b/smolmix/core/src/tunnel.rs @@ -48,7 +48,6 @@ struct TunnelInner { /// (Internet Packet Router). It spawns a background bridge task and a network reactor, /// then provides familiar socket APIs on top. /// -/// /// # Shutdown /// /// Call [`shutdown()`](Self::shutdown) for a clean disconnect. Rust has no async `Drop`, @@ -115,9 +114,6 @@ pub struct Tunnel { /// For full control over the mixnet client (credentials, gateway selection, /// storage, etc.), configure an [`IpMixStream`] directly and pass it to /// [`Tunnel::from_stream()`]. -/// -/// **Note:** deeper builder integration with `MixnetClientBuilder` requires -/// upstream SDK changes to expose `IpMixStream` internals. pub struct TunnelBuilder { ipr_address: Option, } diff --git a/smolmix/dns/src/lib.rs b/smolmix/dns/src/lib.rs index 84409f3b63..e89ede9751 100644 --- a/smolmix/dns/src/lib.rs +++ b/smolmix/dns/src/lib.rs @@ -1,5 +1,4 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only //! DNS resolution through the Nym mixnet. //! @@ -13,10 +12,10 @@ //! //! # How it works //! -//! [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: +//! [hickory-resolver](https://docs.rs/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 @@ -28,7 +27,7 @@ //! hickory's `DnsTcpStream` has a blanket impl for any `futures_io::AsyncRead + //! AsyncWrite`, the wrapped stream satisfies it automatically. //! -//! For UDP, [`SmolmixUdpSocket`] is a thin newtype over `tokio_smoltcp::UdpSocket` +//! For UDP, `SmolmixUdpSocket` is a thin newtype over `tokio_smoltcp::UdpSocket` //! that implements hickory's [`DnsUdpSocket`](hickory_proto::udp::DnsUdpSocket) //! — just delegates `poll_recv_from` and `poll_send_to`. //! @@ -68,7 +67,7 @@ //! use smolmix_dns::{Resolver, ResolverConfig}; //! //! let tunnel = smolmix::Tunnel::new().await?; -//! let resolver = Resolver::with_config(&tunnel, ResolverConfig::quad9()); +//! let resolver = Resolver::with_config(&tunnel, ResolverConfig::cloudflare()); //! # Ok(()) //! # } //! ``` @@ -81,6 +80,7 @@ use std::io; use std::net::SocketAddr; use std::ops::Deref; +use hickory_resolver::config::NameServerConfigGroup; use hickory_resolver::name_server::GenericConnector; use hickory_proto::runtime::TokioHandle; @@ -122,7 +122,17 @@ impl Resolver { } /// Create a resolver with a custom upstream DNS configuration. + /// + /// IPv6 nameservers are filtered out because the tunnel's smoltcp + /// interface is IPv4-only (see [`tokio_smoltcp::NetConfig`]). Passing a + /// config with *only* IPv6 nameservers will cause lookups to fail. pub fn with_config(tunnel: &Tunnel, config: ResolverConfig) -> Self { + // tokio-smoltcp only supports a single IpCidr (IPv4), so contacting + // an IPv6 nameserver panics in smoltcp's wire layer (IP version + // mismatch). Strip IPv6 nameservers until the tunnel supports + // dual-stack. + let config = ipv4_only_nameservers(config); + let provider = SmolmixRuntimeProvider { tunnel: tunnel.clone(), handle: TokioHandle::default(), @@ -172,3 +182,24 @@ pub async fn resolve(tunnel: &Tunnel, host: &str, port: u16) -> io::Result ResolverConfig { + let ipv4_servers: Vec<_> = config + .name_servers() + .iter() + .filter(|ns| ns.socket_addr.is_ipv4()) + .cloned() + .collect(); + + ResolverConfig::from_parts( + config.domain().cloned(), + config.search().to_vec(), + NameServerConfigGroup::from(ipv4_servers), + ) +} diff --git a/smolmix/dns/src/provider.rs b/smolmix/dns/src/provider.rs index 60b0997203..459e118d39 100644 --- a/smolmix/dns/src/provider.rs +++ b/smolmix/dns/src/provider.rs @@ -1,5 +1,4 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only +// Copyright 2024-2026 - Nym Technologies SA //! hickory-resolver [`RuntimeProvider`] routing all DNS I/O through a [`Tunnel`]. diff --git a/smolmix/hyper/src/connector.rs b/smolmix/hyper/src/connector.rs index 0bc240eaa5..952864655a 100644 --- a/smolmix/hyper/src/connector.rs +++ b/smolmix/hyper/src/connector.rs @@ -1,11 +1,11 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only +// Copyright 2024-2026 - Nym Technologies SA //! [`tower::Service`] connector routing TCP + TLS through a [`Tunnel`]. use std::future::Future; use std::io; use std::pin::Pin; +use std::sync::Arc; use std::task::{Context, Poll}; use hyper::Uri; @@ -13,6 +13,7 @@ use hyper_util::rt::TokioIo; use tower::Service; use smolmix::Tunnel; +use smolmix_dns::Resolver; use smolmix_tls::TlsConnector; use crate::tls_stream::MaybeTlsStream; @@ -21,20 +22,30 @@ use crate::tls_stream::MaybeTlsStream; /// /// Implements [`tower::Service`] so it plugs directly into hyper-util's `Client`. /// DNS resolution also goes through the tunnel, preventing hostname leaks. +/// +/// DNS lookups are cached internally via a shared [`smolmix_dns::Resolver`] — +/// repeat requests to the same host reuse cached records (subject to TTL). +/// +/// Use this directly when you need a custom body type (e.g. `Full` for +/// POST requests) — see the [crate-level docs](crate#sending-request-bodies-post-put-etc) +/// for an example. #[derive(Clone)] pub struct SmolmixConnector { tunnel: Tunnel, tls: TlsConnector, + resolver: Arc, } impl SmolmixConnector { /// Create a new connector for the given tunnel. /// - /// Sets up a TLS connector with the standard webpki root certificates. + /// Sets up a TLS connector with the standard webpki root certificates and + /// a DNS resolver that caches lookups across requests. pub fn new(tunnel: &Tunnel) -> Self { Self { tunnel: tunnel.clone(), tls: smolmix_tls::connector(), + resolver: Arc::new(Resolver::new(tunnel)), } } } @@ -51,6 +62,7 @@ impl Service for SmolmixConnector { fn call(&mut self, uri: Uri) -> Self::Future { let tunnel = self.tunnel.clone(); let tls = self.tls.clone(); + let resolver = self.resolver.clone(); Box::pin(async move { let scheme = uri.scheme_str().unwrap_or("https"); @@ -62,7 +74,7 @@ impl Service for SmolmixConnector { .port_u16() .unwrap_or(if scheme == "https" { 443 } else { 80 }); - let addrs = smolmix_dns::resolve(&tunnel, &host, port).await?; + let addrs = resolver.resolve(&host, port).await?; let addr = addrs .into_iter() .next() diff --git a/smolmix/hyper/src/lib.rs b/smolmix/hyper/src/lib.rs index f3a1b4fc89..10e241291c 100644 --- a/smolmix/hyper/src/lib.rs +++ b/smolmix/hyper/src/lib.rs @@ -1,5 +1,4 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only //! HTTP client routing all traffic through the Nym mixnet. //! @@ -19,7 +18,7 @@ //! //! ```text //! SmolmixConnector::call(uri) -//! → smolmix_dns::resolve(host, port) DNS through tunnel +//! → resolver.resolve(host, port) DNS through tunnel (cached) //! → tunnel.tcp_connect(addr) TCP through mixnet //! → smolmix_tls::connect_with(tls, tcp, host) TLS if https //! → MaybeTlsStream::Plain { TcpStream } @@ -116,12 +115,7 @@ pub use http_body_util::Empty as EmptyBody; /// Re-exported hyper types for building requests without depending on hyper directly. pub use hyper::{Request, Response, StatusCode, Uri}; -/// The connector implementing `tower::Service`. Use this directly when you -/// need a custom body type (e.g. `Full` for POST requests). pub use connector::SmolmixConnector; - -/// A stream that may or may not be wrapped in TLS. Returned by [`SmolmixConnector`] -/// — you won't normally interact with this directly. pub use tls_stream::MaybeTlsStream; /// Inner hyper-util client type alias for readability. diff --git a/smolmix/hyper/src/tls_stream.rs b/smolmix/hyper/src/tls_stream.rs index 8d3ae44ecc..caf801710f 100644 --- a/smolmix/hyper/src/tls_stream.rs +++ b/smolmix/hyper/src/tls_stream.rs @@ -1,5 +1,4 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only +// Copyright 2024-2026 - Nym Technologies SA //! TLS stream abstraction for plain and encrypted connections. @@ -13,6 +12,9 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; pin_project! { /// A stream that may or may not be wrapped in TLS. + /// + /// Returned by [`SmolmixConnector`](crate::SmolmixConnector) — you won't + /// normally interact with this directly. #[project = MaybeTlsProj] pub enum MaybeTlsStream { Plain { #[pin] inner: tokio_smoltcp::TcpStream }, diff --git a/smolmix/tls/src/lib.rs b/smolmix/tls/src/lib.rs index 46bc03eb05..993a1a383f 100644 --- a/smolmix/tls/src/lib.rs +++ b/smolmix/tls/src/lib.rs @@ -1,5 +1,4 @@ // Copyright 2024-2026 - Nym Technologies SA -// SPDX-License-Identifier: GPL-2.0-only //! Shared TLS configuration for smolmix tunneled connections. //!