Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71bff333db | |||
| 2b0aaed774 | |||
| e748cb4f14 | |||
| 028e60d0ca | |||
| 46974ffa2a | |||
| a2ef02e512 | |||
| b19f56bd74 | |||
| 8300915c26 | |||
| 0599726bff | |||
| 99063f372f | |||
| 95a46600fd |
@@ -285,7 +285,14 @@ const AnimationBlock = ({ type }: { type: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sdks = [
|
||||
type Sdk = {
|
||||
name: string;
|
||||
description: string;
|
||||
href: string;
|
||||
children?: { name: string; href: string }[];
|
||||
};
|
||||
|
||||
const sdks: Sdk[] = [
|
||||
{
|
||||
name: "Rust SDK",
|
||||
description:
|
||||
@@ -293,10 +300,17 @@ const sdks = [
|
||||
href: "/developers/rust",
|
||||
},
|
||||
{
|
||||
name: "smolmix",
|
||||
name: "smolmix & connectors",
|
||||
description:
|
||||
"TCP/UDP tunnel over the Mixnet. Userspace smoltcp stack exposing AsyncRead/AsyncWrite TcpStream and UdpSocket types.",
|
||||
"Rust crate family for routing networking through the Mixnet: TCP/UDP tunnels, DNS, TLS, and HTTP. Pick the layer you need.",
|
||||
href: "/developers/smolmix",
|
||||
children: [
|
||||
{ name: "smolmix-tunnel", href: "/developers/smolmix/tunnel" },
|
||||
{ name: "smolmix-dns", href: "/developers/smolmix/dns" },
|
||||
{ name: "smolmix-tls", href: "/developers/smolmix/tls" },
|
||||
{ name: "smolmix-hyper", href: "/developers/smolmix/hyper" },
|
||||
{ name: "Building on smolmix", href: "/developers/smolmix/extending" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "TypeScript SDK",
|
||||
@@ -427,46 +441,81 @@ export const LandingPage = () => {
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0" }}>
|
||||
{sdks.map((sdk, i) => (
|
||||
<Link key={i} href={sdk.href} style={{ textDecoration: "none" }}>
|
||||
<div
|
||||
className="landing-card"
|
||||
style={{
|
||||
padding: "1rem 1.2rem",
|
||||
border: "1px solid var(--border)",
|
||||
marginTop: i > 0 ? "-1px" : undefined,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
transition: "background-color 0.15s",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
className="landing-heading"
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{sdk.name}
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
border: "1px solid var(--border)",
|
||||
marginTop: i > 0 ? "-1px" : undefined,
|
||||
}}
|
||||
>
|
||||
<Link href={sdk.href} style={{ textDecoration: "none" }}>
|
||||
<div
|
||||
className="landing-card"
|
||||
style={{
|
||||
padding: "1rem 1.2rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
transition: "background-color 0.15s",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
className="landing-heading"
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{sdk.name}
|
||||
</span>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
color: "var(--textMuted)",
|
||||
margin: "0.25rem 0 0 0",
|
||||
}}
|
||||
>
|
||||
{sdk.description}
|
||||
</p>
|
||||
</div>
|
||||
<span style={{ color: "var(--textMuted)", fontSize: "1rem" }}>
|
||||
›
|
||||
</span>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
color: "var(--textMuted)",
|
||||
margin: "0.25rem 0 0 0",
|
||||
}}
|
||||
>
|
||||
{sdk.description}
|
||||
</p>
|
||||
</div>
|
||||
<span style={{ color: "var(--textMuted)", fontSize: "1rem" }}>
|
||||
›
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</Link>
|
||||
{sdk.children && (
|
||||
<div
|
||||
style={{
|
||||
padding: "0 1.2rem 0.8rem 1.2rem",
|
||||
display: "flex",
|
||||
gap: "0.4rem",
|
||||
flexWrap: "wrap",
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "0.78rem",
|
||||
}}
|
||||
>
|
||||
{sdk.children.map((c) => (
|
||||
<Link
|
||||
key={c.href}
|
||||
href={c.href}
|
||||
className="landing-chip"
|
||||
style={{
|
||||
color: "var(--textMuted)",
|
||||
textDecoration: "none",
|
||||
padding: "0.2rem 0.55rem",
|
||||
border: "1px solid var(--border)",
|
||||
transition: "background-color 0.15s",
|
||||
}}
|
||||
>
|
||||
{c.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"type": "separator",
|
||||
"title": "Rust"
|
||||
},
|
||||
"smolmix": "smolmix",
|
||||
"smolmix": "smolmix & connectors",
|
||||
"rust": "nym-sdk",
|
||||
|
||||
"-": {
|
||||
|
||||
@@ -19,7 +19,7 @@ If you're new, read **[Choosing an Approach](/developers/integrations)** first.
|
||||
| Crate/library | Language | Use it for |
|
||||
|---|---|---|
|
||||
| [`nym-sdk`](/developers/rust) | Rust | E2E messaging, `AsyncRead`/`AsyncWrite` streams, client pooling. Start with the [Tour](/developers/rust/tour). |
|
||||
| [`smolmix`](/developers/smolmix) | Rust | `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tungstenite`. |
|
||||
| [`smolmix` & connectors](/developers/smolmix) | Rust | `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack, plus companion crates [`smolmix-dns`](/developers/smolmix/dns), [`smolmix-tls`](/developers/smolmix/tls), [`smolmix-hyper`](/developers/smolmix/hyper). |
|
||||
| [`mix-fetch`](/developers/mix-fetch) | TypeScript | `fetch()`-compatible API for browser HTTP(S) requests over the Mixnet. |
|
||||
| [TypeScript SDK](/developers/typescript) | TypeScript | Browser-side Mixnet Client (raw messaging) and Nyx Smart Contracts. |
|
||||
| [Standalone Clients](/developers/clients) | Language-agnostic | SOCKS5 and WebSocket binaries for piping traffic through the Mixnet without an SDK. |
|
||||
|
||||
@@ -16,7 +16,7 @@ Any application that integrates with Nym sends its traffic through the Mixnet vi
|
||||
|
||||
| | **End-to-end** (both sides run Nym) | **Proxy mode** (Nym → clearnet exit) |
|
||||
|---|---|---|
|
||||
| **Rust** (native / desktop / CLI) | [`nym-sdk`](/developers/rust) (Stream, Mixnet, Client Pool) | [`smolmix`](/developers/smolmix) (TCP / UDP) · [`nym-sdk`](/developers/rust) SOCKS client |
|
||||
| **Rust** (native / desktop / CLI) | [`nym-sdk`](/developers/rust) (Stream, Mixnet, Client Pool) | [`smolmix` & connectors](/developers/smolmix) (TCP / UDP, plus DNS / TLS / HTTP via companion crates) · [`nym-sdk`](/developers/rust) SOCKS client |
|
||||
| **TypeScript** (browser) | [TypeScript SDK](/developers/typescript) (WASM Mixnet Client, messaging only) | [`mix-fetch`](/developers/mix-fetch) (HTTP) |
|
||||
| **Mobile** (iOS / Android) | via [`nym-vpn-client`](#mobile) (uniffi + cargo-swift / cargo-ndk) | via [`nym-vpn-client`](#mobile) (uniffi + cargo-swift / cargo-ndk) |
|
||||
|
||||
@@ -24,7 +24,7 @@ Any application that integrates with Nym sends its traffic through the Mixnet vi
|
||||
|
||||
Different runtimes have different transport constraints: a browser cannot open raw sockets or access the filesystem, while a desktop app can.
|
||||
|
||||
- **Native / Desktop / CLI**: full access to system networking and persistent storage. Use [`nym-sdk`](/developers/rust) (the Rust SDK) for E2E messaging or byte streams, or [`smolmix`](/developers/smolmix) for TCP/UDP socket-shaped access in proxy mode.
|
||||
- **Native / Desktop / CLI**: full access to system networking and persistent storage. Use [`nym-sdk`](/developers/rust) (the Rust SDK) for E2E messaging or byte streams, or the [`smolmix` family](/developers/smolmix) for socket-level (TCP/UDP), DNS, TLS, or HTTP access in proxy mode.
|
||||
- **Browser**: restricted to WebSockets, Web Transport, and `fetch`; HTTPS-only mixed-content rules; no filesystem access. Use [`mix-fetch`](/developers/mix-fetch) for HTTP(S) requests, or the [TypeScript SDK](/developers/typescript)'s WASM Mixnet Client for raw message passing.
|
||||
|
||||
### Mobile
|
||||
@@ -57,7 +57,9 @@ Once traffic leaves the Exit Gateway, it travels over the public internet to the
|
||||
## Where to go next
|
||||
|
||||
- **Rust, E2E messaging or byte streams**: [`nym-sdk`](/developers/rust)
|
||||
- **Rust, TCP/UDP socket replacements**: [`smolmix`](/developers/smolmix)
|
||||
- **Rust, TCP/UDP socket replacements**: [`smolmix`](/developers/smolmix/tunnel)
|
||||
- **Rust, HTTP(S) requests**: [`smolmix-hyper`](/developers/smolmix/hyper)
|
||||
- **Rust, hostname resolution / TLS without HTTP**: [`smolmix-dns`](/developers/smolmix/dns), [`smolmix-tls`](/developers/smolmix/tls)
|
||||
- **Browser, HTTP(S) requests**: [`mix-fetch`](/developers/mix-fetch)
|
||||
- **Browser, raw mixnet messaging or Nyx smart contracts**: [TypeScript SDK](/developers/typescript)
|
||||
- **Background on Sphinx, gateways, and the mixnet itself**: [Key Concepts](/developers/concepts)
|
||||
|
||||
@@ -95,12 +95,12 @@ The first call bootstraps the WASM Nym client (gateway handshake, key generation
|
||||
|
||||
## When to use mix-fetch
|
||||
|
||||
| | mix-fetch | WASM Mixnet Client | smolmix | Plain fetch (no mixnet) |
|
||||
| | mix-fetch | WASM Mixnet Client | [smolmix family](/developers/smolmix) | Plain fetch (no mixnet) |
|
||||
|---|---|---|---|---|
|
||||
| **Runtime** | Browser, Node.js | Browser | Native (Rust) | Anywhere |
|
||||
| **Architecture** | Proxy (Network Requester → destination) | E2E (both sides Nym) | Proxy | Direct |
|
||||
| **API shape** | `fetch()` replacement | Send/recv text or binary messages | `TcpStream` / `UdpSocket` | `fetch()` |
|
||||
| **HTTP support** | Yes | No | Yes (via `hyper` over `TcpStream`) | Yes |
|
||||
| **API shape** | `fetch()` replacement | Send/recv text or binary messages | `TcpStream` / `UdpSocket`, or `hyper`-style HTTP via [`smolmix-hyper`](/developers/smolmix/hyper) | `fetch()` |
|
||||
| **HTTP support** | Yes | No | Yes (via [`smolmix-hyper`](/developers/smolmix/hyper)) | Yes |
|
||||
| **Sender unlinkability** | Strong (mixnet) | Strong (mixnet) | Strong (mixnet) | None |
|
||||
| **Concurrency** | 10 per host | Unlimited | Unlimited | Unlimited |
|
||||
|
||||
|
||||
@@ -49,5 +49,5 @@ For an overview of what the SDK can do, see the **[Tour](./rust/tour)**. For set
|
||||
|
||||
For proxy-mode integrations (reaching third-party services through an Exit Gateway), see also:
|
||||
|
||||
- [**`smolmix`**](/developers/smolmix): `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem.
|
||||
- [**`smolmix` & connectors**](/developers/smolmix): `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack, with companion crates ([`smolmix-dns`](/developers/smolmix/dns), [`smolmix-tls`](/developers/smolmix/tls), [`smolmix-hyper`](/developers/smolmix/hyper)) for DNS, TLS, and HTTP. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem.
|
||||
- [**SOCKS Client**](./rust/mixnet): SOCKS4/4a/5 proxy via the Exit Gateway's Network Requester. Works with any SOCKS-capable application without code changes.
|
||||
|
||||
@@ -1,130 +1,27 @@
|
||||
---
|
||||
title: "smolmix: TCP/UDP Over the Nym Mixnet"
|
||||
description: "A userspace IP tunnel that provides standard TcpStream and UdpSocket types over the Nym mixnet. Compatible with the async tokio Rust ecosystem."
|
||||
title: "smolmix & connectors"
|
||||
description: "Rust crate family for routing networking through the Nym mixnet. TCP/UDP tunnels, DNS, TLS, and HTTP."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-04-29"
|
||||
lastUpdated: "2026-05-13"
|
||||
---
|
||||
|
||||
# smolmix
|
||||
# smolmix & connectors
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
import { RUST_MSRV } from '../../components/versions'
|
||||
A family of Rust crates for routing standard networking through the Nym mixnet. Each crate handles one concern, so you only pull in what you need.
|
||||
|
||||
`smolmix` is a TCP/UDP tunnel over the Nym mixnet. It uses a userspace network stack [`smoltcp`](https://docs.rs/smoltcp/latest/smoltcp/) to provide `TcpStream` and `UdpSocket` types that work with the async [`tokio`](https://docs.rs/tokio) Rust ecosystem e.g. [`tokio-rustls`](https://docs.rs/tokio-rustls), [`hyper`](https://docs.rs/hyper), [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite), etc.
|
||||
| Crate | Use it for |
|
||||
|---|---|
|
||||
| [`smolmix`](/developers/smolmix/tunnel) | TCP/UDP tunnel. The base layer everything else builds on, exposing `TcpStream` and `UdpSocket` types compatible with the async tokio ecosystem. |
|
||||
| [`smolmix-dns`](/developers/smolmix/dns) | Tunneled hostname resolution. Pairs with `smolmix` to avoid leaking DNS queries to the local network. |
|
||||
| [`smolmix-tls`](/developers/smolmix/tls) | TLS over a `smolmix` `TcpStream`, with webpki root certificates pre-configured. |
|
||||
| [`smolmix-hyper`](/developers/smolmix/hyper) | Drop-in HTTP client wrapping `hyper-util`. Bundles DNS, TLS, and HTTP through the mixnet. |
|
||||
| [Building on smolmix](/developers/smolmix/extending) | Patterns for integrating libraries we don't ship a connector for (WebSocket, libp2p, custom protocols). |
|
||||
|
||||
The `TcpStream` type implements tokio's `AsyncRead`/`AsyncWrite` traits and `UdpSocket` provides `send_to`/`recv_from` for datagrams.
|
||||
All crates share the workspace version (`1.21.0`). Full API on docs.rs: [`smolmix`](https://docs.rs/smolmix), [`smolmix-dns`](https://docs.rs/smolmix-dns), [`smolmix-tls`](https://docs.rs/smolmix-tls), [`smolmix-hyper`](https://docs.rs/smolmix-hyper).
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Your application (TLS, HTTP, WebSocket, DNS, etc.) │
|
||||
│ └─ smolmix::TcpStream / UdpSocket │
|
||||
│ └─ smoltcp (userspace TCP/IP) │
|
||||
│ └─ Nym mixnet → IPR exit gateway → internet │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
## Picking the right layer
|
||||
|
||||
Traffic exits the mixnet at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway. The exit IP is the gateway's, not yours.
|
||||
Most users want [`smolmix-hyper`](/developers/smolmix/hyper): a familiar HTTP client that quietly routes DNS, TCP, and TLS through the mixnet. Reach for the lower-level crates when you need something hyper doesn't cover, e.g. WebSocket, raw UDP, libp2p, or a custom protocol over TCP.
|
||||
|
||||
## Runtime and platform support
|
||||
### Other runtimes
|
||||
`smolmix` currently requires `tokio`. The internal pipeline is tokio-based: the bridge task that shuttles packets to the mixnet, the Nym SDK's `MixnetClient`, and the [`tokio-smoltcp`](https://docs.rs/tokio-smoltcp) reactor that drives the userspace TCP/IP stack all run on the tokio runtime.
|
||||
|
||||
This means `smolmix` is not directly compatible with alternative async runtimes like [`smol`](https://docs.rs/smol) or [`async-std`](https://docs.rs/async-std). If you need to use `smolmix` from another runtime, the [`async-compat`](https://docs.rs/async-compat) crate can bridge the gap.
|
||||
|
||||
### Non-native `smolmix`
|
||||
A WASM version of `smolmix` is planned for a future release.
|
||||
|
||||
## Installation
|
||||
|
||||
Add `smolmix` to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix = "1.21.0"
|
||||
nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
```
|
||||
|
||||
`tokio` is a transitive dependency of smolmix, but you need to enable `rt-multi-thread` (the default runtime spun up by `#[tokio::main]`) and `macros` (for the `#[tokio::main]` attribute itself).
|
||||
|
||||
`nym-bin-common` is optional but recommended: it sets up [`tracing`](https://docs.rs/tracing) logging so you can see mixnet connection progress.
|
||||
|
||||
**Minimum Rust version:** {RUST_MSRV}+
|
||||
|
||||
### From Git
|
||||
|
||||
For unreleased changes, import directly from the repository:
|
||||
|
||||
```toml
|
||||
smolmix = { git = "https://github.com/nymtech/nym", branch = "develop" }
|
||||
```
|
||||
|
||||
## When to use smolmix
|
||||
|
||||
| | smolmix | Stream module | mixFetch | SOCKS client |
|
||||
|---|---|---|---|---|
|
||||
| **Layer** | Transport (TCP/UDP) | Message (multiplexed streams) | HTTP | TCP (SOCKS proxy) |
|
||||
| **Controls both sides?** | No (proxy mode) | Yes (E2E) | No (proxy mode) | No (proxy mode) |
|
||||
| **API** | `TcpStream`, `UdpSocket` | `AsyncRead + AsyncWrite` | `fetch()` replacement | SOCKS4/5 protocol |
|
||||
| **Composability** | Full: TLS, HTTP, WebSocket, DNS, etc. stack on top | Byte streams only | HTTP(S) only | Application-dependent |
|
||||
| **Best for** | Reaching external services from Rust with standard networking | Peer-to-peer / E2E protocols between Nym clients | Browser HTTP requests | Legacy apps with SOCKS support |
|
||||
|
||||
## Security model
|
||||
|
||||
<Callout type="warning">
|
||||
Traffic is Sphinx-encrypted inside the mixnet. Past the Exit Gateway, it travels over the public internet to the destination, the same as any other server-initiated connection. Protect the payload at the application layer with TLS ([`rustls`](https://docs.rs/rustls)), Noise Protocol ([`snow`](https://docs.rs/snow)), or equivalent, as you would on a direct connection.
|
||||
</Callout>
|
||||
|
||||
### What's protected
|
||||
|
||||
| Segment | Mixnet encryption | What's visible |
|
||||
|---|---|---|
|
||||
| Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination |
|
||||
| Inside the mixnet (entry + 3 mix layers + exit) | Sphinx (layered) | Each node only knows prev/next hop |
|
||||
| Exit gateway (IPR) | Sphinx removed, raw IP packet exposed | IPR sees destination IP + port. Payload depends on your application layer (see below). |
|
||||
| IPR → remote host | None (Sphinx is mixnet-only) | Remote host sees IPR's IP, not yours |
|
||||
|
||||
The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. At the exit gateway, the Sphinx layers are stripped and the original IP packet is forwarded to the destination. This is analogous to how a Tor exit node or VPN endpoint unwraps its tunnel.
|
||||
|
||||
What's inside that IP packet is up to you. If you connect with TLS (as in the [TCP example](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs)), the IPR sees encrypted TLS ciphertext going to a destination IP: it knows where but not what. If you send plaintext HTTP, the IPR can read the full request and response.
|
||||
|
||||
### Trust boundaries
|
||||
|
||||
- You trust the mixnet to provide unlinkability between sender and receiver. This is enforced cryptographically by the Sphinx packet format and mixing.
|
||||
- You trust the IPR exit gateway in the same way you trust a VPN exit or Tor exit node: it can inspect your raw IP packets. The difference is that the IPR doesn't know who is sending the traffic (the mixnet hides your identity).
|
||||
- **Application-layer encryption closes the gap.** TLS, Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the IPR. It can see destination IP and port, but not payload content.
|
||||
|
||||
### Comparison with other privacy tools
|
||||
|
||||
| | smolmix | Tor | VPN |
|
||||
|---|---|---|---|
|
||||
| **Exit node sees traffic?** | Yes (encrypt it) | Yes (encrypt it) | Yes (encrypt it) |
|
||||
| **Exit node knows sender?** | No (mixnet hides identity) | No (onion routing) | Yes (VPN provider knows) |
|
||||
| **Timing analysis resistance** | Strong (mixing, cover traffic) | Weak (low-latency) | None |
|
||||
| **UDP support** | Yes | No (TCP only) | Yes |
|
||||
|
||||
## Examples
|
||||
|
||||
Runnable examples in [`smolmix/core/examples/`](https://github.com/nymtech/nym/tree/develop/smolmix/core/examples). Each is self-contained; read the `//!` doc comments at the top of each file for a walkthrough.
|
||||
|
||||
```sh
|
||||
cargo run -p smolmix --example <name>
|
||||
```
|
||||
|
||||
All examples accept `--ipr <ADDRESS>` to target a specific exit node (pass a `Recipient` address to `Tunnel::builder().ipr_address()`).
|
||||
|
||||
| Example | Source | What it demonstrates |
|
||||
|---|---|---|
|
||||
| UDP | [`udp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/udp.rs) | DNS lookup via [`hickory-proto`](https://docs.rs/hickory-proto), sending a raw UDP query to `1.1.1.1:53` through the mixnet. Runs a clearnet [`hickory-resolver`](https://docs.rs/hickory-resolver) lookup alongside it to compare resolved IPs and latency |
|
||||
| TCP | [`tcp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs) | HTTPS request via [`hyper`](https://docs.rs/hyper) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Fetches Cloudflare's `/cdn-cgi/trace` to show that the exit IP differs from clearnet |
|
||||
| WebSocket | [`websocket.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/websocket.rs) | WebSocket echo against `ws.postman-echo.com` via [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Runs the same stack over clearnet first, so the only thing that changes between runs is the underlying TCP stream |
|
||||
| TCP download | [`tcp_download.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp_download.rs) | DNS-over-mixnet + multi-request HTTP/1.1 download over a single keep-alive connection, the full real-world pattern |
|
||||
|
||||
## Architecture
|
||||
|
||||
The internal stack (smoltcp reactor, device adapter, bridge task, packet flow) is documented in [`ARCHITECTURE.md`](https://github.com/nymtech/nym/blob/develop/smolmix/core/src/ARCHITECTURE.md). This is also the crate-level documentation on docs.rs.
|
||||
|
||||
## API reference
|
||||
|
||||
Full API documentation is available on [docs.rs/smolmix](https://docs.rs/smolmix).
|
||||
If you're integrating a library not listed above, the [Building on smolmix](/developers/smolmix/extending) guide shows the patterns: most tokio-native libraries plug into `smolmix::TcpStream` directly, with no glue.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tunnel": "smolmix-tunnel",
|
||||
"dns": "smolmix-dns",
|
||||
"tls": "smolmix-tls",
|
||||
"hyper": "smolmix-hyper",
|
||||
"extending": "Building on smolmix"
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: "smolmix-dns: DNS Resolution Through the Mixnet"
|
||||
description: "Resolve hostnames privately using smolmix-dns. Routes all DNS queries through the Nym mixnet to prevent hostname leaks."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-04-28"
|
||||
---
|
||||
|
||||
# smolmix-dns
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
If you resolve hostnames using the OS resolver or a clearnet DNS library, the queries travel over your local network, leaking which domains you're visiting even though the TCP traffic goes through the mixnet. `smolmix-dns` routes all DNS queries (both UDP and TCP fallback) through the tunnel.
|
||||
|
||||
## Installation
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix = "1.21.0"
|
||||
smolmix-dns = "1.21.0"
|
||||
```
|
||||
|
||||
Both crates share the workspace version. Full API docs: [docs.rs/smolmix-dns](https://docs.rs/smolmix-dns).
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use smolmix::Tunnel;
|
||||
use smolmix_dns::Resolver;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tunnel = 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 method returning SocketAddr with port:
|
||||
let addrs = resolver.resolve("nymtech.net", 443).await?;
|
||||
println!("nymtech.net:443 → {addrs:?}");
|
||||
|
||||
tunnel.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
<Callout type="info">
|
||||
**Reuse your `Resolver` across requests.** hickory-resolver maintains an internal LRU cache for DNS responses. Creating a new `Resolver` per lookup (or using the free function `smolmix_dns::resolve()`) bypasses caching entirely: every lookup triggers a full round-trip through the mixnet.
|
||||
</Callout>
|
||||
|
||||
```rust
|
||||
// Good: resolver created once, cache shared across lookups
|
||||
let resolver = Resolver::new(&tunnel);
|
||||
let a = resolver.resolve("example.com", 443).await?;
|
||||
let b = resolver.resolve("example.com", 80).await?; // cache hit
|
||||
|
||||
// Avoid: creates a fresh resolver (and empty cache) each time
|
||||
let a = smolmix_dns::resolve(&tunnel, "example.com", 443).await?;
|
||||
let b = smolmix_dns::resolve(&tunnel, "example.com", 80).await?; // cache miss
|
||||
```
|
||||
|
||||
## Custom upstream DNS
|
||||
|
||||
By default, queries go to Quad9 (`9.9.9.9`). Use `Resolver::with_config()` for other upstreams:
|
||||
|
||||
```rust
|
||||
use smolmix_dns::{Resolver, ResolverConfig};
|
||||
|
||||
// Cloudflare
|
||||
let resolver = Resolver::with_config(&tunnel, ResolverConfig::cloudflare());
|
||||
|
||||
// Google
|
||||
let resolver = Resolver::with_config(&tunnel, ResolverConfig::google());
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```sh
|
||||
cargo run -p smolmix-dns --example resolve
|
||||
cargo run -p smolmix-dns --example resolve -- --ipr <IPR_ADDRESS>
|
||||
```
|
||||
|
||||
Source: [`dns/examples/resolve.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/dns/examples/resolve.rs)
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "Building on smolmix"
|
||||
description: "How to build your own protocol adapters on top of smolmix. Patterns for WebSocket, libp2p, and any AsyncRead+AsyncWrite library."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-04-28"
|
||||
---
|
||||
|
||||
# Building on smolmix
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
The published companion crates ([`smolmix-dns`](/developers/smolmix/dns), [`smolmix-tls`](/developers/smolmix/tls), [`smolmix-hyper`](/developers/smolmix/hyper)) cover the most common use cases. For other protocols, the same principle applies: if a library accepts `AsyncRead + AsyncWrite` (or can be adapted to it), it works over smolmix with minimal glue.
|
||||
|
||||
## The pattern
|
||||
|
||||
Every companion crate follows the same recipe:
|
||||
|
||||
1. Find the library's I/O extension point (trait or type parameter)
|
||||
2. Bridge smolmix's `TcpStream` to whatever I/O trait the library expects
|
||||
3. Expose a thin wrapper that hides the bridging
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Library's extension point I/O bridge │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ hickory RuntimeProvider → AsyncIoTokioAsStd │
|
||||
│ tokio-rustls TlsConnector → (direct, same │
|
||||
│ AsyncRead/Write) │
|
||||
│ tower::Service<Uri> → TokioIo + pin enum │
|
||||
│ tokio-tungstenite → (direct) │
|
||||
│ libp2p Transport → tokio_util::compat │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Most tokio-native libraries need zero bridging: `smolmix::TcpStream` already implements `tokio::io::AsyncRead + AsyncWrite`. Libraries expecting `futures_io::AsyncRead + AsyncWrite` (like hickory and libp2p) need a one-line adapter.
|
||||
|
||||
## Examples in the repository
|
||||
|
||||
These self-contained examples demonstrate building protocol integration without a dedicated crate:
|
||||
|
||||
| Pattern | Example | Lines of glue | What it demonstrates |
|
||||
|---|---|---:|---|
|
||||
| WebSocket | [`websocket.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/websocket.rs) | ~10 | Composing DNS + TCP + TLS + protocol upgrade. `tokio-tungstenite` accepts any `AsyncRead + AsyncWrite` directly. |
|
||||
| libp2p Transport | [`libp2p_ping.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/libp2p_ping.rs) | ~80 | Implementing libp2p's `Transport` trait. Dial-only (no listening). Uses `tokio_util::compat` to bridge tokio↔futures I/O. |
|
||||
|
||||
Run them with:
|
||||
|
||||
```sh
|
||||
cargo run -p smolmix --example websocket
|
||||
cargo run -p smolmix --example libp2p_ping
|
||||
```
|
||||
|
||||
## WebSocket: the minimal case
|
||||
|
||||
`tokio-tungstenite` takes any stream implementing `AsyncRead + AsyncWrite`. Since `smolmix-tls` returns a `TlsStream<TcpStream>` which implements those traits, no adapter is needed:
|
||||
|
||||
```rust
|
||||
use smolmix::Tunnel;
|
||||
use smolmix_dns::Resolver;
|
||||
use smolmix_tls::connect;
|
||||
use tokio_tungstenite::client_async;
|
||||
|
||||
let tunnel = Tunnel::new().await?;
|
||||
let resolver = Resolver::new(&tunnel);
|
||||
|
||||
let addrs = resolver.resolve("echo.websocket.org", 443).await?;
|
||||
let tcp = tunnel.tcp_connect(addrs[0]).await?;
|
||||
let tls = connect(tcp, "echo.websocket.org").await?;
|
||||
|
||||
let (mut ws, _) = client_async("wss://echo.websocket.org", tls).await?;
|
||||
// ws is a fully functional WebSocket connection over the mixnet
|
||||
```
|
||||
|
||||
## libp2p: implementing a Transport
|
||||
|
||||
libp2p requires a `Transport` trait implementation. The key insight: libp2p uses `futures_io::AsyncRead + AsyncWrite`, not tokio's version. The bridge is `tokio_util::compat::TokioAsyncReadCompatExt`:
|
||||
|
||||
```rust
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
|
||||
let tcp = tunnel.tcp_connect(addr).await?;
|
||||
let compat_stream = tcp.compat(); // tokio → futures_io bridge
|
||||
// Now pass compat_stream to libp2p's noise/yamux upgrade
|
||||
```
|
||||
|
||||
See the full implementation in [`libp2p_ping.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/libp2p_ping.rs): it handles multiaddr parsing, DNS resolution through the tunnel, and the noise+yamux handshake.
|
||||
|
||||
## When to publish a crate vs use an example
|
||||
|
||||
<Callout type="info">
|
||||
Publish a companion crate when the integration involves non-obvious trait implementations or configuration that users would struggle to get right on their own (e.g. hickory's `RuntimeProvider`). Keep it as an example when the glue is straightforward copy-paste (e.g. WebSocket, where the library already accepts the right traits).
|
||||
</Callout>
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: "smolmix-hyper: HTTP Client Over the Mixnet"
|
||||
description: "Make HTTP and HTTPS requests through the Nym mixnet using smolmix-hyper. Full hyper API with DNS, TCP, and TLS routed through the tunnel."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-04-28"
|
||||
---
|
||||
|
||||
# smolmix-hyper
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
`smolmix-hyper` is the highest-level companion crate: a familiar HTTP client that routes DNS resolution, TCP connections, and TLS handshakes through the mixnet. This is what most users want: "fetch a URL anonymously".
|
||||
|
||||
## Installation
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix = "1.21.0"
|
||||
smolmix-hyper = "1.21.0"
|
||||
```
|
||||
|
||||
Both crates share the workspace version. Full API docs: [docs.rs/smolmix-hyper](https://docs.rs/smolmix-hyper).
|
||||
|
||||
You also need a rustls crypto provider installed. Add this at the start of `main()`:
|
||||
|
||||
```rust
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
```
|
||||
|
||||
## GET request
|
||||
|
||||
```rust
|
||||
use smolmix::Tunnel;
|
||||
use smolmix_hyper::{Client, Request, EmptyBody, BodyExt};
|
||||
use bytes::Bytes;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
|
||||
let tunnel = Tunnel::new().await?;
|
||||
let client = Client::new(&tunnel);
|
||||
|
||||
let req = Request::get("https://cloudflare.com/cdn-cgi/trace")
|
||||
.header("Host", "cloudflare.com")
|
||||
.body(EmptyBody::<Bytes>::new())?;
|
||||
|
||||
let resp = client.request(req).await?;
|
||||
println!("Status: {}", resp.status());
|
||||
|
||||
let body = resp.into_body().collect().await?.to_bytes();
|
||||
println!("{}", String::from_utf8_lossy(&body));
|
||||
|
||||
tunnel.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The `ip=` line in the response shows the IPR exit gateway's IP. Your real IP is hidden by the mixnet.
|
||||
|
||||
## POST request
|
||||
|
||||
The convenience `Client` wrapper uses `Empty<Bytes>` as its body type (suitable for GET). For POST/PUT, construct a client with a different body type using `SmolmixConnector` directly:
|
||||
|
||||
```rust
|
||||
use http_body_util::Full;
|
||||
use hyper_util::{client::legacy, rt::TokioExecutor};
|
||||
use bytes::Bytes;
|
||||
use smolmix_hyper::SmolmixConnector;
|
||||
|
||||
let tunnel = smolmix::Tunnel::new().await?;
|
||||
let connector = SmolmixConnector::new(&tunnel);
|
||||
let client = legacy::Client::builder(TokioExecutor::new())
|
||||
.build::<_, Full<Bytes>>(connector);
|
||||
|
||||
let body = Full::new(Bytes::from(r#"{"key": "value"}"#));
|
||||
let req = hyper::Request::post("https://httpbin.org/post")
|
||||
.header("Host", "httpbin.org")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)?;
|
||||
|
||||
let resp = client.request(req).await?;
|
||||
```
|
||||
|
||||
## Performance notes
|
||||
|
||||
<Callout type="info">
|
||||
The first request takes several seconds (mixnet connection setup + multi-hop TCP handshake). Subsequent requests on the same client reuse the tunnel and are significantly faster. hyper-util's connection pooling also helps: HTTP keep-alive connections avoid repeated TCP/TLS setup.
|
||||
</Callout>
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
cargo run -p smolmix-hyper --example get
|
||||
cargo run -p smolmix-hyper --example post
|
||||
cargo run -p smolmix-hyper --example get -- --ipr <IPR_ADDRESS>
|
||||
```
|
||||
|
||||
Source: [`hyper/examples/get.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/hyper/examples/get.rs), [`hyper/examples/post.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/hyper/examples/post.rs)
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "smolmix-tls: TLS Over the Mixnet"
|
||||
description: "Encrypt the final hop between the exit gateway and remote host using smolmix-tls. Pre-configured TLS with webpki roots for smolmix TCP streams."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-04-28"
|
||||
---
|
||||
|
||||
# smolmix-tls
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
The mixnet protects your identity but not the content of traffic between the exit gateway and remote host. `smolmix-tls` provides a pre-configured `TlsConnector` with webpki root certificates, wrapping a smolmix `TcpStream` in TLS with one function call.
|
||||
|
||||
<Callout type="warning">
|
||||
**Always use TLS** (or another encryption layer) when connecting to external services through the mixnet. Without it, the exit gateway can read your traffic in plaintext.
|
||||
</Callout>
|
||||
|
||||
## Installation
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix = "1.21.0"
|
||||
smolmix-tls = "1.21.0"
|
||||
```
|
||||
|
||||
Both crates share the workspace version. Full API docs: [docs.rs/smolmix-tls](https://docs.rs/smolmix-tls).
|
||||
|
||||
You also need a rustls crypto provider installed. Add this at the start of `main()`:
|
||||
|
||||
```rust
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use smolmix::Tunnel;
|
||||
use smolmix_tls::{connect, connector, connect_with};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
|
||||
let tunnel = 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 mut tls = connect(tcp, "example.com").await?;
|
||||
|
||||
tls.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n").await?;
|
||||
let mut buf = Vec::new();
|
||||
tls.read_to_end(&mut buf).await?;
|
||||
println!("{}", String::from_utf8_lossy(&buf));
|
||||
|
||||
tunnel.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Reusing the connector
|
||||
|
||||
If you're making multiple TLS connections, build the connector once and reuse it. This avoids rebuilding the root certificate store each time:
|
||||
|
||||
```rust
|
||||
let tls = connector();
|
||||
let stream1 = connect_with(&tls, tcp1, "a.example.com").await?;
|
||||
let stream2 = connect_with(&tls, tcp2, "b.example.com").await?;
|
||||
```
|
||||
|
||||
The `TlsConnector` clones cheaply via `Arc`.
|
||||
|
||||
## Example
|
||||
|
||||
```sh
|
||||
cargo run -p smolmix-tls --example connect
|
||||
cargo run -p smolmix-tls --example connect -- --ipr <IPR_ADDRESS>
|
||||
```
|
||||
|
||||
Source: [`tls/examples/connect.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/tls/examples/connect.rs)
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: "smolmix: TCP/UDP Over the Nym Mixnet"
|
||||
description: "A userspace IP tunnel that provides standard TcpStream and UdpSocket types over the Nym mixnet. Drop-in compatible with async tokio Rust ecosystem."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-04-23"
|
||||
---
|
||||
|
||||
# smolmix
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
import { RUST_MSRV } from '../../../components/versions'
|
||||
|
||||
`smolmix` is a TCP/UDP tunnel over the Nym mixnet. It uses a userspace network stack [`smoltcp`](https://docs.rs/smoltcp/latest/smoltcp/) to provide `TcpStream` and `UdpSocket` types that work with the async [`tokio`](https://docs.rs/tokio) Rust ecosystem e.g. [`tokio-rustls`](https://docs.rs/tokio-rustls), [`hyper`](https://docs.rs/hyper), [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite), etc.
|
||||
|
||||
The `TcpStream` type implements tokio's `AsyncRead`/`AsyncWrite` traits and `UdpSocket` provides `send_to`/`recv_from` for datagrams.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Your application (TLS, HTTP, WebSocket, DNS, etc.) │
|
||||
│ └─ smolmix::TcpStream / UdpSocket │
|
||||
│ └─ smoltcp (userspace TCP/IP) │
|
||||
│ └─ Nym mixnet → IPR exit gateway → internet │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Traffic exits the mixnet at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway. The exit IP is the gateway's, not yours.
|
||||
|
||||
## Runtime and platform support
|
||||
### Other runtimes
|
||||
`smolmix` currently requires `tokio`. The internal pipeline is tokio-based: the bridge task that shuttles packets to the mixnet, the Nym SDK's `MixnetClient`, and the [`tokio-smoltcp`](https://docs.rs/tokio-smoltcp) reactor that drives the userspace TCP/IP stack all run on the tokio runtime.
|
||||
|
||||
This means `smolmix` is not compatible with alternative async runtimes like [`smol`](https://docs.rs/smol) or [`async-std`](https://docs.rs/async-std) out of the box. If you need to use `smolmix` from another runtime, the [`async-compat`](https://docs.rs/async-compat) crate can bridge the gap.
|
||||
|
||||
### Non-native `smolmix`
|
||||
A WASM version of `smolmix` is planned for a future release.
|
||||
|
||||
## Installation
|
||||
|
||||
Add `smolmix` to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix = "1.21.0"
|
||||
nym-bin-common = { version = "1.20.4", features = ["basic_tracing"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
blake3 = "=1.7.0" # required pin, see note below
|
||||
```
|
||||
|
||||
<Callout type="warning">
|
||||
**Temporary pin required.** You must pin [`blake3`](https://docs.rs/blake3) `= "=1.7.0"` to avoid a build failure caused by a transitive [`digest`](https://docs.rs/digest) version conflict. This will be resolved in a future release.
|
||||
</Callout>
|
||||
|
||||
`tokio` is a transitive dependency of smolmix, but you need to enable `rt-multi-thread` (smolmix spawns multiple tasks internally) and `macros` (for `#[tokio::main]`).
|
||||
|
||||
`nym-bin-common` is optional but recommended: it sets up [`tracing`](https://docs.rs/tracing) logging so you can see mixnet connection progress.
|
||||
|
||||
**Minimum Rust version:** {RUST_MSRV}+
|
||||
|
||||
### From Git
|
||||
|
||||
For unreleased changes, import directly from the repository:
|
||||
|
||||
```toml
|
||||
smolmix = { git = "https://github.com/nymtech/nym", branch = "develop" }
|
||||
```
|
||||
|
||||
## When to use smolmix
|
||||
|
||||
| | smolmix | Stream module | mixFetch | SOCKS client |
|
||||
|---|---|---|---|---|
|
||||
| **Layer** | Transport (TCP/UDP) | Message (multiplexed streams) | HTTP | TCP (SOCKS proxy) |
|
||||
| **Controls both sides?** | No (proxy mode) | Yes (E2E) | No (proxy mode) | No (proxy mode) |
|
||||
| **API** | `TcpStream`, `UdpSocket` | `AsyncRead + AsyncWrite` | `fetch()` drop-in | SOCKS4/5 protocol |
|
||||
| **Composability** | Full: TLS, HTTP, WebSocket, DNS, etc. stack on top | Byte streams only | HTTP(S) only | Application-dependent |
|
||||
| **Best for** | Reaching external services from Rust with standard networking | Peer-to-peer / E2E protocols between Nym clients | Browser HTTP requests | Legacy apps with SOCKS support |
|
||||
|
||||
## Security model
|
||||
|
||||
<Callout type="warning">
|
||||
Traffic is Sphinx-encrypted inside the mixnet, but between the exit gateway and the remote host it travels as **normal internet traffic**. Always encrypt the final hop with TLS ([`rustls`](https://docs.rs/rustls)), Noise Protocol ([`snow`](https://docs.rs/snow)), etc.
|
||||
</Callout>
|
||||
|
||||
### What's protected
|
||||
|
||||
| Segment | Mixnet encryption | What's visible |
|
||||
|---|---|---|
|
||||
| Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination |
|
||||
| Inside the mixnet (entry + 3 mix layers + exit) | Sphinx (layered) | Each node only knows prev/next hop |
|
||||
| Exit gateway (IPR) | Sphinx removed, raw IP packet exposed | IPR sees destination IP + port. Payload depends on your application layer (see below). |
|
||||
| IPR → remote host | None (Sphinx is mixnet-only) | Remote host sees IPR's IP, not yours |
|
||||
|
||||
The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. At the exit gateway, the Sphinx layers are stripped and the original IP packet is forwarded to the destination. This is analogous to how a Tor exit node or VPN endpoint unwraps its tunnel.
|
||||
|
||||
**What's inside that IP packet is entirely up to you.** If you connect with TLS (as in the [TCP example](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs)), the IPR sees encrypted TLS ciphertext going to a destination IP: it knows *where* but not *what*. If you send plaintext HTTP, the IPR can read the full request and response.
|
||||
|
||||
### Trust boundaries
|
||||
|
||||
- **You trust the mixnet** to provide unlinkability between sender and receiver. This is enforced cryptographically by the Sphinx packet format and mixing.
|
||||
- **You trust the IPR exit gateway** in the same way you trust a VPN exit or Tor exit node: it can inspect your raw IP packets. The difference is that the IPR doesn't know *who* is sending the traffic (the mixnet hides your identity).
|
||||
- **Application-layer encryption closes the gap.** TLS, Noise Protocol, or any authenticated encryption ensures the IPR only sees ciphertext. It can see destination IP and port, but not payload content.
|
||||
|
||||
### Comparison with other privacy tools
|
||||
|
||||
| | smolmix | Tor | VPN |
|
||||
|---|---|---|---|
|
||||
| **Exit node sees traffic?** | Yes (encrypt it) | Yes (encrypt it) | Yes (encrypt it) |
|
||||
| **Exit node knows sender?** | No (mixnet hides identity) | No (onion routing) | Yes (VPN provider knows) |
|
||||
| **Timing analysis resistance** | Strong (mixing, cover traffic) | Weak (low-latency) | None |
|
||||
| **UDP support** | Yes | No (TCP only) | Yes |
|
||||
|
||||
## Examples
|
||||
|
||||
Runnable examples in [`smolmix/core/examples/`](https://github.com/nymtech/nym/tree/develop/smolmix/core/examples). Each is self-contained: read the `//!` doc comments at the top of each file for a walkthrough.
|
||||
|
||||
```sh
|
||||
cargo run -p smolmix --example <name>
|
||||
```
|
||||
|
||||
All examples accept `--ipr <ADDRESS>` to target a specific exit node (pass a `Recipient` address to `Tunnel::builder().ipr_address()`).
|
||||
|
||||
| Example | Source | What it demonstrates |
|
||||
|---|---|---|
|
||||
| TCP | [`tcp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs) | HTTPS request via [`hyper`](https://docs.rs/hyper) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Fetches Cloudflare's `/cdn-cgi/trace` to show that the exit IP differs from clearnet |
|
||||
| TCP download | [`tcp_download.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp_download.rs) | DNS-over-mixnet + multi-request HTTP/1.1 download over a single keep-alive connection. The full real-world pattern |
|
||||
| UDP | [`udp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/udp.rs) | DNS lookup via [`hickory-proto`](https://docs.rs/hickory-proto). Sends a raw UDP query to `1.1.1.1:53` through the mixnet |
|
||||
| WebSocket | [`websocket.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/websocket.rs) | WebSocket echo via [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Full TCP → TLS → WebSocket stack composing over smolmix |
|
||||
| libp2p ping | [`libp2p_ping.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/libp2p_ping.rs) | Custom libp2p `Transport` over the mixnet (noise+yamux handshake, dial-only, no listening) |
|
||||
|
||||
## API reference
|
||||
|
||||
Full API documentation including internal architecture is available on [docs.rs/smolmix](https://docs.rs/smolmix). For the companion crates and other connectors, see the [smolmix & connectors overview](/developers/smolmix).
|
||||
@@ -351,6 +351,11 @@ input:focus-visible {
|
||||
background-color: rgba(255, 255, 255, 0.03) !important;
|
||||
}
|
||||
|
||||
.landing-chip:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
color: var(--colorPrimary) !important;
|
||||
}
|
||||
|
||||
/* ── Invert diagrams in dark mode ── */
|
||||
|
||||
html.dark .nextra-content img:not([src*="landing"]) {
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# rust-libp2p-nym
|
||||
|
||||
This repo contains an example implementation of a libp2p transport using the Nym mixnet. It relies on the ChainSafe's fork of libp2p: https://github.com/ChainSafe/rust-libp2p
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 1.68.2
|
||||
- `Protoc` protobuf compiler. On Debian/Ubuntu distributed via `apt` as `protobuf-compiler` & on Arch/Manjaro via AUR as `[python-protobuf-compiler](https://aur.archlinux.org/packages/python-protobuf-compiler)`.
|
||||
|
||||
## Usage
|
||||
|
||||
To instantiate a libp2p swarm using the transport:
|
||||
|
||||
```rust
|
||||
use libp2p::core::{muxing::StreamMuxerBox, transport::Transport};
|
||||
use libp2p::swarm::{keep_alive::Behaviour, SwarmBuilder};
|
||||
use libp2p::{identity, PeerId};
|
||||
use nym_sdk::mixnet::MixnetClient;
|
||||
use rust_libp2p_nym::transport::NymTransport;
|
||||
use rust_libp2p_nym::test_utils::create_nym_client;
|
||||
use std::error::Error;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let local_key = identity::Keypair::generate_ed25519();
|
||||
let local_peer_id = PeerId::from(local_key.public());
|
||||
info!("Local peer id: {local_peer_id:?}");
|
||||
|
||||
let nym_id = rand::random::<u64>().to_string();
|
||||
let nym_client = MixnetClient::connect_new().await.unwrap();
|
||||
let transport = NymTransport::new(nym_client, local_key.clone()).await?;
|
||||
let _swarm = SwarmBuilder::with_tokio_executor(
|
||||
transport
|
||||
.map(|a, _| (a.0, StreamMuxerBox::new(a.1)))
|
||||
.boxed(),
|
||||
Behaviour::default(),
|
||||
local_peer_id,
|
||||
)
|
||||
.build();
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Chat example
|
||||
|
||||
To run the libp2p chat example, run the following in one terminal:
|
||||
```bash
|
||||
cargo run --example libp2p_chat
|
||||
# Local peer id: PeerId("12D3KooWLukBu6q2FerWPFhFFhiYaJkhn2sBmceh9UCaXe6hJf5D")
|
||||
# Listening on "/nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve"
|
||||
```
|
||||
|
||||
In another terminal, run ping again, passing the Nym multiaddress printed previously:
|
||||
```bash
|
||||
cargo run --example libp2p_chat -- /nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve
|
||||
# Local peer id: PeerId("12D3KooWNsuRwG6DHnFJCDR8B3zdvja6xLcfnbtKCsQWJ8eppyWC")
|
||||
# Dialed /nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve
|
||||
# Listening on "/nym/2oiRW5C9ivyF3Bo3Gpm4H9EqSKH7A6GpcrRRwVSDVUQ9.EajgCnhzimsP6KskUwKcEj8VFCmHR78s2J6FHWcZ4etR@Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf"
|
||||
```
|
||||
|
||||
You should see that the nodes connected and sent messages to each other:
|
||||
```bash
|
||||
# 2023-08-10T14:06:28.116Z INFO libp2p_chat > Got message: 'hello world' with id: 37393732353836333838333537303637303237 from peer: 12D3KooWB6k8ZGDF44N4FMRhgVBNihwk1wMYSumosxiZq9pUTbAz
|
||||
```
|
||||
@@ -1,173 +0,0 @@
|
||||
// Copyright 2018 Parity Technologies (UK) Ltd.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
//! A basic chat application with logs demonstrating libp2p and the gossipsub protocol
|
||||
//! combined with mDNS for the discovery of peers to gossip with.
|
||||
//!
|
||||
//! Using two terminal windows, start two instances, typing the following in each:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo run
|
||||
//! ```
|
||||
//!
|
||||
//! Mutual mDNS discovery may take a few seconds. When each peer does discover the other
|
||||
//! it will print a message like:
|
||||
//!
|
||||
//! ```sh
|
||||
//! mDNS discovered a new peer: {peerId}
|
||||
//! ```
|
||||
//!
|
||||
//! Type a message and hit return: the message is sent and printed in the other terminal.
|
||||
//! Close with Ctrl-c.
|
||||
//!
|
||||
//! You can open more terminal windows and add more peers using the same line above.
|
||||
//!
|
||||
//! Once an additional peer is mDNS discovered it can participate in the conversation
|
||||
//! and all peers will receive messages sent from it.
|
||||
//!
|
||||
//! If a participant exits (Control-C or otherwise) the other peers will receive an mDNS expired
|
||||
//! event and remove the expired peer from the list of known peers.
|
||||
|
||||
// use crate::rust_libp2p_nym::transport::NymTransport;
|
||||
// use futures::{prelude::*, select};
|
||||
// use libp2p::Multiaddr;
|
||||
// use libp2p::{
|
||||
// core::muxing::StreamMuxerBox,
|
||||
// gossipsub, identity,
|
||||
// swarm::NetworkBehaviour,
|
||||
// swarm::{SwarmBuilder, SwarmEvent},
|
||||
// PeerId, Transport,
|
||||
// };
|
||||
// use log::{error, info, LevelFilter};
|
||||
// use nym_sdk::mixnet::MixnetClient;
|
||||
// use std::collections::hash_map::DefaultHasher;
|
||||
use std::error::Error;
|
||||
// use std::hash::{Hash, Hasher};
|
||||
// use std::time::Duration;
|
||||
// use tokio::io;
|
||||
// use tokio_util::codec;
|
||||
|
||||
// #[path = "../libp2p_shared/lib.rs"]
|
||||
// mod rust_libp2p_nym;
|
||||
//
|
||||
// // We create a custom network behaviour that uses Gossipsub
|
||||
// #[derive(NetworkBehaviour)]
|
||||
// struct Behaviour {
|
||||
// gossipsub: gossipsub::Behaviour,
|
||||
// }
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
unimplemented!("temporarily disabled")
|
||||
|
||||
// pretty_env_logger::formatted_timed_builder()
|
||||
// .filter_level(LevelFilter::Warn)
|
||||
// .filter(Some("libp2p_chat"), LevelFilter::Info)
|
||||
// .init();
|
||||
//
|
||||
// // Create a random PeerId
|
||||
// let id_keys = identity::Keypair::generate_ed25519();
|
||||
// let local_peer_id = PeerId::from(id_keys.public());
|
||||
// info!("Local peer id: {local_peer_id}");
|
||||
//
|
||||
// // To content-address message, we can take the hash of message and use it as an ID.
|
||||
// let message_id_fn = |message: &gossipsub::Message| {
|
||||
// let mut s = DefaultHasher::new();
|
||||
// message.data.hash(&mut s);
|
||||
// gossipsub::MessageId::from(s.finish().to_string())
|
||||
// };
|
||||
//
|
||||
// // Set a custom gossipsub configuration
|
||||
// let gossipsub_config = gossipsub::ConfigBuilder::default()
|
||||
// .heartbeat_interval(Duration::from_secs(10)) // This is set to aid debugging by not cluttering the log space
|
||||
// .validation_mode(gossipsub::ValidationMode::Strict) // This sets the kind of message validation. The default is Strict (enforce message signing)
|
||||
// .message_id_fn(message_id_fn) // content-address messages. No two messages of the same content will be propagated.
|
||||
// .build()
|
||||
// .expect("Valid config");
|
||||
//
|
||||
// // build a gossipsub network behaviour
|
||||
// let mut gossipsub = gossipsub::Behaviour::new(
|
||||
// gossipsub::MessageAuthenticity::Signed(id_keys),
|
||||
// gossipsub_config,
|
||||
// )
|
||||
// .expect("Correct configuration");
|
||||
// // Create a Gossipsub topic
|
||||
// let topic = gossipsub::IdentTopic::new("test-net");
|
||||
// // subscribes to our topic
|
||||
// gossipsub.subscribe(&topic)?;
|
||||
//
|
||||
// let client = MixnetClient::connect_new().await.unwrap();
|
||||
// info!("client address: {}", client.nym_address());
|
||||
//
|
||||
// let local_key = identity::Keypair::generate_ed25519();
|
||||
// let local_peer_id = PeerId::from(local_key.public());
|
||||
// info!("Local peer id: {local_peer_id:?}");
|
||||
//
|
||||
// let transport = NymTransport::new(client, local_key).await?;
|
||||
//
|
||||
// let mut swarm = SwarmBuilder::with_tokio_executor(
|
||||
// transport
|
||||
// .map(|a, _| (a.0, StreamMuxerBox::new(a.1)))
|
||||
// .boxed(),
|
||||
// Behaviour { gossipsub },
|
||||
// local_peer_id,
|
||||
// )
|
||||
// .build();
|
||||
//
|
||||
// if let Some(addr) = std::env::args().nth(1) {
|
||||
// let remote: Multiaddr = addr.parse()?;
|
||||
// swarm.dial(remote)?;
|
||||
// info!("Dialed {addr}")
|
||||
// }
|
||||
//
|
||||
// // Read full lines from stdin
|
||||
// let mut stdin = codec::FramedRead::new(io::stdin(), codec::LinesCodec::new()).fuse();
|
||||
//
|
||||
// info!("Enter messages via STDIN and they will be sent to connected peers using Gossipsub");
|
||||
//
|
||||
// // Kick it off
|
||||
// loop {
|
||||
// select! {
|
||||
// line = stdin.select_next_some() => {
|
||||
// if let Err(e) = swarm
|
||||
// .behaviour_mut().gossipsub
|
||||
// .publish(topic.clone(), line.expect("Stdin not to close").as_bytes()) {
|
||||
// error!("Publish error: {e:?}");
|
||||
// }
|
||||
// },
|
||||
// event = swarm.select_next_some() => {
|
||||
// match event {
|
||||
// SwarmEvent::Behaviour(BehaviourEvent::Gossipsub(gossipsub::Event::Message {
|
||||
// propagation_source: peer_id,
|
||||
// message_id: id,
|
||||
// message,
|
||||
// })) => info!(
|
||||
// "Got message: '{}' with id: {id} from peer: {peer_id}",
|
||||
// String::from_utf8_lossy(&message.data),
|
||||
// ),
|
||||
// SwarmEvent::NewListenAddr { address, .. } => {
|
||||
// info!("Local node is listening on {address}");
|
||||
// }
|
||||
// other => {info!("other event: {:?}", other)}
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
# rust-libp2p-nym
|
||||
|
||||
This repo contains an example implementation of a libp2p transport using the Nym mixnet. It relies on the ChainSafe's fork of libp2p: https://github.com/ChainSafe/rust-libp2p
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 1.68.2
|
||||
- `Protoc` protobuf compiler. On Debian/Ubuntu distributed via `apt` as `protobuf-compiler` & on Arch/Manjaro via AUR as `[python-protobuf-compiler](https://aur.archlinux.org/packages/python-protobuf-compiler)`.
|
||||
|
||||
## Usage
|
||||
|
||||
To instantiate a libp2p swarm using the transport:
|
||||
|
||||
```rust
|
||||
use libp2p::core::{muxing::StreamMuxerBox, transport::Transport};
|
||||
use libp2p::swarm::{keep_alive::Behaviour, SwarmBuilder};
|
||||
use libp2p::{identity, PeerId};
|
||||
use nym_sdk::mixnet::MixnetClient;
|
||||
use rust_libp2p_nym::transport::NymTransport;
|
||||
use rust_libp2p_nym::test_utils::create_nym_client;
|
||||
use std::error::Error;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let local_key = identity::Keypair::generate_ed25519();
|
||||
let local_peer_id = PeerId::from(local_key.public());
|
||||
info!("Local peer id: {local_peer_id:?}");
|
||||
|
||||
let nym_id = rand::random::<u64>().to_string();
|
||||
let nym_client = MixnetClient::connect_new().await.unwrap();
|
||||
let transport = NymTransport::new(nym_client, local_key.clone()).await?;
|
||||
let _swarm = SwarmBuilder::with_tokio_executor(
|
||||
transport
|
||||
.map(|a, _| (a.0, StreamMuxerBox::new(a.1)))
|
||||
.boxed(),
|
||||
Behaviour::default(),
|
||||
local_peer_id,
|
||||
)
|
||||
.build();
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Ping example
|
||||
|
||||
To run the libp2p ping example, run the following in one terminal:
|
||||
```bash
|
||||
cargo run --example libp2p_ping
|
||||
# Local peer id: PeerId("12D3KooWLukBu6q2FerWPFhFFhiYaJkhn2sBmceh9UCaXe6hJf5D")
|
||||
# Listening on "/nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve"
|
||||
```
|
||||
|
||||
In another terminal, run ping again, passing the Nym multiaddress printed previously:
|
||||
```bash
|
||||
cargo run --example libp2p_ping -- /nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve
|
||||
# Local peer id: PeerId("12D3KooWNsuRwG6DHnFJCDR8B3zdvja6xLcfnbtKCsQWJ8eppyWC")
|
||||
# Dialed /nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve
|
||||
# Listening on "/nym/2oiRW5C9ivyF3Bo3Gpm4H9EqSKH7A6GpcrRRwVSDVUQ9.EajgCnhzimsP6KskUwKcEj8VFCmHR78s2J6FHWcZ4etR@Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf"
|
||||
```
|
||||
|
||||
You should see that the nodes connected and pinged each other:
|
||||
```bash
|
||||
# Mar 30 22:56:36.400 INFO ping: BehaviourEvent: Event { peer: PeerId("12D3KooWGf2oYd6U2nrLzfDrN9zxsjSQjPsMh2oDJPUQ9hiHMNtf"), result: Ok(Ping { rtt: 1.06836675s }) }
|
||||
```
|
||||
```bash
|
||||
# Mar 30 22:56:35.595 INFO ping: BehaviourEvent: Event { peer: PeerId("12D3KooWMd5ak31DXuZq7x1JuFSR6toA5RDQrPaHrfXEhy7vqqpC"), result: Ok(Pong) }
|
||||
```
|
||||
|
||||
In order to run the ping example with vanilla libp2p, which uses tcp, pass the
|
||||
`--features libp2p-vanilla` flag to the example and follow the instructions on the
|
||||
rust-libp2p project as usual.
|
||||
|
||||
```bash
|
||||
RUST_LOG=ping=debug cargo run --example ping --features libp2p-vanilla
|
||||
```
|
||||
|
||||
```bash
|
||||
RUST_LOG=ping=debug cargo run --example ping --features libp2p-vanilla -- "/ip4/127.0.0.1/tcp/$PORT"
|
||||
```
|
||||
@@ -1,143 +0,0 @@
|
||||
// Copyright 2018 Parity Technologies (UK) Ltd.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
//! Ping example
|
||||
//!
|
||||
//! See ../src/tutorial.rs for a step-by-step guide building the example below.
|
||||
//!
|
||||
//! In the first terminal window, run:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo run --example ping --features=full
|
||||
//! ```
|
||||
//!
|
||||
//! It will print the PeerId and the listening addresses, e.g. `Listening on
|
||||
//! "/ip4/0.0.0.0/tcp/24915"`
|
||||
//!
|
||||
//! In the second terminal window, start a new instance of the example with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo run --example ping --features=full -- /ip4/127.0.0.1/tcp/24915
|
||||
//! ```
|
||||
//!
|
||||
//! The two nodes establish a connection, negotiate the ping protocol
|
||||
//! and begin pinging each other.
|
||||
|
||||
// use libp2p::futures::StreamExt;
|
||||
// use libp2p::ping::Success;
|
||||
// use libp2p::swarm::{keep_alive, NetworkBehaviour, SwarmEvent};
|
||||
// use libp2p::{identity, ping, Multiaddr, PeerId};
|
||||
// use log::{debug, info, LevelFilter};
|
||||
use std::error::Error;
|
||||
// use std::time::Duration;
|
||||
//
|
||||
// #[path = "../libp2p_shared/lib.rs"]
|
||||
// mod rust_libp2p_nym;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
unimplemented!("temporarily disabled")
|
||||
//
|
||||
// pretty_env_logger::formatted_timed_builder()
|
||||
// .filter_level(LevelFilter::Warn)
|
||||
// .filter(Some("libp2p_ping"), LevelFilter::Debug)
|
||||
// .init();
|
||||
//
|
||||
// let local_key = identity::Keypair::generate_ed25519();
|
||||
// let local_peer_id = PeerId::from(local_key.public());
|
||||
// info!("Local peer id: {local_peer_id:?}");
|
||||
//
|
||||
// #[cfg(not(feature = "libp2p-vanilla"))]
|
||||
// let mut swarm = {
|
||||
// debug!("Running `ping` example using NymTransport");
|
||||
// use libp2p::core::{muxing::StreamMuxerBox, transport::Transport};
|
||||
// use libp2p::swarm::SwarmBuilder;
|
||||
// use rust_libp2p_nym::transport::NymTransport;
|
||||
//
|
||||
// let client = nym_sdk::mixnet::MixnetClient::connect_new().await.unwrap();
|
||||
//
|
||||
// let transport = NymTransport::new(client, local_key.clone()).await?;
|
||||
// SwarmBuilder::with_tokio_executor(
|
||||
// transport
|
||||
// .map(|a, _| (a.0, StreamMuxerBox::new(a.1)))
|
||||
// .boxed(),
|
||||
// Behaviour::default(),
|
||||
// local_peer_id,
|
||||
// )
|
||||
// .build()
|
||||
// };
|
||||
//
|
||||
// #[cfg(feature = "libp2p-vanilla")]
|
||||
// let mut swarm = {
|
||||
// debug!("Running `ping` example using the vanilla libp2p tokio_development_transport");
|
||||
// let transport = libp2p::tokio_development_transport(local_key)?;
|
||||
// let mut swarm =
|
||||
// libp2p::Swarm::with_tokio_executor(transport, Behaviour::default(), local_peer_id);
|
||||
// swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
|
||||
// swarm
|
||||
// };
|
||||
//
|
||||
// // Dial the peer identified by the multi-address given as the second
|
||||
// // command-line argument, if any.
|
||||
// if let Some(addr) = std::env::args().nth(1) {
|
||||
// let remote: Multiaddr = addr.parse()?;
|
||||
// swarm.dial(remote)?;
|
||||
// info!("Dialed {addr}")
|
||||
// }
|
||||
//
|
||||
// let mut total_ping_rtt: Duration = Duration::from_micros(0);
|
||||
// let mut counter: u128 = 0;
|
||||
// loop {
|
||||
// match swarm.select_next_some().await {
|
||||
// SwarmEvent::NewListenAddr { address, .. } => info!("Listening on {address:?}"),
|
||||
// SwarmEvent::Behaviour(event) => {
|
||||
// // Get the round-trip duration for the pings.
|
||||
// // This value is already captured in the BehaviourEvent::Ping's `Success::Ping`
|
||||
// // field.
|
||||
// debug!("{event:?}");
|
||||
// if let BehaviourEvent::Ping(ping_event) = event {
|
||||
// let result: Success = ping_event.result?;
|
||||
// match result {
|
||||
// Success::Ping { rtt } => {
|
||||
// counter += 1;
|
||||
// total_ping_rtt += rtt;
|
||||
// let average_ping_rtt = Duration::from_micros(
|
||||
// (total_ping_rtt.as_micros() / counter).try_into().unwrap(),
|
||||
// );
|
||||
// info!("Ping RTT: {rtt:?} AVERAGE RTT: ({counter} pings): {average_ping_rtt:?}");
|
||||
// }
|
||||
// Success::Pong => info!("Pong Event"),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// }
|
||||
}
|
||||
//
|
||||
// /// Our network behaviour.
|
||||
// ///
|
||||
// /// For illustrative purposes, this includes the [`KeepAlive`](behaviour::KeepAlive) behaviour so a continuous sequence of
|
||||
// /// pings can be observed.
|
||||
// #[derive(NetworkBehaviour, Default)]
|
||||
// struct Behaviour {
|
||||
// keep_alive: keep_alive::Behaviour,
|
||||
// ping: ping::Behaviour,
|
||||
// }
|
||||
@@ -1,418 +0,0 @@
|
||||
use libp2p::core::{muxing::StreamMuxerEvent, PeerId, StreamMuxer};
|
||||
use log::debug;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
use tokio::sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
oneshot,
|
||||
};
|
||||
|
||||
use super::error::Error;
|
||||
use super::message::{
|
||||
ConnectionId, Message, OutboundMessage, SubstreamId, SubstreamMessage, SubstreamMessageType,
|
||||
TransportMessage,
|
||||
};
|
||||
use super::substream::Substream;
|
||||
|
||||
/// Connection represents the result of a connection setup process.
|
||||
/// It implements `StreamMuxer` and thus has stream multiplexing built in.
|
||||
#[derive(Debug)]
|
||||
pub struct Connection {
|
||||
pub(crate) peer_id: PeerId,
|
||||
pub(crate) remote_recipient: Recipient,
|
||||
pub(crate) id: ConnectionId,
|
||||
|
||||
/// receive inbound messages from the `InnerConnection`
|
||||
pub(crate) inbound_rx: UnboundedReceiver<SubstreamMessage>,
|
||||
|
||||
/// substream ID -> outbound pending substream exists
|
||||
/// the key is deleted when the response is received, or the request times out
|
||||
pending_substreams: HashSet<SubstreamId>,
|
||||
|
||||
/// substream ID -> substream's inbound_tx channel
|
||||
substream_inbound_txs: HashMap<SubstreamId, UnboundedSender<Vec<u8>>>,
|
||||
|
||||
/// substream ID -> substream's close_tx channel
|
||||
substream_close_txs: HashMap<SubstreamId, oneshot::Sender<()>>,
|
||||
|
||||
/// send messages to the mixnet
|
||||
/// used for sending `SubstreamMessageType::OpenRequest` messages
|
||||
/// also passed to each substream so they can write to the mixnet
|
||||
pub(crate) mixnet_outbound_tx: UnboundedSender<OutboundMessage>,
|
||||
|
||||
/// inbound substream open requests; used in poll_inbound
|
||||
inbound_open_tx: UnboundedSender<Substream>,
|
||||
inbound_open_rx: UnboundedReceiver<Substream>,
|
||||
|
||||
/// closed substream IDs; used in poll_close
|
||||
close_tx: UnboundedSender<SubstreamId>,
|
||||
close_rx: UnboundedReceiver<SubstreamId>,
|
||||
|
||||
/// message nonce contains the next nonce that should be used when
|
||||
/// sending a message over the connection
|
||||
pub(crate) message_nonce: Arc<AtomicU64>,
|
||||
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub(crate) fn new(
|
||||
peer_id: PeerId,
|
||||
remote_recipient: Recipient,
|
||||
id: ConnectionId,
|
||||
inbound_rx: UnboundedReceiver<SubstreamMessage>,
|
||||
mixnet_outbound_tx: UnboundedSender<OutboundMessage>,
|
||||
) -> Self {
|
||||
let (inbound_open_tx, inbound_open_rx) = unbounded_channel();
|
||||
let (close_tx, close_rx) = unbounded_channel();
|
||||
|
||||
Connection {
|
||||
peer_id,
|
||||
remote_recipient,
|
||||
id,
|
||||
inbound_rx,
|
||||
pending_substreams: HashSet::new(),
|
||||
substream_inbound_txs: HashMap::new(),
|
||||
substream_close_txs: HashMap::new(),
|
||||
mixnet_outbound_tx,
|
||||
inbound_open_tx,
|
||||
inbound_open_rx,
|
||||
close_tx,
|
||||
close_rx,
|
||||
message_nonce: Arc::new(AtomicU64::new(1)),
|
||||
waker: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_outbound_substream(&mut self) -> Result<Substream, Error> {
|
||||
let substream_id = SubstreamId::generate();
|
||||
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
// send the substream open request that requests to open a substream with the given ID
|
||||
self.mixnet_outbound_tx
|
||||
.send(OutboundMessage {
|
||||
recipient: self.remote_recipient,
|
||||
message: Message::TransportMessage(TransportMessage {
|
||||
nonce,
|
||||
id: self.id.clone(),
|
||||
message: SubstreamMessage {
|
||||
substream_id: substream_id.clone(),
|
||||
message_type: SubstreamMessageType::OpenRequest,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.map_err(|e| Error::OutboundSendFailure(e.to_string()))?;
|
||||
|
||||
// track pending outbound substreams
|
||||
// TODO we should probably lock this? storing map values should be atomic
|
||||
let res = self.new_substream(substream_id.clone());
|
||||
if res.is_ok() {
|
||||
self.pending_substreams.insert(substream_id);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
// creates a new substream instance with the given ID.
|
||||
fn new_substream(&mut self, id: SubstreamId) -> Result<Substream, Error> {
|
||||
// check we don't already have a substream with this ID
|
||||
if self.substream_inbound_txs.contains_key(&id) {
|
||||
return Err(Error::SubstreamIdExists(id));
|
||||
}
|
||||
|
||||
let (inbound_tx, inbound_rx) = unbounded_channel::<Vec<u8>>();
|
||||
let (close_tx, close_rx) = oneshot::channel::<()>();
|
||||
self.substream_inbound_txs.insert(id.clone(), inbound_tx);
|
||||
self.substream_close_txs.insert(id.clone(), close_tx);
|
||||
|
||||
if let Some(waker) = self.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
Ok(Substream::new(
|
||||
self.remote_recipient,
|
||||
self.id.clone(),
|
||||
id,
|
||||
inbound_rx,
|
||||
self.mixnet_outbound_tx.clone(),
|
||||
close_rx,
|
||||
self.message_nonce.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_close(&mut self, substream_id: SubstreamId) -> Result<(), Error> {
|
||||
if self.substream_inbound_txs.remove(&substream_id).is_none() {
|
||||
return Err(Error::SubstreamIdDoesNotExist(substream_id));
|
||||
}
|
||||
|
||||
// notify substream that it's closed
|
||||
let close_tx = self.substream_close_txs.remove(&substream_id);
|
||||
close_tx.unwrap().send(()).unwrap();
|
||||
|
||||
// notify poll_close that the substream is closed
|
||||
self.close_tx
|
||||
.send(substream_id)
|
||||
.map_err(|e| Error::InboundSendFailure(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamMuxer for Connection {
|
||||
type Substream = Substream;
|
||||
type Error = Error;
|
||||
|
||||
fn poll_inbound(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Self::Substream, Self::Error>> {
|
||||
if let Poll::Ready(Some(substream)) = self.inbound_open_rx.poll_recv(cx) {
|
||||
return Poll::Ready(Ok(substream));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn poll_outbound(
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Self::Substream, Self::Error>> {
|
||||
Poll::Ready(self.new_outbound_substream())
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
if let Poll::Ready(Some(_)) = self.close_rx.poll_recv(cx) {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn poll(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<StreamMuxerEvent, Self::Error>> {
|
||||
while let Poll::Ready(Some(msg)) = self.inbound_rx.poll_recv(cx) {
|
||||
match msg.message_type {
|
||||
SubstreamMessageType::OpenRequest => {
|
||||
// create a new substream with the given ID
|
||||
let substream = self.new_substream(msg.substream_id.clone())?;
|
||||
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
// send the response to the remote peer
|
||||
self.mixnet_outbound_tx
|
||||
.send(OutboundMessage {
|
||||
recipient: self.remote_recipient,
|
||||
message: Message::TransportMessage(TransportMessage {
|
||||
nonce,
|
||||
id: self.id.clone(),
|
||||
message: SubstreamMessage {
|
||||
substream_id: msg.substream_id.clone(),
|
||||
message_type: SubstreamMessageType::OpenResponse,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.map_err(|e| Error::OutboundSendFailure(e.to_string()))?;
|
||||
debug!("wrote OpenResponse for substream: {:?}", &msg.substream_id);
|
||||
|
||||
// send the substream to our own channel to be returned in poll_inbound
|
||||
self.inbound_open_tx
|
||||
.send(substream)
|
||||
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
|
||||
|
||||
debug!("new inbound substream: {:?}", &msg.substream_id);
|
||||
}
|
||||
SubstreamMessageType::OpenResponse => {
|
||||
if !self.pending_substreams.remove(&msg.substream_id) {
|
||||
debug!(
|
||||
"SubstreamMessageType::OpenResponse no substream pending for ID: {:?}",
|
||||
&msg.substream_id
|
||||
);
|
||||
}
|
||||
}
|
||||
SubstreamMessageType::Close => {
|
||||
self.handle_close(msg.substream_id)?;
|
||||
}
|
||||
SubstreamMessageType::Data(data) => {
|
||||
debug!("SubstreamMessageType::Data: {:?}", &data);
|
||||
let inbound_tx = self
|
||||
.substream_inbound_txs
|
||||
.get_mut(&msg.substream_id)
|
||||
.expect("must have a substream channel for substream");
|
||||
|
||||
// NOTE: this ignores channel closed errors, which is fine because the substream
|
||||
// might have been closed/dropped
|
||||
inbound_tx.send(data).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.waker = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// PendingConnection represents a connection that's been initiated, but not completed.
|
||||
pub(crate) struct PendingConnection {
|
||||
pub(crate) remote_recipient: Recipient,
|
||||
pub(crate) connection_tx: oneshot::Sender<Connection>,
|
||||
}
|
||||
|
||||
impl PendingConnection {
|
||||
pub(crate) fn new(
|
||||
remote_recipient: Recipient,
|
||||
connection_tx: oneshot::Sender<Connection>,
|
||||
) -> Self {
|
||||
PendingConnection {
|
||||
remote_recipient,
|
||||
connection_tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::message::InboundMessage;
|
||||
use super::super::mixnet::initialize_mixnet;
|
||||
use super::*;
|
||||
use futures::future::poll_fn;
|
||||
use futures::{AsyncReadExt, AsyncWriteExt, FutureExt};
|
||||
use nym_sdk::mixnet::MixnetClient;
|
||||
|
||||
async fn inbound_receive_and_send(
|
||||
connection_id: ConnectionId,
|
||||
mixnet_inbound_rx: &mut UnboundedReceiver<InboundMessage>,
|
||||
inbound_tx: &UnboundedSender<SubstreamMessage>,
|
||||
expected_nonce: u64,
|
||||
) {
|
||||
let recv_msg = mixnet_inbound_rx.recv().await.unwrap();
|
||||
match recv_msg.0 {
|
||||
Message::TransportMessage(TransportMessage {
|
||||
nonce,
|
||||
id,
|
||||
message: msg,
|
||||
}) => {
|
||||
assert_eq!(nonce, expected_nonce);
|
||||
assert_eq!(id, connection_id);
|
||||
inbound_tx.send(msg).unwrap();
|
||||
}
|
||||
_ => panic!("unexpected message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_connection_stream_muxer() {
|
||||
let client = MixnetClient::connect_new().await.unwrap();
|
||||
let (sender_address, mut sender_mixnet_inbound_rx, sender_outbound_tx) =
|
||||
initialize_mixnet(client, None).await.unwrap();
|
||||
|
||||
let client2 = MixnetClient::connect_new().await.unwrap();
|
||||
|
||||
let (recipient_address, mut recipient_mixnet_inbound_rx, recipient_outbound_tx) =
|
||||
initialize_mixnet(client2, None).await.unwrap();
|
||||
|
||||
let connection_id = ConnectionId::generate();
|
||||
|
||||
let recipient_peer_id = PeerId::random();
|
||||
let sender_peer_id = PeerId::random();
|
||||
|
||||
// create the connections
|
||||
let (sender_inbound_tx, sender_inbound_rx) = unbounded_channel::<SubstreamMessage>();
|
||||
let mut sender_connection = Connection::new(
|
||||
recipient_peer_id,
|
||||
recipient_address,
|
||||
connection_id.clone(),
|
||||
sender_inbound_rx,
|
||||
sender_outbound_tx,
|
||||
);
|
||||
let (recipient_inbound_tx, recipient_inbound_rx) = unbounded_channel::<SubstreamMessage>();
|
||||
let mut recipient_connection = Connection::new(
|
||||
sender_peer_id,
|
||||
sender_address,
|
||||
connection_id.clone(),
|
||||
recipient_inbound_rx,
|
||||
recipient_outbound_tx,
|
||||
);
|
||||
|
||||
// send the substream OpenRequest to the mixnet
|
||||
let mut sender_substream = sender_connection.new_outbound_substream().unwrap();
|
||||
assert!(sender_connection
|
||||
.pending_substreams
|
||||
.contains(&sender_substream.substream_id));
|
||||
assert_eq!(sender_connection.message_nonce.load(Ordering::SeqCst), 2);
|
||||
|
||||
// poll the recipient inbound stream; should receive the OpenRequest and create the substream
|
||||
inbound_receive_and_send(
|
||||
connection_id.clone(),
|
||||
&mut recipient_mixnet_inbound_rx,
|
||||
&recipient_inbound_tx,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
poll_fn(|cx| Pin::new(&mut recipient_connection).as_mut().poll(cx)).now_or_never();
|
||||
assert_eq!(recipient_connection.message_nonce.load(Ordering::SeqCst), 2);
|
||||
|
||||
// poll recipient's poll_inbound to receive the substream
|
||||
let maybe_recipient_substream = poll_fn(|cx| {
|
||||
Pin::new(&mut recipient_connection)
|
||||
.as_mut()
|
||||
.poll_inbound(cx)
|
||||
})
|
||||
.now_or_never();
|
||||
let mut recipient_substream = maybe_recipient_substream.unwrap().unwrap();
|
||||
|
||||
// poll sender's connection to receive the OpenResponse and send it to the Connection inbound channel
|
||||
inbound_receive_and_send(
|
||||
connection_id.clone(),
|
||||
&mut sender_mixnet_inbound_rx,
|
||||
&sender_inbound_tx,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
// poll sender's poll_outbound to get the substream
|
||||
poll_fn(|cx| Pin::new(&mut sender_connection).as_mut().poll(cx)).now_or_never();
|
||||
assert!(sender_connection.pending_substreams.is_empty());
|
||||
|
||||
// finally, write message to the substream
|
||||
let data = b"hello world";
|
||||
sender_substream.write_all(data).await.unwrap();
|
||||
assert_eq!(sender_connection.message_nonce.load(Ordering::SeqCst), 3);
|
||||
|
||||
// receive message from the mixnet, push to the recipient Connection inbound channel
|
||||
inbound_receive_and_send(
|
||||
connection_id.clone(),
|
||||
&mut recipient_mixnet_inbound_rx,
|
||||
&recipient_inbound_tx,
|
||||
2,
|
||||
)
|
||||
.await;
|
||||
|
||||
// poll the sender's connection to send the msg from the connection inbound channel to the substream's
|
||||
poll_fn(|cx| Pin::new(&mut sender_connection).as_mut().poll(cx)).now_or_never();
|
||||
|
||||
// poll the recipient's connection to read the msg from the mixnet and mux it into the substream
|
||||
poll_fn(|cx| Pin::new(&mut recipient_connection).as_mut().poll(cx)).now_or_never();
|
||||
|
||||
let mut buf = [0u8; 11];
|
||||
let n = recipient_substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(n, 11);
|
||||
assert_eq!(buf, data[..]);
|
||||
|
||||
// test closing the stream; assert the stream is closed on both sides
|
||||
sender_substream.close().await.unwrap();
|
||||
assert_eq!(sender_connection.message_nonce.load(Ordering::SeqCst), 4);
|
||||
inbound_receive_and_send(
|
||||
connection_id.clone(),
|
||||
&mut recipient_mixnet_inbound_rx,
|
||||
&recipient_inbound_tx,
|
||||
3,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
use libp2p::core::multiaddr;
|
||||
use nym_sphinx::addressing::clients::RecipientFormattingError;
|
||||
|
||||
use super::message::SubstreamId;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("unimplemented")]
|
||||
Unimplemented,
|
||||
#[error("failed to format multiaddress from nym address")]
|
||||
FailedToFormatMultiaddr(#[from] multiaddr::Error),
|
||||
#[error("unexpected protocol in multiaddress")]
|
||||
InvalidProtocolForMultiaddr,
|
||||
#[error("failed to decode message")]
|
||||
InvalidMessageBytes,
|
||||
#[error("no connection found for ConnectionResponse")]
|
||||
NoConnectionForResponse,
|
||||
#[error("received ConnectionResponse but connection was already established")]
|
||||
ConnectionAlreadyEstablished,
|
||||
#[error("received None recipient in ConnectionRequest")]
|
||||
NoneRecipientInConnectionRequest,
|
||||
#[error("cannot handle connection request; already have connection with given ID")]
|
||||
ConnectionIDExists,
|
||||
#[error("no connection found for TransportMessage")]
|
||||
NoConnectionForTransportMessage,
|
||||
#[error("failed to decode ConnectionMessage; too short")]
|
||||
ConnectionMessageBytesTooShort,
|
||||
#[error("failed to decode ConnectionMessage; no recipient")]
|
||||
ConnectionMessageBytesNoRecipient,
|
||||
#[error("failed to decode ConnectionMessage; no peer ID")]
|
||||
ConnectionMessageBytesNoPeerId,
|
||||
#[error("invalid peer ID bytes")]
|
||||
InvalidPeerIdBytes,
|
||||
#[error("invalid recipient bytes")]
|
||||
InvalidRecipientBytes(#[from] RecipientFormattingError),
|
||||
#[error("invalid recipient prefix byte")]
|
||||
InvalidRecipientPrefixByte,
|
||||
#[error("failed to decode TransportMessage; too short")]
|
||||
TransportMessageBytesTooShort,
|
||||
#[error("failed to decode TransportMessage; invalid nonce")]
|
||||
InvalidNonce,
|
||||
#[error("invalid substream ID")]
|
||||
InvalidSubstreamMessageBytes,
|
||||
#[error("invalid substream message type byte")]
|
||||
InvalidSubstreamMessageType,
|
||||
#[error("substrean with given ID already exists")]
|
||||
SubstreamIdExists(SubstreamId),
|
||||
#[error("no substream found for given ID")]
|
||||
SubstreamIdDoesNotExist(SubstreamId),
|
||||
#[error("recv error: channel closed")]
|
||||
OneshotRecvFailure(#[from] tokio::sync::oneshot::error::RecvError),
|
||||
#[error("recv error: channel closed")]
|
||||
RecvFailure,
|
||||
#[error("outbound send error")]
|
||||
OutboundSendFailure(String),
|
||||
#[error("inbound send error")]
|
||||
InboundSendFailure(String),
|
||||
#[error("failed to send new connection; receiver dropped")]
|
||||
ConnectionSendFailure,
|
||||
#[error("failed to send initial TransportEvent::NewAddress")]
|
||||
SendErrorTransportEvent,
|
||||
#[error("dial timed out")]
|
||||
DialTimeout(#[from] tokio::time::error::Elapsed),
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
pub(crate) mod connection;
|
||||
pub mod error;
|
||||
pub(crate) mod message;
|
||||
pub(crate) mod mixnet;
|
||||
pub(crate) mod queue;
|
||||
pub mod substream;
|
||||
pub mod transport;
|
||||
|
||||
/// The deafult timeout secs for [`transport::Upgrade`] future.
|
||||
const DEFAULT_HANDSHAKE_TIMEOUT_SECS: u64 = 5;
|
||||
@@ -1,335 +0,0 @@
|
||||
use libp2p::core::PeerId;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
use super::error::Error;
|
||||
|
||||
const RECIPIENT_LENGTH: usize = Recipient::LEN;
|
||||
const CONNECTION_ID_LENGTH: usize = 32;
|
||||
const SUBSTREAM_ID_LENGTH: usize = 32;
|
||||
|
||||
const NONCE_BYTES_LEN: usize = 8; // length of u64
|
||||
const MIN_CONNECTION_MESSAGE_LEN: usize = CONNECTION_ID_LENGTH + NONCE_BYTES_LEN;
|
||||
|
||||
/// ConnectionId is a unique, randomly-generated per-connection ID that's used to
|
||||
/// identify which connection a message belongs to.
|
||||
#[derive(Clone, Default, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct ConnectionId([u8; 32]);
|
||||
|
||||
impl ConnectionId {
|
||||
pub(crate) fn generate() -> Self {
|
||||
let mut bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut bytes);
|
||||
ConnectionId(bytes)
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
let mut id = [0u8; 32];
|
||||
id[..].copy_from_slice(&bytes[0..CONNECTION_ID_LENGTH]);
|
||||
ConnectionId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for ConnectionId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", hex::encode(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// SubstreamId is a unique, randomly-generated per-substream ID that's used to
|
||||
/// identify which substream a message belongs to.
|
||||
#[derive(Clone, Default, Eq, Hash, PartialEq)]
|
||||
pub struct SubstreamId(pub(crate) [u8; 32]);
|
||||
|
||||
impl SubstreamId {
|
||||
pub(crate) fn generate() -> Self {
|
||||
let mut bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut bytes);
|
||||
SubstreamId(bytes)
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
let mut id = [0u8; 32];
|
||||
id[..].copy_from_slice(&bytes[0..SUBSTREAM_ID_LENGTH]);
|
||||
SubstreamId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SubstreamId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", hex::encode(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(crate) enum Message {
|
||||
ConnectionRequest(ConnectionMessage),
|
||||
ConnectionResponse(ConnectionMessage),
|
||||
TransportMessage(TransportMessage),
|
||||
}
|
||||
|
||||
/// ConnectionMessage is exchanged to open a new connection.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ConnectionMessage {
|
||||
pub(crate) peer_id: PeerId,
|
||||
pub(crate) id: ConnectionId,
|
||||
/// recipient is the sender's Nym address.
|
||||
/// only required if this is a ConnectionRequest.
|
||||
pub(crate) recipient: Option<Recipient>,
|
||||
}
|
||||
|
||||
/// TransportMessage is sent over a connection after establishment.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TransportMessage {
|
||||
/// increments by 1 for every TransportMessage sent over a connection.
|
||||
/// required for ordering, since Nym does not guarantee ordering.
|
||||
/// ConnectionMessages do not need nonces, as we know that they will
|
||||
/// be the first messages sent over a connection.
|
||||
/// the first TransportMessage sent over a connection will have nonce 1.
|
||||
pub(crate) nonce: u64,
|
||||
pub(crate) message: SubstreamMessage,
|
||||
pub(crate) id: ConnectionId,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, Error> {
|
||||
if bytes.len() < 2 {
|
||||
return Err(Error::InvalidMessageBytes);
|
||||
}
|
||||
|
||||
Ok(match bytes[0] {
|
||||
0 => Message::ConnectionRequest(ConnectionMessage::try_from_bytes(&bytes[1..])?),
|
||||
1 => Message::ConnectionResponse(ConnectionMessage::try_from_bytes(&bytes[1..])?),
|
||||
2 => Message::TransportMessage(TransportMessage::try_from_bytes(&bytes[1..])?),
|
||||
_ => return Err(Error::InvalidMessageBytes),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionMessage {
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.id.0.to_vec();
|
||||
match self.recipient {
|
||||
Some(recipient) => {
|
||||
bytes.push(1u8);
|
||||
bytes.append(&mut recipient.to_bytes().to_vec());
|
||||
}
|
||||
None => bytes.push(0u8),
|
||||
}
|
||||
bytes.append(&mut self.peer_id.to_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn try_from_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
if bytes.len() < CONNECTION_ID_LENGTH + 1 {
|
||||
return Err(Error::ConnectionMessageBytesTooShort);
|
||||
}
|
||||
|
||||
let id = ConnectionId::from_bytes(&bytes[0..CONNECTION_ID_LENGTH]);
|
||||
let recipient = match bytes[CONNECTION_ID_LENGTH] {
|
||||
0u8 => None,
|
||||
1u8 => {
|
||||
if bytes.len() < CONNECTION_ID_LENGTH + 1 + RECIPIENT_LENGTH {
|
||||
return Err(Error::ConnectionMessageBytesNoRecipient);
|
||||
}
|
||||
|
||||
let mut recipient_bytes = [0u8; RECIPIENT_LENGTH];
|
||||
recipient_bytes[..].copy_from_slice(
|
||||
&bytes[CONNECTION_ID_LENGTH + 1..CONNECTION_ID_LENGTH + 1 + RECIPIENT_LENGTH],
|
||||
);
|
||||
Some(
|
||||
Recipient::try_from_bytes(recipient_bytes)
|
||||
.map_err(Error::InvalidRecipientBytes)?,
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::InvalidRecipientPrefixByte);
|
||||
}
|
||||
};
|
||||
let peer_id = match recipient {
|
||||
Some(_) => {
|
||||
if bytes.len() < CONNECTION_ID_LENGTH + RECIPIENT_LENGTH + 2 {
|
||||
return Err(Error::ConnectionMessageBytesNoPeerId);
|
||||
}
|
||||
PeerId::from_bytes(&bytes[CONNECTION_ID_LENGTH + 1 + RECIPIENT_LENGTH..])
|
||||
.map_err(|_| Error::InvalidPeerIdBytes)?
|
||||
}
|
||||
None => {
|
||||
if bytes.len() < CONNECTION_ID_LENGTH + 2 {
|
||||
return Err(Error::ConnectionMessageBytesNoPeerId);
|
||||
}
|
||||
PeerId::from_bytes(&bytes[CONNECTION_ID_LENGTH + 1..])
|
||||
.map_err(|_| Error::InvalidPeerIdBytes)?
|
||||
}
|
||||
};
|
||||
Ok(ConnectionMessage {
|
||||
peer_id,
|
||||
recipient,
|
||||
id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportMessage {
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.nonce.to_be_bytes().to_vec();
|
||||
bytes.extend_from_slice(self.id.0.as_ref());
|
||||
bytes.extend_from_slice(&self.message.to_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn try_from_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
if bytes.len() < MIN_CONNECTION_MESSAGE_LEN + 1 {
|
||||
return Err(Error::TransportMessageBytesTooShort);
|
||||
}
|
||||
|
||||
let nonce = u64::from_be_bytes(
|
||||
bytes[0..NONCE_BYTES_LEN]
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.map_err(|_| Error::InvalidNonce)?,
|
||||
);
|
||||
let id = ConnectionId::from_bytes(&bytes[NONCE_BYTES_LEN..MIN_CONNECTION_MESSAGE_LEN]);
|
||||
let message = SubstreamMessage::try_from_bytes(&bytes[MIN_CONNECTION_MESSAGE_LEN..])?;
|
||||
Ok(TransportMessage { nonce, message, id })
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TransportMessage {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.nonce.cmp(&other.nonce)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::PartialOrd for TransportMessage {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::Eq for TransportMessage {}
|
||||
|
||||
impl std::cmp::PartialEq for TransportMessage {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.nonce == other.nonce
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum SubstreamMessageType {
|
||||
OpenRequest,
|
||||
OpenResponse,
|
||||
Close,
|
||||
Data(Vec<u8>),
|
||||
}
|
||||
|
||||
impl SubstreamMessageType {
|
||||
fn to_u8(&self) -> u8 {
|
||||
match self {
|
||||
SubstreamMessageType::OpenRequest => 0,
|
||||
SubstreamMessageType::OpenResponse => 1,
|
||||
SubstreamMessageType::Close => 2,
|
||||
SubstreamMessageType::Data(_) => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SubstreamMessage is a message sent over a substream.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SubstreamMessage {
|
||||
pub(crate) substream_id: SubstreamId,
|
||||
pub(crate) message_type: SubstreamMessageType,
|
||||
}
|
||||
|
||||
impl SubstreamMessage {
|
||||
pub(crate) fn new_with_data(substream_id: SubstreamId, message: Vec<u8>) -> Self {
|
||||
SubstreamMessage {
|
||||
substream_id,
|
||||
message_type: SubstreamMessageType::Data(message),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_close(substream_id: SubstreamId) -> Self {
|
||||
SubstreamMessage {
|
||||
substream_id,
|
||||
message_type: SubstreamMessageType::Close,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.substream_id.0.clone().to_vec();
|
||||
bytes.push(self.message_type.to_u8());
|
||||
if let SubstreamMessageType::Data(message) = &self.message_type {
|
||||
bytes.extend_from_slice(message);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
if bytes.len() < SUBSTREAM_ID_LENGTH + 1 {
|
||||
return Err(Error::InvalidSubstreamMessageBytes);
|
||||
}
|
||||
|
||||
let substream_id = SubstreamId::from_bytes(&bytes[0..SUBSTREAM_ID_LENGTH]);
|
||||
let message_type = match bytes[SUBSTREAM_ID_LENGTH] {
|
||||
0 => SubstreamMessageType::OpenRequest,
|
||||
1 => SubstreamMessageType::OpenResponse,
|
||||
2 => SubstreamMessageType::Close,
|
||||
3 => {
|
||||
if bytes.len() < SUBSTREAM_ID_LENGTH + 2 {
|
||||
return Err(Error::InvalidSubstreamMessageBytes);
|
||||
}
|
||||
SubstreamMessageType::Data(bytes[SUBSTREAM_ID_LENGTH + 1..].to_vec())
|
||||
}
|
||||
_ => return Err(Error::InvalidSubstreamMessageType),
|
||||
};
|
||||
|
||||
Ok(SubstreamMessage {
|
||||
substream_id,
|
||||
message_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Message::ConnectionRequest(msg) => {
|
||||
let mut bytes = 0_u8.to_be_bytes().to_vec();
|
||||
bytes.append(&mut msg.to_bytes());
|
||||
bytes
|
||||
}
|
||||
Message::ConnectionResponse(msg) => {
|
||||
let mut bytes = 1_u8.to_be_bytes().to_vec();
|
||||
bytes.append(&mut msg.to_bytes());
|
||||
bytes
|
||||
}
|
||||
Message::TransportMessage(msg) => {
|
||||
let mut bytes = 2_u8.to_be_bytes().to_vec();
|
||||
bytes.append(&mut msg.to_bytes());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// InboundMessage represents an inbound mixnet message.
|
||||
pub(crate) struct InboundMessage(pub(crate) Message);
|
||||
|
||||
/// OutboundMessage represents an outbound mixnet message.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OutboundMessage {
|
||||
pub(crate) message: Message,
|
||||
pub(crate) recipient: Recipient,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_message_data(data: &[u8]) -> Result<InboundMessage, Error> {
|
||||
if data.len() < 2 {
|
||||
return Err(Error::InvalidMessageBytes);
|
||||
}
|
||||
let msg = Message::try_from_bytes(data.to_vec())?;
|
||||
Ok(InboundMessage(msg))
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
use futures::{pin_mut, select};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use log::debug;
|
||||
use nym_sdk::mixnet::{IncludedSurbs, MixnetClient, MixnetClientSender, MixnetMessageSender};
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::receiver::ReconstructedMessage;
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||
|
||||
use super::error::Error;
|
||||
use super::message::*;
|
||||
|
||||
/// initialize_mixnet initializes a read/write connection to a Nym websockets endpoint.
|
||||
/// It starts a task that listens for inbound messages from the endpoint and writes outbound messages to the endpoint.
|
||||
pub(crate) async fn initialize_mixnet(
|
||||
client: MixnetClient,
|
||||
notify_inbound_tx: Option<UnboundedSender<()>>,
|
||||
) -> Result<
|
||||
(
|
||||
Recipient,
|
||||
UnboundedReceiver<InboundMessage>,
|
||||
UnboundedSender<OutboundMessage>,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let recipient = *client.nym_address();
|
||||
|
||||
// a channel of inbound messages from the mixnet..
|
||||
// the transport reads from (listens) to the inbound_rx.
|
||||
// TODO: this is probably a DOS vector; we should limit the size of the channel.
|
||||
let (inbound_tx, inbound_rx) = unbounded_channel::<InboundMessage>();
|
||||
|
||||
// a channel of outbound messages to be written to the mixnet.
|
||||
// the transport writes to outbound_tx.
|
||||
let (outbound_tx, mut outbound_rx) = unbounded_channel::<OutboundMessage>();
|
||||
|
||||
let sink = client.split_sender();
|
||||
let mut stream = client;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
loop {
|
||||
let t1 = check_inbound(&mut stream, &inbound_tx, ¬ify_inbound_tx).fuse();
|
||||
let t2 = check_outbound(&sink, &mut outbound_rx).fuse();
|
||||
|
||||
pin_mut!(t1, t2);
|
||||
|
||||
select! {
|
||||
_ = t1 => {},
|
||||
_ = t2 => {},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Ok((recipient, inbound_rx, outbound_tx))
|
||||
}
|
||||
|
||||
async fn check_inbound(
|
||||
client: &mut MixnetClient,
|
||||
inbound_tx: &UnboundedSender<InboundMessage>,
|
||||
notify_inbound_tx: &Option<UnboundedSender<()>>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(msg) = client.next().await {
|
||||
if let Some(notify_tx) = notify_inbound_tx {
|
||||
notify_tx
|
||||
.send(())
|
||||
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
|
||||
}
|
||||
|
||||
handle_inbound(msg, inbound_tx).await?;
|
||||
}
|
||||
|
||||
Err(Error::Unimplemented)
|
||||
}
|
||||
|
||||
async fn handle_inbound(
|
||||
msg: ReconstructedMessage,
|
||||
inbound_tx: &UnboundedSender<InboundMessage>,
|
||||
) -> Result<(), Error> {
|
||||
let data = parse_message_data(&msg.message)?;
|
||||
inbound_tx
|
||||
.send(data)
|
||||
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_outbound(
|
||||
mixnet_sender: &MixnetClientSender,
|
||||
outbound_rx: &mut UnboundedReceiver<OutboundMessage>,
|
||||
) -> Result<(), Error> {
|
||||
match outbound_rx.recv().await {
|
||||
Some(message) => {
|
||||
write_bytes(
|
||||
mixnet_sender,
|
||||
message.recipient,
|
||||
&message.message.to_bytes(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => Err(Error::RecvFailure),
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_bytes(
|
||||
mixnet_sender: &MixnetClientSender,
|
||||
recipient: Recipient,
|
||||
message: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
if let Err(_err) = mixnet_sender
|
||||
.send_message(recipient, message, IncludedSurbs::ExposeSelfAddress)
|
||||
.await
|
||||
{
|
||||
return Err(Error::Unimplemented);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"wrote message to mixnet: recipient: {:?}",
|
||||
recipient.to_string()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::message::{
|
||||
self, ConnectionId, Message, SubstreamId, SubstreamMessage, SubstreamMessageType,
|
||||
TransportMessage,
|
||||
};
|
||||
use super::super::mixnet::initialize_mixnet;
|
||||
use nym_sdk::mixnet::MixnetClient;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mixnet_poll_inbound_and_outbound() {
|
||||
let client = MixnetClient::connect_new().await.unwrap();
|
||||
let (self_address, mut inbound_rx, outbound_tx) =
|
||||
initialize_mixnet(client, None).await.unwrap();
|
||||
let msg_inner = "hello".as_bytes();
|
||||
let substream_id = SubstreamId::generate();
|
||||
let msg = Message::TransportMessage(TransportMessage {
|
||||
nonce: 1, // arbitrary
|
||||
id: ConnectionId::generate(),
|
||||
message: SubstreamMessage::new_with_data(substream_id.clone(), msg_inner.to_vec()),
|
||||
});
|
||||
|
||||
// send a message to ourselves through the mixnet
|
||||
let out_msg = message::OutboundMessage {
|
||||
message: msg,
|
||||
recipient: self_address,
|
||||
};
|
||||
|
||||
outbound_tx.send(out_msg).unwrap();
|
||||
|
||||
// receive the message from ourselves over the mixnet
|
||||
let received_msg = inbound_rx.recv().await.unwrap();
|
||||
if let Message::TransportMessage(recv_msg) = received_msg.0 {
|
||||
assert_eq!(substream_id, recv_msg.message.substream_id);
|
||||
if let SubstreamMessageType::Data(data) = recv_msg.message.message_type {
|
||||
assert_eq!(msg_inner, data.as_slice());
|
||||
} else {
|
||||
panic!("expected SubstreamMessage::Data")
|
||||
}
|
||||
} else {
|
||||
panic!("expected Message::TransportMessage")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
use log::{debug, warn};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::message::TransportMessage;
|
||||
|
||||
/// MessageQueue is a queue of messages, ordered by nonce, that we've
|
||||
/// received but are not yet able to process because we're waiting for
|
||||
/// a message with the next expected nonce first.
|
||||
/// This is required because Nym does not guarantee any sort of message
|
||||
/// ordering, only delivery.
|
||||
/// TODO: is there a DOS vector here where a malicious peer sends us
|
||||
/// messages only with nonce higher than the next expected nonce?
|
||||
pub(crate) struct MessageQueue {
|
||||
/// nonce of the next message we expect to receive on the
|
||||
/// connection.
|
||||
/// any messages with a nonce greater than this are pushed into
|
||||
/// the queue.
|
||||
/// if we get a message with a nonce equal to this, then we
|
||||
/// immediately handle it in the transport and increment the nonce.
|
||||
next_expected_nonce: u64,
|
||||
|
||||
/// the actual queue of messages, ordered by nonce.
|
||||
/// the head of the queue's nonce is always greater
|
||||
/// than the next expected nonce.
|
||||
queue: BTreeSet<TransportMessage>,
|
||||
}
|
||||
|
||||
impl MessageQueue {
|
||||
pub(crate) fn new() -> Self {
|
||||
MessageQueue {
|
||||
next_expected_nonce: 0,
|
||||
queue: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn print_nonces(&self) {
|
||||
let nonces = self.queue.iter().map(|msg| msg.nonce).collect::<Vec<_>>();
|
||||
debug!("MessageQueue: {:?}", nonces);
|
||||
}
|
||||
|
||||
/// sets the next expected nonce to 1, indicating that we've received
|
||||
/// a ConnectionRequest or ConnectionResponse.
|
||||
pub(crate) fn set_connection_message_received(&mut self) {
|
||||
if self.next_expected_nonce != 0 {
|
||||
panic!("connection message received twice");
|
||||
}
|
||||
|
||||
self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1);
|
||||
}
|
||||
|
||||
/// tries to push a message into the queue.
|
||||
/// if the message has the next expected nonce, then the message is returned,
|
||||
/// and should be processed by the caller.
|
||||
/// in that case, the internal queue's next expected nonce is incremented.
|
||||
pub(crate) fn try_push(&mut self, msg: TransportMessage) -> Option<TransportMessage> {
|
||||
if msg.nonce == self.next_expected_nonce {
|
||||
self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1);
|
||||
Some(msg)
|
||||
} else {
|
||||
if msg.nonce < self.next_expected_nonce {
|
||||
// this shouldn't happen normally, only if the other node
|
||||
// is not following the protocol
|
||||
warn!("received a message with a nonce that is too low");
|
||||
return None;
|
||||
}
|
||||
|
||||
if !self.queue.insert(msg) {
|
||||
// this shouldn't happen normally, only if the other node
|
||||
// is not following the protocol
|
||||
warn!("received a message with a duplicate nonce");
|
||||
return None;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pop(&mut self) -> Option<TransportMessage> {
|
||||
let head = self.queue.first()?;
|
||||
|
||||
if head.nonce == self.next_expected_nonce {
|
||||
self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1);
|
||||
Some(self.queue.pop_first().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::message::{ConnectionId, SubstreamId, SubstreamMessage};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl TransportMessage {
|
||||
fn new(nonce: u64, message: SubstreamMessage, id: ConnectionId) -> Self {
|
||||
TransportMessage { nonce, message, id }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_queue() {
|
||||
let mut queue = MessageQueue::new();
|
||||
|
||||
let test_substream_message =
|
||||
SubstreamMessage::new_with_data(SubstreamId::generate(), vec![1, 2, 3]);
|
||||
let connection_id = ConnectionId::generate();
|
||||
|
||||
let msg1 = TransportMessage::new(1, test_substream_message.clone(), connection_id.clone());
|
||||
let msg2 = TransportMessage::new(2, test_substream_message.clone(), connection_id.clone());
|
||||
let msg3 = TransportMessage::new(3, test_substream_message.clone(), connection_id.clone());
|
||||
|
||||
assert_eq!(queue.try_push(msg1.clone()), None);
|
||||
assert_eq!(queue.try_push(msg3.clone()), None);
|
||||
assert_eq!(queue.try_push(msg2.clone()), None);
|
||||
|
||||
assert_eq!(queue.pop(), None);
|
||||
|
||||
// set expected nonce to 1
|
||||
queue.set_connection_message_received();
|
||||
assert_eq!(queue.pop(), Some(msg1));
|
||||
|
||||
let msg4 = TransportMessage::new(4, test_substream_message.clone(), connection_id.clone());
|
||||
assert_eq!(queue.try_push(msg4.clone()), None);
|
||||
|
||||
assert_eq!(queue.pop(), Some(msg2));
|
||||
assert_eq!(queue.pop(), Some(msg3));
|
||||
assert_eq!(queue.pop(), Some(msg4));
|
||||
assert_eq!(queue.pop(), None);
|
||||
assert_eq!(queue.next_expected_nonce, 5);
|
||||
|
||||
// should just return the message and increment nonce when message nonce = next expected nonce
|
||||
let msg5 = TransportMessage::new(5, test_substream_message, connection_id);
|
||||
assert_eq!(queue.try_push(msg5.clone()), Some(msg5));
|
||||
assert_eq!(queue.next_expected_nonce, 6);
|
||||
}
|
||||
}
|
||||
@@ -1,412 +0,0 @@
|
||||
use super::message::{
|
||||
ConnectionId, Message, OutboundMessage, SubstreamId, SubstreamMessage, TransportMessage,
|
||||
};
|
||||
use futures::{
|
||||
io::{Error as IoError, ErrorKind},
|
||||
AsyncRead, AsyncWrite,
|
||||
};
|
||||
use log::debug;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::sync::{
|
||||
mpsc::{UnboundedReceiver, UnboundedSender},
|
||||
oneshot::Receiver,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Substream {
|
||||
remote_recipient: Recipient,
|
||||
connection_id: ConnectionId,
|
||||
pub(crate) substream_id: SubstreamId,
|
||||
|
||||
/// inbound messages; inbound_tx is in the corresponding Connection
|
||||
pub(crate) inbound_rx: UnboundedReceiver<Vec<u8>>,
|
||||
|
||||
/// outbound messages; go directly to the mixnet
|
||||
outbound_tx: UnboundedSender<OutboundMessage>,
|
||||
|
||||
/// used to signal when the substream is closed
|
||||
close_rx: Receiver<()>,
|
||||
closed: Mutex<bool>,
|
||||
|
||||
// buffer of data that's been written to the stream,
|
||||
// but not yet read by the application.
|
||||
unread_data: Mutex<Vec<u8>>,
|
||||
|
||||
message_nonce: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl Substream {
|
||||
pub(crate) fn new(
|
||||
remote_recipient: Recipient,
|
||||
connection_id: ConnectionId,
|
||||
substream_id: SubstreamId,
|
||||
inbound_rx: UnboundedReceiver<Vec<u8>>,
|
||||
outbound_tx: UnboundedSender<OutboundMessage>,
|
||||
close_rx: Receiver<()>,
|
||||
message_nonce: Arc<AtomicU64>,
|
||||
) -> Self {
|
||||
Substream {
|
||||
remote_recipient,
|
||||
connection_id,
|
||||
substream_id,
|
||||
inbound_rx,
|
||||
outbound_tx,
|
||||
close_rx,
|
||||
closed: Mutex::new(false),
|
||||
unread_data: Mutex::new(vec![]),
|
||||
message_nonce,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_closed(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Result<(), IoError> {
|
||||
let closed_err = IoError::other("stream closed");
|
||||
|
||||
// close_rx will return an error if the channel is closed (ie. sender was dropped),
|
||||
// or if it's empty
|
||||
let received_closed = self.close_rx.try_recv();
|
||||
|
||||
let mut closed = self.closed.lock();
|
||||
if *closed {
|
||||
return Err(closed_err);
|
||||
}
|
||||
|
||||
if received_closed.is_ok() {
|
||||
*closed = true;
|
||||
return Err(closed_err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Substream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<Result<usize, IoError>> {
|
||||
let closed_result = self.as_mut().check_closed(cx);
|
||||
if let Err(e) = closed_result {
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
|
||||
let inbound_rx_data = self.inbound_rx.poll_recv(cx);
|
||||
|
||||
// first, write any previously unread data to the buf
|
||||
let mut unread_data = self.unread_data.lock();
|
||||
let filled_len = if unread_data.len() > 0 {
|
||||
let unread_len = unread_data.len();
|
||||
let buf_len = buf.len();
|
||||
let copy_len = std::cmp::min(unread_len, buf_len);
|
||||
buf[..copy_len].copy_from_slice(&unread_data[..copy_len]);
|
||||
*unread_data = unread_data[copy_len..].to_vec();
|
||||
copy_len
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if let Poll::Ready(Some(data)) = inbound_rx_data {
|
||||
if filled_len == buf.len() {
|
||||
// we've filled the buffer, so we'll have to save the rest for later
|
||||
let mut new = vec![];
|
||||
new.extend(unread_data.drain(..));
|
||||
new.extend(data.iter());
|
||||
*unread_data = new;
|
||||
return Poll::Ready(Ok(filled_len));
|
||||
}
|
||||
|
||||
// otherwise, there's still room in the buffer, so we'll copy the rest of the data
|
||||
let remaining_len = buf.len() - filled_len;
|
||||
let data_len = data.len();
|
||||
|
||||
// we have more data than buffer room remaining, save the extra for later
|
||||
if remaining_len < data_len {
|
||||
unread_data.extend_from_slice(&data[remaining_len..]);
|
||||
}
|
||||
|
||||
let copied = std::cmp::min(remaining_len, data_len);
|
||||
buf[filled_len..filled_len + copied].copy_from_slice(&data[..copied]);
|
||||
debug!("poll_read copied {} bytes: data {:?}", copied, buf);
|
||||
return Poll::Ready(Ok(copied));
|
||||
}
|
||||
|
||||
if filled_len > 0 {
|
||||
debug!("poll_read copied {} bytes: data {:?}", filled_len, buf);
|
||||
return Poll::Ready(Ok(filled_len));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Substream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, IoError>> {
|
||||
if let Err(e) = self.as_mut().check_closed(cx) {
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
|
||||
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
self.outbound_tx
|
||||
.send(OutboundMessage {
|
||||
recipient: self.remote_recipient,
|
||||
message: Message::TransportMessage(TransportMessage {
|
||||
nonce,
|
||||
id: self.connection_id.clone(),
|
||||
message: SubstreamMessage::new_with_data(
|
||||
self.substream_id.clone(),
|
||||
buf.to_vec(),
|
||||
),
|
||||
}),
|
||||
})
|
||||
.map_err(|e| IoError::other(format!("poll_write outbound_tx error: {}", e)))?;
|
||||
|
||||
Poll::Ready(Ok(buf.len()))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
|
||||
if let Err(e) = self.check_closed(cx) {
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
|
||||
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let mut closed = self.closed.lock();
|
||||
if *closed {
|
||||
return Poll::Ready(Err(IoError::other("stream closed")));
|
||||
}
|
||||
|
||||
*closed = true;
|
||||
|
||||
// send a close message to the mixnet
|
||||
self.outbound_tx
|
||||
.send(OutboundMessage {
|
||||
recipient: self.remote_recipient,
|
||||
message: Message::TransportMessage(TransportMessage {
|
||||
nonce,
|
||||
id: self.connection_id.clone(),
|
||||
message: SubstreamMessage::new_close(self.substream_id.clone()),
|
||||
}),
|
||||
})
|
||||
.map_err(|e| IoError::other(format!("poll_close outbound_rx error: {}", e)))?;
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::message::{
|
||||
ConnectionId, Message, SubstreamId, SubstreamMessage, TransportMessage,
|
||||
};
|
||||
use super::super::mixnet::initialize_mixnet;
|
||||
use super::Substream;
|
||||
use futures::{AsyncReadExt, AsyncWriteExt};
|
||||
use nym_sdk::mixnet::MixnetClient;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_substream_poll_read_unread_data() {
|
||||
let (outbound_tx, _) = tokio::sync::mpsc::unbounded_channel();
|
||||
let connection_id = ConnectionId::generate();
|
||||
let substream_id = SubstreamId::generate();
|
||||
|
||||
let (inbound_tx, inbound_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (_, close_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
let mut substream = Substream::new(
|
||||
Recipient::try_from_base58_string("D1rrpsysCGCYXy9saP8y3kmNpGtJZUXN9SvFoUcqAsM9.9Ssso1ea5NfkbMASdiseDSjTN1fSWda5SgEVjdSN4CvV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN").unwrap(),
|
||||
connection_id,
|
||||
substream_id,
|
||||
inbound_rx,
|
||||
outbound_tx,
|
||||
close_rx,
|
||||
Arc::new(AtomicU64::new(1)),
|
||||
);
|
||||
|
||||
// test writing and reading w/ same length data
|
||||
let data = b"hello".to_vec();
|
||||
inbound_tx.send(data.clone()).unwrap();
|
||||
let mut buf = [0u8; 5];
|
||||
let read_len = substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(read_len, data.len());
|
||||
assert_eq!(buf.to_vec(), data);
|
||||
|
||||
// test writing data longer than read buffer
|
||||
let data = b"nootwashere".to_vec();
|
||||
inbound_tx.send(data.clone()).unwrap();
|
||||
|
||||
let mut buf = [0u8; 4];
|
||||
let read_len = substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(read_len, buf.len());
|
||||
assert_eq!(buf.to_vec(), b"noot".to_vec());
|
||||
|
||||
let mut buf = [0u8; 7];
|
||||
let read_len = substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(read_len, buf.len());
|
||||
assert_eq!(buf.to_vec(), b"washere".to_vec());
|
||||
|
||||
// test read buffer larger than written data
|
||||
let data = b"nootwashere".to_vec();
|
||||
inbound_tx.send(data.clone()).unwrap();
|
||||
let mut buf = [0u8; 16];
|
||||
let read_len = substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(read_len, data.len());
|
||||
assert_eq!(buf[..data.len()], data);
|
||||
assert_eq!(buf[data.len()..].to_vec(), vec![0u8; 16 - data.len()]);
|
||||
|
||||
// test writing data longer than read buffer multiple times
|
||||
let data = b"nootwashere".to_vec();
|
||||
inbound_tx.send(data.clone()).unwrap();
|
||||
|
||||
let mut buf = [0u8; 4];
|
||||
let read_len = substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(read_len, buf.len());
|
||||
assert_eq!(buf.to_vec(), b"noot".to_vec());
|
||||
|
||||
let data = b"asdf".to_vec();
|
||||
inbound_tx.send(data.clone()).unwrap();
|
||||
|
||||
let mut buf = [0u8; 4];
|
||||
let read_len = substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(read_len, buf.len());
|
||||
assert_eq!(buf.to_vec(), b"wash".to_vec());
|
||||
|
||||
let mut buf = [0u8; 8];
|
||||
let read_len = substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(read_len, 7);
|
||||
assert_eq!(buf[..7], b"ereasdf".to_vec());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_substream_read_write() {
|
||||
let client = MixnetClient::connect_new().await.unwrap();
|
||||
let (self_address, mut mixnet_inbound_rx, outbound_tx) =
|
||||
initialize_mixnet(client, None).await.unwrap();
|
||||
|
||||
const MSG_INNER: &[u8] = "hello".as_bytes();
|
||||
let connection_id = ConnectionId::generate();
|
||||
let substream_id = SubstreamId::generate();
|
||||
|
||||
let (inbound_tx, inbound_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (_, close_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
let mut substream = Substream::new(
|
||||
self_address,
|
||||
connection_id,
|
||||
substream_id,
|
||||
inbound_rx,
|
||||
outbound_tx,
|
||||
close_rx,
|
||||
Arc::new(AtomicU64::new(1)),
|
||||
);
|
||||
|
||||
// send message to ourselves over the mixnet
|
||||
substream.write_all(MSG_INNER).await.unwrap();
|
||||
|
||||
// receive full message over the mixnet
|
||||
let recv_msg = mixnet_inbound_rx.recv().await.unwrap();
|
||||
match recv_msg.0 {
|
||||
Message::TransportMessage(TransportMessage {
|
||||
nonce,
|
||||
id: _,
|
||||
message:
|
||||
SubstreamMessage {
|
||||
substream_id: _,
|
||||
message_type: msg,
|
||||
},
|
||||
}) => {
|
||||
assert_eq!(nonce, 1);
|
||||
match msg {
|
||||
super::super::message::SubstreamMessageType::Data(data) => {
|
||||
assert_eq!(data, MSG_INNER);
|
||||
// send message to substream inbound channel
|
||||
inbound_tx.send(data).unwrap();
|
||||
}
|
||||
_ => panic!("unexpected message type"),
|
||||
}
|
||||
}
|
||||
_ => panic!("unexpected message"),
|
||||
}
|
||||
|
||||
// read message from substream
|
||||
let mut buf = [0u8; MSG_INNER.len()];
|
||||
substream.read_exact(&mut buf).await.unwrap();
|
||||
assert_eq!(buf, MSG_INNER);
|
||||
|
||||
// close substream
|
||||
substream.close().await.unwrap();
|
||||
|
||||
// try to read/write to closed substream; should error
|
||||
substream.write_all(MSG_INNER).await.unwrap_err();
|
||||
substream.read_exact(&mut buf).await.unwrap_err();
|
||||
|
||||
// assert a close message was sent over the mixnet
|
||||
let recv_msg = mixnet_inbound_rx.recv().await.unwrap();
|
||||
match recv_msg.0 {
|
||||
Message::TransportMessage(TransportMessage {
|
||||
nonce: _,
|
||||
id: _,
|
||||
message:
|
||||
SubstreamMessage {
|
||||
substream_id: _,
|
||||
message_type: msg,
|
||||
},
|
||||
}) => match msg {
|
||||
super::super::message::SubstreamMessageType::Close => {}
|
||||
_ => panic!("unexpected message type"),
|
||||
},
|
||||
_ => panic!("unexpected message: {:?}", recv_msg.0),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_substream_recv_close() {
|
||||
let client = MixnetClient::connect_new().await.unwrap();
|
||||
let (self_address, _, outbound_tx) = initialize_mixnet(client, None).await.unwrap();
|
||||
|
||||
const MSG_INNER: &[u8] = "hello".as_bytes();
|
||||
let connection_id = ConnectionId::generate();
|
||||
let substream_id = SubstreamId::generate();
|
||||
|
||||
let (_, inbound_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (close_tx, close_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
let mut substream = Substream::new(
|
||||
self_address,
|
||||
connection_id,
|
||||
substream_id,
|
||||
inbound_rx,
|
||||
outbound_tx,
|
||||
close_rx,
|
||||
Arc::new(AtomicU64::new(1)),
|
||||
);
|
||||
|
||||
// close substream
|
||||
close_tx.send(()).unwrap();
|
||||
|
||||
// try to read/write to closed substream; should error
|
||||
substream.write_all(MSG_INNER).await.unwrap_err();
|
||||
let mut buf = [0u8; MSG_INNER.len()];
|
||||
substream.read_exact(&mut buf).await.unwrap_err();
|
||||
}
|
||||
}
|
||||
@@ -1,898 +0,0 @@
|
||||
use futures::prelude::*;
|
||||
use libp2p::core::{
|
||||
identity::Keypair,
|
||||
multiaddr::{Multiaddr, Protocol},
|
||||
transport::{ListenerId, TransportError, TransportEvent},
|
||||
PeerId, Transport,
|
||||
};
|
||||
use log::debug;
|
||||
use nym_sdk::mixnet::MixnetClient;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
pin::Pin,
|
||||
str::FromStr,
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
oneshot,
|
||||
},
|
||||
time::{timeout, Duration},
|
||||
};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use super::connection::{Connection, PendingConnection};
|
||||
use super::error::Error;
|
||||
use super::message::{
|
||||
ConnectionId, ConnectionMessage, InboundMessage, Message, OutboundMessage, SubstreamMessage,
|
||||
TransportMessage,
|
||||
};
|
||||
use super::mixnet::initialize_mixnet;
|
||||
use super::queue::MessageQueue;
|
||||
use super::DEFAULT_HANDSHAKE_TIMEOUT_SECS;
|
||||
|
||||
/// InboundTransportEvent represents an inbound event from the mixnet.
|
||||
pub enum InboundTransportEvent {
|
||||
ConnectionRequest(Upgrade),
|
||||
ConnectionResponse,
|
||||
TransportMessage,
|
||||
}
|
||||
|
||||
/// NymTransport implements the Transport trait using the Nym mixnet.
|
||||
pub struct NymTransport {
|
||||
/// our Nym address
|
||||
self_address: Recipient,
|
||||
pub(crate) listen_addr: Multiaddr,
|
||||
pub(crate) listener_id: ListenerId,
|
||||
|
||||
/// our libp2p keypair; currently not really used
|
||||
keypair: Keypair,
|
||||
|
||||
/// established connections -> channel which sends messages received from
|
||||
/// the mixnet to the corresponding Connection
|
||||
connections: HashMap<ConnectionId, UnboundedSender<SubstreamMessage>>,
|
||||
|
||||
/// outbound pending dials
|
||||
pending_dials: HashMap<ConnectionId, PendingConnection>,
|
||||
|
||||
/// connection message queues
|
||||
message_queues: HashMap<ConnectionId, MessageQueue>,
|
||||
|
||||
/// inbound mixnet messages
|
||||
inbound_stream: UnboundedReceiverStream<InboundMessage>,
|
||||
|
||||
/// outbound mixnet messages
|
||||
outbound_tx: UnboundedSender<OutboundMessage>,
|
||||
|
||||
/// inbound messages for Transport.poll()
|
||||
poll_rx: UnboundedReceiver<TransportEvent<Upgrade, Error>>,
|
||||
|
||||
/// outbound messages to Transport.poll()
|
||||
poll_tx: UnboundedSender<TransportEvent<Upgrade, Error>>,
|
||||
|
||||
waker: Option<Waker>,
|
||||
|
||||
/// Timeout for the [`Upgrade`] future.
|
||||
handshake_timeout: Duration,
|
||||
}
|
||||
|
||||
impl NymTransport {
|
||||
/// New transport.
|
||||
#[allow(unused)]
|
||||
pub async fn new(client: MixnetClient, keypair: Keypair) -> Result<Self, Error> {
|
||||
Self::new_maybe_with_notify_inbound(client, keypair, None, None).await
|
||||
}
|
||||
|
||||
/// New transport with a timeout.
|
||||
#[allow(dead_code)]
|
||||
pub async fn new_with_timeout(
|
||||
client: MixnetClient,
|
||||
keypair: Keypair,
|
||||
timeout: Duration,
|
||||
) -> Result<Self, Error> {
|
||||
Self::new_maybe_with_notify_inbound(client, keypair, None, Some(timeout)).await
|
||||
}
|
||||
|
||||
/// Add timeout to transport and return self.
|
||||
#[allow(dead_code)]
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.handshake_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
async fn new_maybe_with_notify_inbound(
|
||||
client: MixnetClient,
|
||||
keypair: Keypair,
|
||||
notify_inbound_tx: Option<UnboundedSender<()>>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self, Error> {
|
||||
let (self_address, inbound_rx, outbound_tx) =
|
||||
initialize_mixnet(client, notify_inbound_tx).await?;
|
||||
let listen_addr = nym_address_to_multiaddress(self_address)?;
|
||||
let listener_id = ListenerId::new();
|
||||
|
||||
let (poll_tx, poll_rx) = unbounded_channel::<TransportEvent<Upgrade, Error>>();
|
||||
|
||||
poll_tx
|
||||
.send(TransportEvent::NewAddress {
|
||||
listener_id,
|
||||
listen_addr: listen_addr.clone(),
|
||||
})
|
||||
.map_err(|_| Error::SendErrorTransportEvent)?;
|
||||
|
||||
let inbound_stream = UnboundedReceiverStream::new(inbound_rx);
|
||||
let handshake_timeout =
|
||||
timeout.unwrap_or_else(|| Duration::from_secs(DEFAULT_HANDSHAKE_TIMEOUT_SECS));
|
||||
|
||||
Ok(Self {
|
||||
self_address,
|
||||
listen_addr,
|
||||
listener_id,
|
||||
keypair,
|
||||
connections: HashMap::new(),
|
||||
pending_dials: HashMap::new(),
|
||||
message_queues: HashMap::new(),
|
||||
inbound_stream,
|
||||
outbound_tx,
|
||||
poll_rx,
|
||||
poll_tx,
|
||||
waker: None,
|
||||
handshake_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn peer_id(&self) -> PeerId {
|
||||
PeerId::from_public_key(&self.keypair.public())
|
||||
}
|
||||
|
||||
fn handle_message_queue_on_connection_initiation(
|
||||
&mut self,
|
||||
id: &ConnectionId,
|
||||
) -> Result<(), Error> {
|
||||
debug!("handle_message_queue_on_connection_initiation");
|
||||
let Some(inbound_tx) = self.connections.get(id) else {
|
||||
// this should not happen
|
||||
return Err(Error::NoConnectionForTransportMessage);
|
||||
};
|
||||
|
||||
match self.message_queues.get_mut(id) {
|
||||
Some(queue) => {
|
||||
// update expected nonce
|
||||
queue.set_connection_message_received();
|
||||
|
||||
// push pending inbound some messages in this case
|
||||
while let Some(msg) = queue.pop() {
|
||||
debug!(
|
||||
"popped queued message with nonce {} for connection",
|
||||
msg.nonce
|
||||
);
|
||||
inbound_tx
|
||||
.send(msg.message.clone())
|
||||
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// no queue exists for this connection, create one
|
||||
let queue = MessageQueue::new();
|
||||
self.message_queues.insert(id.clone(), queue);
|
||||
let queue = self.message_queues.get_mut(id).unwrap();
|
||||
queue.set_connection_message_received();
|
||||
}
|
||||
};
|
||||
|
||||
debug!("returning from handle_message_queue_on_connection_initiation");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// handle_connection_response resolves the pending connection corresponding to the response
|
||||
// (if there is one) into a Connection.
|
||||
fn handle_connection_response(&mut self, msg: &ConnectionMessage) -> Result<(), Error> {
|
||||
if self.connections.contains_key(&msg.id) {
|
||||
return Err(Error::ConnectionAlreadyEstablished);
|
||||
}
|
||||
|
||||
if let Some(pending_conn) = self.pending_dials.remove(&msg.id) {
|
||||
// resolve connection and put into pending_conn channel
|
||||
let (conn, conn_tx) = self.create_connection_types(
|
||||
msg.peer_id,
|
||||
pending_conn.remote_recipient,
|
||||
msg.id.clone(),
|
||||
);
|
||||
|
||||
self.connections.insert(msg.id.clone(), conn_tx);
|
||||
self.handle_message_queue_on_connection_initiation(&msg.id)?;
|
||||
|
||||
pending_conn
|
||||
.connection_tx
|
||||
.send(conn)
|
||||
.map_err(|_| Error::ConnectionSendFailure)?;
|
||||
|
||||
if let Some(waker) = self.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::NoConnectionForResponse)
|
||||
}
|
||||
}
|
||||
|
||||
/// handle_connection_request handles an incoming connection request, sends back a
|
||||
/// connection response, and finally completes the upgrade into a Connection.
|
||||
fn handle_connection_request(&mut self, msg: &ConnectionMessage) -> Result<Connection, Error> {
|
||||
if msg.recipient.is_none() {
|
||||
return Err(Error::NoneRecipientInConnectionRequest);
|
||||
}
|
||||
|
||||
// ensure we don't already have a conn with the same id
|
||||
if self.connections.contains_key(&msg.id) {
|
||||
return Err(Error::ConnectionIDExists);
|
||||
}
|
||||
|
||||
let (conn, conn_tx) =
|
||||
self.create_connection_types(msg.peer_id, msg.recipient.unwrap(), msg.id.clone());
|
||||
self.connections.insert(msg.id.clone(), conn_tx);
|
||||
self.handle_message_queue_on_connection_initiation(&msg.id)?;
|
||||
|
||||
let resp = ConnectionMessage {
|
||||
peer_id: self.peer_id(),
|
||||
recipient: None,
|
||||
id: msg.id.clone(),
|
||||
};
|
||||
|
||||
self.outbound_tx
|
||||
.send(OutboundMessage {
|
||||
message: Message::ConnectionResponse(resp),
|
||||
recipient: msg.recipient.unwrap(),
|
||||
})
|
||||
.map_err(|e| Error::OutboundSendFailure(e.to_string()))?;
|
||||
|
||||
if let Some(waker) = self.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
fn handle_transport_message(&mut self, msg: TransportMessage) -> Result<(), Error> {
|
||||
let queue = match self.message_queues.get_mut(&msg.id) {
|
||||
Some(queue) => queue,
|
||||
None => {
|
||||
// no queue exists for this connection, create one
|
||||
let queue = MessageQueue::new();
|
||||
self.message_queues.insert(msg.id.clone(), queue);
|
||||
self.message_queues.get_mut(&msg.id).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
queue.print_nonces();
|
||||
|
||||
let nonce = msg.nonce;
|
||||
let Some(msg) = queue.try_push(msg) else {
|
||||
// don't push the message yet, it's been queued
|
||||
debug!("message with nonce {} queued for connection", nonce);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(inbound_tx) = self.connections.get(&msg.id) else {
|
||||
return Err(Error::NoConnectionForTransportMessage);
|
||||
};
|
||||
|
||||
// send original message
|
||||
debug!(
|
||||
"sending original message with nonce {} for connection",
|
||||
nonce
|
||||
);
|
||||
inbound_tx
|
||||
.send(msg.message.clone())
|
||||
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
|
||||
|
||||
// try to pop queued messages and send them on inbound channel
|
||||
while let Some(msg) = queue.pop() {
|
||||
debug!(
|
||||
"popped queued message with nonce {} for connection",
|
||||
msg.nonce
|
||||
);
|
||||
inbound_tx
|
||||
.send(msg.message.clone())
|
||||
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
|
||||
}
|
||||
|
||||
if let Some(waker) = self.waker.clone().take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_connection_types(
|
||||
&self,
|
||||
remote_peer_id: PeerId,
|
||||
recipient: Recipient,
|
||||
id: ConnectionId,
|
||||
) -> (Connection, UnboundedSender<SubstreamMessage>) {
|
||||
let (inbound_tx, inbound_rx) = unbounded_channel::<SubstreamMessage>();
|
||||
|
||||
// representation of a connection; this contains channels for applications to read/write to.
|
||||
let conn = Connection::new(
|
||||
remote_peer_id,
|
||||
recipient,
|
||||
id,
|
||||
inbound_rx,
|
||||
self.outbound_tx.clone(),
|
||||
);
|
||||
|
||||
// inbound_tx is what we write to when receiving messages on the mixnet,
|
||||
(conn, inbound_tx)
|
||||
}
|
||||
|
||||
/// handle_inbound handles an inbound message from the mixnet, received via self.inbound_stream.
|
||||
fn handle_inbound(&mut self, msg: Message) -> Result<InboundTransportEvent, Error> {
|
||||
match msg {
|
||||
Message::ConnectionRequest(inner) => {
|
||||
debug!("got inbound connection request {:?}", inner);
|
||||
match self.handle_connection_request(&inner) {
|
||||
Ok(conn) => {
|
||||
let (connection_tx, connection_rx) =
|
||||
oneshot::channel::<(PeerId, Connection)>();
|
||||
let upgrade = Upgrade::new(connection_rx);
|
||||
connection_tx
|
||||
.send((inner.peer_id, conn))
|
||||
.map_err(|_| Error::ConnectionSendFailure)?;
|
||||
Ok(InboundTransportEvent::ConnectionRequest(upgrade))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
Message::ConnectionResponse(msg) => {
|
||||
debug!("got inbound connection response {:?}", msg);
|
||||
self.handle_connection_response(&msg)
|
||||
.map(|_| InboundTransportEvent::ConnectionResponse)
|
||||
}
|
||||
Message::TransportMessage(msg) => {
|
||||
debug!("got inbound TransportMessage: {:?}", msg);
|
||||
self.handle_transport_message(msg)
|
||||
.map(|_| InboundTransportEvent::TransportMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upgrade represents a transport listener upgrade.
|
||||
/// Note: we immediately upgrade a connection request to a connection,
|
||||
/// so this only contains a channel for receiving that connection.
|
||||
pub struct Upgrade {
|
||||
connection_tx: oneshot::Receiver<(PeerId, Connection)>,
|
||||
}
|
||||
|
||||
impl Upgrade {
|
||||
fn new(connection_tx: oneshot::Receiver<(PeerId, Connection)>) -> Upgrade {
|
||||
Upgrade { connection_tx }
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for Upgrade {
|
||||
type Output = Result<(PeerId, Connection), Error>;
|
||||
|
||||
// poll checks if the upgrade has turned into a connection yet
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.connection_tx
|
||||
.poll_unpin(cx)
|
||||
.map_err(|_| Error::RecvFailure)
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for NymTransport {
|
||||
type Output = (PeerId, Connection);
|
||||
type Error = Error;
|
||||
type ListenerUpgrade = Upgrade;
|
||||
type Dial = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
|
||||
fn listen_on(&mut self, _: Multiaddr) -> Result<ListenerId, TransportError<Self::Error>> {
|
||||
// we should only allow listening on the multiaddress containing our Nym address
|
||||
Ok(self.listener_id)
|
||||
}
|
||||
|
||||
fn remove_listener(&mut self, id: ListenerId) -> bool {
|
||||
if self.listener_id != id {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: close channels?
|
||||
self.poll_tx
|
||||
.send(TransportEvent::ListenerClosed {
|
||||
listener_id: id,
|
||||
reason: Ok(()),
|
||||
})
|
||||
.expect("failed to send listener closed event");
|
||||
true
|
||||
}
|
||||
|
||||
fn dial(&mut self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
|
||||
debug!("dialing {}", addr);
|
||||
|
||||
let id = ConnectionId::generate();
|
||||
|
||||
// create remote recipient address
|
||||
let recipient = multiaddress_to_nym_address(addr).map_err(TransportError::Other)?;
|
||||
|
||||
// create pending conn structs and store
|
||||
let (connection_tx, connection_rx) = oneshot::channel::<Connection>();
|
||||
|
||||
let inner_pending_conn = PendingConnection::new(recipient, connection_tx);
|
||||
self.pending_dials.insert(id.clone(), inner_pending_conn);
|
||||
|
||||
// put ConnectionRequest message into outbound message channel
|
||||
let msg = ConnectionMessage {
|
||||
peer_id: self.peer_id(),
|
||||
recipient: Some(self.self_address),
|
||||
id,
|
||||
};
|
||||
|
||||
let outbound_tx = self.outbound_tx.clone();
|
||||
|
||||
let mut waker = self.waker.clone();
|
||||
let handshake_timeout = self.handshake_timeout;
|
||||
Ok(async move {
|
||||
outbound_tx
|
||||
.send(OutboundMessage {
|
||||
message: Message::ConnectionRequest(msg),
|
||||
recipient,
|
||||
})
|
||||
.map_err(|e| Error::OutboundSendFailure(e.to_string()))?;
|
||||
|
||||
debug!("sent outbound ConnectionRequest");
|
||||
if let Some(waker) = waker.take() {
|
||||
waker.wake();
|
||||
};
|
||||
|
||||
let conn = timeout(handshake_timeout, connection_rx).await??;
|
||||
Ok((conn.peer_id, conn))
|
||||
}
|
||||
.boxed())
|
||||
}
|
||||
|
||||
// dial_as_listener currently just calls self.dial().
|
||||
fn dial_as_listener(
|
||||
&mut self,
|
||||
addr: Multiaddr,
|
||||
) -> Result<Self::Dial, TransportError<Self::Error>> {
|
||||
self.dial(addr)
|
||||
}
|
||||
|
||||
fn poll(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
|
||||
// new addresses + listener close events
|
||||
if let Poll::Ready(Some(res)) = self.poll_rx.recv().boxed().poll_unpin(cx) {
|
||||
return Poll::Ready(res);
|
||||
}
|
||||
|
||||
// check for and handle inbound messages
|
||||
while let Poll::Ready(Some(msg)) = self.inbound_stream.poll_next_unpin(cx) {
|
||||
match self.handle_inbound(msg.0) {
|
||||
Ok(event) => match event {
|
||||
InboundTransportEvent::ConnectionRequest(upgrade) => {
|
||||
debug!("InboundTransportEvent::ConnectionRequest");
|
||||
return Poll::Ready(TransportEvent::Incoming {
|
||||
listener_id: self.listener_id,
|
||||
upgrade,
|
||||
local_addr: self.listen_addr.clone(),
|
||||
send_back_addr: self.listen_addr.clone(),
|
||||
});
|
||||
}
|
||||
InboundTransportEvent::ConnectionResponse => {
|
||||
debug!("InboundTransportEvent::ConnectionResponse");
|
||||
}
|
||||
InboundTransportEvent::TransportMessage => {
|
||||
debug!("InboundTransportEvent::TransportMessage");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
return Poll::Ready(TransportEvent::ListenerError {
|
||||
listener_id: self.listener_id,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
self.waker = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option<Multiaddr> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn nym_address_to_multiaddress(addr: Recipient) -> Result<Multiaddr, Error> {
|
||||
Multiaddr::from_str(&format!("/nym/{}", addr)).map_err(Error::FailedToFormatMultiaddr)
|
||||
}
|
||||
|
||||
fn multiaddress_to_nym_address(multiaddr: Multiaddr) -> Result<Recipient, Error> {
|
||||
let mut multiaddr = multiaddr;
|
||||
match multiaddr.pop().unwrap() {
|
||||
Protocol::Nym(addr) => Recipient::from_str(&addr).map_err(Error::InvalidRecipientBytes),
|
||||
_ => Err(Error::InvalidProtocolForMultiaddr),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::connection::Connection;
|
||||
use super::super::error::Error;
|
||||
use super::super::message::{
|
||||
Message, OutboundMessage, SubstreamId, SubstreamMessage, SubstreamMessageType,
|
||||
TransportMessage,
|
||||
};
|
||||
use super::super::substream::Substream;
|
||||
use super::{nym_address_to_multiaddress, NymTransport};
|
||||
use futures::{future::poll_fn, AsyncReadExt, AsyncWriteExt, FutureExt};
|
||||
use libp2p::core::{
|
||||
identity::Keypair,
|
||||
transport::{Transport, TransportEvent},
|
||||
Multiaddr, StreamMuxer,
|
||||
};
|
||||
use log::info;
|
||||
use nym_bin_common::logging::setup_tracing_logger;
|
||||
use nym_sdk::mixnet::MixnetClient;
|
||||
use std::{pin::Pin, str::FromStr, sync::atomic::Ordering};
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||
|
||||
impl Connection {
|
||||
fn write(&self, msg: SubstreamMessage) -> Result<(), Error> {
|
||||
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
|
||||
self.mixnet_outbound_tx
|
||||
.send(OutboundMessage {
|
||||
recipient: self.remote_recipient,
|
||||
message: Message::TransportMessage(TransportMessage {
|
||||
nonce,
|
||||
id: self.id.clone(),
|
||||
message: msg,
|
||||
}),
|
||||
})
|
||||
.map_err(|e| Error::OutboundSendFailure(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl NymTransport {
|
||||
async fn new_with_notify_inbound(
|
||||
client: MixnetClient,
|
||||
notify_inbound_tx: UnboundedSender<()>,
|
||||
) -> Result<Self, Error> {
|
||||
let local_key = Keypair::generate_ed25519();
|
||||
Self::new_maybe_with_notify_inbound(client, local_key, Some(notify_inbound_tx), None)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_transport_connection() {
|
||||
setup_tracing_logger();
|
||||
|
||||
let client = MixnetClient::connect_new().await.unwrap();
|
||||
let (dialer_notify_inbound_tx, mut dialer_notify_inbound_rx) = unbounded_channel();
|
||||
let mut dialer_transport =
|
||||
NymTransport::new_with_notify_inbound(client, dialer_notify_inbound_tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client2 = MixnetClient::connect_new().await.unwrap();
|
||||
let (listener_notify_inbound_tx, mut listener_notify_inbound_rx) = unbounded_channel();
|
||||
let mut listener_transport =
|
||||
NymTransport::new_with_notify_inbound(client2, listener_notify_inbound_tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let listener_multiaddr =
|
||||
nym_address_to_multiaddress(listener_transport.self_address).unwrap();
|
||||
assert_new_address_event(Pin::new(&mut dialer_transport)).await;
|
||||
assert_new_address_event(Pin::new(&mut listener_transport)).await;
|
||||
|
||||
// dial the remote peer
|
||||
let mut dial = dialer_transport.dial(listener_multiaddr).unwrap();
|
||||
|
||||
// poll the dial to send the connection request message
|
||||
assert!(poll_fn(|cx| Pin::new(&mut dial).as_mut().poll_unpin(cx))
|
||||
.now_or_never()
|
||||
.is_none());
|
||||
listener_notify_inbound_rx.recv().await.unwrap();
|
||||
|
||||
// should receive the connection request from the mixnet and send the connection response
|
||||
let res = poll_fn(|cx| Pin::new(&mut listener_transport).as_mut().poll(cx)).await;
|
||||
let mut upgrade = match res {
|
||||
TransportEvent::Incoming {
|
||||
listener_id,
|
||||
upgrade,
|
||||
local_addr,
|
||||
send_back_addr,
|
||||
} => {
|
||||
assert_eq!(listener_id, listener_transport.listener_id);
|
||||
assert_eq!(local_addr, listener_transport.listen_addr);
|
||||
assert_eq!(send_back_addr, listener_transport.listen_addr);
|
||||
upgrade
|
||||
}
|
||||
_ => panic!("expected TransportEvent::Incoming, got {:?}", res),
|
||||
};
|
||||
dialer_notify_inbound_rx.recv().await.unwrap();
|
||||
|
||||
// should receive the connection response from the mixnet
|
||||
assert!(
|
||||
poll_fn(|cx| Pin::new(&mut dialer_transport).as_mut().poll(cx))
|
||||
.now_or_never()
|
||||
.is_none()
|
||||
);
|
||||
info!("waiting for connections...");
|
||||
|
||||
// should be able to resolve the connections now
|
||||
let (_, mut listener_conn) = poll_fn(|cx| Pin::new(&mut upgrade).as_mut().poll_unpin(cx))
|
||||
.now_or_never()
|
||||
.expect("the upgrade should be ready")
|
||||
.expect("the upgrade should not error");
|
||||
let (_, mut dialer_conn) = poll_fn(|cx| Pin::new(&mut dial).as_mut().poll_unpin(cx))
|
||||
.now_or_never()
|
||||
.expect("the upgrade should be ready")
|
||||
.expect("the upgrade should not error");
|
||||
info!("connections established");
|
||||
|
||||
// write messages from the dialer to the listener and vice versa
|
||||
send_and_receive_over_conns(
|
||||
b"hello".to_vec(),
|
||||
&mut dialer_conn,
|
||||
&mut listener_conn,
|
||||
Pin::new(&mut listener_transport),
|
||||
&mut listener_notify_inbound_rx,
|
||||
)
|
||||
.await;
|
||||
send_and_receive_over_conns(
|
||||
b"hi".to_vec(),
|
||||
&mut dialer_conn,
|
||||
&mut listener_conn,
|
||||
Pin::new(&mut listener_transport),
|
||||
&mut listener_notify_inbound_rx,
|
||||
)
|
||||
.await;
|
||||
send_and_receive_over_conns(
|
||||
b"world".to_vec(),
|
||||
&mut listener_conn,
|
||||
&mut dialer_conn,
|
||||
Pin::new(&mut dialer_transport),
|
||||
&mut dialer_notify_inbound_rx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn assert_new_address_event(mut transport: Pin<&mut NymTransport>) {
|
||||
match poll_fn(|cx| transport.as_mut().poll(cx)).await {
|
||||
TransportEvent::NewAddress {
|
||||
listener_id,
|
||||
listen_addr,
|
||||
} => {
|
||||
assert_eq!(listener_id, transport.listener_id);
|
||||
assert_eq!(listen_addr, transport.listen_addr);
|
||||
}
|
||||
_ => panic!("expected TransportEvent::NewAddress"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_and_receive_over_conns(
|
||||
msg: Vec<u8>,
|
||||
conn1: &mut Connection,
|
||||
conn2: &mut Connection,
|
||||
mut transport2: Pin<&mut NymTransport>,
|
||||
notify_inbound_rx: &mut UnboundedReceiver<()>,
|
||||
) {
|
||||
// send message over conn1 to conn2
|
||||
let substream_id = SubstreamId::generate();
|
||||
conn1
|
||||
.write(SubstreamMessage::new_with_data(
|
||||
substream_id.clone(),
|
||||
msg.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
notify_inbound_rx.recv().await.unwrap();
|
||||
|
||||
// poll transport2 to push message from transport to connection
|
||||
assert!(poll_fn(|cx| transport2.as_mut().poll(cx))
|
||||
.now_or_never()
|
||||
.is_none());
|
||||
let substream_msg = conn2.inbound_rx.recv().await.unwrap();
|
||||
if let SubstreamMessageType::Data(data) = substream_msg.message_type {
|
||||
assert_eq!(data, msg);
|
||||
} else {
|
||||
panic!("expected data message");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_transport_substream() {
|
||||
let client = MixnetClient::connect_new().await.unwrap();
|
||||
|
||||
let (dialer_notify_inbound_tx, mut dialer_notify_inbound_rx) = unbounded_channel();
|
||||
let mut dialer_transport =
|
||||
NymTransport::new_with_notify_inbound(client, dialer_notify_inbound_tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client2 = MixnetClient::connect_new().await.unwrap();
|
||||
|
||||
let (listener_notify_inbound_tx, mut listener_notify_inbound_rx) = unbounded_channel();
|
||||
let mut listener_transport =
|
||||
NymTransport::new_with_notify_inbound(client2, listener_notify_inbound_tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let listener_multiaddr =
|
||||
nym_address_to_multiaddress(listener_transport.self_address).unwrap();
|
||||
assert_new_address_event(Pin::new(&mut dialer_transport)).await;
|
||||
assert_new_address_event(Pin::new(&mut listener_transport)).await;
|
||||
|
||||
// dial the remote peer
|
||||
let mut dial = dialer_transport.dial(listener_multiaddr).unwrap();
|
||||
|
||||
// poll the dial to send the connection request message
|
||||
assert!(poll_fn(|cx| Pin::new(&mut dial).as_mut().poll_unpin(cx))
|
||||
.now_or_never()
|
||||
.is_none());
|
||||
listener_notify_inbound_rx.recv().await.unwrap();
|
||||
|
||||
// should receive the connection request from the mixnet and send the connection response
|
||||
let res = poll_fn(|cx| Pin::new(&mut listener_transport).as_mut().poll(cx)).await;
|
||||
let mut upgrade = match res {
|
||||
TransportEvent::Incoming {
|
||||
listener_id,
|
||||
upgrade,
|
||||
local_addr,
|
||||
send_back_addr,
|
||||
} => {
|
||||
assert_eq!(listener_id, listener_transport.listener_id);
|
||||
assert_eq!(local_addr, listener_transport.listen_addr);
|
||||
assert_eq!(send_back_addr, listener_transport.listen_addr);
|
||||
upgrade
|
||||
}
|
||||
_ => panic!("expected TransportEvent::Incoming, got {:?}", res),
|
||||
};
|
||||
dialer_notify_inbound_rx.recv().await.unwrap();
|
||||
|
||||
// should receive the connection response from the mixnet
|
||||
assert!(
|
||||
poll_fn(|cx| Pin::new(&mut dialer_transport).as_mut().poll(cx))
|
||||
.now_or_never()
|
||||
.is_none()
|
||||
);
|
||||
info!("waiting for connections...");
|
||||
|
||||
// should be able to resolve the connections now
|
||||
let (_, mut listener_conn) = poll_fn(|cx| Pin::new(&mut upgrade).as_mut().poll_unpin(cx))
|
||||
.now_or_never()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (_, mut dialer_conn) = poll_fn(|cx| Pin::new(&mut dial).as_mut().poll_unpin(cx))
|
||||
.now_or_never()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
info!("connections established");
|
||||
|
||||
// initiate a new substream from the dialer
|
||||
let mut dialer_substream =
|
||||
poll_fn(|cx| Pin::new(&mut dialer_conn).as_mut().poll_outbound(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
listener_notify_inbound_rx.recv().await.unwrap();
|
||||
|
||||
// accept the substream on the listener
|
||||
assert!(
|
||||
poll_fn(|cx| Pin::new(&mut listener_transport).as_mut().poll(cx))
|
||||
.now_or_never()
|
||||
.is_none()
|
||||
);
|
||||
poll_fn(|cx| Pin::new(&mut listener_conn).as_mut().poll(cx)).now_or_never();
|
||||
|
||||
// poll recipient's poll_inbound to receive the substream; sends a response to the sender
|
||||
let mut listener_substream =
|
||||
poll_fn(|cx| Pin::new(&mut listener_conn).as_mut().poll_inbound(cx))
|
||||
.now_or_never()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
info!("got listener substream");
|
||||
dialer_notify_inbound_rx.recv().await.unwrap();
|
||||
|
||||
// poll sender to finalize the substream
|
||||
assert!(
|
||||
poll_fn(|cx| Pin::new(&mut dialer_transport).as_mut().poll(cx))
|
||||
.now_or_never()
|
||||
.is_none()
|
||||
);
|
||||
poll_fn(|cx| Pin::new(&mut dialer_conn).as_mut().poll(cx)).now_or_never();
|
||||
info!("got dialer substream");
|
||||
|
||||
// write message from dialer to listener
|
||||
send_and_receive_substream_message(
|
||||
b"hello world".to_vec(),
|
||||
Pin::new(&mut dialer_substream),
|
||||
Pin::new(&mut listener_substream),
|
||||
Pin::new(&mut listener_transport),
|
||||
Pin::new(&mut listener_conn),
|
||||
&mut listener_notify_inbound_rx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// write message from listener to dialer
|
||||
send_and_receive_substream_message(
|
||||
b"hello back".to_vec(),
|
||||
Pin::new(&mut listener_substream),
|
||||
Pin::new(&mut dialer_substream),
|
||||
Pin::new(&mut dialer_transport),
|
||||
Pin::new(&mut dialer_conn),
|
||||
&mut dialer_notify_inbound_rx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// close the substream from the dialer side
|
||||
info!("closing dialer substream");
|
||||
dialer_substream.close().await.unwrap();
|
||||
listener_notify_inbound_rx.recv().await.unwrap();
|
||||
info!("dialer substream closed");
|
||||
|
||||
// assert we can't read or write to either substream
|
||||
dialer_substream.write_all(b"hello").await.unwrap_err();
|
||||
// poll listener transport and conn to receive the substream close message
|
||||
poll_fn(|cx| Pin::new(&mut listener_transport).as_mut().poll(cx)).now_or_never();
|
||||
poll_fn(|cx| Pin::new(&mut listener_conn).as_mut().poll(cx)).now_or_never();
|
||||
listener_substream.write_all(b"hello").await.unwrap_err();
|
||||
let mut buf = vec![0u8; 5];
|
||||
dialer_substream.read(&mut buf).await.unwrap_err();
|
||||
listener_substream.read(&mut buf).await.unwrap_err();
|
||||
dialer_substream.close().await.unwrap_err();
|
||||
listener_substream.close().await.unwrap_err();
|
||||
}
|
||||
|
||||
async fn send_and_receive_substream_message(
|
||||
data: Vec<u8>,
|
||||
mut sender_substream: Pin<&mut Substream>,
|
||||
mut recipient_substream: Pin<&mut Substream>,
|
||||
mut recipient_transport: Pin<&mut NymTransport>,
|
||||
mut recipient_conn: Pin<&mut Connection>,
|
||||
recipient_notify_inbound_rx: &mut UnboundedReceiver<()>,
|
||||
) {
|
||||
// write message
|
||||
sender_substream.write_all(&data).await.unwrap();
|
||||
recipient_notify_inbound_rx.recv().await.unwrap();
|
||||
|
||||
// poll recipient for message
|
||||
poll_fn(|cx| recipient_transport.as_mut().poll(cx)).now_or_never();
|
||||
poll_fn(|cx| recipient_conn.as_mut().poll(cx)).now_or_never();
|
||||
let mut buf = vec![0u8; data.len()];
|
||||
let n = recipient_substream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(n, data.len());
|
||||
assert_eq!(buf, data[..]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_transport_timeout() {
|
||||
let client = MixnetClient::connect_new().await.unwrap();
|
||||
|
||||
let (dialer_notify_inbound_tx, _) = unbounded_channel();
|
||||
let mut dialer_transport =
|
||||
NymTransport::new_with_notify_inbound(client, dialer_notify_inbound_tx)
|
||||
.await
|
||||
.unwrap()
|
||||
.with_timeout(std::time::Duration::from_millis(100));
|
||||
|
||||
// mock a transport that will never resolve the connection.
|
||||
let empty_addr = Multiaddr::from_str(
|
||||
"/nym/Hmer6Ndt3PV13YW53HM8ri4NvqqtfDQUQBhzvKqb1dag.2g478dyxtrQXGWc1Mk2VEqdPcWXpz7EhAcjhdAJtVZdA@AnnYnEtBjB2a5sHmeRCnBq43qxyHDf95Bqd7cwQyKNLR"
|
||||
)
|
||||
.expect("unable to parse multiaddress");
|
||||
|
||||
let dial = dialer_transport.dial(empty_addr).unwrap();
|
||||
assert!(dial
|
||||
.await
|
||||
.expect_err("should have timed out")
|
||||
.to_string()
|
||||
.contains("dial timed out"));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,23 @@ to provide real `TcpStream` and `UdpSocket` types that work with the async
|
||||
Rust ecosystem: tokio-rustls, hyper, tokio-tungstenite, libp2p, and anything
|
||||
else built on `AsyncRead + AsyncWrite`.
|
||||
|
||||
## Workspace layout
|
||||
|
||||
```text
|
||||
smolmix-hyper
|
||||
(top-level)
|
||||
/ \
|
||||
v v
|
||||
smolmix-dns ←→ smolmix-tls
|
||||
(resolution) (encryption)
|
||||
\ /
|
||||
v v
|
||||
smolmix
|
||||
(tunnel)
|
||||
```
|
||||
|
||||
`smolmix` provides the underlying TCP/UDP tunnel. `smolmix-dns` and `smolmix-tls` are companion crates that each handle one concern; `smolmix-hyper` glues them into a complete HTTP client. Pick the level of abstraction that matches your needs; the lower-level crates remain useful when you want manual control (e.g. websockets, libp2p, custom protocols).
|
||||
|
||||
## Why IP, not messages
|
||||
|
||||
The Nym SDK works at the message layer: you send and receive `Vec<u8>`
|
||||
|
||||
@@ -5,6 +5,23 @@ smolmix tunnels TCP and UDP over the Nym mixnet. It exposes `TcpStream` and
|
||||
hyper, tokio-tungstenite), with all traffic routed through the mixnet so a
|
||||
network observer cannot correlate source and destination.
|
||||
|
||||
## Workspace layout
|
||||
|
||||
```text
|
||||
smolmix-hyper
|
||||
(top-level)
|
||||
/ \
|
||||
v v
|
||||
smolmix-dns ←→ smolmix-tls
|
||||
(resolution) (encryption)
|
||||
\ /
|
||||
v v
|
||||
smolmix
|
||||
(tunnel)
|
||||
```
|
||||
|
||||
This crate (`smolmix`) provides the underlying TCP/UDP tunnel. The companion crates each handle one concern: [`smolmix-dns`](https://crates.io/crates/smolmix-dns) for tunneled DNS resolution, [`smolmix-tls`](https://crates.io/crates/smolmix-tls) for TLS over the tunnel, and [`smolmix-hyper`](https://crates.io/crates/smolmix-hyper) which combines them into a complete HTTP client. The arrows show conceptual layering, not strict Cargo dependencies.
|
||||
|
||||
## Stack
|
||||
|
||||
```text
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "smolmix-dns"
|
||||
description = "DNS resolution through the Nym mixnet via hickory-resolver"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
smolmix = { workspace = true }
|
||||
hickory-proto = { workspace = true, features = ["tokio"] }
|
||||
hickory-resolver = { workspace = true, features = ["tokio"] }
|
||||
tokio-smoltcp = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hickory-resolver = { workspace = true, features = ["tokio", "system-config"] }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
|
||||
tracing = { workspace = true }
|
||||
nym-bin-common = { workspace = true, features = ["basic_tracing"] }
|
||||
|
||||
[[example]]
|
||||
name = "resolve"
|
||||
@@ -0,0 +1,75 @@
|
||||
# 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.
|
||||
|
||||
## Workspace layout
|
||||
|
||||
```text
|
||||
smolmix-hyper
|
||||
(top-level)
|
||||
/ \
|
||||
v v
|
||||
smolmix-dns ←→ smolmix-tls
|
||||
(resolution) (encryption)
|
||||
\ /
|
||||
v v
|
||||
smolmix
|
||||
(tunnel)
|
||||
```
|
||||
|
||||
`smolmix-dns` is one of three companion crates around [`smolmix`](https://crates.io/crates/smolmix). It pairs with [`smolmix-tls`](https://crates.io/crates/smolmix-tls) for HTTPS connections, or use [`smolmix-hyper`](https://crates.io/crates/smolmix-hyper) for a complete HTTP client built on top. Arrows show conceptual layering, not strict Cargo dependencies.
|
||||
|
||||
## 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)`**: Quad9 upstream (`9.9.9.9`)
|
||||
- **`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 = "1.21.0"
|
||||
smolmix-dns = "1.21.0"
|
||||
```
|
||||
|
||||
This crate depends on `smolmix` (for the `Tunnel` type), `hickory-proto`, and `hickory-resolver`.
|
||||
|
||||
## See also
|
||||
|
||||
- [`smolmix-tls`](../tls) for TLS over the tunnel once you have a resolved address
|
||||
- [`smolmix-hyper`](../hyper) for a complete HTTP client that bundles DNS + TLS + HTTP
|
||||
@@ -0,0 +1,85 @@
|
||||
//! 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> {
|
||||
nym_bin_common::logging::setup_tracing_logger();
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
//! DNS resolution through the Nym mixnet.
|
||||
//!
|
||||
//! # Why a separate DNS crate?
|
||||
//!
|
||||
//! If you resolve hostnames using the OS resolver or a clearnet DNS library,
|
||||
//! the queries travel over your local network — leaking which domains you're
|
||||
//! visiting even though the TCP traffic itself goes through the mixnet. This
|
||||
//! crate routes all DNS queries (both UDP and TCP) through the smolmix
|
||||
//! [`Tunnel`], so hostname lookups are as private as the rest of your traffic.
|
||||
//!
|
||||
//! # How it works
|
||||
//!
|
||||
//! [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<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.
|
||||
//!
|
||||
//! 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`.
|
||||
//!
|
||||
//! # Quick start
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! 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?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! # Caching
|
||||
//!
|
||||
//! hickory-resolver maintains an internal LRU cache for DNS responses. To
|
||||
//! benefit from caching, **reuse the [`Resolver`] across requests** rather
|
||||
//! than creating a new one per lookup. The free function [`resolve()`]
|
||||
//! constructs a fresh resolver each time and does not cache.
|
||||
//!
|
||||
//! # Custom upstream DNS
|
||||
//!
|
||||
//! By default, queries go to Quad9 (`9.9.9.9`). Use
|
||||
//! [`Resolver::with_config()`] for other upstreams:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use smolmix_dns::{Resolver, ResolverConfig};
|
||||
//!
|
||||
//! let tunnel = smolmix::Tunnel::new().await?;
|
||||
//! let resolver = Resolver::with_config(&tunnel, ResolverConfig::cloudflare());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! [`RuntimeProvider`]: hickory_proto::runtime::RuntimeProvider
|
||||
|
||||
mod provider;
|
||||
|
||||
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;
|
||||
use smolmix::Tunnel;
|
||||
|
||||
/// Re-exported from hickory-resolver. Used with [`Resolver::with_config()`]
|
||||
/// to select a custom upstream DNS server (Quad9, Cloudflare, Google, etc.).
|
||||
pub use hickory_resolver::config::ResolverConfig;
|
||||
|
||||
/// Re-exported from hickory-resolver. The result of a successful `lookup_ip()`
|
||||
/// call — iterate with `.iter()` to get `IpAddr` values.
|
||||
pub use hickory_resolver::lookup_ip::LookupIp;
|
||||
|
||||
/// Re-exported from hickory-resolver. The error type for DNS resolution failures.
|
||||
pub use hickory_resolver::ResolveError;
|
||||
|
||||
/// The runtime provider that routes DNS I/O through the tunnel.
|
||||
///
|
||||
/// You don't usually need to use this directly — [`Resolver::new()`] wires it
|
||||
/// up for you. Exposed for advanced use cases (custom resolver configurations
|
||||
/// beyond what `with_config` covers).
|
||||
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 Quad9 (`9.9.9.9`) as upstream DNS.
|
||||
pub fn new(tunnel: &Tunnel) -> Self {
|
||||
Self::with_config(tunnel, ResolverConfig::quad9())
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
};
|
||||
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 Quad9 (`9.9.9.9`) as the upstream DNS server. Equivalent to
|
||||
/// [`Resolver::new()`].
|
||||
pub fn resolver(tunnel: &Tunnel) -> Resolver {
|
||||
Resolver::new(tunnel)
|
||||
}
|
||||
|
||||
/// Resolve a hostname through the tunnel (uncached).
|
||||
///
|
||||
/// Convenience wrapper for one-shot lookups. Creates a fresh [`Resolver`]
|
||||
/// internally, so **DNS responses are not cached** across calls. If you're
|
||||
/// making multiple lookups, create a [`Resolver`] once and reuse it.
|
||||
pub async fn resolve(tunnel: &Tunnel, host: &str, port: u16) -> io::Result<Vec<SocketAddr>> {
|
||||
let r = resolver(tunnel);
|
||||
r.resolve(host, port).await
|
||||
}
|
||||
|
||||
/// Strip IPv6 nameservers from a resolver config.
|
||||
///
|
||||
/// hickory's preset configs (quad9, cloudflare, google) all include IPv6
|
||||
/// nameservers. The smolmix tunnel is IPv4-only (tokio-smoltcp's `NetConfig`
|
||||
/// takes a single `IpCidr`), so sending a UDP packet to an IPv6 nameserver
|
||||
/// panics in smoltcp's wire layer with "IP version mismatch".
|
||||
fn ipv4_only_nameservers(config: ResolverConfig) -> 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),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
|
||||
//! 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "smolmix-hyper"
|
||||
description = "HTTP client routing all traffic through the Nym mixnet via hyper"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
smolmix = { workspace = true }
|
||||
smolmix-dns = { workspace = true }
|
||||
smolmix-tls = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
hyper = { workspace = true, features = ["client", "http1"] }
|
||||
hyper-util = { workspace = true, features = ["tokio", "client-legacy"] }
|
||||
http-body-util = { workspace = true }
|
||||
tokio-smoltcp = { workspace = true }
|
||||
pin-project-lite = "0.2"
|
||||
tower = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
|
||||
tracing = { workspace = true }
|
||||
nym-bin-common = { workspace = true, features = ["basic_tracing"] }
|
||||
|
||||
[[example]]
|
||||
name = "get"
|
||||
|
||||
[[example]]
|
||||
name = "post"
|
||||
@@ -0,0 +1,97 @@
|
||||
# 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`.
|
||||
|
||||
## Workspace layout
|
||||
|
||||
```text
|
||||
smolmix-hyper
|
||||
(top-level)
|
||||
/ \
|
||||
v v
|
||||
smolmix-dns ←→ smolmix-tls
|
||||
(resolution) (encryption)
|
||||
\ /
|
||||
v v
|
||||
smolmix
|
||||
(tunnel)
|
||||
```
|
||||
|
||||
`smolmix-hyper` is the top-level convenience crate, bundling DNS, TLS, and HTTP. For lower-level control, use [`smolmix-dns`](https://crates.io/crates/smolmix-dns) and [`smolmix-tls`](https://crates.io/crates/smolmix-tls) directly over a [`smolmix`](https://crates.io/crates/smolmix) tunnel. Arrows show conceptual layering, not strict Cargo dependencies.
|
||||
|
||||
## 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> # If you want to use a specific IPR on an Exit Gateway
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix = "1.21.0"
|
||||
smolmix-hyper = "1.21.0"
|
||||
```
|
||||
|
||||
This crate depends on `smolmix`, `smolmix-dns` (DNS resolution through the tunnel), and `smolmix-tls` (webpki roots, TLS handshake).
|
||||
|
||||
## See also
|
||||
|
||||
If you want lower-level control over individual steps rather than the full HTTP client:
|
||||
|
||||
- [`smolmix-dns`](../dns) for tunneled DNS resolution only
|
||||
- [`smolmix-tls`](../tls) for TLS only (bring your own `TcpStream` from the tunnel)
|
||||
@@ -0,0 +1,85 @@
|
||||
//! 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> {
|
||||
nym_bin_common::logging::setup_tracing_logger();
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! 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> {
|
||||
nym_bin_common::logging::setup_tracing_logger();
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
|
||||
// Clearnet baseline via reqwest
|
||||
info!("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(())
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
|
||||
//! [`tower::Service<Uri>`] 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;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tower::Service;
|
||||
|
||||
use smolmix::Tunnel;
|
||||
use smolmix_dns::Resolver;
|
||||
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.
|
||||
///
|
||||
/// 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<Bytes>` 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<Resolver>,
|
||||
}
|
||||
|
||||
impl SmolmixConnector {
|
||||
/// Create a new connector for the given tunnel.
|
||||
///
|
||||
/// 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
let resolver = self.resolver.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 = resolver.resolve(&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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
//! HTTP client routing all traffic through the Nym mixnet.
|
||||
//!
|
||||
//! # What this crate does
|
||||
//!
|
||||
//! Wraps [hyper-util]'s legacy `Client` with a [`SmolmixConnector`] that
|
||||
//! routes DNS resolution, TCP connections, and TLS handshakes through a
|
||||
//! smolmix [`Tunnel`]. From the outside it behaves like a normal HTTP client,
|
||||
//! but all traffic travels through the mixnet.
|
||||
//!
|
||||
//! # How it works
|
||||
//!
|
||||
//! hyper-util's `Client` uses the [`tower::Service<Uri>`] trait to open
|
||||
//! connections. [`SmolmixConnector`] implements this: given a URI, it resolves
|
||||
//! the hostname via [`smolmix_dns`], connects TCP via the tunnel, and wraps
|
||||
//! in TLS for `https://` URIs:
|
||||
//!
|
||||
//! ```text
|
||||
//! SmolmixConnector::call(uri)
|
||||
//! → 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 }
|
||||
//! or MaybeTlsStream::Tls { TlsStream<TcpStream> }
|
||||
//! → TokioIo<MaybeTlsStream> (implements hyper's Read/Write/Connection)
|
||||
//! ```
|
||||
//!
|
||||
//! [`MaybeTlsStream`] is a two-variant enum with [`pin_project_lite`] for safe
|
||||
//! pin projection through `AsyncRead`/`AsyncWrite`. hyper-util's `TokioIo`
|
||||
//! wraps it to satisfy hyper's own I/O traits.
|
||||
//!
|
||||
//! # Quick start (GET)
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! 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();
|
||||
//! println!("{}", String::from_utf8_lossy(&body));
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! # Sending request bodies (POST, PUT, etc.)
|
||||
//!
|
||||
//! The convenience [`Client`] wrapper uses `Empty<Bytes>` as its body type,
|
||||
//! which is suitable for GET/HEAD/DELETE. For requests that carry a body,
|
||||
//! construct a hyper-util client directly with [`SmolmixConnector`]:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use http_body_util::Full;
|
||||
//! use hyper_util::{client::legacy, rt::TokioExecutor};
|
||||
//! use bytes::Bytes;
|
||||
//! use smolmix_hyper::SmolmixConnector;
|
||||
//!
|
||||
//! let tunnel = smolmix::Tunnel::new().await?;
|
||||
//! let connector = SmolmixConnector::new(&tunnel);
|
||||
//! let client = legacy::Client::builder(TokioExecutor::new())
|
||||
//! .build::<_, Full<Bytes>>(connector);
|
||||
//!
|
||||
//! let body = Full::new(Bytes::from(r#"{"key": "value"}"#));
|
||||
//! let req = hyper::Request::post("https://httpbin.org/post")
|
||||
//! .header("Host", "httpbin.org")
|
||||
//! .header("Content-Type", "application/json")
|
||||
//! .body(body)?;
|
||||
//! let resp = client.request(req).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! # Re-exports
|
||||
//!
|
||||
//! This crate re-exports the most commonly needed types so you don't need
|
||||
//! `hyper`, `http-body-util`, or `bytes` in your `Cargo.toml` for basic use:
|
||||
//!
|
||||
//! - [`Request`], [`Response`], [`StatusCode`], [`Uri`] — from hyper
|
||||
//! - [`BodyExt`], [`EmptyBody`] — from http-body-util
|
||||
//! - [`bytes`] — the bytes crate
|
||||
//!
|
||||
//! [hyper-util]: https://docs.rs/hyper-util
|
||||
//! [`pin_project_lite`]: https://docs.rs/pin-project-lite
|
||||
|
||||
mod connector;
|
||||
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-exported [`bytes`](https://docs.rs/bytes) crate for constructing request bodies.
|
||||
pub use bytes;
|
||||
|
||||
/// Extension trait for consuming HTTP response bodies. Provides `.collect()`,
|
||||
/// `.frame()`, etc.
|
||||
pub use http_body_util::BodyExt;
|
||||
|
||||
/// An empty HTTP body. Use `EmptyBody::<Bytes>::new()` for GET/HEAD requests.
|
||||
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};
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
|
||||
//! 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.
|
||||
///
|
||||
/// Returned by [`SmolmixConnector`](crate::SmolmixConnector) — you won't
|
||||
/// normally interact with this directly.
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "smolmix-tls"
|
||||
description = "TLS connector with webpki roots for smolmix tunneled connections"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
tokio-smoltcp = { workspace = true }
|
||||
tokio-rustls = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
webpki-roots = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
smolmix = { workspace = true }
|
||||
smolmix-dns = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time", "net", "io-util"] }
|
||||
tracing = { workspace = true }
|
||||
nym-bin-common = { workspace = true, features = ["basic_tracing"] }
|
||||
|
||||
[[example]]
|
||||
name = "connect"
|
||||
@@ -0,0 +1,84 @@
|
||||
# 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`.
|
||||
|
||||
## Workspace layout
|
||||
|
||||
```text
|
||||
smolmix-hyper
|
||||
(top-level)
|
||||
/ \
|
||||
v v
|
||||
smolmix-dns ←→ smolmix-tls
|
||||
(resolution) (encryption)
|
||||
\ /
|
||||
v v
|
||||
smolmix
|
||||
(tunnel)
|
||||
```
|
||||
|
||||
`smolmix-tls` is one of three companion crates around [`smolmix`](https://crates.io/crates/smolmix). It pairs with [`smolmix-dns`](https://crates.io/crates/smolmix-dns) for hostname-based connections, or use [`smolmix-hyper`](https://crates.io/crates/smolmix-hyper) for a complete HTTP client built on top. Arrows show conceptual layering, not strict Cargo dependencies.
|
||||
|
||||
## 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`)
|
||||
|
||||
## Resolving hostnames
|
||||
|
||||
The quick-start above uses a literal IP. For real hostnames, pair with [`smolmix-dns`](../dns) so resolution also goes through the tunnel (no DNS leaks to the local network):
|
||||
|
||||
```rust
|
||||
use smolmix_dns::Resolver;
|
||||
use smolmix_tls::{connect_with, connector};
|
||||
|
||||
let resolver = Resolver::new(&tunnel);
|
||||
let addr = resolver.resolve("example.com", 443).await?
|
||||
.into_iter().next().ok_or("no addresses resolved")?;
|
||||
|
||||
let tcp = tunnel.tcp_connect(addr).await?;
|
||||
let tls = connector();
|
||||
let stream = connect_with(&tls, tcp, "example.com").await?;
|
||||
```
|
||||
|
||||
`smolmix-tls` itself stays DNS-agnostic on purpose: it accepts any `TcpStream`, so callers with a pre-resolved address or a different resolver don't pay for `hickory-resolver`. See `examples/connect.rs` for a runnable end-to-end version.
|
||||
|
||||
## Dependencies
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix = "1.21.0"
|
||||
smolmix-tls = "1.21.0"
|
||||
```
|
||||
|
||||
This crate depends on `tokio-rustls`, `rustls`, and `webpki-roots`.
|
||||
|
||||
## See also
|
||||
|
||||
- [`smolmix-dns`](../dns) for tunneled hostname resolution
|
||||
- [`smolmix-hyper`](../hyper) for a complete HTTP client (DNS + TLS + HTTP) built on top of this
|
||||
@@ -0,0 +1,250 @@
|
||||
//! TLS connection: clearnet vs Nym mixnet comparison.
|
||||
//!
|
||||
//! Performs a TLS handshake and HTTPS GET request via both clearnet (tokio-rustls
|
||||
//! over a system TCP socket) and the mixnet (smolmix-tls over a tunnel), then
|
||||
//! compares timing and verifies both see the same content.
|
||||
//!
|
||||
//! Run with:
|
||||
//! cargo run -p smolmix-tls --example connect
|
||||
//! cargo run -p smolmix-tls --example connect -- --ipr <IPR_ADDRESS>
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustls::ClientConfig;
|
||||
use smolmix::{Recipient, Tunnel};
|
||||
use smolmix_dns::Resolver;
|
||||
use smolmix_tls::{connect_with, connector};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tracing::info;
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), BoxError> {
|
||||
nym_bin_common::logging::setup_tracing_logger();
|
||||
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
|
||||
let host = "example.com";
|
||||
let port = 443u16;
|
||||
|
||||
// Clearnet baseline via tokio + tokio-rustls
|
||||
info!("Clearnet TLS to {host}:{port}...");
|
||||
let clearnet_start = std::time::Instant::now();
|
||||
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let config = ClientConfig::builder()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let tls_connector = tokio_rustls::TlsConnector::from(Arc::new(config));
|
||||
|
||||
let tcp = tokio::net::TcpStream::connect((host, port)).await?;
|
||||
let server_name = rustls::pki_types::ServerName::try_from(host.to_string())?;
|
||||
let mut tls = tls_connector.connect(server_name, tcp).await?;
|
||||
|
||||
tls.write_all(
|
||||
format!("GET / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n").as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
let mut clearnet_buf = Vec::new();
|
||||
tls.read_to_end(&mut clearnet_buf).await?;
|
||||
let clearnet_duration = clearnet_start.elapsed();
|
||||
|
||||
let clearnet_status = extract_status(&clearnet_buf);
|
||||
info!(
|
||||
"{clearnet_status} {} in {clearnet_duration:.1?}",
|
||||
format_bytes(clearnet_buf.len() as u64)
|
||||
);
|
||||
|
||||
// Mixnet tunnel setup
|
||||
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_str) = ipr_addr {
|
||||
let recipient: Recipient = addr_str.parse().expect("invalid IPR address");
|
||||
Tunnel::new_with_ipr(recipient).await?
|
||||
} else {
|
||||
Tunnel::new().await?
|
||||
};
|
||||
info!(
|
||||
"Tunnel ready — allocated IP: {}",
|
||||
tunnel.allocated_ips().ipv4
|
||||
);
|
||||
|
||||
let tls_conn = connector();
|
||||
let resolver = Resolver::new(&tunnel);
|
||||
let overall_start = std::time::Instant::now();
|
||||
|
||||
// DNS resolution via mixnet
|
||||
info!("Mixnet TLS to {host}:{port}...");
|
||||
let spinner = spin(&format!("Resolving {host} via mixnet DNS..."));
|
||||
let dns_start = std::time::Instant::now();
|
||||
let addrs = resolver.resolve(host, port).await?;
|
||||
let addr = addrs.into_iter().next().ok_or("no addresses resolved")?;
|
||||
let dns_duration = dns_start.elapsed();
|
||||
spinner.abort();
|
||||
eprint!("\r \r");
|
||||
info!("DNS: {host} → {addr} ({dns_duration:.1?})");
|
||||
|
||||
// TCP connection through mixnet
|
||||
let spinner = spin(&format!("TCP connecting to {addr}..."));
|
||||
let tcp_start = std::time::Instant::now();
|
||||
let tcp = tunnel.tcp_connect(addr).await?;
|
||||
let tcp_duration = tcp_start.elapsed();
|
||||
spinner.abort();
|
||||
eprint!("\r \r");
|
||||
info!("TCP: connected to {addr} ({tcp_duration:.1?})");
|
||||
|
||||
// TLS handshake
|
||||
let spinner = spin(&format!("TLS handshake with {host}..."));
|
||||
let tls_start = std::time::Instant::now();
|
||||
let mut tls = connect_with(&tls_conn, tcp, host).await?;
|
||||
let tls_duration = tls_start.elapsed();
|
||||
spinner.abort();
|
||||
eprint!("\r \r");
|
||||
info!("TLS: handshake complete ({tls_duration:.1?})");
|
||||
|
||||
// First HTTP GET (keep-alive)
|
||||
let spinner = spin("GET / (first request)...");
|
||||
let http1_start = std::time::Instant::now();
|
||||
tls.write_all(format!("GET / HTTP/1.1\r\nHost: {host}\r\n\r\n").as_bytes())
|
||||
.await?;
|
||||
let mixnet_buf = read_http_response(&mut tls).await?;
|
||||
let http1_duration = http1_start.elapsed();
|
||||
spinner.abort();
|
||||
|
||||
let mixnet_status = extract_status(&mixnet_buf);
|
||||
info!(
|
||||
"GET1: {mixnet_status} {} ({http1_duration:.1?})",
|
||||
format_bytes(mixnet_buf.len() as u64)
|
||||
);
|
||||
|
||||
// Second HTTP GET over same connection (no DNS/TCP/TLS overhead)
|
||||
let spinner = spin("GET / (reusing connection)...");
|
||||
let http2_start = std::time::Instant::now();
|
||||
tls.write_all(
|
||||
format!("GET / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n").as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
let mut mixnet_buf2 = Vec::new();
|
||||
tls.read_to_end(&mut mixnet_buf2).await?;
|
||||
let http2_duration = http2_start.elapsed();
|
||||
spinner.abort();
|
||||
|
||||
let mixnet_status2 = extract_status(&mixnet_buf2);
|
||||
info!(
|
||||
"GET2: {mixnet_status2} {} ({http2_duration:.1?}) (reused connection)",
|
||||
format_bytes(mixnet_buf2.len() as u64)
|
||||
);
|
||||
|
||||
let mixnet_duration = overall_start.elapsed();
|
||||
|
||||
// Compare
|
||||
info!("Results");
|
||||
info!(
|
||||
"Clearnet: {} in {clearnet_duration:.1?}",
|
||||
format_bytes(clearnet_buf.len() as u64)
|
||||
);
|
||||
info!(
|
||||
"Mixnet #1: {} in {mixnet_duration:.1?} (DNS {dns_duration:.1?} + TCP {tcp_duration:.1?} + TLS {tls_duration:.1?} + HTTP {http1_duration:.1?})",
|
||||
format_bytes(mixnet_buf.len() as u64),
|
||||
);
|
||||
info!(
|
||||
"Mixnet #2: {} in {http2_duration:.1?} (reused connection)",
|
||||
format_bytes(mixnet_buf2.len() as u64),
|
||||
);
|
||||
|
||||
let slowdown1 =
|
||||
mixnet_duration.as_millis() as f64 / clearnet_duration.as_millis().max(1) as f64;
|
||||
let slowdown2 = http2_duration.as_millis() as f64 / clearnet_duration.as_millis().max(1) as f64;
|
||||
info!("Slowdown: {slowdown1:.1}x (cold) / {slowdown2:.1}x (warm)");
|
||||
|
||||
tunnel.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spin(msg: &str) -> tokio::task::JoinHandle<()> {
|
||||
let msg = msg.to_string();
|
||||
tokio::spawn(async move {
|
||||
let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
let mut i = 0;
|
||||
loop {
|
||||
eprint!("\r {} {}", frames[i % frames.len()], msg);
|
||||
i += 1;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a complete HTTP/1.1 response from a keep-alive connection.
|
||||
///
|
||||
/// Parses headers to find `Content-Length`, then reads exactly that many body
|
||||
/// bytes. Returns the full response (headers + body) as a single buffer.
|
||||
async fn read_http_response<R: tokio::io::AsyncRead + Unpin>(
|
||||
reader: &mut R,
|
||||
) -> Result<Vec<u8>, BoxError> {
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
let mut tmp = [0u8; 1024];
|
||||
|
||||
// Read until we find the end-of-headers marker
|
||||
let header_end = loop {
|
||||
let n = reader.read(&mut tmp).await?;
|
||||
if n == 0 {
|
||||
return Err("connection closed before headers complete".into());
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..n]);
|
||||
if let Some(pos) = find_subsequence(&buf, b"\r\n\r\n") {
|
||||
break pos + 4;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse Content-Length from headers
|
||||
let headers = std::str::from_utf8(&buf[..header_end]).unwrap_or("");
|
||||
let content_length = headers
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
let (key, val) = line.split_once(':')?;
|
||||
if key.trim().eq_ignore_ascii_case("content-length") {
|
||||
val.trim().parse::<usize>().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
// Read remaining body bytes
|
||||
let body_so_far = buf.len() - header_end;
|
||||
let remaining = content_length.saturating_sub(body_so_far);
|
||||
if remaining > 0 {
|
||||
let mut body_buf = vec![0u8; remaining];
|
||||
reader.read_exact(&mut body_buf).await?;
|
||||
buf.extend_from_slice(&body_buf);
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||
haystack.windows(needle.len()).position(|w| w == needle)
|
||||
}
|
||||
|
||||
fn extract_status(buf: &[u8]) -> &str {
|
||||
let s = std::str::from_utf8(&buf[..buf.len().min(40)]).unwrap_or("");
|
||||
s.lines().next().unwrap_or("")
|
||||
}
|
||||
|
||||
fn format_bytes(n: u64) -> String {
|
||||
if n >= 1_000_000 {
|
||||
format!("{:.1} MB", n as f64 / 1_000_000.0)
|
||||
} else if n >= 1_000 {
|
||||
format!("{:.1} KB", n as f64 / 1_000.0)
|
||||
} else {
|
||||
format!("{n} B")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2024-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
//! Shared TLS configuration for smolmix tunneled connections.
|
||||
//!
|
||||
//! # Why a separate TLS crate?
|
||||
//!
|
||||
//! Every protocol that needs encryption over smolmix (HTTPS, WebSocket, etc.)
|
||||
//! requires the same setup: build a `ClientConfig` with webpki root
|
||||
//! certificates, wrap it in a `TlsConnector`. Rather than duplicating this
|
||||
//! in every crate, `smolmix-tls` provides a single source of truth.
|
||||
//!
|
||||
//! The crate is deliberately minimal — 60 lines of pure configuration, no
|
||||
//! trait impls needed. `tokio-rustls` works directly with anything that
|
||||
//! implements tokio's `AsyncRead + AsyncWrite`, which `smolmix::TcpStream`
|
||||
//! does out of the box.
|
||||
//!
|
||||
//! # Usage patterns
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! # let tunnel = smolmix::Tunnel::new().await?;
|
||||
//! // One-shot: creates a fresh connector internally.
|
||||
//! // Simple but rebuilds the root cert store each time.
|
||||
//! let tcp = tunnel.tcp_connect("93.184.216.34:443".parse()?).await?;
|
||||
//! let tls = smolmix_tls::connect(tcp, "example.com").await?;
|
||||
//!
|
||||
//! // Reusable: create a connector once, use for many connections.
|
||||
//! // The TlsConnector wraps an Arc<ClientConfig> — cloning is cheap.
|
||||
//! let connector = smolmix_tls::connector();
|
||||
//! let tcp1 = tunnel.tcp_connect("1.1.1.1:443".parse()?).await?;
|
||||
//! let stream1 = smolmix_tls::connect_with(&connector, tcp1, "one.one.one.one").await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! # What's inside
|
||||
//!
|
||||
//! - [`connector()`] — builds a `TlsConnector` with Mozilla's root CA bundle
|
||||
//! ([`webpki-roots`](https://docs.rs/webpki-roots))
|
||||
//! - [`connect()`] — one-shot TLS handshake (convenience, creates connector internally)
|
||||
//! - [`connect_with()`] — TLS handshake using a pre-built connector (preferred for repeated use)
|
||||
//! - Re-exports [`TlsStream`] and [`TlsConnector`] so downstream code doesn't
|
||||
//! need `tokio-rustls` in its `Cargo.toml`
|
||||
//!
|
||||
//! # Security
|
||||
//!
|
||||
//! The connector uses rustls with the standard webpki root certificates and
|
||||
//! no client authentication. SNI (Server Name Indication) is set from the
|
||||
//! hostname you pass to `connect`/`connect_with`. There is no way to disable
|
||||
//! certificate verification — this is intentional.
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user