From 11556c6bd811c12f794bbae7ca1a128fac374ba8 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Mon, 29 Jun 2026 08:27:13 +0100 Subject: [PATCH 01/13] country specific autodiscovery + example + docs from other branch --- Cargo.lock | 1 + .../docs/pages/developers/_meta.json | 7 +- .../docs/pages/developers/clients/socks5.mdx | 2 +- documentation/docs/pages/developers/index.mdx | 2 +- documentation/docs/pages/developers/rust.mdx | 3 +- .../docs/pages/developers/rust/_meta.json | 1 + .../docs/pages/developers/rust/socks5.mdx | 130 ++++++++++++++++ sdk/rust/nym-sdk/Cargo.toml | 1 + .../nym-sdk/examples/socks5_autodiscover.rs | 42 ++++++ sdk/rust/nym-sdk/src/error.rs | 6 + sdk/rust/nym-sdk/src/mixnet.rs | 7 +- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 129 +++++++++++++++- .../nym-sdk/src/mixnet/socks5_discovery.rs | 140 ++++++++++++++++++ 13 files changed, 465 insertions(+), 6 deletions(-) create mode 100644 documentation/docs/pages/developers/rust/socks5.mdx create mode 100644 sdk/rust/nym-sdk/examples/socks5_autodiscover.rs create mode 100644 sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs diff --git a/Cargo.lock b/Cargo.lock index 6db3d172571..a3f7babe007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8050,6 +8050,7 @@ dependencies = [ "bip39", "bytecodec", "bytes", + "celes", "clap", "dashmap", "dirs", diff --git a/documentation/docs/pages/developers/_meta.json b/documentation/docs/pages/developers/_meta.json index fc769b039a3..7c860b29075 100644 --- a/documentation/docs/pages/developers/_meta.json +++ b/documentation/docs/pages/developers/_meta.json @@ -10,7 +10,12 @@ "title": "Rust" }, "smolmix": "smolmix (TCP/UDP tunnel)", - "rust": "nym-sdk", + "rust": { + "title": "nym-sdk", + "theme": { + "collapsed": false + } + }, "-": { "type": "separator", diff --git a/documentation/docs/pages/developers/clients/socks5.mdx b/documentation/docs/pages/developers/clients/socks5.mdx index e0a70f0164d..215960e8d33 100644 --- a/documentation/docs/pages/developers/clients/socks5.mdx +++ b/documentation/docs/pages/developers/clients/socks5.mdx @@ -1,6 +1,6 @@ # Socks5 Client (Standalone) -> This client can also be utilised via the [Rust SDK](/developers/rust). +> This proxy is also available embedded in a Rust application via the [nym-sdk SOCKS5 module](/developers/rust/socks5). Many existing applications are able to use either the SOCKS4, SOCKS4A, or SOCKS5 proxy protocols. If you want to send such an application's traffic through the mixnet, you can use the `nym-socks5-client` to bounce network traffic through the Nym network, like this: diff --git a/documentation/docs/pages/developers/index.mdx b/documentation/docs/pages/developers/index.mdx index b441f391773..a1a57a3a3ea 100644 --- a/documentation/docs/pages/developers/index.mdx +++ b/documentation/docs/pages/developers/index.mdx @@ -21,7 +21,7 @@ The table below maps those two answers to a package. | Runtime | End-to-end (both sides run Nym) | Proxy (exit to clearnet) | |---|---|---| -| **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS](/developers/rust) | +| **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS5](/developers/rust/socks5) | | **Browser / WebView** (JS + WASM) | [TypeScript SDK](/developers/typescript): `@nymproject/sdk` raw messaging | [`mix-fetch`](/developers/mix-fetch) HTTP/S · [`mix-dns`](/developers/mix-dns) DNS · [`mix-websocket`](/developers/mix-websocket) WS/WSS | diff --git a/documentation/docs/pages/developers/rust.mdx b/documentation/docs/pages/developers/rust.mdx index 04229f6175e..d728865371f 100644 --- a/documentation/docs/pages/developers/rust.mdx +++ b/documentation/docs/pages/developers/rust.mdx @@ -37,6 +37,7 @@ For an overview of what the SDK can do, see the **[Tour](./rust/tour)**. For set |---|---|---| | [**Stream**](./rust/stream) | Multiplexed `AsyncRead + AsyncWrite` byte streams over the Mixnet, the closest analogue to TCP sockets. | Recommended | | [**Mixnet**](./rust/mixnet) | Raw message payloads, independently routed, no connections or ordering. Full control over the communication model. | Stable | +| [**SOCKS5**](./rust/socks5) | Local SOCKS5 proxy that routes any SOCKS-capable application through the Mixnet to a network requester (proxy mode, exits to clearnet). | Stable | | [**Client Pool**](./rust/client-pool) | Keeps ready-to-use `MixnetClient` instances warm for bursty workloads. | Stable | | [**TcpProxy**](./rust/tcpproxy) | TCP socket proxying with session management and message ordering. | Deprecated | | [**FFI**](./rust/ffi) | Go and C/C++ bindings. | Stable | @@ -50,4 +51,4 @@ 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. -- [**SOCKS Client**](./rust/mixnet): SOCKS4/4a/5 proxy via the Exit Gateway's Network Requester. Works with any SOCKS-capable application without code changes. +- [**SOCKS5 module**](./rust/socks5): SOCKS4/4a/5 proxy via the Exit Gateway's network requester. Works with any SOCKS-capable application without code changes. diff --git a/documentation/docs/pages/developers/rust/_meta.json b/documentation/docs/pages/developers/rust/_meta.json index d00e161c61d..d9344078c4a 100644 --- a/documentation/docs/pages/developers/rust/_meta.json +++ b/documentation/docs/pages/developers/rust/_meta.json @@ -3,6 +3,7 @@ "importing": "Installation", "mixnet": "Mixnet Module", "stream": "Stream Module", + "socks5": "SOCKS5 Module", "tcpproxy": "TcpProxy Module (Deprecated)", "client-pool": "Client Pool Module", "ffi": "FFI" diff --git a/documentation/docs/pages/developers/rust/socks5.mdx b/documentation/docs/pages/developers/rust/socks5.mdx new file mode 100644 index 00000000000..edfa0950784 --- /dev/null +++ b/documentation/docs/pages/developers/rust/socks5.mdx @@ -0,0 +1,130 @@ +--- +title: "Nym Rust SDK: SOCKS5 Proxy Module" +description: "Use the Nym Rust SDK SOCKS5 module to route any SOCKS4/4a/5-capable application through the mixnet. Covers Socks5MixnetClient, the socks5h proxy URL, automatic network requester discovery, country-based selection, and a reqwest example." +schemaType: "TechArticle" +section: "Developers" +lastUpdated: "2026-06-29" +--- + +# SOCKS5 Module + +import { Callout } from 'nextra/components' + +The `socks5` module provides [`Socks5MixnetClient`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html): a local SOCKS5 proxy that routes any SOCKS4, SOCKS4a, or SOCKS5-capable application's traffic through the mixnet. Your application connects to a normal-looking SOCKS5 proxy on `localhost`; the client forwards that traffic over the mixnet to a **network requester** running on an Exit Gateway, which makes the real request on your behalf. + + +This is a **proxy-mode** integration, not end-to-end. Unlike the [Mixnet](./mixnet) and [Stream](./stream) modules (where both sides run a Nym client), traffic here leaves the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS) is your application's job. See [Exit security](/developers/concepts/exit-security) for what the exit can and cannot observe. + + +## How it works + +```text +Your machine + Application (reqwest, curl, a browser, ...) + │ SOCKS5 (socks5h://127.0.0.1:1080) + ▼ + Socks5MixnetClient (local listener + MixnetClient) + │ Sphinx packets + ▼ + Entry Gateway → 3 mix layers → Exit Gateway + │ network requester + ▼ makes the real request + Destination (clearnet) +``` + +Any application that speaks SOCKS sees a standard proxy on `localhost`. It never has to know the mixnet exists. The client chops the TCP stream into Sphinx packets, sends them through the mixnet, and the network requester at the exit reassembles the stream and performs the request, returning the response the same way. + +## Quick example + +You need the Nym address of a **network requester** (an Exit Gateway running in network-requester mode) to act as the service provider, or let the SDK find one for you ([Automatic discovery](#automatic-discovery)). Point an HTTP client at the SOCKS5 URL the client exposes, and every request travels through the mixnet: + +```rust +use nym_sdk::mixnet::Socks5MixnetClient; + +#[tokio::main] +async fn main() -> Result<(), Box> { + nym_bin_common::logging::setup_tracing_logger(); + + // Connect to a network requester service provider over the mixnet. + // This opens a local SOCKS5 listener (default 127.0.0.1:1080). + let client = Socks5MixnetClient::connect_new("provider_nym_address...").await?; + + // Point any SOCKS5-capable HTTP client at the proxy URL. + let proxy = reqwest::Proxy::all(client.socks5_url())?; + let http = reqwest::Client::builder().proxy(proxy).build()?; + + // This request now travels through the mixnet and exits at the requester. + let body = http.get("https://nymtech.net").send().await?.text().await?; + println!("{body}"); + + client.disconnect().await; + Ok(()) +} +``` + + +`socks5_url()` returns a **`socks5h://`** URL, not `socks5://`. The trailing `h` tells the HTTP client to hand the hostname to the proxy and let the network requester resolve DNS at the exit, rather than resolving locally and leaking the destination through a local DNS query. + + +For finer control (binding a different address, reusing persistent keys), build the client through `MixnetClientBuilder` instead of `connect_new`: + +```rust +use nym_sdk::mixnet; + +let socks5_config = mixnet::Socks5::new("provider_nym_address...".to_string()); +let client = mixnet::MixnetClientBuilder::new_ephemeral() + .socks5_config(socks5_config) + .build()? + .connect_to_mixnet_via_socks5() + .await?; +``` + +## Automatic discovery + +You do not have to hardcode a network requester address. The SDK can find one from the Nym API and connect in a single call: + +```rust +use nym_sdk::mixnet::Socks5MixnetClient; + +// Pick any available network requester: +let client = Socks5MixnetClient::connect_new_with_discovery().await?; +``` + +Discovery queries mainnet for Exit Gateways that advertise a network requester and selects one weighted by performance. + +### Selecting by country + +To constrain where your traffic leaves the mixnet, use the discovery builder and pass one or more [ISO 3166 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. The chosen requester must be physically located in one of them: + +```rust +use nym_sdk::mixnet::Socks5MixnetClient; + +let client = Socks5MixnetClient::discover() + .countries(["CH", "DE"])? // Switzerland or Germany + .port(1081) // optional: bind off the default 1080 + .connect() + .await?; +``` + +Codes are case-insensitive and validated up front, so a typo fails immediately rather than at connect time. Pass a list to `.countries([...])`, or call `.country("CH")` repeatedly; several countries are treated as "any of these". Use `.port()` or `.bind_address()` to move the local listener off the default `127.0.0.1:1080`, for example to run more than one client at once. + + +A requester's location is **self-reported by its operator and optional**. Requesters that have not declared a location are excluded once you set a country filter, so a narrow filter can return `NoGatewayInCountries` even when requesters exist there but have not advertised it. Discovery does not silently fall back to another country: an empty result is an error, since routing through an unintended jurisdiction would defeat the point of asking. + + +## SDK module or standalone binary + +The same proxy logic ships two ways. They are interchangeable on the wire; choose by how you want to run it: + +| | Use it when | +|---|---| +| **`socks5` module** (this page) | You want the proxy embedded in a Rust application and managed in-process, alongside other SDK clients. | +| [**Standalone `nym-socks5-client`**](/developers/clients/socks5) | You want a language-agnostic local proxy binary that any application (in any language) can point at, with no code changes. | + +## Further reading + +- [API reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html): all methods, configuration, and types +- [Example: fixed provider](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/socks5.rs): connect to a known network requester +- [Example: autodiscovery](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs): discover a requester by country and proxy a request +- [Standalone SOCKS5 client](/developers/clients/socks5): the language-agnostic binary form +- [Exit security](/developers/concepts/exit-security): what an Exit Gateway can observe in proxy mode diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index fd691d0b9f3..ea9290e5afc 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -59,6 +59,7 @@ nym-bin-common = { workspace = true, features = [ bytecodec = { workspace = true } httpcodec = { workspace = true } bytes = { workspace = true } +celes = { workspace = true } http = { workspace = true } zeroize = { workspace = true } diff --git a/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs b/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs new file mode 100644 index 00000000000..480fe9f1411 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs @@ -0,0 +1,42 @@ +//! SOCKS5 proxy client that auto-discovers a network requester, optionally +//! pinned to a set of countries. +//! +//! Unlike `socks5.rs`, this takes no provider address. It queries the Nym API +//! for exit gateways advertising a network requester, optionally keeps only +//! those physically located in the requested countries, picks one weighted by +//! performance, and routes an HTTPS request through it. +//! +//! Run with: cargo run --example socks5_autodiscover + +use nym_sdk::mixnet::Socks5MixnetClient; + +#[tokio::main] +async fn main() -> Result<(), Box> { + nym_bin_common::logging::setup_tracing_logger(); + + // Discover a network requester in Switzerland or Germany and connect to it. + // Drop `.countries(...)` to accept any country. The SOCKS5 listener binds + // 127.0.0.1:1080 by default; `.port()` overrides it (1081 here to avoid + // colliding with any proxy already on 1080). + println!("Discovering a network requester in CH/DE and connecting"); + let client = Socks5MixnetClient::discover() + .countries(["CH", "DE"])? + .port(1081) + .connect() + .await?; + println!("SOCKS5 proxy listening at {}", client.socks5_url()); + + // Point an HTTP client at the proxy and make a request through the mixnet. + let proxy = reqwest::Proxy::all(client.socks5_url())?; + let http = reqwest::Client::builder().proxy(proxy).build()?; + + // nymtech.net is on the default Nym exit policy. If you change this URL to a + // destination the exit policy does not allow, the request fails at the exit, + // which is not a discovery failure. + println!("Sending request through the mixnet"); + let status = http.get("https://nymtech.net").send().await?.status(); + println!("Got response status: {status}"); + + client.disconnect().await; + Ok(()) +} diff --git a/sdk/rust/nym-sdk/src/error.rs b/sdk/rust/nym-sdk/src/error.rs index 499ee4618ec..7537864d67e 100644 --- a/sdk/rust/nym-sdk/src/error.rs +++ b/sdk/rust/nym-sdk/src/error.rs @@ -139,6 +139,12 @@ pub enum Error { #[error("no available gateway")] NoGatewayAvailable, + #[error("invalid ISO 3166 alpha-2 country code: {0}")] + InvalidCountryCode(String), + + #[error("no available gateway in the requested countries")] + NoGatewayInCountries, + #[error("tunnel disconnected by IPR")] IprTunnelDisconnected, diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index ca43fd73184..05e638ec007 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -82,6 +82,7 @@ mod native_client; mod paths; mod sink; mod socks5_client; +mod socks5_discovery; pub mod stream; mod traits; @@ -92,7 +93,11 @@ pub use native_client::MixnetClient; pub use native_client::MixnetClientSender; pub use paths::StoragePaths; pub use sink::{MixnetMessageSink, MixnetMessageSinkTranslator}; -pub use socks5_client::Socks5MixnetClient; +pub use socks5_client::{Socks5DiscoveryBuilder, Socks5MixnetClient}; +pub use socks5_discovery::{ + get_best_network_requester, get_best_network_requester_in, + retrieve_network_requesters_with_performance, NetworkRequesterWithPerformance, +}; pub use stream::{MixnetListener, MixnetStream, StreamId}; pub use traits::MixnetMessageSender; diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 9dc649f7ddf..99670835f78 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -1,3 +1,4 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; use nym_client_core::client::base_client::ClientState; @@ -7,10 +8,13 @@ use nym_task::connections::LaneQueueLengths; use nym_task::ShutdownTracker; use tokio::sync::RwLockReadGuard; +use celes::Country; use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; +use crate::ip_packet_client::discovery::create_nym_api_client; use crate::mixnet::client::MixnetClientBuilder; -use crate::Result; +use crate::mixnet::socks5_discovery::get_best_network_requester_in; +use crate::{Error, NymNetworkDetails, Result}; /// A SOCKS5 proxy client connected to the Nym mixnet. /// @@ -92,6 +96,44 @@ impl Socks5MixnetClient { .await } + /// Start building a client that connects to an automatically discovered + /// network requester. Restrict the requester's physical location with + /// [`country`](Socks5DiscoveryBuilder::country) / + /// [`countries`](Socks5DiscoveryBuilder::countries), then + /// [`connect`](Socks5DiscoveryBuilder::connect). + /// + /// # Examples + /// + /// ```no_run + /// use nym_sdk::mixnet::Socks5MixnetClient; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// // Any country: + /// let any = Socks5MixnetClient::discover().connect().await?; + /// + /// // Pinned to Switzerland or Germany: + /// let pinned = Socks5MixnetClient::discover() + /// .countries(["CH", "DE"])? + /// .connect() + /// .await?; + /// Ok(()) + /// } + /// ``` + pub fn discover() -> Socks5DiscoveryBuilder { + Socks5DiscoveryBuilder::default() + } + + /// Create a new client and connect to an automatically discovered network + /// requester in any country. Shorthand for `discover().connect()`. + /// + /// Discovery always targets mainnet. The discovered requester enforces the + /// Nym exit policy, so destinations outside that policy will be refused at + /// the exit regardless of which requester is selected. + pub async fn connect_new_with_discovery() -> Result { + Self::discover().connect().await + } + /// Get the nym address for this client, if it is available. The nym address is composed of the /// client identity, the client encryption key, and the gateway identity. pub fn nym_address(&self) -> &Recipient { @@ -153,3 +195,88 @@ impl Socks5MixnetClient { } } } + +/// Builder for connecting a [`Socks5MixnetClient`] to an automatically +/// discovered network requester, optionally restricted by country. +/// +/// Create one with [`Socks5MixnetClient::discover`]. With no country set, +/// discovery selects from any country; otherwise the chosen requester must be +/// physically located in one of the requested countries. +#[derive(Debug, Default, Clone)] +#[must_use] +pub struct Socks5DiscoveryBuilder { + countries: Vec, + bind_address: Option, +} + +impl Socks5DiscoveryBuilder { + /// Require the discovered network requester to be located in the given + /// country, identified by its ISO 3166 alpha-2 code (e.g. `"CH"`). + /// Case-insensitive. Call repeatedly to allow several countries. + /// + /// Returns [`Error::InvalidCountryCode`] if the code is not a valid alpha-2 + /// country code. + #[allow(clippy::result_large_err)] + pub fn country(mut self, code: impl AsRef) -> Result { + let country = Country::from_alpha2(code.as_ref()) + .map_err(|_| Error::InvalidCountryCode(code.as_ref().to_string()))?; + self.countries.push(country); + Ok(self) + } + + /// Require the discovered network requester to be located in one of the + /// given countries, each an ISO 3166 alpha-2 code. Case-insensitive. + /// + /// Returns [`Error::InvalidCountryCode`] on the first invalid code. + #[allow(clippy::result_large_err)] + pub fn countries(mut self, codes: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + for code in codes { + self = self.country(code)?; + } + Ok(self) + } + + /// Bind the local SOCKS5 listener to a specific address instead of the + /// default `127.0.0.1:1080`. Set this to run more than one SOCKS5 client at + /// once, or to avoid a port already in use. + pub fn bind_address(mut self, address: SocketAddr) -> Self { + self.bind_address = Some(address); + self + } + + /// Bind the local SOCKS5 listener to `127.0.0.1:` instead of the + /// default port 1080. Shorthand for the loopback case of + /// [`bind_address`](Self::bind_address); this resets the host to loopback, + /// overriding any address previously set with `bind_address`. + pub fn port(mut self, port: u16) -> Self { + self.bind_address = Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)); + self + } + + /// Discover a matching network requester on mainnet and connect to it. + /// + /// If a country filter is set and no requester matches, returns + /// [`Error::NoGatewayInCountries`]. + pub async fn connect(self) -> Result { + let nym_api_urls = NymNetworkDetails::new_mainnet() + .nym_api_urls + .ok_or(Error::NoNymAPIUrl)?; + let api_client = create_nym_api_client(nym_api_urls)?; + let provider = get_best_network_requester_in(api_client, &self.countries).await?; + + let mut socks5_config = Socks5::new(provider.to_string()); + if let Some(bind_address) = self.bind_address { + socks5_config.bind_address = bind_address; + } + + MixnetClientBuilder::new_ephemeral() + .socks5_config(socks5_config) + .build()? + .connect_to_mixnet_via_socks5() + .await + } +} diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs new file mode 100644 index 00000000000..e8dfaa00421 --- /dev/null +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs @@ -0,0 +1,140 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! Network requester discovery — find and rank network-requester-enabled exit +//! gateways via the Nym API, optionally restricted by physical country. +//! +//! This mirrors the IPR discovery in [`crate::ip_packet_client::discovery`]. +//! Exit gateways self-announce both an IPR and a network requester in the same +//! described-node payload (`NymNodeDataV2`), so the selection logic is the same; +//! only the field we read differs (`network_requester` instead of +//! `ip_packet_router`), and there is no protocol-version gate. +//! +//! The node's physical location (`auxiliary_details.location`) rides along in +//! that same payload, so country filtering needs no extra requests. + +use std::collections::HashMap; + +use celes::Country; +use nym_crypto::asymmetric::ed25519; +use nym_sphinx::addressing::clients::Recipient; +use nym_validator_client::nym_api::NymApiClientExt; +use rand::seq::SliceRandom; +use tracing::info; + +use crate::Error; + +/// A network requester exit gateway and the metadata the directory reports for it. +#[derive(Clone)] +pub struct NetworkRequesterWithPerformance { + pub address: Recipient, + pub identity: ed25519::PublicKey, + pub performance: u8, + /// Physical location the operator self-reported, if any. `None` means the + /// operator did not declare a location, not that the node is unlocated. + pub country: Option, +} + +/// Collect every exit gateway that advertises a network requester address, +/// paired with its performance score and self-reported country. +pub async fn retrieve_network_requesters_with_performance( + client: nym_http_api_client::Client, +) -> Result, Error> { + let all_nodes = client + .get_all_described_nodes_v2() + .await? + .into_iter() + .map(|described| (described.ed25519_identity_key(), described)) + .collect::>(); + + let exit_gateways = client.get_all_basic_nodes_with_metadata().await?.nodes; + + let mut requesters = Vec::new(); + + for exit in exit_gateways { + let Some(node) = all_nodes.get(&exit.ed25519_identity_pubkey) else { + continue; + }; + + if let Some(nr_info) = node.description.network_requester.clone() { + if let Ok(parsed_address) = nr_info.address.parse() { + requesters.push(NetworkRequesterWithPerformance { + address: parsed_address, + identity: exit.ed25519_identity_pubkey, + performance: exit.performance.round_to_integer(), + country: node.description.auxiliary_details.location, + }) + } + } + } + + Ok(requesters) +} + +/// Select the best network requester from any country, weighted by performance. +pub async fn get_best_network_requester( + client: nym_http_api_client::Client, +) -> Result { + get_best_network_requester_in(client, &[]).await +} + +/// Select a network requester weighted by performance, restricted to the given +/// countries. An empty `countries` slice means any country is acceptable. +/// +/// Requesters that did not declare a location are excluded whenever a country +/// filter is active: an undeclared exit cannot be assumed to be in a requested +/// country. If the filter leaves no candidates, this returns +/// [`Error::NoGatewayInCountries`] rather than silently falling back. +pub async fn get_best_network_requester_in( + client: nym_http_api_client::Client, + countries: &[Country], +) -> Result { + let requesters = retrieve_network_requesters_with_performance(client).await?; + let total = requesters.len(); + + let pool: Vec = if countries.is_empty() { + requesters + } else { + requesters + .into_iter() + .filter(|nr| match nr.country { + Some(c) => countries + .iter() + .any(|want| want.alpha2.eq_ignore_ascii_case(c.alpha2)), + None => false, + }) + .collect() + }; + + info!( + "Found {} network requesters ({} after country filter)", + total, + pool.len() + ); + + if pool.is_empty() { + return Err(if countries.is_empty() { + Error::NoGatewayAvailable + } else { + Error::NoGatewayInCountries + }); + } + + // Weight by performance. If every candidate scored zero (e.g. a low score + // rounded down to 0), fall back to a uniform pick rather than failing as if + // no requester existed — the pool is non-empty here. + let mut rng = rand::thread_rng(); + let selected = pool + .choose_weighted(&mut rng, |nr| nr.performance as f64) + .or_else(|_| pool.choose(&mut rng).ok_or(Error::NoGatewayAvailable))?; + + info!( + "Using network requester: {} (Gateway: {}, Country: {:?}, Performance: {:?})", + selected.address, + selected.identity, + selected.country.map(|c| c.alpha2), + selected.performance + ); + + Ok(selected.address) +} From 30fb410bf6f9198ce7f04310fb35c4a65d1f3adf Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Mon, 29 Jun 2026 08:27:43 +0100 Subject: [PATCH 02/13] update links + exit policy tweak + add discovery to socks5 docs page --- .../circulating-supply.json | 4 +- .../api-scraping-outputs/nodes-count.json | 8 +- .../nyx-outputs/circulating-supply.md | 2 +- .../nyx-outputs/epoch-reward-budget.md | 2 +- .../nyx-outputs/stake-saturation.md | 2 +- .../nyx-outputs/staking-target.md | 2 +- .../nyx-outputs/staking_supply.md | 2 +- .../nyx-outputs/token-table.md | 6 +- .../api-scraping-outputs/reward-params.json | 8 +- .../outputs/api-scraping-outputs/time-now.md | 2 +- .../outputs/command-outputs/nym-api-help.md | 7 +- .../outputs/command-outputs/nym-node-help.md | 11 +- .../command-outputs/nym-node-run-help.md | 188 ++++++++++-------- .../outputs/command-outputs/nymvisor-help.md | 3 +- .../docs/pages/developers/_meta.json | 8 +- .../developers/concepts/exit-security.mdx | 59 +++--- .../docs/pages/developers/rust/socks5.mdx | 4 +- 17 files changed, 165 insertions(+), 153 deletions(-) diff --git a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json index b6de7488260..ef45f627240 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json @@ -5,7 +5,7 @@ }, "mixmining_reserve": { "denom": "unym", - "amount": "164623226345363" + "amount": "162624623318934" }, "vesting_tokens": { "denom": "unym", @@ -13,6 +13,6 @@ }, "circulating_supply": { "denom": "unym", - "amount": "835376773654637" + "amount": "837375376681066" } } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json b/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json index 00aa644a2b7..5d74984a99b 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json @@ -1,6 +1,6 @@ { - "nodes": 694, - "locations": 73, - "mixnodes": 234, - "exit_gateways": 452 + "nodes": 705, + "locations": 72, + "mixnodes": 237, + "exit_gateways": 460 } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md index 42252b8a5e4..2be6cbfc2c4 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md @@ -1 +1 @@ -835_376_773 +837_375_376 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md index 519f5836895..aa5fd96e550 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md @@ -1 +1 @@ -4_572 +4_517 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md index 435fbd506fa..56e75274e6c 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md @@ -1 +1 @@ -255_586 +256_198 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md index 1b5fa7f459b..e6a8116fcd8 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md @@ -1 +1 @@ -61_340_814 +61_487_569 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md index 1016ec7ef9a..7cb110b72d5 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md @@ -1 +1 @@ -61_340_813 +61_487_568 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md index eafb4039118..5e2124805eb 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md @@ -1,7 +1,7 @@ | **Item** | **Description** | **Amount in NYM** | |:-------------------|:------------------------------------------------------|--------------------:| | Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 | -| Mixmining Reserve | Tokens releasing for operators rewards | 164_623_226 | +| Mixmining Reserve | Tokens releasing for operators rewards | 162_624_623 | | Vesting Tokens | Tokens locked outside of circulation for future claim | 0 | -| Circulating Supply | Amount of unlocked tokens | 835_376_773 | -| Stake Saturation | Optimal size of node self-bond + delegation | 255_586 | +| Circulating Supply | Amount of unlocked tokens | 837_375_376 | +| Stake Saturation | Optimal size of node self-bond + delegation | 256_198 | diff --git a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json index 40f531502a0..d07226cfecb 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json @@ -1,10 +1,10 @@ { "interval": { - "reward_pool": "164623226345363.285226429762152046", - "staking_supply": "61340813321050.793375219026221616", + "reward_pool": "162624623318934.288753913229372056", + "staking_supply": "61487568582790.206042879724905795", "staking_supply_scale_factor": "0.07342892", - "epoch_reward_budget": "4572867398.482313478511937837", - "stake_saturation_point": "255586722171.04497239674594259", + "epoch_reward_budget": "4517350647.748174687608700815", + "stake_saturation_point": "256198202428.29252517866552044", "sybil_resistance": "0.3", "active_set_work_factor": "10", "interval_pool_emission": "0.02" diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 92cd6628d91..36907323ef6 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Tuesday, June 16th 2026, 14:01:39 UTC +Monday, June 29th 2026, 00:21:46 UTC diff --git a/documentation/docs/components/outputs/command-outputs/nym-api-help.md b/documentation/docs/components/outputs/command-outputs/nym-api-help.md index 3a20efee55e..1c382a7dcf0 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-api-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-api-help.md @@ -9,10 +9,11 @@ Commands: Options: -c, --config-env-file - Path pointing to an env file that configures the Nym API [env: NYMAPI_CONFIG_ENV_FILE_ARG=] + Path pointing to an env file that configures the Nym API [env: + NYMAPI_CONFIG_ENV_FILE_ARG=] --no-banner - A no-op flag included for consistency with other binaries (and compatibility with nymvisor, - oops) [env: NYMAPI_NO_BANNER_ARG=] + A no-op flag included for consistency with other binaries (and compatibility with + nymvisor, oops) [env: NYMAPI_NO_BANNER_ARG=] -h, --help Print help -V, --version diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-help.md index 4a476a60d73..f4dc606a364 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-node-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-node-help.md @@ -3,19 +3,20 @@ Usage: nym-node [OPTIONS] Commands: build-info Show build information of this binary - bonding-information Show bonding information of this node depending on its currently selected mode + bonding-information Show bonding information of this node depending on its currently + selected mode node-details Show details of this node migrate Attempt to migrate an existing mixnode or gateway into a nym-node run Start this nym-node sign Use identity key of this node to sign provided message - unsafe-reset-sphinx-keys UNSAFE: reset existing sphinx keys and attempt to generate fresh one for the - current network state + unsafe-reset-sphinx-keys UNSAFE: reset existing sphinx keys and attempt to generate fresh + one for the current network state help Print this message or the help of the given subcommand(s) Options: -c, --config-env-file - Path pointing to an env file that configures the nym-node and overrides any preconfigured values - [env: NYMNODE_CONFIG_ENV_FILE_ARG=] + Path pointing to an env file that configures the nym-node and overrides any + preconfigured values [env: NYMNODE_CONFIG_ENV_FILE_ARG=] --no-banner Flag used for disabling the printed banner in tty [env: NYMNODE_NO_BANNER=] -h, --help diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md index 453d9073261..28edeb9ea39 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md @@ -9,154 +9,170 @@ Options: --config-file Path to a configuration file of this node [env: NYMNODE_CONFIG=] --accept-operator-terms-and-conditions - Explicitly specify whether you agree with the terms and conditions of a nym node operator as - defined at [env: - NYMNODE_ACCEPT_OPERATOR_TERMS=] + Explicitly specify whether you agree with the terms and conditions of a nym node + operator as defined at + [env: NYMNODE_ACCEPT_OPERATOR_TERMS=] --deny-init - Forbid a new node from being initialised if configuration file for the provided specification - doesn't already exist [env: NYMNODE_DENY_INIT=] + Forbid a new node from being initialised if configuration file for the provided + specification doesn't already exist [env: NYMNODE_DENY_INIT=] --init-only - If this is a brand new nym-node, specify whether it should only be initialised without actually - running the subprocesses [env: NYMNODE_INIT_ONLY=] + If this is a brand new nym-node, specify whether it should only be initialised + without actually running the subprocesses [env: NYMNODE_INIT_ONLY=] --local Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=] --mode [...] - Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, - entry-gateway, exit-gateway, exit-providers-only] + Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: + mixnode, entry-gateway, exit-gateway, exit-providers-only] --modes - Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible - values: mixnode, entry-gateway, exit-gateway, exit-providers-only] + Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] + [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only] -w, --write-changes - If this node has been initialised before, specify whether to write any new changes to the config - file [env: NYMNODE_WRITE_CONFIG_CHANGES=] + If this node has been initialised before, specify whether to write any new changes to + the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=] --bonding-information-output - Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the - required bonding information is still a subject to change and this argument should be treated - only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=] + Specify output file for bonding information of this nym-node, i.e. its encoded keys. + NOTE: the required bonding information is still a subject to change and this argument + should be treated only as a preview of future features [env: + NYMNODE_BONDING_INFORMATION_OUTPUT=] -o, --output - Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] - [default: text] [possible values: text, json] + Specify the output format of the bonding information (`text` or `json`) [env: + NYMNODE_OUTPUT=] [default: text] [possible values: text, json] --public-ips Comma separated list of public ip addresses that will be announced to the nym-api and - subsequently to the clients. In nearly all circumstances, it's going to be identical to the - address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=] + subsequently to the clients. In nearly all circumstances, it's going to be identical + to the address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=] --hostname - Optional hostname associated with this gateway that will be announced to the nym-api and - subsequently to the clients [env: NYMNODE_HOSTNAME=] + Optional hostname associated with this gateway that will be announced to the nym-api + and subsequently to the clients [env: NYMNODE_HOSTNAME=] --location - Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), - two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. - '616') can be provided [env: NYMNODE_LOCATION=] + Optional **physical** location of this node's server. Either full country name (e.g. + 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or + three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=] --http-bind-address - Socket address this node will use for binding its http API. default: `[::]:8080` [env: - NYMNODE_HTTP_BIND_ADDRESS=] + Socket address this node will use for binding its http API. default: `[::]:8080` + [env: NYMNODE_HTTP_BIND_ADDRESS=] --landing-page-assets-path - Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=] + Path to assets directory of custom landing page of this node [env: + NYMNODE_HTTP_LANDING_ASSETS=] --http-access-token - An optional bearer token for accessing certain http endpoints. Currently only used for - prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=] + An optional bearer token for accessing certain http endpoints. Currently only used + for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=] --expose-system-info Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false] --expose-system-hardware - Specify whether basic system hardware information should be exposed. default: true [env: - NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false] + Specify whether basic system hardware information should be exposed. default: true + [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false] --expose-crypto-hardware - Specify whether detailed system crypto hardware information should be exposed. default: true - [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false] + Specify whether detailed system crypto hardware information should be exposed. + default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, + false] --nyxd-urls Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=] --nyxd-websocket-url - Url to the websocket endpoint of a nyx validator, for example `wss://rpc.nymtech.net/websocket`. - It is used for subscribing to new block events [env: NYMNODE_NYXD_WEBSOCKET=] + Url to the websocket endpoint of a nyx validator, for example + `wss://rpc.nymtech.net/websocket`. It is used for subscribing to new block events + [env: NYMNODE_NYXD_WEBSOCKET=] --mixnet-bind-address - Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: - NYMNODE_MIXNET_BIND_ADDRESS=] + Address this node will bind to for listening for mixnet packets default: `[::]:1789` + [env: NYMNODE_MIXNET_BIND_ADDRESS=] --mixnet-announce-port - If applicable, custom port announced in the self-described API that other clients and nodes will - use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=] + If applicable, custom port announced in the self-described API that other clients and + nodes will use. Useful when the node is behind a proxy [env: + NYMNODE_MIXNET_ANNOUNCE_PORT=] --nym-api-urls - Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=] + Addresses to nym APIs from which the node gets the view of the network [env: + NYMNODE_NYM_APIS=] --enable-console-logging Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false] --wireguard-enabled - Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] - [possible values: true, false] + Specifies whether the wireguard service is enabled on this node [env: + NYMNODE_WG_ENABLED=] [possible values: true, false] --wireguard-bind-address - Socket address this node will use for binding its wireguard interface. default: `[::]:51822` - [env: NYMNODE_WG_BIND_ADDRESS=] + Socket address this node will use for binding its wireguard interface. default: + `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=] --wireguard-tunnel-announced-port - Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful - in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=] + Tunnel port announced to external clients wishing to connect to the wireguard + interface. Useful in the instances where the node is behind a proxy [env: + NYMNODE_WG_ANNOUNCED_PORT=] --wireguard-private-network-prefix - The prefix denoting the maximum number of the clients that can be connected via Wireguard. The - maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=] + The prefix denoting the maximum number of the clients that can be connected via + Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: + NYMNODE_WG_PRIVATE_NETWORK_PREFIX=] --wireguard-userspace - Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in - containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] - [possible values: true, false] + Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. + Useful in containerized environments without kernel WireGuard support [env: + NYMNODE_WG_USERSPACE=] [possible values: true, false] --verloc-bind-address - Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: - NYMNODE_VERLOC_BIND_ADDRESS=] + Socket address this node will use for binding its verloc API. default: `[::]:1790` + [env: NYMNODE_VERLOC_BIND_ADDRESS=] --verloc-announce-port - If applicable, custom port announced in the self-described API that other clients and nodes will - use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=] + If applicable, custom port announced in the self-described API that other clients and + nodes will use. Useful when the node is behind a proxy [env: + NYMNODE_VERLOC_ANNOUNCE_PORT=] --entry-bind-address - Socket address this node will use for binding its client websocket API. default: `[::]:9000` - [env: NYMNODE_ENTRY_BIND_ADDRESS=] + Socket address this node will use for binding its client websocket API. default: + `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=] --announce-ws-port - Custom announced port for listening for websocket client traffic. If unspecified, the value from - the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=] + Custom announced port for listening for websocket client traffic. If unspecified, the + value from the `bind_address` will be used instead [env: + NYMNODE_ENTRY_ANNOUNCE_WS_PORT=] --announce-wss-port If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=] --enforce-zk-nyms - Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or - if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, - false] + Indicates whether this gateway is accepting only coconut credentials for accessing + the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] + [possible values: true, false] --mnemonic - Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh - mnemonic is going to be generated [env: NYMNODE_MNEMONIC=] + Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a + fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=] --upgrade-mode-attestation-url - Endpoint to query to retrieve current upgrade mode attestation. This argument should never be - set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=] + Endpoint to query to retrieve current upgrade mode attestation. This argument should + never be set outside testnets and local networks [env: + NYMNODE_UPGRADE_MODE_ATTESTATION_URL=] --upgrade-mode-attester-public-key - Expected public key of the entity signing the published attestation. This argument should never - be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=] + Expected public key of the entity signing the published attestation. This argument + should never be set outside testnets and local networks [env: + NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=] --upstream-exit-policy-url Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=] --open-proxy - Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to - resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false] + Specifies whether this exit node should run in 'open-proxy' mode and thus would + attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible + values: true, false] --nr-allow-local-ips - Allow the network requester to forward traffic to non-globally-routable addresses. Intended for - local development, private-network deployments, and testnet scenarios. Not recommended on - production exit gateway unless you know what you're doing [env: NYMNODE_NR_ALLOW_LOCAL_IPS=] - [possible values: true, false] + Allow the network requester to forward traffic to non-globally-routable addresses. + Intended for local development, private-network deployments, and testnet scenarios. + Not recommended on production exit gateway unless you know what you're doing [env: + NYMNODE_NR_ALLOW_LOCAL_IPS=] [possible values: true, false] --ipr-allow-local-ips - Allow the IP packet router to forward traffic to non-globally-routable addresses. Intended for - local development, private-network deployments, and testnet scenarios. Not recommended on - production exit gateway unless you know what you're doing [env: NYMNODE_IPR_ALLOW_LOCAL_IPS=] - [possible values: true, false] + Allow the IP packet router to forward traffic to non-globally-routable addresses. + Intended for local development, private-network deployments, and testnet scenarios. + Not recommended on production exit gateway unless you know what you're doing [env: + NYMNODE_IPR_ALLOW_LOCAL_IPS=] [possible values: true, false] --lp-control-bind-address Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=] --lp-control-announce-port - Custom announced port for listening for the TCP LP control traffic. If unspecified, the value - from the `lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=] + Custom announced port for listening for the TCP LP control traffic. If unspecified, + the value from the `lp_control_bind_address` will be used instead [env: + NYMNODE_LP_CONTROL_ANNOUNCE_PORT=] --lp-data-bind-address Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=] --lp-data-announce-port - Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from - the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=] + Custom announced port for listening for the UDP LP data traffic. If unspecified, the + value from the `lp_data_bind_address` will be used instead [env: + NYMNODE_LP_DATA_ANNOUNCE_PORT=] --lp-use-mock-ecash - Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in - production. When enabled, the LP listener will accept any credential without blockchain - verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false] + Use mock ecash manager for LP testing. WARNING: Only use this for local testing! + Never enable in production. When enabled, the LP listener will accept any credential + without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: + true, false] -h, --help Print help ``` diff --git a/documentation/docs/components/outputs/command-outputs/nymvisor-help.md b/documentation/docs/components/outputs/command-outputs/nymvisor-help.md index e4124a3ea9c..ba058d74f32 100644 --- a/documentation/docs/components/outputs/command-outputs/nymvisor-help.md +++ b/documentation/docs/components/outputs/command-outputs/nymvisor-help.md @@ -12,7 +12,8 @@ Commands: Options: -c, --config-env-file - Path pointing to an env file that configures the nymvisor and overrides any preconfigured values + Path pointing to an env file that configures the nymvisor and overrides any + preconfigured values -h, --help Print help -V, --version diff --git a/documentation/docs/pages/developers/_meta.json b/documentation/docs/pages/developers/_meta.json index 7c860b29075..bf8c714a71c 100644 --- a/documentation/docs/pages/developers/_meta.json +++ b/documentation/docs/pages/developers/_meta.json @@ -1,7 +1,6 @@ { "index": "Overview", "concepts": "Key Concepts", - "sep-intro": { "type": "separator" }, @@ -11,12 +10,8 @@ }, "smolmix": "smolmix (TCP/UDP tunnel)", "rust": { - "title": "nym-sdk", - "theme": { - "collapsed": false - } + "title": "nym-sdk" }, - "-": { "type": "separator", "title": "TypeScript" @@ -29,7 +24,6 @@ "mix-websocket": "mix-websocket (ws / wss)", "mix-architecture": "mix-* Family Architecture", "typescript": "Raw Messaging SDK", - "sep-extras": { "type": "separator" }, diff --git a/documentation/docs/pages/developers/concepts/exit-security.mdx b/documentation/docs/pages/developers/concepts/exit-security.mdx index fb56aa678e1..0bfdec611cc 100644 --- a/documentation/docs/pages/developers/concepts/exit-security.mdx +++ b/documentation/docs/pages/developers/concepts/exit-security.mdx @@ -1,68 +1,69 @@ --- title: "Exit Security: What the Mixnet Protects and What It Doesn't" -description: "The canonical security model for traffic that leaves the Nym mixnet at an IPR exit gateway. Applies to smolmix, mix-tunnel, mix-fetch, mix-dns, and mix-websocket alike." +description: "The security model for traffic that exits the Nym mixnet at an Exit Gateway, via the IP Packet Router or the SOCKS-based Network Requester, and how it changes for end-to-end traffic that never exits. Applies to smolmix, the SOCKS5 module, mix-tunnel, mix-fetch, mix-dns, and mix-websocket." schemaType: "TechArticle" section: "Developers" -lastUpdated: "2026-06-03" +lastUpdated: "2026-06-29" --- # Exit security import { Callout } from 'nextra/components' -Every tool that reaches an external service through the Nym mixnet shares the same security model, whether it's the Rust [`smolmix`](/developers/smolmix) crate or the mix-* packages built on [`mix-tunnel`](/developers/mix-tunnel) ([`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). They all exit the mixnet at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) gateway, so they inherit the same properties and the same single caveat. This page is the canonical statement of that model; the package pages link here rather than restating it. +Every tool that reaches an external service through the Nym mixnet shares the same exit security model, whether it's the Rust [`smolmix`](/developers/smolmix) crate, the [SOCKS5 module](/developers/rust/socks5), or the mix-* packages built on [`mix-tunnel`](/developers/mix-tunnel) ([`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). They leave the mixnet at an [Exit Gateway](/network/infrastructure/exit-services), either through the [IP Packet Router](/network/infrastructure/exit-services#ip-packet-router) (which forwards raw IP packets) or the SOCKS-based [Network Requester](/network/infrastructure/exit-services#network-requester) (which makes the request from a SOCKS stream), so they share the same properties and the same caveat. Tools where both ends run a Nym client ([`nym-sdk`](/developers/rust), the [TypeScript SDK](/developers/typescript)) never exit the mixnet at all; the [end-to-end case](#proxy-mode-or-end-to-end) is covered below. This page is the canonical statement of the model; the package pages link here rather than restating it. ## The one-sentence version -The mixnet hides **who** you are from the destination and **where** you're going from the network, but the exit gateway sees your **destination** and any payload you didn't encrypt yourself. +In **proxy mode** the mixnet hides **who** you are from the destination and **where** you're going from the network, but the exit gateway sees your **destination** and any payload you didn't encrypt yourself. In **end-to-end** mode there is no exit gateway: traffic stays Sphinx-encrypted the whole way. ## Proxy mode or end-to-end? -This page is about **proxy mode**: your traffic leaves the mixnet at an IPR exit and continues to a third-party server over clearnet, where the security trade-offs apply. +Most of this page is about **proxy mode**: your traffic leaves the mixnet at an Exit Gateway, via either the IP Packet Router (raw IP packets) or the SOCKS-based Network Requester (which sees the destination hostname), and continues to a third-party server over clearnet, where the security trade-offs apply. -If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no IPR, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript). +If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no exit gateway, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript). ## What each hop sees +**Proxy mode**: only your side runs Nym, and traffic exits to a third party: + ```text - you - │ Sphinx - ▼ - entry gateway - │ Sphinx - ▼ - 3 mix layers - │ Sphinx - ▼ - IPR exit gateway - │ plain IP (Sphinx removed here) - ▼ - destination +you → entry gateway → 3 mix layers → exit gateway → destination +└──────── Sphinx-encrypted ────────┘ └── clearnet ──┘ + exit gateway strips Sphinx here ``` +**End-to-end**: both sides run Nym, and traffic never leaves the mixnet: + +```text +you → entry gateway → 3 mix layers → entry gateway → peer Nym client +└─────────────── Sphinx-encrypted the whole way ───────────────┘ +``` + +The table below is the proxy-mode path. In end-to-end mode there is no exit and no clearnet hop, so only the first two rows apply and the far end is another Nym client rather than a remote host. + | 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 gateway + 3 mix layers) | Sphinx (layered) | Each node only knows its previous and 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; your own TLS, if any, still applies) | Remote host sees the IPR's IP, not yours | +| Exit gateway (IPR or NR) | Sphinx removed; raw IP packet (IPR) or SOCKS request (NR) exposed | The IPR sees the destination IP and port; the NR sees the destination hostname. Either way the payload depends on your application layer (see below). | +| Exit gateway → remote host | None (Sphinx is mixnet-only; your own TLS, if any, still applies) | Remote host sees the exit gateway'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. +The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. In proxy mode the exit gateway strips the Sphinx layers and forwards the request to the destination, analogous to how a Tor exit node or VPN endpoint unwraps its tunnel. In end-to-end mode the Sphinx layers are only removed at the receiving Nym client, so nothing is ever exposed on clearnet. ## Encrypt your own payload -Because the IPR removes the Sphinx layers, whatever is inside that IP packet is visible to the exit unless you encrypted it yourself. +Because the exit gateway removes the Sphinx layers, whatever is inside is visible to the exit unless you encrypted it yourself. -- **Application-layer encryption closes the gap.** TLS, the Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the IPR. It still sees the destination IP and port, but not the content. Over TLS the IPR only ever handles ciphertext bound for that destination; the bytes inside stay opaque to it. +- **Application-layer encryption closes the gap.** TLS, the Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the exit. It still sees the destination, but not the content. Over TLS the exit only ever handles ciphertext bound for that destination; the bytes inside stay opaque to it. - **Unencrypted payloads are fully visible.** Plain HTTP, unencrypted WebSocket (`ws://`), and plain UDP DNS are readable in full at the exit. The mixnet still hides your identity, so the exit reads the content without being able to attribute it to you. ## Trust boundaries - You trust the mixnet to provide unlinkability between sender and receiver. Sphinx provides this cryptographically at the per-packet level: a node cannot read addressing beyond its own hop. Unlinkability of your *traffic pattern* over time is weaker, and statistical rather than absolute. It comes from mixing and cover traffic, and degrades with low network traffic, with cover traffic or Poisson timing disabled, and against an adversary that can observe a large fraction of the network. -- 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). +- You trust the Exit Gateway (whether IPR or Network Requester) in the same way you trust a VPN exit or Tor exit node: it can inspect whatever leaves the mixnet (raw IP packets for the IPR, the SOCKS request for the NR). The difference is that the exit doesn't know who is sending the traffic (the mixnet hides your identity). -Treat the IPR exactly as you would a VPN exit or a Tor exit node: it can inspect your raw IP packets. The difference Nym adds is that the IPR doesn't know who's sending the traffic. Protect the payload with TLS or equivalent, and pin a trusted exit (`preferredIpr`) if the exit operator matters to you. +Treat the Exit Gateway exactly as you would a VPN exit or a Tor exit node: it can inspect whatever you send past it. The difference Nym adds is that the exit doesn't know who's sending the traffic. Protect the payload with TLS or equivalent, and pin a trusted exit (`preferredIpr`) if the exit operator matters to you. ## What the mixnet does not protect @@ -91,7 +92,5 @@ The timing-analysis rating assumes the defaults. Cover traffic and Poisson timin ## Read more -The package pages add the parts specific to their transport (where TLS terminates, what the resolver sees, WSS vs `ws://`): - -- [Exit Gateway Services](/network/infrastructure/exit-services#ip-packet-router): how the IPR allocates addresses and routes raw IP packets, and how it differs from the SOCKS-based Network Requester. -- The per-package "Security model" section on [mix-fetch](/developers/mix-fetch/concepts#security-model), [mix-dns](/developers/mix-dns/guides#security-model), and [mix-websocket](/developers/mix-websocket/concepts#security-model) for the transport-specific exposure. +- [Exit Gateway Services](/network/infrastructure/exit-services): how the SOCKS-based Network Requester and the IP Packet Router each route traffic past the exit, and how they differ. +- Per-transport exposure (where TLS terminates, what the resolver sees, `wss://` vs `ws://`) lives on each package's own page: [smolmix](/developers/smolmix), the [SOCKS5 module](/developers/rust/socks5), [mix-fetch](/developers/mix-fetch), [mix-dns](/developers/mix-dns), and [mix-websocket](/developers/mix-websocket). diff --git a/documentation/docs/pages/developers/rust/socks5.mdx b/documentation/docs/pages/developers/rust/socks5.mdx index edfa0950784..b8e3d693792 100644 --- a/documentation/docs/pages/developers/rust/socks5.mdx +++ b/documentation/docs/pages/developers/rust/socks5.mdx @@ -10,10 +10,10 @@ lastUpdated: "2026-06-29" import { Callout } from 'nextra/components' -The `socks5` module provides [`Socks5MixnetClient`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html): a local SOCKS5 proxy that routes any SOCKS4, SOCKS4a, or SOCKS5-capable application's traffic through the mixnet. Your application connects to a normal-looking SOCKS5 proxy on `localhost`; the client forwards that traffic over the mixnet to a **network requester** running on an Exit Gateway, which makes the real request on your behalf. +The `socks5` module provides [`Socks5MixnetClient`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html): a local SOCKS5 proxy that routes any SOCKS4, SOCKS4a, or SOCKS5-capable application's traffic through the mixnet. Your application connects to a normal-looking SOCKS5 proxy on `localhost`; the client forwards that traffic over the mixnet to a [**network requester**](/network/infrastructure/exit-services#network-requester) running on an Exit Gateway, which makes the real request on your behalf. -This is a **proxy-mode** integration, not end-to-end. Unlike the [Mixnet](./mixnet) and [Stream](./stream) modules (where both sides run a Nym client), traffic here leaves the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS) is your application's job. See [Exit security](/developers/concepts/exit-security) for what the exit can and cannot observe. +This is a **proxy-mode** integration, not end-to-end. Unlike the [Mixnet](./mixnet) and [Stream](./stream) modules (where both sides run a Nym client), traffic here leaves the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS) is your application's job. See [Exit security](/developers/concepts/exit-security) for the full model: what each hop sees, the trust boundaries, and how it compares with Tor and VPNs. ## How it works From 70a58d9787898823c7ae74d2a54927cce1999a81 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Mon, 29 Jun 2026 09:15:41 +0100 Subject: [PATCH 03/13] Coderabbit --- .../developers/concepts/exit-security.mdx | 9 +++-- .../nym-sdk/src/mixnet/socks5_discovery.rs | 36 +++++++++++++------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/documentation/docs/pages/developers/concepts/exit-security.mdx b/documentation/docs/pages/developers/concepts/exit-security.mdx index 0bfdec611cc..2d0c94d524f 100644 --- a/documentation/docs/pages/developers/concepts/exit-security.mdx +++ b/documentation/docs/pages/developers/concepts/exit-security.mdx @@ -60,10 +60,15 @@ Because the exit gateway removes the Sphinx layers, whatever is inside is visibl ## Trust boundaries - You trust the mixnet to provide unlinkability between sender and receiver. Sphinx provides this cryptographically at the per-packet level: a node cannot read addressing beyond its own hop. Unlinkability of your *traffic pattern* over time is weaker, and statistical rather than absolute. It comes from mixing and cover traffic, and degrades with low network traffic, with cover traffic or Poisson timing disabled, and against an adversary that can observe a large fraction of the network. -- You trust the Exit Gateway (whether IPR or Network Requester) in the same way you trust a VPN exit or Tor exit node: it can inspect whatever leaves the mixnet (raw IP packets for the IPR, the SOCKS request for the NR). The difference is that the exit doesn't know who is sending the traffic (the mixnet hides your identity). +- You trust the Exit Gateway in the same way you trust a VPN exit or Tor exit node: it can inspect whatever leaves the mixnet, but it does not know who is sending the traffic (the mixnet hides your identity). What it can inspect, and the shape of the trust, differs between the two exit types: + - **IP Packet Router (IPR).** A packet forwarder, like a VPN exit. It receives raw IP packets, so it sees the destination IP and port and reads any packet payload you did not encrypt. It works at the network layer and does not parse your application protocol. This is the exit used by `smolmix` and the `mix-*` packages ([`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). + - **Network Requester (NR).** A SOCKS proxy that makes the request on your behalf, so your trust in it is exactly the trust you place in any SOCKS5 proxy. With the `socks5h` URL the SDK hands it the destination *hostname* (not just an IP) plus the byte stream, and the NR opens the TCP connection itself and relays bytes back. It therefore sees the destination hostname and port, and any payload you did not encrypt, just as a SOCKS proxy you ran your traffic through would. This is the exit used by the [SOCKS5 module](/developers/rust/socks5) and the standalone [`nym-socks5-client`](/developers/clients/socks5). -Treat the Exit Gateway exactly as you would a VPN exit or a Tor exit node: it can inspect whatever you send past it. The difference Nym adds is that the exit doesn't know who's sending the traffic. Protect the payload with TLS or equivalent, and pin a trusted exit (`preferredIpr`) if the exit operator matters to you. +Treat the Exit Gateway exactly as you would a VPN exit or a Tor exit node: it can inspect whatever you send past it. The difference Nym adds is that the exit doesn't know who's sending the traffic. Protect the payload with TLS or equivalent. If the exit operator matters to you, pin one, but the control depends on the exit type: + +- **IPR-backed clients** (`smolmix` and the `mix-*` packages): set the preferred IPR (`preferredIpr` in the TypeScript and wasm packages; see each package page for the native equivalent). +- **SOCKS5 module (network requester):** pass a fixed requester address to `Socks5MixnetClient::connect_new(...)`, or constrain the jurisdiction with the discovery builder (`discover().countries([...])`). ## What the mixnet does not protect diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs index e8dfaa00421..5428fd68554 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs @@ -20,7 +20,7 @@ use nym_crypto::asymmetric::ed25519; use nym_sphinx::addressing::clients::Recipient; use nym_validator_client::nym_api::NymApiClientExt; use rand::seq::SliceRandom; -use tracing::info; +use tracing::{debug, info, warn}; use crate::Error; @@ -53,18 +53,34 @@ pub async fn retrieve_network_requesters_with_performance( for exit in exit_gateways { let Some(node) = all_nodes.get(&exit.ed25519_identity_pubkey) else { + // The skimmed and described sets come from two separate API calls + // and can be momentarily out of sync, so a node present in one but + // not the other is expected churn rather than an error; log at debug. + debug!( + "{} has no described-node record; skipping", + exit.ed25519_identity_pubkey + ); continue; }; - if let Some(nr_info) = node.description.network_requester.clone() { - if let Ok(parsed_address) = nr_info.address.parse() { - requesters.push(NetworkRequesterWithPerformance { - address: parsed_address, - identity: exit.ed25519_identity_pubkey, - performance: exit.performance.round_to_integer(), - country: node.description.auxiliary_details.location, - }) - } + let Some(nr_info) = node.description.network_requester.clone() else { + continue; + }; + + match nr_info.address.parse() { + Ok(parsed_address) => requesters.push(NetworkRequesterWithPerformance { + address: parsed_address, + identity: exit.ed25519_identity_pubkey, + performance: exit.performance.round_to_integer(), + country: node.description.auxiliary_details.location, + }), + // A node that advertises a requester but with an unparseable address + // is malformed metadata. Drop it from the pool, but say which node + // and why rather than shrinking the pool silently. + Err(err) => warn!( + "{} advertises an unparseable network requester address {:?}: {err}; skipping", + exit.ed25519_identity_pubkey, nr_info.address + ), } } From ff0b6f08060ce2142df79a13e43c861acb944fee Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Mon, 29 Jun 2026 09:34:54 +0100 Subject: [PATCH 04/13] Update exit services reference with mix-fetch switch from NR to IPR --- .../docs/pages/network/infrastructure/exit-services.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/docs/pages/network/infrastructure/exit-services.mdx b/documentation/docs/pages/network/infrastructure/exit-services.mdx index 73b2c62c6ad..653bdbf6ecc 100644 --- a/documentation/docs/pages/network/infrastructure/exit-services.mdx +++ b/documentation/docs/pages/network/infrastructure/exit-services.mdx @@ -32,7 +32,7 @@ Because it operates at the application layer, the NR: - Can enforce allow/deny lists on destination hosts and ports - Sees the destination hostname and port, but not the contents if TLS is used -**Used by:** the [SDK's SOCKS client](/developers/rust/mixnet), [standalone SOCKS5 client](/developers/clients/socks5), and [mixFetch](/developers/mix-fetch) (which wraps SOCKS requests in a browser-friendly `fetch` API). +**Used by:** the [SDK's SOCKS5 module](/developers/rust/socks5) and the [standalone SOCKS5 client](/developers/clients/socks5). ## IP Packet Router @@ -59,7 +59,7 @@ Because it operates at the IP layer, the IPR: In both services, traffic between the Exit Gateway and the destination travels over the public internet, exactly as it would from any other server. The mixnet protects sender anonymity (the destination sees the gateway's IP, not yours), but does not encrypt the payload past the gateway. Use TLS or another application-layer cipher to protect payload confidentiality, just as you would on a direct connection. -**Used by:** [NymVPN anonymous mode](/network/dvpn-mode/protocol) (5-hop mixnet routing to the IPR), and [`smolmix`](/developers/smolmix) (programmatic `TcpStream`/`UdpSocket` access to the IPR via the Rust SDK). +**Used by:** [NymVPN anonymous mode](/network/dvpn-mode/protocol) (5-hop mixnet routing to the IPR), [`smolmix`](/developers/smolmix) (programmatic `TcpStream`/`UdpSocket` access to the IPR via the Rust SDK), and the browser `mix-*` packages built on `smolmix`: [`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket). ## Comparison @@ -70,7 +70,7 @@ In both services, traffic between the Exit Gateway and the destination travels o | **DNS** | Resolved by the NR | Client resolves its own | | **Client gets** | Proxied connections | An allocated IP address | | **Connection model** | Per-request | Persistent tunnel | -| **Used by** | SDK SOCKS client, mixFetch | NymVPN (anonymous mode), smolmix | +| **Used by** | SDK SOCKS5 module, standalone SOCKS5 client | NymVPN (anonymous mode), smolmix, mix-* packages | ## Trust model From d78b28dcdc8875684edf346e59f1cccd9aff0abb Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Mon, 29 Jun 2026 12:39:27 +0100 Subject: [PATCH 05/13] Fix capitalisation --- .../docs/pages/developers/concepts/exit-security.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/documentation/docs/pages/developers/concepts/exit-security.mdx b/documentation/docs/pages/developers/concepts/exit-security.mdx index 2d0c94d524f..eb495b35981 100644 --- a/documentation/docs/pages/developers/concepts/exit-security.mdx +++ b/documentation/docs/pages/developers/concepts/exit-security.mdx @@ -14,13 +14,13 @@ Every tool that reaches an external service through the Nym mixnet shares the sa ## The one-sentence version -In **proxy mode** the mixnet hides **who** you are from the destination and **where** you're going from the network, but the exit gateway sees your **destination** and any payload you didn't encrypt yourself. In **end-to-end** mode there is no exit gateway: traffic stays Sphinx-encrypted the whole way. +In **proxy mode** the mixnet hides **who** you are from the destination and **where** you're going from the network, but the Exit Gateway sees your **destination** and any payload you didn't encrypt yourself. In **end-to-end** mode there is no Exit Gateway: traffic stays Sphinx-encrypted the whole way. ## Proxy mode or end-to-end? Most of this page is about **proxy mode**: your traffic leaves the mixnet at an Exit Gateway, via either the IP Packet Router (raw IP packets) or the SOCKS-based Network Requester (which sees the destination hostname), and continues to a third-party server over clearnet, where the security trade-offs apply. -If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no exit gateway, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript). +If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no Exit Gateway, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript). ## What each hop sees @@ -45,14 +45,14 @@ The table below is the proxy-mode path. In end-to-end mode there is no exit and |---|---|---| | Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination | | Inside the mixnet (entry gateway + 3 mix layers) | Sphinx (layered) | Each node only knows its previous and next hop | -| Exit gateway (IPR or NR) | Sphinx removed; raw IP packet (IPR) or SOCKS request (NR) exposed | The IPR sees the destination IP and port; the NR sees the destination hostname. Either way the payload depends on your application layer (see below). | -| Exit gateway → remote host | None (Sphinx is mixnet-only; your own TLS, if any, still applies) | Remote host sees the exit gateway's IP, not yours | +| Exit Gateway (IPR or NR) | Sphinx removed; raw IP packet (IPR) or SOCKS request (NR) exposed | The IPR sees the destination IP and port; the NR sees the destination hostname. Either way the payload depends on your application layer (see below). | +| Exit Gateway → remote host | None (Sphinx is mixnet-only; your own TLS, if any, still applies) | Remote host sees the Exit Gateway's IP, not yours | -The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. In proxy mode the exit gateway strips the Sphinx layers and forwards the request to the destination, analogous to how a Tor exit node or VPN endpoint unwraps its tunnel. In end-to-end mode the Sphinx layers are only removed at the receiving Nym client, so nothing is ever exposed on clearnet. +The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. In proxy mode the Exit Gateway strips the Sphinx layers and forwards the request to the destination, analogous to how a Tor exit node or VPN endpoint unwraps its tunnel. In end-to-end mode the Sphinx layers are only removed at the receiving Nym client, so nothing is ever exposed on clearnet. ## Encrypt your own payload -Because the exit gateway removes the Sphinx layers, whatever is inside is visible to the exit unless you encrypted it yourself. +Because the Exit Gateway removes the Sphinx layers, whatever is inside is visible to the exit unless you encrypted it yourself. - **Application-layer encryption closes the gap.** TLS, the Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the exit. It still sees the destination, but not the content. Over TLS the exit only ever handles ciphertext bound for that destination; the bytes inside stay opaque to it. - **Unencrypted payloads are fully visible.** Plain HTTP, unencrypted WebSocket (`ws://`), and plain UDP DNS are readable in full at the exit. The mixnet still hides your identity, so the exit reads the content without being able to attribute it to you. From bea3242bc00aea8f592bca43e23b35f80c191bf6 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Mon, 29 Jun 2026 14:49:39 +0100 Subject: [PATCH 06/13] First pass rework of structure + docs --- .../docs/pages/developers/rust/socks5.mdx | 28 ++-- .../nym-sdk/examples/socks5_autodiscover.rs | 21 +-- sdk/rust/nym-sdk/src/error.rs | 6 + sdk/rust/nym-sdk/src/mixnet.rs | 7 +- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 153 +++++------------- .../nym-sdk/src/mixnet/socks5_discovery.rs | 129 ++++++++++++--- 6 files changed, 175 insertions(+), 169 deletions(-) diff --git a/documentation/docs/pages/developers/rust/socks5.mdx b/documentation/docs/pages/developers/rust/socks5.mdx index b8e3d693792..d088a729b5e 100644 --- a/documentation/docs/pages/developers/rust/socks5.mdx +++ b/documentation/docs/pages/developers/rust/socks5.mdx @@ -81,32 +81,34 @@ let client = mixnet::MixnetClientBuilder::new_ephemeral() ## Automatic discovery -You do not have to hardcode a network requester address. The SDK can find one from the Nym API and connect in a single call: +You do not have to hardcode a network requester address. [`connect_with`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html) takes a `NetworkRequester` describing how to pick one plus an optional listener address, and connects in a single call. There are three ways to choose: ```rust -use nym_sdk::mixnet::Socks5MixnetClient; +use nym_sdk::mixnet::{NetworkRequester, Socks5MixnetClient}; + +// Any available requester, weighted by performance (None = default 127.0.0.1:1080): +let client = Socks5MixnetClient::connect_with(NetworkRequester::any(), None).await?; -// Pick any available network requester: -let client = Socks5MixnetClient::connect_new_with_discovery().await?; +// A specific requester you already know: +let client = Socks5MixnetClient::connect_with(NetworkRequester::exact("address...")?, None).await?; ``` -Discovery queries mainnet for Exit Gateways that advertise a network requester and selects one weighted by performance. +For `any`, discovery queries mainnet for Exit Gateways that advertise a network requester and selects one weighted by performance. ### Selecting by country -To constrain where your traffic leaves the mixnet, use the discovery builder and pass one or more [ISO 3166 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. The chosen requester must be physically located in one of them: +To constrain where your traffic leaves the mixnet, build the requester with [ISO 3166 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. The chosen requester must be physically located in one of them: ```rust -use nym_sdk::mixnet::Socks5MixnetClient; +use nym_sdk::mixnet::{NetworkRequester, Socks5MixnetClient}; -let client = Socks5MixnetClient::discover() - .countries(["CH", "DE"])? // Switzerland or Germany - .port(1081) // optional: bind off the default 1080 - .connect() - .await?; +let requester = NetworkRequester::in_countries(["CH", "DE"])?; // Switzerland or Germany + +// Listen on 127.0.0.1:1081 instead of the default 1080: +let client = Socks5MixnetClient::connect_with(requester, Some("127.0.0.1:1081".parse()?)).await?; ``` -Codes are case-insensitive and validated up front, so a typo fails immediately rather than at connect time. Pass a list to `.countries([...])`, or call `.country("CH")` repeatedly; several countries are treated as "any of these". Use `.port()` or `.bind_address()` to move the local listener off the default `127.0.0.1:1080`, for example to run more than one client at once. +Codes are case-insensitive and validated up front, so a typo fails immediately rather than at connect time. Several countries are treated as "any of these". Pass `Some(addr)` as the second argument to move the local listener off the default `127.0.0.1:1080`, for example when that port is taken or to run more than one client at once. A requester's location is **self-reported by its operator and optional**. Requesters that have not declared a location are excluded once you set a country filter, so a narrow filter can return `NoGatewayInCountries` even when requesters exist there but have not advertised it. Discovery does not silently fall back to another country: an empty result is an error, since routing through an unintended jurisdiction would defeat the point of asking. diff --git a/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs b/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs index 480fe9f1411..a394a2f46b8 100644 --- a/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs +++ b/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs @@ -8,22 +8,23 @@ //! //! Run with: cargo run --example socks5_autodiscover -use nym_sdk::mixnet::Socks5MixnetClient; +use nym_sdk::mixnet::{NetworkRequester, Socks5MixnetClient}; #[tokio::main] async fn main() -> Result<(), Box> { nym_bin_common::logging::setup_tracing_logger(); - // Discover a network requester in Switzerland or Germany and connect to it. - // Drop `.countries(...)` to accept any country. The SOCKS5 listener binds - // 127.0.0.1:1080 by default; `.port()` overrides it (1081 here to avoid - // colliding with any proxy already on 1080). + // Pick how to choose the requester. Three options: + // NetworkRequester::any() -> any, weighted by performance + // NetworkRequester::in_countries(["CH", "DE"])? -> restricted to those countries + // NetworkRequester::exact("address...")? -> a specific known requester + let requester = NetworkRequester::in_countries(["CH", "DE"])?; + + // Connect. `None` binds the SOCKS5 listener to 127.0.0.1:1080; here we pass + // 1081 to avoid colliding with any proxy already on the default port. println!("Discovering a network requester in CH/DE and connecting"); - let client = Socks5MixnetClient::discover() - .countries(["CH", "DE"])? - .port(1081) - .connect() - .await?; + let bind = Some("127.0.0.1:1081".parse()?); + let client = Socks5MixnetClient::connect_with(requester, bind).await?; println!("SOCKS5 proxy listening at {}", client.socks5_url()); // Point an HTTP client at the proxy and make a request through the mixnet. diff --git a/sdk/rust/nym-sdk/src/error.rs b/sdk/rust/nym-sdk/src/error.rs index 7537864d67e..98d992f8dde 100644 --- a/sdk/rust/nym-sdk/src/error.rs +++ b/sdk/rust/nym-sdk/src/error.rs @@ -145,6 +145,12 @@ pub enum Error { #[error("no available gateway in the requested countries")] NoGatewayInCountries, + #[error("no countries specified; use NetworkRequester::any() to accept any country")] + NoCountriesSpecified, + + #[error("invalid network requester address: {0}")] + InvalidRecipientAddress(String), + #[error("tunnel disconnected by IPR")] IprTunnelDisconnected, diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index 05e638ec007..0ddc8964e64 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -93,11 +93,8 @@ pub use native_client::MixnetClient; pub use native_client::MixnetClientSender; pub use paths::StoragePaths; pub use sink::{MixnetMessageSink, MixnetMessageSinkTranslator}; -pub use socks5_client::{Socks5DiscoveryBuilder, Socks5MixnetClient}; -pub use socks5_discovery::{ - get_best_network_requester, get_best_network_requester_in, - retrieve_network_requesters_with_performance, NetworkRequesterWithPerformance, -}; +pub use socks5_client::Socks5MixnetClient; +pub use socks5_discovery::NetworkRequester; pub use stream::{MixnetListener, MixnetStream, StreamId}; pub use traits::MixnetMessageSender; diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 99670835f78..1e923548328 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -1,4 +1,4 @@ -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::SocketAddr; use std::time::Duration; use nym_client_core::client::base_client::ClientState; @@ -8,13 +8,11 @@ use nym_task::connections::LaneQueueLengths; use nym_task::ShutdownTracker; use tokio::sync::RwLockReadGuard; -use celes::Country; use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; -use crate::ip_packet_client::discovery::create_nym_api_client; use crate::mixnet::client::MixnetClientBuilder; -use crate::mixnet::socks5_discovery::get_best_network_requester_in; -use crate::{Error, NymNetworkDetails, Result}; +use crate::mixnet::socks5_discovery::NetworkRequester; +use crate::Result; /// A SOCKS5 proxy client connected to the Nym mixnet. /// @@ -96,42 +94,52 @@ impl Socks5MixnetClient { .await } - /// Start building a client that connects to an automatically discovered - /// network requester. Restrict the requester's physical location with - /// [`country`](Socks5DiscoveryBuilder::country) / - /// [`countries`](Socks5DiscoveryBuilder::countries), then - /// [`connect`](Socks5DiscoveryBuilder::connect). + /// Create a new client and connect to a network requester chosen per the + /// given [`NetworkRequester`]: auto-discovered ([`Any`](NetworkRequester::Any)), + /// country-restricted ([`InCountries`](NetworkRequester::InCountries)), or a + /// known address ([`Exact`](NetworkRequester::Exact)). + /// + /// Discovery always targets mainnet. The discovered requester enforces the + /// Nym exit policy, so destinations outside that policy are refused at the + /// exit regardless of which requester is selected. + /// + /// `bind` sets the local SOCKS5 listener address; pass `None` for the default + /// `127.0.0.1:1080`, or `Some(addr)` to move it (for example when 1080 is + /// already taken, or to run more than one client at once). /// /// # Examples /// /// ```no_run - /// use nym_sdk::mixnet::Socks5MixnetClient; + /// use nym_sdk::mixnet::{NetworkRequester, Socks5MixnetClient}; /// /// #[tokio::main] /// async fn main() -> Result<(), Box> { - /// // Any country: - /// let any = Socks5MixnetClient::discover().connect().await?; + /// // Any requester, weighted by performance, on the default port: + /// let any = Socks5MixnetClient::connect_with(NetworkRequester::any(), None).await?; /// - /// // Pinned to Switzerland or Germany: - /// let pinned = Socks5MixnetClient::discover() - /// .countries(["CH", "DE"])? - /// .connect() - /// .await?; + /// // Pinned to Switzerland or Germany, listening on 127.0.0.1:1081: + /// let pinned = Socks5MixnetClient::connect_with( + /// NetworkRequester::in_countries(["CH", "DE"])?, + /// Some("127.0.0.1:1081".parse()?), + /// ) + /// .await?; /// Ok(()) /// } /// ``` - pub fn discover() -> Socks5DiscoveryBuilder { - Socks5DiscoveryBuilder::default() - } - - /// Create a new client and connect to an automatically discovered network - /// requester in any country. Shorthand for `discover().connect()`. - /// - /// Discovery always targets mainnet. The discovered requester enforces the - /// Nym exit policy, so destinations outside that policy will be refused at - /// the exit regardless of which requester is selected. - pub async fn connect_new_with_discovery() -> Result { - Self::discover().connect().await + pub async fn connect_with( + requester: NetworkRequester, + bind: Option, + ) -> Result { + let provider = requester.resolve().await?; + let mut socks5_config = Socks5::new(provider.to_string()); + if let Some(addr) = bind { + socks5_config.bind_address = addr; + } + MixnetClientBuilder::new_ephemeral() + .socks5_config(socks5_config) + .build()? + .connect_to_mixnet_via_socks5() + .await } /// Get the nym address for this client, if it is available. The nym address is composed of the @@ -195,88 +203,3 @@ impl Socks5MixnetClient { } } } - -/// Builder for connecting a [`Socks5MixnetClient`] to an automatically -/// discovered network requester, optionally restricted by country. -/// -/// Create one with [`Socks5MixnetClient::discover`]. With no country set, -/// discovery selects from any country; otherwise the chosen requester must be -/// physically located in one of the requested countries. -#[derive(Debug, Default, Clone)] -#[must_use] -pub struct Socks5DiscoveryBuilder { - countries: Vec, - bind_address: Option, -} - -impl Socks5DiscoveryBuilder { - /// Require the discovered network requester to be located in the given - /// country, identified by its ISO 3166 alpha-2 code (e.g. `"CH"`). - /// Case-insensitive. Call repeatedly to allow several countries. - /// - /// Returns [`Error::InvalidCountryCode`] if the code is not a valid alpha-2 - /// country code. - #[allow(clippy::result_large_err)] - pub fn country(mut self, code: impl AsRef) -> Result { - let country = Country::from_alpha2(code.as_ref()) - .map_err(|_| Error::InvalidCountryCode(code.as_ref().to_string()))?; - self.countries.push(country); - Ok(self) - } - - /// Require the discovered network requester to be located in one of the - /// given countries, each an ISO 3166 alpha-2 code. Case-insensitive. - /// - /// Returns [`Error::InvalidCountryCode`] on the first invalid code. - #[allow(clippy::result_large_err)] - pub fn countries(mut self, codes: I) -> Result - where - I: IntoIterator, - S: AsRef, - { - for code in codes { - self = self.country(code)?; - } - Ok(self) - } - - /// Bind the local SOCKS5 listener to a specific address instead of the - /// default `127.0.0.1:1080`. Set this to run more than one SOCKS5 client at - /// once, or to avoid a port already in use. - pub fn bind_address(mut self, address: SocketAddr) -> Self { - self.bind_address = Some(address); - self - } - - /// Bind the local SOCKS5 listener to `127.0.0.1:` instead of the - /// default port 1080. Shorthand for the loopback case of - /// [`bind_address`](Self::bind_address); this resets the host to loopback, - /// overriding any address previously set with `bind_address`. - pub fn port(mut self, port: u16) -> Self { - self.bind_address = Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)); - self - } - - /// Discover a matching network requester on mainnet and connect to it. - /// - /// If a country filter is set and no requester matches, returns - /// [`Error::NoGatewayInCountries`]. - pub async fn connect(self) -> Result { - let nym_api_urls = NymNetworkDetails::new_mainnet() - .nym_api_urls - .ok_or(Error::NoNymAPIUrl)?; - let api_client = create_nym_api_client(nym_api_urls)?; - let provider = get_best_network_requester_in(api_client, &self.countries).await?; - - let mut socks5_config = Socks5::new(provider.to_string()); - if let Some(bind_address) = self.bind_address { - socks5_config.bind_address = bind_address; - } - - MixnetClientBuilder::new_ephemeral() - .socks5_config(socks5_config) - .build()? - .connect_to_mixnet_via_socks5() - .await - } -} diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs index 5428fd68554..d012f93dae5 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs @@ -1,17 +1,20 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -//! Network requester discovery — find and rank network-requester-enabled exit -//! gateways via the Nym API, optionally restricted by physical country. +//! Choosing the network requester (the mixnet exit) a SOCKS5 client routes +//! through, and the directory query that backs auto-discovery. //! -//! This mirrors the IPR discovery in [`crate::ip_packet_client::discovery`]. -//! Exit gateways self-announce both an IPR and a network requester in the same -//! described-node payload (`NymNodeDataV2`), so the selection logic is the same; -//! only the field we read differs (`network_requester` instead of -//! `ip_packet_router`), and there is no protocol-version gate. +//! [`NetworkRequester`] is the public face: `Any`, `InCountries`, or `Exact`. +//! The directory crawl and performance weighting below are private detail that +//! [`NetworkRequester::resolve`] drives. //! -//! The node's physical location (`auxiliary_details.location`) rides along in -//! that same payload, so country filtering needs no extra requests. +//! Discovery mirrors the IPR discovery in [`crate::ip_packet_client::discovery`]: +//! exit gateways self-announce both an IPR and a network requester in the same +//! described-node payload (`NymNodeDataV2`), so the selection logic matches; only +//! the field we read differs (`network_requester` instead of `ip_packet_router`), +//! and there is no protocol-version gate. The node's physical location +//! (`auxiliary_details.location`) rides along in that same payload, so country +//! filtering needs no extra requests. use std::collections::HashMap; @@ -22,22 +25,103 @@ use nym_validator_client::nym_api::NymApiClientExt; use rand::seq::SliceRandom; use tracing::{debug, info, warn}; -use crate::Error; +use crate::ip_packet_client::discovery::create_nym_api_client; +use crate::{Error, NymNetworkDetails}; + +/// Which network requester (the mixnet exit that makes requests on the client's +/// behalf) a SOCKS5 client routes through. Three ways, increasing specificity. +#[derive(Debug, Clone, Default)] +pub enum NetworkRequester { + /// Auto-discover one from the directory, weighted by performance. (default) + #[default] + Any, + /// Auto-discover, restricted to requesters physically located in one of + /// these ISO 3166 alpha-2 countries (e.g. `["CH", "DE"]`). + InCountries(Vec), + /// A specific requester address you already know. + Exact(Box), +} + +impl NetworkRequester { + /// Any requester, weighted by performance. + pub fn any() -> Self { + Self::Any + } + + /// Restrict discovery to the given ISO 3166 alpha-2 country codes. + /// Case-insensitive. Returns [`Error::InvalidCountryCode`] on the first + /// code that is not a valid alpha-2 code, or [`Error::NoCountriesSpecified`] + /// if the list is empty (use [`any`](Self::any) to accept any country). + #[allow(clippy::result_large_err)] + pub fn in_countries(codes: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + let countries = codes + .into_iter() + .map(|c| { + Country::from_alpha2(c.as_ref()) + .map_err(|_| Error::InvalidCountryCode(c.as_ref().to_string())) + }) + .collect::, _>>()?; + + // An empty filter would resolve as "any country", silently ignoring the + // caller's intent to scope by location. Reject it so the mistake surfaces + // at the call site rather than as a surprising any-country pick. + if countries.is_empty() { + return Err(Error::NoCountriesSpecified); + } + + Ok(Self::InCountries(countries)) + } + + /// A specific requester by its Nym address. Returns + /// [`Error::InvalidRecipientAddress`] if the address does not parse. + #[allow(clippy::result_large_err)] + pub fn exact(address: impl AsRef) -> Result { + let recipient = address + .as_ref() + .parse() + .map_err(|_| Error::InvalidRecipientAddress(address.as_ref().to_string()))?; + Ok(Self::Exact(Box::new(recipient))) + } + + /// Resolve to a concrete requester address. `Exact` returns its address + /// directly; `Any` / `InCountries` query the mainnet directory and pick one + /// weighted by performance. + pub async fn resolve(&self) -> Result { + match self { + Self::Exact(addr) => Ok((**addr).clone()), + Self::Any => discover(&[]).await, + Self::InCountries(countries) => discover(countries).await, + } + } +} + +/// Query the mainnet directory for network requesters and pick one weighted by +/// performance, optionally restricted to `countries` (empty slice = any). +async fn discover(countries: &[Country]) -> Result { + let nym_api_urls = NymNetworkDetails::new_mainnet() + .nym_api_urls + .ok_or(Error::NoNymAPIUrl)?; + let client = create_nym_api_client(nym_api_urls)?; + get_best_network_requester_in(client, countries).await +} /// A network requester exit gateway and the metadata the directory reports for it. -#[derive(Clone)] -pub struct NetworkRequesterWithPerformance { - pub address: Recipient, - pub identity: ed25519::PublicKey, - pub performance: u8, +struct NetworkRequesterWithPerformance { + address: Recipient, + identity: ed25519::PublicKey, + performance: u8, /// Physical location the operator self-reported, if any. `None` means the /// operator did not declare a location, not that the node is unlocated. - pub country: Option, + country: Option, } /// Collect every exit gateway that advertises a network requester address, /// paired with its performance score and self-reported country. -pub async fn retrieve_network_requesters_with_performance( +async fn retrieve_network_requesters_with_performance( client: nym_http_api_client::Client, ) -> Result, Error> { let all_nodes = client @@ -87,13 +171,6 @@ pub async fn retrieve_network_requesters_with_performance( Ok(requesters) } -/// Select the best network requester from any country, weighted by performance. -pub async fn get_best_network_requester( - client: nym_http_api_client::Client, -) -> Result { - get_best_network_requester_in(client, &[]).await -} - /// Select a network requester weighted by performance, restricted to the given /// countries. An empty `countries` slice means any country is acceptable. /// @@ -101,7 +178,7 @@ pub async fn get_best_network_requester( /// filter is active: an undeclared exit cannot be assumed to be in a requested /// country. If the filter leaves no candidates, this returns /// [`Error::NoGatewayInCountries`] rather than silently falling back. -pub async fn get_best_network_requester_in( +async fn get_best_network_requester_in( client: nym_http_api_client::Client, countries: &[Country], ) -> Result { @@ -138,7 +215,7 @@ pub async fn get_best_network_requester_in( // Weight by performance. If every candidate scored zero (e.g. a low score // rounded down to 0), fall back to a uniform pick rather than failing as if - // no requester existed — the pool is non-empty here. + // no requester existed. The pool is non-empty here. let mut rng = rand::thread_rng(); let selected = pool .choose_weighted(&mut rng, |nr| nr.performance as f64) From 2a719eedf64d1b1521c5f1c95bdda80aae8f5932 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Tue, 30 Jun 2026 10:16:04 +0100 Subject: [PATCH 07/13] Restructure again: strip down --- sdk/rust/nym-sdk/examples/socks5.rs | 78 +++--- .../nym-sdk/examples/socks5_autodiscover.rs | 43 ---- sdk/rust/nym-sdk/src/mixnet.rs | 4 +- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 235 +++++++++++++++++- .../nym-sdk/src/mixnet/socks5_discovery.rs | 233 ----------------- 5 files changed, 267 insertions(+), 326 deletions(-) delete mode 100644 sdk/rust/nym-sdk/examples/socks5_autodiscover.rs delete mode 100644 sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs diff --git a/sdk/rust/nym-sdk/examples/socks5.rs b/sdk/rust/nym-sdk/examples/socks5.rs index 77e6952c92a..cd82e609033 100644 --- a/sdk/rust/nym-sdk/examples/socks5.rs +++ b/sdk/rust/nym-sdk/examples/socks5.rs @@ -1,54 +1,42 @@ -//! SOCKS5 proxy client that routes HTTP requests through the mixnet. +//! SOCKS5 proxy client that routes an HTTPS request through the mixnet. //! -//! Connects a `Socks5MixnetClient` to a receiving `MixnetClient` acting -//! as the service provider, then sends an HTTP GET via the SOCKS5 proxy. +//! Picks a network requester (the mixnet exit), starts a local SOCKS5 listener, +//! points an HTTP client at it, and makes a real request end to end. //! //! Run with: cargo run --example socks5 -use nym_sdk::mixnet; +use nym_sdk::mixnet::{NetworkRequester, Socks5MixnetClient}; #[tokio::main] -async fn main() { +async fn main() -> Result<(), Box> { nym_bin_common::logging::setup_tracing_logger(); - // Connect a receiving client (acts as the "network requester"). - println!("Connecting receiver"); - let mut receiving_client = mixnet::MixnetClient::connect_new().await.unwrap(); - - // Build and connect a SOCKS5 sending client pointed at the receiver. - let socks5_config = mixnet::Socks5::new(receiving_client.nym_address().to_string()); - let sending_client = mixnet::MixnetClientBuilder::new_ephemeral() - .socks5_config(socks5_config) - .build() - .unwrap(); - - println!("Connecting sender"); - let sending_client = sending_client.connect_to_mixnet_via_socks5().await.unwrap(); - - // Configure an HTTP client to use the SOCKS5 proxy. - let proxy = reqwest::Proxy::all(sending_client.socks5_url()).unwrap(); - let reqwest_client = reqwest::Client::builder().proxy(proxy).build().unwrap(); - - // Send an HTTP request through the mixnet via SOCKS5. - // No network requester is running on the other end, so we won't - // get a real HTTP response — but the receiver sees the raw bytes. - tokio::spawn(async move { - println!("Sending socks5-wrapped http request"); - reqwest_client.get("https://nymtech.net").send().await.ok() - }); - - // The receiver sees the raw SOCKS5/HTTP bytes arrive. - println!("Waiting for message"); - if let Some(received) = receiving_client.wait_for_messages().await { - for r in received { - println!( - "Received socks5 message requesting for endpoint: {}", - String::from_utf8_lossy(&r.message[10..27]) - ); - } - } - - // Disconnect both clients. - receiving_client.disconnect().await; - sending_client.disconnect().await; + // How to choose the requester. `any()` auto-discovers one from the directory, + // weighted by performance, and is the most robust default. The alternatives: + // NetworkRequester::in_countries(["CH", "DE"])? -> discovery pinned to those countries + // NetworkRequester::exact("address...")? -> a specific known requester + // If you already have the address, Socks5MixnetClient::connect_new("address...") + // is the one-line shorthand for the `exact` case. + let requester = NetworkRequester::any(); + + // Passing `None` binds the SOCKS5 listener to the default 127.0.0.1:1080. Pass Some(addr) + // to move it, for example when that port is taken or to run more than one client. + println!("Selecting a network requester and connecting"); + let client = + Socks5MixnetClient::connect_with(requester, Some("127.0.0.1:1081".parse()?)).await?; + println!("SOCKS5 proxy listening at {}", client.socks5_url()); + + // Point an HTTP client at the proxy and make a request through the mixnet. + let proxy = reqwest::Proxy::all(client.socks5_url())?; + let http = reqwest::Client::builder().proxy(proxy).build()?; + + // nymtech.net is on the default Nym exit policy. If you change this URL to a + // destination the exit policy does not allow, the request fails at the exit, + // which is not a discovery failure. + println!("Sending request through the mixnet"); + let status = http.get("https://nymtech.net").send().await?.status(); + println!("Got response status: {status}"); + + client.disconnect().await; + Ok(()) } diff --git a/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs b/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs deleted file mode 100644 index a394a2f46b8..00000000000 --- a/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! SOCKS5 proxy client that auto-discovers a network requester, optionally -//! pinned to a set of countries. -//! -//! Unlike `socks5.rs`, this takes no provider address. It queries the Nym API -//! for exit gateways advertising a network requester, optionally keeps only -//! those physically located in the requested countries, picks one weighted by -//! performance, and routes an HTTPS request through it. -//! -//! Run with: cargo run --example socks5_autodiscover - -use nym_sdk::mixnet::{NetworkRequester, Socks5MixnetClient}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - nym_bin_common::logging::setup_tracing_logger(); - - // Pick how to choose the requester. Three options: - // NetworkRequester::any() -> any, weighted by performance - // NetworkRequester::in_countries(["CH", "DE"])? -> restricted to those countries - // NetworkRequester::exact("address...")? -> a specific known requester - let requester = NetworkRequester::in_countries(["CH", "DE"])?; - - // Connect. `None` binds the SOCKS5 listener to 127.0.0.1:1080; here we pass - // 1081 to avoid colliding with any proxy already on the default port. - println!("Discovering a network requester in CH/DE and connecting"); - let bind = Some("127.0.0.1:1081".parse()?); - let client = Socks5MixnetClient::connect_with(requester, bind).await?; - println!("SOCKS5 proxy listening at {}", client.socks5_url()); - - // Point an HTTP client at the proxy and make a request through the mixnet. - let proxy = reqwest::Proxy::all(client.socks5_url())?; - let http = reqwest::Client::builder().proxy(proxy).build()?; - - // nymtech.net is on the default Nym exit policy. If you change this URL to a - // destination the exit policy does not allow, the request fails at the exit, - // which is not a discovery failure. - println!("Sending request through the mixnet"); - let status = http.get("https://nymtech.net").send().await?.status(); - println!("Got response status: {status}"); - - client.disconnect().await; - Ok(()) -} diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index 0ddc8964e64..72abb732bc2 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -82,7 +82,6 @@ mod native_client; mod paths; mod sink; mod socks5_client; -mod socks5_discovery; pub mod stream; mod traits; @@ -93,8 +92,7 @@ pub use native_client::MixnetClient; pub use native_client::MixnetClientSender; pub use paths::StoragePaths; pub use sink::{MixnetMessageSink, MixnetMessageSinkTranslator}; -pub use socks5_client::Socks5MixnetClient; -pub use socks5_discovery::NetworkRequester; +pub use socks5_client::{NetworkRequester, Socks5MixnetClient}; pub use stream::{MixnetListener, MixnetStream, StreamId}; pub use traits::MixnetMessageSender; diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 1e923548328..148ac1d3670 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -8,11 +8,11 @@ use nym_task::connections::LaneQueueLengths; use nym_task::ShutdownTracker; use tokio::sync::RwLockReadGuard; +use celes::Country; use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; use crate::mixnet::client::MixnetClientBuilder; -use crate::mixnet::socks5_discovery::NetworkRequester; -use crate::Result; +use crate::{Error, Result}; /// A SOCKS5 proxy client connected to the Nym mixnet. /// @@ -74,6 +74,10 @@ impl Socks5MixnetClient { /// Create a new client and connect to a service provider over the mixnet via SOCKS5 using /// ephemeral in-memory keys that are discarded at application close. /// + /// This is the zero-ceremony path when you already know the requester's + /// address; it is shorthand for [`connect_with`](Self::connect_with) with + /// [`NetworkRequester::exact`] and the default listener bind. + /// /// # Examples /// /// ```no_run @@ -203,3 +207,230 @@ impl Socks5MixnetClient { } } } + +/// Which network requester (the mixnet exit that makes requests on the client's +/// behalf) a SOCKS5 client routes through. Three ways, increasing specificity. +#[derive(Debug, Clone, Default)] +pub enum NetworkRequester { + /// Auto-discover one from the directory, weighted by performance. (default) + #[default] + Any, + /// Auto-discover, restricted to requesters physically located in one of + /// these ISO 3166 alpha-2 countries (e.g. `["CH", "DE"]`). + InCountries(Vec), + /// A specific requester address you already know. + Exact(Box), +} + +impl NetworkRequester { + /// Any requester, weighted by performance. + pub fn any() -> Self { + Self::Any + } + + /// Restrict discovery to the given ISO 3166 alpha-2 country codes. + /// Case-insensitive. Returns [`Error::InvalidCountryCode`] on the first + /// code that is not a valid alpha-2 code, or [`Error::NoCountriesSpecified`] + /// if the list is empty (use [`any`](Self::any) to accept any country). + #[allow(clippy::result_large_err)] + pub fn in_countries(codes: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + let countries = codes + .into_iter() + .map(|c| { + Country::from_alpha2(c.as_ref()) + .map_err(|_| Error::InvalidCountryCode(c.as_ref().to_string())) + }) + .collect::, _>>()?; + + // An empty filter would resolve as "any country", silently ignoring the + // caller's intent to scope by location. Reject it so the mistake surfaces + // at the call site rather than as a surprising any-country pick. + if countries.is_empty() { + return Err(Error::NoCountriesSpecified); + } + + Ok(Self::InCountries(countries)) + } + + /// A specific requester by its Nym address. Returns + /// [`Error::InvalidRecipientAddress`] if the address does not parse. + #[allow(clippy::result_large_err)] + pub fn exact(address: impl AsRef) -> Result { + let recipient = address + .as_ref() + .parse() + .map_err(|_| Error::InvalidRecipientAddress(address.as_ref().to_string()))?; + Ok(Self::Exact(Box::new(recipient))) + } + + /// Resolve to a concrete requester address. `Exact` returns its address + /// directly; `Any` / `InCountries` query the mainnet directory and pick one + /// weighted by performance. + pub async fn resolve(&self) -> Result { + match self { + Self::Exact(addr) => Ok((**addr).clone()), + Self::Any => discovery::discover(&[]).await, + Self::InCountries(countries) => discovery::discover(countries).await, + } + } +} + +/// Directory crawl backing [`NetworkRequester`] auto-discovery. +/// +/// This mirrors the IPR discovery in [`crate::ip_packet_client::discovery`]: +/// exit gateways self-announce both an IPR and a network requester in the same +/// described-node payload (`NymNodeDataV2`), so the selection logic matches; only +/// the field we read differs (`network_requester` instead of `ip_packet_router`), +/// and there is no protocol-version gate. The node's physical location +/// (`auxiliary_details.location`) rides along in that same payload, so country +/// filtering needs no extra requests. +mod discovery { + use std::collections::HashMap; + + use celes::Country; + use nym_crypto::asymmetric::ed25519; + use nym_sphinx::addressing::clients::Recipient; + use nym_validator_client::nym_api::NymApiClientExt; + use rand::seq::SliceRandom; + use tracing::{debug, info, warn}; + + use crate::ip_packet_client::discovery::create_nym_api_client; + use crate::{Error, NymNetworkDetails}; + + /// Query the mainnet directory for network requesters and pick one weighted by + /// performance, optionally restricted to `countries` (empty slice = any). + pub(super) async fn discover(countries: &[Country]) -> Result { + let nym_api_urls = NymNetworkDetails::new_mainnet() + .nym_api_urls + .ok_or(Error::NoNymAPIUrl)?; + let client = create_nym_api_client(nym_api_urls)?; + get_best_network_requester_in(client, countries).await + } + + /// A network requester exit gateway and the metadata the directory reports for it. + struct NetworkRequesterWithPerformance { + address: Recipient, + identity: ed25519::PublicKey, + performance: u8, + /// Physical location the operator self-reported, if any. `None` means the + /// operator did not declare a location, not that the node is unlocated. + country: Option, + } + + /// Collect every exit gateway that advertises a network requester address, + /// paired with its performance score and self-reported country. + async fn retrieve_network_requesters_with_performance( + client: nym_http_api_client::Client, + ) -> Result, Error> { + let all_nodes = client + .get_all_described_nodes_v2() + .await? + .into_iter() + .map(|described| (described.ed25519_identity_key(), described)) + .collect::>(); + + let exit_gateways = client.get_all_basic_nodes_with_metadata().await?.nodes; + + let mut requesters = Vec::new(); + + for exit in exit_gateways { + let Some(node) = all_nodes.get(&exit.ed25519_identity_pubkey) else { + // The skimmed and described sets come from two separate API calls + // and can be momentarily out of sync, so a node present in one but + // not the other is expected churn rather than an error; log at debug. + debug!( + "{} has no described-node record; skipping", + exit.ed25519_identity_pubkey + ); + continue; + }; + + let Some(nr_info) = node.description.network_requester.clone() else { + continue; + }; + + match nr_info.address.parse() { + Ok(parsed_address) => requesters.push(NetworkRequesterWithPerformance { + address: parsed_address, + identity: exit.ed25519_identity_pubkey, + performance: exit.performance.round_to_integer(), + country: node.description.auxiliary_details.location, + }), + // A node that advertises a requester but with an unparseable address + // is malformed metadata. Drop it from the pool, but say which node + // and why rather than shrinking the pool silently. + Err(err) => warn!( + "{} advertises an unparseable network requester address {:?}: {err}; skipping", + exit.ed25519_identity_pubkey, nr_info.address + ), + } + } + + Ok(requesters) + } + + /// Select a network requester weighted by performance, restricted to the given + /// countries. An empty `countries` slice means any country is acceptable. + /// + /// Requesters that did not declare a location are excluded whenever a country + /// filter is active: an undeclared exit cannot be assumed to be in a requested + /// country. If the filter leaves no candidates, this returns + /// [`Error::NoGatewayInCountries`] rather than silently falling back. + async fn get_best_network_requester_in( + client: nym_http_api_client::Client, + countries: &[Country], + ) -> Result { + let requesters = retrieve_network_requesters_with_performance(client).await?; + let total = requesters.len(); + + let pool: Vec = if countries.is_empty() { + requesters + } else { + requesters + .into_iter() + .filter(|nr| match nr.country { + Some(c) => countries + .iter() + .any(|want| want.alpha2.eq_ignore_ascii_case(c.alpha2)), + None => false, + }) + .collect() + }; + + info!( + "Found {} network requesters ({} after country filter)", + total, + pool.len() + ); + + if pool.is_empty() { + return Err(if countries.is_empty() { + Error::NoGatewayAvailable + } else { + Error::NoGatewayInCountries + }); + } + + // Weight by performance. If every candidate scored zero (e.g. a low score + // rounded down to 0), fall back to a uniform pick rather than failing as if + // no requester existed. The pool is non-empty here. + let mut rng = rand::thread_rng(); + let selected = pool + .choose_weighted(&mut rng, |nr| nr.performance as f64) + .or_else(|_| pool.choose(&mut rng).ok_or(Error::NoGatewayAvailable))?; + + info!( + "Using network requester: {} (Gateway: {}, Country: {:?}, Performance: {:?})", + selected.address, + selected.identity, + selected.country.map(|c| c.alpha2), + selected.performance + ); + + Ok(selected.address) + } +} diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs deleted file mode 100644 index d012f93dae5..00000000000 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_discovery.rs +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2026 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -//! Choosing the network requester (the mixnet exit) a SOCKS5 client routes -//! through, and the directory query that backs auto-discovery. -//! -//! [`NetworkRequester`] is the public face: `Any`, `InCountries`, or `Exact`. -//! The directory crawl and performance weighting below are private detail that -//! [`NetworkRequester::resolve`] drives. -//! -//! Discovery mirrors the IPR discovery in [`crate::ip_packet_client::discovery`]: -//! exit gateways self-announce both an IPR and a network requester in the same -//! described-node payload (`NymNodeDataV2`), so the selection logic matches; only -//! the field we read differs (`network_requester` instead of `ip_packet_router`), -//! and there is no protocol-version gate. The node's physical location -//! (`auxiliary_details.location`) rides along in that same payload, so country -//! filtering needs no extra requests. - -use std::collections::HashMap; - -use celes::Country; -use nym_crypto::asymmetric::ed25519; -use nym_sphinx::addressing::clients::Recipient; -use nym_validator_client::nym_api::NymApiClientExt; -use rand::seq::SliceRandom; -use tracing::{debug, info, warn}; - -use crate::ip_packet_client::discovery::create_nym_api_client; -use crate::{Error, NymNetworkDetails}; - -/// Which network requester (the mixnet exit that makes requests on the client's -/// behalf) a SOCKS5 client routes through. Three ways, increasing specificity. -#[derive(Debug, Clone, Default)] -pub enum NetworkRequester { - /// Auto-discover one from the directory, weighted by performance. (default) - #[default] - Any, - /// Auto-discover, restricted to requesters physically located in one of - /// these ISO 3166 alpha-2 countries (e.g. `["CH", "DE"]`). - InCountries(Vec), - /// A specific requester address you already know. - Exact(Box), -} - -impl NetworkRequester { - /// Any requester, weighted by performance. - pub fn any() -> Self { - Self::Any - } - - /// Restrict discovery to the given ISO 3166 alpha-2 country codes. - /// Case-insensitive. Returns [`Error::InvalidCountryCode`] on the first - /// code that is not a valid alpha-2 code, or [`Error::NoCountriesSpecified`] - /// if the list is empty (use [`any`](Self::any) to accept any country). - #[allow(clippy::result_large_err)] - pub fn in_countries(codes: I) -> Result - where - I: IntoIterator, - S: AsRef, - { - let countries = codes - .into_iter() - .map(|c| { - Country::from_alpha2(c.as_ref()) - .map_err(|_| Error::InvalidCountryCode(c.as_ref().to_string())) - }) - .collect::, _>>()?; - - // An empty filter would resolve as "any country", silently ignoring the - // caller's intent to scope by location. Reject it so the mistake surfaces - // at the call site rather than as a surprising any-country pick. - if countries.is_empty() { - return Err(Error::NoCountriesSpecified); - } - - Ok(Self::InCountries(countries)) - } - - /// A specific requester by its Nym address. Returns - /// [`Error::InvalidRecipientAddress`] if the address does not parse. - #[allow(clippy::result_large_err)] - pub fn exact(address: impl AsRef) -> Result { - let recipient = address - .as_ref() - .parse() - .map_err(|_| Error::InvalidRecipientAddress(address.as_ref().to_string()))?; - Ok(Self::Exact(Box::new(recipient))) - } - - /// Resolve to a concrete requester address. `Exact` returns its address - /// directly; `Any` / `InCountries` query the mainnet directory and pick one - /// weighted by performance. - pub async fn resolve(&self) -> Result { - match self { - Self::Exact(addr) => Ok((**addr).clone()), - Self::Any => discover(&[]).await, - Self::InCountries(countries) => discover(countries).await, - } - } -} - -/// Query the mainnet directory for network requesters and pick one weighted by -/// performance, optionally restricted to `countries` (empty slice = any). -async fn discover(countries: &[Country]) -> Result { - let nym_api_urls = NymNetworkDetails::new_mainnet() - .nym_api_urls - .ok_or(Error::NoNymAPIUrl)?; - let client = create_nym_api_client(nym_api_urls)?; - get_best_network_requester_in(client, countries).await -} - -/// A network requester exit gateway and the metadata the directory reports for it. -struct NetworkRequesterWithPerformance { - address: Recipient, - identity: ed25519::PublicKey, - performance: u8, - /// Physical location the operator self-reported, if any. `None` means the - /// operator did not declare a location, not that the node is unlocated. - country: Option, -} - -/// Collect every exit gateway that advertises a network requester address, -/// paired with its performance score and self-reported country. -async fn retrieve_network_requesters_with_performance( - client: nym_http_api_client::Client, -) -> Result, Error> { - let all_nodes = client - .get_all_described_nodes_v2() - .await? - .into_iter() - .map(|described| (described.ed25519_identity_key(), described)) - .collect::>(); - - let exit_gateways = client.get_all_basic_nodes_with_metadata().await?.nodes; - - let mut requesters = Vec::new(); - - for exit in exit_gateways { - let Some(node) = all_nodes.get(&exit.ed25519_identity_pubkey) else { - // The skimmed and described sets come from two separate API calls - // and can be momentarily out of sync, so a node present in one but - // not the other is expected churn rather than an error; log at debug. - debug!( - "{} has no described-node record; skipping", - exit.ed25519_identity_pubkey - ); - continue; - }; - - let Some(nr_info) = node.description.network_requester.clone() else { - continue; - }; - - match nr_info.address.parse() { - Ok(parsed_address) => requesters.push(NetworkRequesterWithPerformance { - address: parsed_address, - identity: exit.ed25519_identity_pubkey, - performance: exit.performance.round_to_integer(), - country: node.description.auxiliary_details.location, - }), - // A node that advertises a requester but with an unparseable address - // is malformed metadata. Drop it from the pool, but say which node - // and why rather than shrinking the pool silently. - Err(err) => warn!( - "{} advertises an unparseable network requester address {:?}: {err}; skipping", - exit.ed25519_identity_pubkey, nr_info.address - ), - } - } - - Ok(requesters) -} - -/// Select a network requester weighted by performance, restricted to the given -/// countries. An empty `countries` slice means any country is acceptable. -/// -/// Requesters that did not declare a location are excluded whenever a country -/// filter is active: an undeclared exit cannot be assumed to be in a requested -/// country. If the filter leaves no candidates, this returns -/// [`Error::NoGatewayInCountries`] rather than silently falling back. -async fn get_best_network_requester_in( - client: nym_http_api_client::Client, - countries: &[Country], -) -> Result { - let requesters = retrieve_network_requesters_with_performance(client).await?; - let total = requesters.len(); - - let pool: Vec = if countries.is_empty() { - requesters - } else { - requesters - .into_iter() - .filter(|nr| match nr.country { - Some(c) => countries - .iter() - .any(|want| want.alpha2.eq_ignore_ascii_case(c.alpha2)), - None => false, - }) - .collect() - }; - - info!( - "Found {} network requesters ({} after country filter)", - total, - pool.len() - ); - - if pool.is_empty() { - return Err(if countries.is_empty() { - Error::NoGatewayAvailable - } else { - Error::NoGatewayInCountries - }); - } - - // Weight by performance. If every candidate scored zero (e.g. a low score - // rounded down to 0), fall back to a uniform pick rather than failing as if - // no requester existed. The pool is non-empty here. - let mut rng = rand::thread_rng(); - let selected = pool - .choose_weighted(&mut rng, |nr| nr.performance as f64) - .or_else(|_| pool.choose(&mut rng).ok_or(Error::NoGatewayAvailable))?; - - info!( - "Using network requester: {} (Gateway: {}, Country: {:?}, Performance: {:?})", - selected.address, - selected.identity, - selected.country.map(|c| c.alpha2), - selected.performance - ); - - Ok(selected.address) -} From e900fb33d3b1b4c9478c91f0d050a2e276459aff Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Tue, 30 Jun 2026 10:16:27 +0100 Subject: [PATCH 08/13] Remove doubled examples --- clients/socks5/src/main.rs | 1 + documentation/docs/pages/developers/rust/socks5.mdx | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs index 36161d3dcdf..d8bb33ffda1 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -19,6 +19,7 @@ async fn main() -> Result<(), Box> { if !args.no_banner { maybe_print_banner(crate_name!(), crate_version!()); } + setup_tracing_logger(); if let Err(err) = commands::execute(args).await { diff --git a/documentation/docs/pages/developers/rust/socks5.mdx b/documentation/docs/pages/developers/rust/socks5.mdx index d088a729b5e..d3ddcaea018 100644 --- a/documentation/docs/pages/developers/rust/socks5.mdx +++ b/documentation/docs/pages/developers/rust/socks5.mdx @@ -126,7 +126,6 @@ The same proxy logic ships two ways. They are interchangeable on the wire; choos ## Further reading - [API reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html): all methods, configuration, and types -- [Example: fixed provider](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/socks5.rs): connect to a known network requester -- [Example: autodiscovery](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/socks5_autodiscover.rs): discover a requester by country and proxy a request +- [Example: SOCKS5 proxy](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/socks5.rs): select a requester (auto-discover, pin by country, or a known address) and proxy a request - [Standalone SOCKS5 client](/developers/clients/socks5): the language-agnostic binary form - [Exit security](/developers/concepts/exit-security): what an Exit Gateway can observe in proxy mode From de5aaa07b901cf8f6a6ebe067b0fc87156f67ff2 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Tue, 30 Jun 2026 12:40:30 +0100 Subject: [PATCH 09/13] Redo example + code comments --- sdk/rust/nym-sdk/examples/socks5.rs | 7 ++--- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 29 +++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/sdk/rust/nym-sdk/examples/socks5.rs b/sdk/rust/nym-sdk/examples/socks5.rs index cd82e609033..3f316c1249e 100644 --- a/sdk/rust/nym-sdk/examples/socks5.rs +++ b/sdk/rust/nym-sdk/examples/socks5.rs @@ -13,10 +13,9 @@ async fn main() -> Result<(), Box> { // How to choose the requester. `any()` auto-discovers one from the directory, // weighted by performance, and is the most robust default. The alternatives: - // NetworkRequester::in_countries(["CH", "DE"])? -> discovery pinned to those countries - // NetworkRequester::exact("address...")? -> a specific known requester - // If you already have the address, Socks5MixnetClient::connect_new("address...") - // is the one-line shorthand for the `exact` case. + // NetworkRequester::in_countries(["CH", "DE"])? -> discovery pinned to those countries + // NetworkRequester::exact(Entztfv6Uaz2hpYHQJ6JKoaCTpDL5dja18SuQWVJAmmx.Cvhn9rBJw5Ay9wgHcbgCnVg89MPSV5s2muPV2YF1BXYu@Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf)? -> a specific known requester + // If you already have the address, Socks5MixnetClient::connect_new(Entztfv6Uaz2hpYHQJ6JKoaCTpDL5dja18SuQWVJAmmx.Cvhn9rBJw5Ay9wgHcbgCnVg89MPSV5s2muPV2YF1BXYu@Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf) is the one-line shorthand for the `exact` case. let requester = NetworkRequester::any(); // Passing `None` binds the SOCKS5 listener to the default 127.0.0.1:1080. Pass Some(addr) diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 148ac1d3670..1f1d9c0314f 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -18,13 +18,21 @@ use crate::{Error, Result}; /// /// `Socks5MixnetClient` provides a SOCKS5 proxy interface to the Nym mixnet, /// allowing HTTP(S) clients and other SOCKS5-compatible applications to route -/// their traffic through the mixnet for enhanced privacy. +/// their traffic through the mixnet without having to modify their networking +/// code. +/// +/// Traffic leaves the mixnet through a network requester: a service running on +/// an Exit Gateway that makes requests on the client's behalf and enforces the +/// Nym exit policy. You can let the client discover one for you or name a specific +/// one; see [`connect_with`](Self::connect_with) and [`NetworkRequester`]. /// /// ## Usage /// -/// 1. Connect to a service provider via [`connect_new`](Self::connect_new) +/// 1. Connect, either by discovering a requester with +/// [`connect_with`](Self::connect_with) or naming a known one with +/// [`connect_new`](Self::connect_new) /// 2. Get the SOCKS5 URL via [`socks5_url`](Self::socks5_url) -/// 3. Configure your HTTP client to use this SOCKS5 proxy +/// 3. Point your HTTP client at that SOCKS5 proxy /// /// ## Example /// @@ -33,7 +41,7 @@ use crate::{Error, Result}; /// /// #[tokio::main] /// async fn main() -> Result<(), Box> { -/// // Connect to a network requester service provider +/// // Connect to a known network requester by address /// let client = Socks5MixnetClient::connect_new("provider_nym_address...").await?; /// /// // Get the SOCKS5 proxy URL @@ -49,12 +57,7 @@ use crate::{Error, Result}; /// Ok(()) /// } /// ``` -/// -/// ## Service Providers -/// -/// The SOCKS5 client connects to a "network requester" service provider that -/// makes HTTP requests on behalf of the client. The service provider's Nym -/// address must be provided when creating the client. + pub struct Socks5MixnetClient { /// The nym address of this connected client. pub(crate) nym_address: Recipient, @@ -63,7 +66,7 @@ pub struct Socks5MixnetClient { /// current message send queue length. pub(crate) client_state: ClientState, - /// The task manager that controls all the spawned tasks that the clients uses to do it's job. + /// The task manager controlling all the spawned tasks the client uses to do its job. pub(crate) task_handle: ShutdownTracker, /// SOCKS5 configuration parameters. @@ -71,7 +74,7 @@ pub struct Socks5MixnetClient { } impl Socks5MixnetClient { - /// Create a new client and connect to a service provider over the mixnet via SOCKS5 using + /// Create a new client and connect to a network requester over the mixnet via SOCKS5 using /// ephemeral in-memory keys that are discarded at application close. /// /// This is the zero-ceremony path when you already know the requester's @@ -146,7 +149,7 @@ impl Socks5MixnetClient { .await } - /// Get the nym address for this client, if it is available. The nym address is composed of the + /// Get the nym address of this client. The nym address is composed of the /// client identity, the client encryption key, and the gateway identity. pub fn nym_address(&self) -> &Recipient { &self.nym_address From 71866435c3c6ec4fd4804c8ec7263d9a8699b447 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Tue, 30 Jun 2026 13:17:08 +0100 Subject: [PATCH 10/13] clippy --- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 1f1d9c0314f..80408123c5c 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -275,7 +275,7 @@ impl NetworkRequester { /// weighted by performance. pub async fn resolve(&self) -> Result { match self { - Self::Exact(addr) => Ok((**addr).clone()), + Self::Exact(addr) => Ok(**addr), Self::Any => discovery::discover(&[]).await, Self::InCountries(countries) => discovery::discover(countries).await, } From 58467e45040aea7551c59bb63841897ce792781c Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Tue, 30 Jun 2026 13:23:17 +0100 Subject: [PATCH 11/13] Clippy + comment tweak --- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 80408123c5c..afe130c1e02 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -56,8 +56,7 @@ use crate::{Error, Result}; /// client.disconnect().await; /// Ok(()) /// } -/// ``` - +// ``` pub struct Socks5MixnetClient { /// The nym address of this connected client. pub(crate) nym_address: Recipient, @@ -106,9 +105,9 @@ impl Socks5MixnetClient { /// country-restricted ([`InCountries`](NetworkRequester::InCountries)), or a /// known address ([`Exact`](NetworkRequester::Exact)). /// - /// Discovery always targets mainnet. The discovered requester enforces the - /// Nym exit policy, so destinations outside that policy are refused at the - /// exit regardless of which requester is selected. + /// The discovered requester enforces the Nym exit policy, so destinations + /// outside that policy are refused at the exit regardless of which + /// requester is selected. /// /// `bind` sets the local SOCKS5 listener address; pass `None` for the default /// `127.0.0.1:1080`, or `Some(addr)` to move it (for example when 1080 is @@ -215,7 +214,7 @@ impl Socks5MixnetClient { /// behalf) a SOCKS5 client routes through. Three ways, increasing specificity. #[derive(Debug, Clone, Default)] pub enum NetworkRequester { - /// Auto-discover one from the directory, weighted by performance. (default) + /// Auto-discover one from the current topology, weighted by performance. (default) #[default] Any, /// Auto-discover, restricted to requesters physically located in one of From 0a8b09cf139f75b02f42059176de439d53063c41 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Tue, 30 Jun 2026 13:57:24 +0100 Subject: [PATCH 12/13] Reorder imports --- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index afe130c1e02..064fb41b551 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -6,14 +6,14 @@ use nym_socks5_client_core::config::Socks5; use nym_sphinx::addressing::clients::Recipient; use nym_task::connections::LaneQueueLengths; use nym_task::ShutdownTracker; -use tokio::sync::RwLockReadGuard; - -use celes::Country; use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; use crate::mixnet::client::MixnetClientBuilder; use crate::{Error, Result}; +use celes::Country; +use tokio::sync::RwLockReadGuard; + /// A SOCKS5 proxy client connected to the Nym mixnet. /// /// `Socks5MixnetClient` provides a SOCKS5 proxy interface to the Nym mixnet, From 1d513d23a37a0387f1f70ff4125fc94ebcd5a320 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Tue, 30 Jun 2026 14:07:50 +0100 Subject: [PATCH 13/13] Remove docs from feature branch (split to max/socks5-autodiscovery-docs) --- .../circulating-supply.json | 4 +- .../api-scraping-outputs/nodes-count.json | 8 +- .../nyx-outputs/circulating-supply.md | 2 +- .../nyx-outputs/epoch-reward-budget.md | 2 +- .../nyx-outputs/stake-saturation.md | 2 +- .../nyx-outputs/staking-target.md | 2 +- .../nyx-outputs/staking_supply.md | 2 +- .../nyx-outputs/token-table.md | 6 +- .../api-scraping-outputs/reward-params.json | 8 +- .../outputs/api-scraping-outputs/time-now.md | 2 +- .../outputs/command-outputs/nym-api-help.md | 7 +- .../outputs/command-outputs/nym-node-help.md | 11 +- .../command-outputs/nym-node-run-help.md | 188 ++++++++---------- .../outputs/command-outputs/nymvisor-help.md | 3 +- .../docs/pages/developers/_meta.json | 7 +- .../docs/pages/developers/clients/socks5.mdx | 2 +- .../developers/concepts/exit-security.mdx | 64 +++--- documentation/docs/pages/developers/index.mdx | 2 +- documentation/docs/pages/developers/rust.mdx | 3 +- .../docs/pages/developers/rust/_meta.json | 1 - .../docs/pages/developers/rust/socks5.mdx | 131 ------------ .../network/infrastructure/exit-services.mdx | 6 +- 22 files changed, 154 insertions(+), 309 deletions(-) delete mode 100644 documentation/docs/pages/developers/rust/socks5.mdx diff --git a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json index ef45f627240..b6de7488260 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json @@ -5,7 +5,7 @@ }, "mixmining_reserve": { "denom": "unym", - "amount": "162624623318934" + "amount": "164623226345363" }, "vesting_tokens": { "denom": "unym", @@ -13,6 +13,6 @@ }, "circulating_supply": { "denom": "unym", - "amount": "837375376681066" + "amount": "835376773654637" } } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json b/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json index 5d74984a99b..00aa644a2b7 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json @@ -1,6 +1,6 @@ { - "nodes": 705, - "locations": 72, - "mixnodes": 237, - "exit_gateways": 460 + "nodes": 694, + "locations": 73, + "mixnodes": 234, + "exit_gateways": 452 } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md index 2be6cbfc2c4..42252b8a5e4 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md @@ -1 +1 @@ -837_375_376 +835_376_773 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md index aa5fd96e550..519f5836895 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md @@ -1 +1 @@ -4_517 +4_572 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md index 56e75274e6c..435fbd506fa 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md @@ -1 +1 @@ -256_198 +255_586 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md index e6a8116fcd8..1b5fa7f459b 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md @@ -1 +1 @@ -61_487_569 +61_340_814 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md index 7cb110b72d5..1016ec7ef9a 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md @@ -1 +1 @@ -61_487_568 +61_340_813 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md index 5e2124805eb..eafb4039118 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md @@ -1,7 +1,7 @@ | **Item** | **Description** | **Amount in NYM** | |:-------------------|:------------------------------------------------------|--------------------:| | Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 | -| Mixmining Reserve | Tokens releasing for operators rewards | 162_624_623 | +| Mixmining Reserve | Tokens releasing for operators rewards | 164_623_226 | | Vesting Tokens | Tokens locked outside of circulation for future claim | 0 | -| Circulating Supply | Amount of unlocked tokens | 837_375_376 | -| Stake Saturation | Optimal size of node self-bond + delegation | 256_198 | +| Circulating Supply | Amount of unlocked tokens | 835_376_773 | +| Stake Saturation | Optimal size of node self-bond + delegation | 255_586 | diff --git a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json index d07226cfecb..40f531502a0 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json @@ -1,10 +1,10 @@ { "interval": { - "reward_pool": "162624623318934.288753913229372056", - "staking_supply": "61487568582790.206042879724905795", + "reward_pool": "164623226345363.285226429762152046", + "staking_supply": "61340813321050.793375219026221616", "staking_supply_scale_factor": "0.07342892", - "epoch_reward_budget": "4517350647.748174687608700815", - "stake_saturation_point": "256198202428.29252517866552044", + "epoch_reward_budget": "4572867398.482313478511937837", + "stake_saturation_point": "255586722171.04497239674594259", "sybil_resistance": "0.3", "active_set_work_factor": "10", "interval_pool_emission": "0.02" diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 36907323ef6..92cd6628d91 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Monday, June 29th 2026, 00:21:46 UTC +Tuesday, June 16th 2026, 14:01:39 UTC diff --git a/documentation/docs/components/outputs/command-outputs/nym-api-help.md b/documentation/docs/components/outputs/command-outputs/nym-api-help.md index 1c382a7dcf0..3a20efee55e 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-api-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-api-help.md @@ -9,11 +9,10 @@ Commands: Options: -c, --config-env-file - Path pointing to an env file that configures the Nym API [env: - NYMAPI_CONFIG_ENV_FILE_ARG=] + Path pointing to an env file that configures the Nym API [env: NYMAPI_CONFIG_ENV_FILE_ARG=] --no-banner - A no-op flag included for consistency with other binaries (and compatibility with - nymvisor, oops) [env: NYMAPI_NO_BANNER_ARG=] + A no-op flag included for consistency with other binaries (and compatibility with nymvisor, + oops) [env: NYMAPI_NO_BANNER_ARG=] -h, --help Print help -V, --version diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-help.md index f4dc606a364..4a476a60d73 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-node-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-node-help.md @@ -3,20 +3,19 @@ Usage: nym-node [OPTIONS] Commands: build-info Show build information of this binary - bonding-information Show bonding information of this node depending on its currently - selected mode + bonding-information Show bonding information of this node depending on its currently selected mode node-details Show details of this node migrate Attempt to migrate an existing mixnode or gateway into a nym-node run Start this nym-node sign Use identity key of this node to sign provided message - unsafe-reset-sphinx-keys UNSAFE: reset existing sphinx keys and attempt to generate fresh - one for the current network state + unsafe-reset-sphinx-keys UNSAFE: reset existing sphinx keys and attempt to generate fresh one for the + current network state help Print this message or the help of the given subcommand(s) Options: -c, --config-env-file - Path pointing to an env file that configures the nym-node and overrides any - preconfigured values [env: NYMNODE_CONFIG_ENV_FILE_ARG=] + Path pointing to an env file that configures the nym-node and overrides any preconfigured values + [env: NYMNODE_CONFIG_ENV_FILE_ARG=] --no-banner Flag used for disabling the printed banner in tty [env: NYMNODE_NO_BANNER=] -h, --help diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md index 28edeb9ea39..453d9073261 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md @@ -9,170 +9,154 @@ Options: --config-file Path to a configuration file of this node [env: NYMNODE_CONFIG=] --accept-operator-terms-and-conditions - Explicitly specify whether you agree with the terms and conditions of a nym node - operator as defined at - [env: NYMNODE_ACCEPT_OPERATOR_TERMS=] + Explicitly specify whether you agree with the terms and conditions of a nym node operator as + defined at [env: + NYMNODE_ACCEPT_OPERATOR_TERMS=] --deny-init - Forbid a new node from being initialised if configuration file for the provided - specification doesn't already exist [env: NYMNODE_DENY_INIT=] + Forbid a new node from being initialised if configuration file for the provided specification + doesn't already exist [env: NYMNODE_DENY_INIT=] --init-only - If this is a brand new nym-node, specify whether it should only be initialised - without actually running the subprocesses [env: NYMNODE_INIT_ONLY=] + If this is a brand new nym-node, specify whether it should only be initialised without actually + running the subprocesses [env: NYMNODE_INIT_ONLY=] --local Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=] --mode [...] - Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: - mixnode, entry-gateway, exit-gateway, exit-providers-only] + Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, + entry-gateway, exit-gateway, exit-providers-only] --modes - Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] - [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only] + Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible + values: mixnode, entry-gateway, exit-gateway, exit-providers-only] -w, --write-changes - If this node has been initialised before, specify whether to write any new changes to - the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=] + If this node has been initialised before, specify whether to write any new changes to the config + file [env: NYMNODE_WRITE_CONFIG_CHANGES=] --bonding-information-output - Specify output file for bonding information of this nym-node, i.e. its encoded keys. - NOTE: the required bonding information is still a subject to change and this argument - should be treated only as a preview of future features [env: - NYMNODE_BONDING_INFORMATION_OUTPUT=] + Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the + required bonding information is still a subject to change and this argument should be treated + only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=] -o, --output - Specify the output format of the bonding information (`text` or `json`) [env: - NYMNODE_OUTPUT=] [default: text] [possible values: text, json] + Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] + [default: text] [possible values: text, json] --public-ips Comma separated list of public ip addresses that will be announced to the nym-api and - subsequently to the clients. In nearly all circumstances, it's going to be identical - to the address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=] + subsequently to the clients. In nearly all circumstances, it's going to be identical to the + address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=] --hostname - Optional hostname associated with this gateway that will be announced to the nym-api - and subsequently to the clients [env: NYMNODE_HOSTNAME=] + Optional hostname associated with this gateway that will be announced to the nym-api and + subsequently to the clients [env: NYMNODE_HOSTNAME=] --location - Optional **physical** location of this node's server. Either full country name (e.g. - 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or - three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=] + Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), + two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. + '616') can be provided [env: NYMNODE_LOCATION=] --http-bind-address - Socket address this node will use for binding its http API. default: `[::]:8080` - [env: NYMNODE_HTTP_BIND_ADDRESS=] + Socket address this node will use for binding its http API. default: `[::]:8080` [env: + NYMNODE_HTTP_BIND_ADDRESS=] --landing-page-assets-path - Path to assets directory of custom landing page of this node [env: - NYMNODE_HTTP_LANDING_ASSETS=] + Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=] --http-access-token - An optional bearer token for accessing certain http endpoints. Currently only used - for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=] + An optional bearer token for accessing certain http endpoints. Currently only used for + prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=] --expose-system-info Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false] --expose-system-hardware - Specify whether basic system hardware information should be exposed. default: true - [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false] + Specify whether basic system hardware information should be exposed. default: true [env: + NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false] --expose-crypto-hardware - Specify whether detailed system crypto hardware information should be exposed. - default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, - false] + Specify whether detailed system crypto hardware information should be exposed. default: true + [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false] --nyxd-urls Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=] --nyxd-websocket-url - Url to the websocket endpoint of a nyx validator, for example - `wss://rpc.nymtech.net/websocket`. It is used for subscribing to new block events - [env: NYMNODE_NYXD_WEBSOCKET=] + Url to the websocket endpoint of a nyx validator, for example `wss://rpc.nymtech.net/websocket`. + It is used for subscribing to new block events [env: NYMNODE_NYXD_WEBSOCKET=] --mixnet-bind-address - Address this node will bind to for listening for mixnet packets default: `[::]:1789` - [env: NYMNODE_MIXNET_BIND_ADDRESS=] + Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: + NYMNODE_MIXNET_BIND_ADDRESS=] --mixnet-announce-port - If applicable, custom port announced in the self-described API that other clients and - nodes will use. Useful when the node is behind a proxy [env: - NYMNODE_MIXNET_ANNOUNCE_PORT=] + If applicable, custom port announced in the self-described API that other clients and nodes will + use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=] --nym-api-urls - Addresses to nym APIs from which the node gets the view of the network [env: - NYMNODE_NYM_APIS=] + Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=] --enable-console-logging Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false] --wireguard-enabled - Specifies whether the wireguard service is enabled on this node [env: - NYMNODE_WG_ENABLED=] [possible values: true, false] + Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] + [possible values: true, false] --wireguard-bind-address - Socket address this node will use for binding its wireguard interface. default: - `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=] + Socket address this node will use for binding its wireguard interface. default: `[::]:51822` + [env: NYMNODE_WG_BIND_ADDRESS=] --wireguard-tunnel-announced-port - Tunnel port announced to external clients wishing to connect to the wireguard - interface. Useful in the instances where the node is behind a proxy [env: - NYMNODE_WG_ANNOUNCED_PORT=] + Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful + in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=] --wireguard-private-network-prefix - The prefix denoting the maximum number of the clients that can be connected via - Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: - NYMNODE_WG_PRIVATE_NETWORK_PREFIX=] + The prefix denoting the maximum number of the clients that can be connected via Wireguard. The + maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=] --wireguard-userspace - Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. - Useful in containerized environments without kernel WireGuard support [env: - NYMNODE_WG_USERSPACE=] [possible values: true, false] + Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in + containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] + [possible values: true, false] --verloc-bind-address - Socket address this node will use for binding its verloc API. default: `[::]:1790` - [env: NYMNODE_VERLOC_BIND_ADDRESS=] + Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: + NYMNODE_VERLOC_BIND_ADDRESS=] --verloc-announce-port - If applicable, custom port announced in the self-described API that other clients and - nodes will use. Useful when the node is behind a proxy [env: - NYMNODE_VERLOC_ANNOUNCE_PORT=] + If applicable, custom port announced in the self-described API that other clients and nodes will + use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=] --entry-bind-address - Socket address this node will use for binding its client websocket API. default: - `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=] + Socket address this node will use for binding its client websocket API. default: `[::]:9000` + [env: NYMNODE_ENTRY_BIND_ADDRESS=] --announce-ws-port - Custom announced port for listening for websocket client traffic. If unspecified, the - value from the `bind_address` will be used instead [env: - NYMNODE_ENTRY_ANNOUNCE_WS_PORT=] + Custom announced port for listening for websocket client traffic. If unspecified, the value from + the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=] --announce-wss-port If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=] --enforce-zk-nyms - Indicates whether this gateway is accepting only coconut credentials for accessing - the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] - [possible values: true, false] + Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or + if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, + false] --mnemonic - Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a - fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=] + Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh + mnemonic is going to be generated [env: NYMNODE_MNEMONIC=] --upgrade-mode-attestation-url - Endpoint to query to retrieve current upgrade mode attestation. This argument should - never be set outside testnets and local networks [env: - NYMNODE_UPGRADE_MODE_ATTESTATION_URL=] + Endpoint to query to retrieve current upgrade mode attestation. This argument should never be + set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=] --upgrade-mode-attester-public-key - Expected public key of the entity signing the published attestation. This argument - should never be set outside testnets and local networks [env: - NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=] + Expected public key of the entity signing the published attestation. This argument should never + be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=] --upstream-exit-policy-url Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=] --open-proxy - Specifies whether this exit node should run in 'open-proxy' mode and thus would - attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible - values: true, false] + Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to + resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false] --nr-allow-local-ips - Allow the network requester to forward traffic to non-globally-routable addresses. - Intended for local development, private-network deployments, and testnet scenarios. - Not recommended on production exit gateway unless you know what you're doing [env: - NYMNODE_NR_ALLOW_LOCAL_IPS=] [possible values: true, false] + Allow the network requester to forward traffic to non-globally-routable addresses. Intended for + local development, private-network deployments, and testnet scenarios. Not recommended on + production exit gateway unless you know what you're doing [env: NYMNODE_NR_ALLOW_LOCAL_IPS=] + [possible values: true, false] --ipr-allow-local-ips - Allow the IP packet router to forward traffic to non-globally-routable addresses. - Intended for local development, private-network deployments, and testnet scenarios. - Not recommended on production exit gateway unless you know what you're doing [env: - NYMNODE_IPR_ALLOW_LOCAL_IPS=] [possible values: true, false] + Allow the IP packet router to forward traffic to non-globally-routable addresses. Intended for + local development, private-network deployments, and testnet scenarios. Not recommended on + production exit gateway unless you know what you're doing [env: NYMNODE_IPR_ALLOW_LOCAL_IPS=] + [possible values: true, false] --lp-control-bind-address Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=] --lp-control-announce-port - Custom announced port for listening for the TCP LP control traffic. If unspecified, - the value from the `lp_control_bind_address` will be used instead [env: - NYMNODE_LP_CONTROL_ANNOUNCE_PORT=] + Custom announced port for listening for the TCP LP control traffic. If unspecified, the value + from the `lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=] --lp-data-bind-address Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=] --lp-data-announce-port - Custom announced port for listening for the UDP LP data traffic. If unspecified, the - value from the `lp_data_bind_address` will be used instead [env: - NYMNODE_LP_DATA_ANNOUNCE_PORT=] + Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from + the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=] --lp-use-mock-ecash - Use mock ecash manager for LP testing. WARNING: Only use this for local testing! - Never enable in production. When enabled, the LP listener will accept any credential - without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: - true, false] + Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in + production. When enabled, the LP listener will accept any credential without blockchain + verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false] -h, --help Print help ``` diff --git a/documentation/docs/components/outputs/command-outputs/nymvisor-help.md b/documentation/docs/components/outputs/command-outputs/nymvisor-help.md index ba058d74f32..e4124a3ea9c 100644 --- a/documentation/docs/components/outputs/command-outputs/nymvisor-help.md +++ b/documentation/docs/components/outputs/command-outputs/nymvisor-help.md @@ -12,8 +12,7 @@ Commands: Options: -c, --config-env-file - Path pointing to an env file that configures the nymvisor and overrides any - preconfigured values + Path pointing to an env file that configures the nymvisor and overrides any preconfigured values -h, --help Print help -V, --version diff --git a/documentation/docs/pages/developers/_meta.json b/documentation/docs/pages/developers/_meta.json index bf8c714a71c..fc769b039a3 100644 --- a/documentation/docs/pages/developers/_meta.json +++ b/documentation/docs/pages/developers/_meta.json @@ -1,6 +1,7 @@ { "index": "Overview", "concepts": "Key Concepts", + "sep-intro": { "type": "separator" }, @@ -9,9 +10,8 @@ "title": "Rust" }, "smolmix": "smolmix (TCP/UDP tunnel)", - "rust": { - "title": "nym-sdk" - }, + "rust": "nym-sdk", + "-": { "type": "separator", "title": "TypeScript" @@ -24,6 +24,7 @@ "mix-websocket": "mix-websocket (ws / wss)", "mix-architecture": "mix-* Family Architecture", "typescript": "Raw Messaging SDK", + "sep-extras": { "type": "separator" }, diff --git a/documentation/docs/pages/developers/clients/socks5.mdx b/documentation/docs/pages/developers/clients/socks5.mdx index 215960e8d33..e0a70f0164d 100644 --- a/documentation/docs/pages/developers/clients/socks5.mdx +++ b/documentation/docs/pages/developers/clients/socks5.mdx @@ -1,6 +1,6 @@ # Socks5 Client (Standalone) -> This proxy is also available embedded in a Rust application via the [nym-sdk SOCKS5 module](/developers/rust/socks5). +> This client can also be utilised via the [Rust SDK](/developers/rust). Many existing applications are able to use either the SOCKS4, SOCKS4A, or SOCKS5 proxy protocols. If you want to send such an application's traffic through the mixnet, you can use the `nym-socks5-client` to bounce network traffic through the Nym network, like this: diff --git a/documentation/docs/pages/developers/concepts/exit-security.mdx b/documentation/docs/pages/developers/concepts/exit-security.mdx index eb495b35981..fb56aa678e1 100644 --- a/documentation/docs/pages/developers/concepts/exit-security.mdx +++ b/documentation/docs/pages/developers/concepts/exit-security.mdx @@ -1,74 +1,68 @@ --- title: "Exit Security: What the Mixnet Protects and What It Doesn't" -description: "The security model for traffic that exits the Nym mixnet at an Exit Gateway, via the IP Packet Router or the SOCKS-based Network Requester, and how it changes for end-to-end traffic that never exits. Applies to smolmix, the SOCKS5 module, mix-tunnel, mix-fetch, mix-dns, and mix-websocket." +description: "The canonical security model for traffic that leaves the Nym mixnet at an IPR exit gateway. Applies to smolmix, mix-tunnel, mix-fetch, mix-dns, and mix-websocket alike." schemaType: "TechArticle" section: "Developers" -lastUpdated: "2026-06-29" +lastUpdated: "2026-06-03" --- # Exit security import { Callout } from 'nextra/components' -Every tool that reaches an external service through the Nym mixnet shares the same exit security model, whether it's the Rust [`smolmix`](/developers/smolmix) crate, the [SOCKS5 module](/developers/rust/socks5), or the mix-* packages built on [`mix-tunnel`](/developers/mix-tunnel) ([`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). They leave the mixnet at an [Exit Gateway](/network/infrastructure/exit-services), either through the [IP Packet Router](/network/infrastructure/exit-services#ip-packet-router) (which forwards raw IP packets) or the SOCKS-based [Network Requester](/network/infrastructure/exit-services#network-requester) (which makes the request from a SOCKS stream), so they share the same properties and the same caveat. Tools where both ends run a Nym client ([`nym-sdk`](/developers/rust), the [TypeScript SDK](/developers/typescript)) never exit the mixnet at all; the [end-to-end case](#proxy-mode-or-end-to-end) is covered below. This page is the canonical statement of the model; the package pages link here rather than restating it. +Every tool that reaches an external service through the Nym mixnet shares the same security model, whether it's the Rust [`smolmix`](/developers/smolmix) crate or the mix-* packages built on [`mix-tunnel`](/developers/mix-tunnel) ([`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). They all exit the mixnet at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) gateway, so they inherit the same properties and the same single caveat. This page is the canonical statement of that model; the package pages link here rather than restating it. ## The one-sentence version -In **proxy mode** the mixnet hides **who** you are from the destination and **where** you're going from the network, but the Exit Gateway sees your **destination** and any payload you didn't encrypt yourself. In **end-to-end** mode there is no Exit Gateway: traffic stays Sphinx-encrypted the whole way. +The mixnet hides **who** you are from the destination and **where** you're going from the network, but the exit gateway sees your **destination** and any payload you didn't encrypt yourself. ## Proxy mode or end-to-end? -Most of this page is about **proxy mode**: your traffic leaves the mixnet at an Exit Gateway, via either the IP Packet Router (raw IP packets) or the SOCKS-based Network Requester (which sees the destination hostname), and continues to a third-party server over clearnet, where the security trade-offs apply. +This page is about **proxy mode**: your traffic leaves the mixnet at an IPR exit and continues to a third-party server over clearnet, where the security trade-offs apply. -If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no Exit Gateway, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript). +If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no IPR, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript). ## What each hop sees -**Proxy mode**: only your side runs Nym, and traffic exits to a third party: - -```text -you → entry gateway → 3 mix layers → exit gateway → destination -└──────── Sphinx-encrypted ────────┘ └── clearnet ──┘ - exit gateway strips Sphinx here -``` - -**End-to-end**: both sides run Nym, and traffic never leaves the mixnet: - ```text -you → entry gateway → 3 mix layers → entry gateway → peer Nym client -└─────────────── Sphinx-encrypted the whole way ───────────────┘ + you + │ Sphinx + ▼ + entry gateway + │ Sphinx + ▼ + 3 mix layers + │ Sphinx + ▼ + IPR exit gateway + │ plain IP (Sphinx removed here) + ▼ + destination ``` -The table below is the proxy-mode path. In end-to-end mode there is no exit and no clearnet hop, so only the first two rows apply and the far end is another Nym client rather than a remote host. - | 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 gateway + 3 mix layers) | Sphinx (layered) | Each node only knows its previous and next hop | -| Exit Gateway (IPR or NR) | Sphinx removed; raw IP packet (IPR) or SOCKS request (NR) exposed | The IPR sees the destination IP and port; the NR sees the destination hostname. Either way the payload depends on your application layer (see below). | -| Exit Gateway → remote host | None (Sphinx is mixnet-only; your own TLS, if any, still applies) | Remote host sees the Exit Gateway's IP, not yours | +| 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; your own TLS, if any, still applies) | Remote host sees the IPR's IP, not yours | -The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. In proxy mode the Exit Gateway strips the Sphinx layers and forwards the request to the destination, analogous to how a Tor exit node or VPN endpoint unwraps its tunnel. In end-to-end mode the Sphinx layers are only removed at the receiving Nym client, so nothing is ever exposed on clearnet. +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. ## Encrypt your own payload -Because the Exit Gateway removes the Sphinx layers, whatever is inside is visible to the exit unless you encrypted it yourself. +Because the IPR removes the Sphinx layers, whatever is inside that IP packet is visible to the exit unless you encrypted it yourself. -- **Application-layer encryption closes the gap.** TLS, the Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the exit. It still sees the destination, but not the content. Over TLS the exit only ever handles ciphertext bound for that destination; the bytes inside stay opaque to it. +- **Application-layer encryption closes the gap.** TLS, the Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the IPR. It still sees the destination IP and port, but not the content. Over TLS the IPR only ever handles ciphertext bound for that destination; the bytes inside stay opaque to it. - **Unencrypted payloads are fully visible.** Plain HTTP, unencrypted WebSocket (`ws://`), and plain UDP DNS are readable in full at the exit. The mixnet still hides your identity, so the exit reads the content without being able to attribute it to you. ## Trust boundaries - You trust the mixnet to provide unlinkability between sender and receiver. Sphinx provides this cryptographically at the per-packet level: a node cannot read addressing beyond its own hop. Unlinkability of your *traffic pattern* over time is weaker, and statistical rather than absolute. It comes from mixing and cover traffic, and degrades with low network traffic, with cover traffic or Poisson timing disabled, and against an adversary that can observe a large fraction of the network. -- You trust the Exit Gateway in the same way you trust a VPN exit or Tor exit node: it can inspect whatever leaves the mixnet, but it does not know who is sending the traffic (the mixnet hides your identity). What it can inspect, and the shape of the trust, differs between the two exit types: - - **IP Packet Router (IPR).** A packet forwarder, like a VPN exit. It receives raw IP packets, so it sees the destination IP and port and reads any packet payload you did not encrypt. It works at the network layer and does not parse your application protocol. This is the exit used by `smolmix` and the `mix-*` packages ([`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). - - **Network Requester (NR).** A SOCKS proxy that makes the request on your behalf, so your trust in it is exactly the trust you place in any SOCKS5 proxy. With the `socks5h` URL the SDK hands it the destination *hostname* (not just an IP) plus the byte stream, and the NR opens the TCP connection itself and relays bytes back. It therefore sees the destination hostname and port, and any payload you did not encrypt, just as a SOCKS proxy you ran your traffic through would. This is the exit used by the [SOCKS5 module](/developers/rust/socks5) and the standalone [`nym-socks5-client`](/developers/clients/socks5). +- 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). -Treat the Exit Gateway exactly as you would a VPN exit or a Tor exit node: it can inspect whatever you send past it. The difference Nym adds is that the exit doesn't know who's sending the traffic. Protect the payload with TLS or equivalent. If the exit operator matters to you, pin one, but the control depends on the exit type: - -- **IPR-backed clients** (`smolmix` and the `mix-*` packages): set the preferred IPR (`preferredIpr` in the TypeScript and wasm packages; see each package page for the native equivalent). -- **SOCKS5 module (network requester):** pass a fixed requester address to `Socks5MixnetClient::connect_new(...)`, or constrain the jurisdiction with the discovery builder (`discover().countries([...])`). +Treat the IPR exactly as you would a VPN exit or a Tor exit node: it can inspect your raw IP packets. The difference Nym adds is that the IPR doesn't know who's sending the traffic. Protect the payload with TLS or equivalent, and pin a trusted exit (`preferredIpr`) if the exit operator matters to you. ## What the mixnet does not protect @@ -97,5 +91,7 @@ The timing-analysis rating assumes the defaults. Cover traffic and Poisson timin ## Read more -- [Exit Gateway Services](/network/infrastructure/exit-services): how the SOCKS-based Network Requester and the IP Packet Router each route traffic past the exit, and how they differ. -- Per-transport exposure (where TLS terminates, what the resolver sees, `wss://` vs `ws://`) lives on each package's own page: [smolmix](/developers/smolmix), the [SOCKS5 module](/developers/rust/socks5), [mix-fetch](/developers/mix-fetch), [mix-dns](/developers/mix-dns), and [mix-websocket](/developers/mix-websocket). +The package pages add the parts specific to their transport (where TLS terminates, what the resolver sees, WSS vs `ws://`): + +- [Exit Gateway Services](/network/infrastructure/exit-services#ip-packet-router): how the IPR allocates addresses and routes raw IP packets, and how it differs from the SOCKS-based Network Requester. +- The per-package "Security model" section on [mix-fetch](/developers/mix-fetch/concepts#security-model), [mix-dns](/developers/mix-dns/guides#security-model), and [mix-websocket](/developers/mix-websocket/concepts#security-model) for the transport-specific exposure. diff --git a/documentation/docs/pages/developers/index.mdx b/documentation/docs/pages/developers/index.mdx index a1a57a3a3ea..b441f391773 100644 --- a/documentation/docs/pages/developers/index.mdx +++ b/documentation/docs/pages/developers/index.mdx @@ -21,7 +21,7 @@ The table below maps those two answers to a package. | Runtime | End-to-end (both sides run Nym) | Proxy (exit to clearnet) | |---|---|---| -| **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS5](/developers/rust/socks5) | +| **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS](/developers/rust) | | **Browser / WebView** (JS + WASM) | [TypeScript SDK](/developers/typescript): `@nymproject/sdk` raw messaging | [`mix-fetch`](/developers/mix-fetch) HTTP/S · [`mix-dns`](/developers/mix-dns) DNS · [`mix-websocket`](/developers/mix-websocket) WS/WSS | diff --git a/documentation/docs/pages/developers/rust.mdx b/documentation/docs/pages/developers/rust.mdx index d728865371f..04229f6175e 100644 --- a/documentation/docs/pages/developers/rust.mdx +++ b/documentation/docs/pages/developers/rust.mdx @@ -37,7 +37,6 @@ For an overview of what the SDK can do, see the **[Tour](./rust/tour)**. For set |---|---|---| | [**Stream**](./rust/stream) | Multiplexed `AsyncRead + AsyncWrite` byte streams over the Mixnet, the closest analogue to TCP sockets. | Recommended | | [**Mixnet**](./rust/mixnet) | Raw message payloads, independently routed, no connections or ordering. Full control over the communication model. | Stable | -| [**SOCKS5**](./rust/socks5) | Local SOCKS5 proxy that routes any SOCKS-capable application through the Mixnet to a network requester (proxy mode, exits to clearnet). | Stable | | [**Client Pool**](./rust/client-pool) | Keeps ready-to-use `MixnetClient` instances warm for bursty workloads. | Stable | | [**TcpProxy**](./rust/tcpproxy) | TCP socket proxying with session management and message ordering. | Deprecated | | [**FFI**](./rust/ffi) | Go and C/C++ bindings. | Stable | @@ -51,4 +50,4 @@ 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. -- [**SOCKS5 module**](./rust/socks5): SOCKS4/4a/5 proxy via the Exit Gateway's network requester. Works with any SOCKS-capable application without code changes. +- [**SOCKS Client**](./rust/mixnet): SOCKS4/4a/5 proxy via the Exit Gateway's Network Requester. Works with any SOCKS-capable application without code changes. diff --git a/documentation/docs/pages/developers/rust/_meta.json b/documentation/docs/pages/developers/rust/_meta.json index d9344078c4a..d00e161c61d 100644 --- a/documentation/docs/pages/developers/rust/_meta.json +++ b/documentation/docs/pages/developers/rust/_meta.json @@ -3,7 +3,6 @@ "importing": "Installation", "mixnet": "Mixnet Module", "stream": "Stream Module", - "socks5": "SOCKS5 Module", "tcpproxy": "TcpProxy Module (Deprecated)", "client-pool": "Client Pool Module", "ffi": "FFI" diff --git a/documentation/docs/pages/developers/rust/socks5.mdx b/documentation/docs/pages/developers/rust/socks5.mdx deleted file mode 100644 index d3ddcaea018..00000000000 --- a/documentation/docs/pages/developers/rust/socks5.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: "Nym Rust SDK: SOCKS5 Proxy Module" -description: "Use the Nym Rust SDK SOCKS5 module to route any SOCKS4/4a/5-capable application through the mixnet. Covers Socks5MixnetClient, the socks5h proxy URL, automatic network requester discovery, country-based selection, and a reqwest example." -schemaType: "TechArticle" -section: "Developers" -lastUpdated: "2026-06-29" ---- - -# SOCKS5 Module - -import { Callout } from 'nextra/components' - -The `socks5` module provides [`Socks5MixnetClient`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html): a local SOCKS5 proxy that routes any SOCKS4, SOCKS4a, or SOCKS5-capable application's traffic through the mixnet. Your application connects to a normal-looking SOCKS5 proxy on `localhost`; the client forwards that traffic over the mixnet to a [**network requester**](/network/infrastructure/exit-services#network-requester) running on an Exit Gateway, which makes the real request on your behalf. - - -This is a **proxy-mode** integration, not end-to-end. Unlike the [Mixnet](./mixnet) and [Stream](./stream) modules (where both sides run a Nym client), traffic here leaves the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS) is your application's job. See [Exit security](/developers/concepts/exit-security) for the full model: what each hop sees, the trust boundaries, and how it compares with Tor and VPNs. - - -## How it works - -```text -Your machine - Application (reqwest, curl, a browser, ...) - │ SOCKS5 (socks5h://127.0.0.1:1080) - ▼ - Socks5MixnetClient (local listener + MixnetClient) - │ Sphinx packets - ▼ - Entry Gateway → 3 mix layers → Exit Gateway - │ network requester - ▼ makes the real request - Destination (clearnet) -``` - -Any application that speaks SOCKS sees a standard proxy on `localhost`. It never has to know the mixnet exists. The client chops the TCP stream into Sphinx packets, sends them through the mixnet, and the network requester at the exit reassembles the stream and performs the request, returning the response the same way. - -## Quick example - -You need the Nym address of a **network requester** (an Exit Gateway running in network-requester mode) to act as the service provider, or let the SDK find one for you ([Automatic discovery](#automatic-discovery)). Point an HTTP client at the SOCKS5 URL the client exposes, and every request travels through the mixnet: - -```rust -use nym_sdk::mixnet::Socks5MixnetClient; - -#[tokio::main] -async fn main() -> Result<(), Box> { - nym_bin_common::logging::setup_tracing_logger(); - - // Connect to a network requester service provider over the mixnet. - // This opens a local SOCKS5 listener (default 127.0.0.1:1080). - let client = Socks5MixnetClient::connect_new("provider_nym_address...").await?; - - // Point any SOCKS5-capable HTTP client at the proxy URL. - let proxy = reqwest::Proxy::all(client.socks5_url())?; - let http = reqwest::Client::builder().proxy(proxy).build()?; - - // This request now travels through the mixnet and exits at the requester. - let body = http.get("https://nymtech.net").send().await?.text().await?; - println!("{body}"); - - client.disconnect().await; - Ok(()) -} -``` - - -`socks5_url()` returns a **`socks5h://`** URL, not `socks5://`. The trailing `h` tells the HTTP client to hand the hostname to the proxy and let the network requester resolve DNS at the exit, rather than resolving locally and leaking the destination through a local DNS query. - - -For finer control (binding a different address, reusing persistent keys), build the client through `MixnetClientBuilder` instead of `connect_new`: - -```rust -use nym_sdk::mixnet; - -let socks5_config = mixnet::Socks5::new("provider_nym_address...".to_string()); -let client = mixnet::MixnetClientBuilder::new_ephemeral() - .socks5_config(socks5_config) - .build()? - .connect_to_mixnet_via_socks5() - .await?; -``` - -## Automatic discovery - -You do not have to hardcode a network requester address. [`connect_with`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html) takes a `NetworkRequester` describing how to pick one plus an optional listener address, and connects in a single call. There are three ways to choose: - -```rust -use nym_sdk::mixnet::{NetworkRequester, Socks5MixnetClient}; - -// Any available requester, weighted by performance (None = default 127.0.0.1:1080): -let client = Socks5MixnetClient::connect_with(NetworkRequester::any(), None).await?; - -// A specific requester you already know: -let client = Socks5MixnetClient::connect_with(NetworkRequester::exact("address...")?, None).await?; -``` - -For `any`, discovery queries mainnet for Exit Gateways that advertise a network requester and selects one weighted by performance. - -### Selecting by country - -To constrain where your traffic leaves the mixnet, build the requester with [ISO 3166 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. The chosen requester must be physically located in one of them: - -```rust -use nym_sdk::mixnet::{NetworkRequester, Socks5MixnetClient}; - -let requester = NetworkRequester::in_countries(["CH", "DE"])?; // Switzerland or Germany - -// Listen on 127.0.0.1:1081 instead of the default 1080: -let client = Socks5MixnetClient::connect_with(requester, Some("127.0.0.1:1081".parse()?)).await?; -``` - -Codes are case-insensitive and validated up front, so a typo fails immediately rather than at connect time. Several countries are treated as "any of these". Pass `Some(addr)` as the second argument to move the local listener off the default `127.0.0.1:1080`, for example when that port is taken or to run more than one client at once. - - -A requester's location is **self-reported by its operator and optional**. Requesters that have not declared a location are excluded once you set a country filter, so a narrow filter can return `NoGatewayInCountries` even when requesters exist there but have not advertised it. Discovery does not silently fall back to another country: an empty result is an error, since routing through an unintended jurisdiction would defeat the point of asking. - - -## SDK module or standalone binary - -The same proxy logic ships two ways. They are interchangeable on the wire; choose by how you want to run it: - -| | Use it when | -|---|---| -| **`socks5` module** (this page) | You want the proxy embedded in a Rust application and managed in-process, alongside other SDK clients. | -| [**Standalone `nym-socks5-client`**](/developers/clients/socks5) | You want a language-agnostic local proxy binary that any application (in any language) can point at, with no code changes. | - -## Further reading - -- [API reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.Socks5MixnetClient.html): all methods, configuration, and types -- [Example: SOCKS5 proxy](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/socks5.rs): select a requester (auto-discover, pin by country, or a known address) and proxy a request -- [Standalone SOCKS5 client](/developers/clients/socks5): the language-agnostic binary form -- [Exit security](/developers/concepts/exit-security): what an Exit Gateway can observe in proxy mode diff --git a/documentation/docs/pages/network/infrastructure/exit-services.mdx b/documentation/docs/pages/network/infrastructure/exit-services.mdx index 653bdbf6ecc..73b2c62c6ad 100644 --- a/documentation/docs/pages/network/infrastructure/exit-services.mdx +++ b/documentation/docs/pages/network/infrastructure/exit-services.mdx @@ -32,7 +32,7 @@ Because it operates at the application layer, the NR: - Can enforce allow/deny lists on destination hosts and ports - Sees the destination hostname and port, but not the contents if TLS is used -**Used by:** the [SDK's SOCKS5 module](/developers/rust/socks5) and the [standalone SOCKS5 client](/developers/clients/socks5). +**Used by:** the [SDK's SOCKS client](/developers/rust/mixnet), [standalone SOCKS5 client](/developers/clients/socks5), and [mixFetch](/developers/mix-fetch) (which wraps SOCKS requests in a browser-friendly `fetch` API). ## IP Packet Router @@ -59,7 +59,7 @@ Because it operates at the IP layer, the IPR: In both services, traffic between the Exit Gateway and the destination travels over the public internet, exactly as it would from any other server. The mixnet protects sender anonymity (the destination sees the gateway's IP, not yours), but does not encrypt the payload past the gateway. Use TLS or another application-layer cipher to protect payload confidentiality, just as you would on a direct connection. -**Used by:** [NymVPN anonymous mode](/network/dvpn-mode/protocol) (5-hop mixnet routing to the IPR), [`smolmix`](/developers/smolmix) (programmatic `TcpStream`/`UdpSocket` access to the IPR via the Rust SDK), and the browser `mix-*` packages built on `smolmix`: [`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket). +**Used by:** [NymVPN anonymous mode](/network/dvpn-mode/protocol) (5-hop mixnet routing to the IPR), and [`smolmix`](/developers/smolmix) (programmatic `TcpStream`/`UdpSocket` access to the IPR via the Rust SDK). ## Comparison @@ -70,7 +70,7 @@ In both services, traffic between the Exit Gateway and the destination travels o | **DNS** | Resolved by the NR | Client resolves its own | | **Client gets** | Proxied connections | An allocated IP address | | **Connection model** | Per-request | Persistent tunnel | -| **Used by** | SDK SOCKS5 module, standalone SOCKS5 client | NymVPN (anonymous mode), smolmix, mix-* packages | +| **Used by** | SDK SOCKS client, mixFetch | NymVPN (anonymous mode), smolmix | ## Trust model