Skip to main content

nym_sdk/mixnet/
socks5_client.rs

1use std::net::SocketAddr;
2use std::time::Duration;
3
4use tokio::sync::RwLockReadGuard;
5
6use nym_client_core::client::base_client::ClientState;
7use nym_socks5_client_core::config::Socks5;
8use nym_sphinx::addressing::clients::Recipient;
9use nym_task::connections::LaneQueueLengths;
10use nym_task::ShutdownTracker;
11use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError};
12
13use crate::mixnet::client::MixnetClientBuilder;
14use crate::mixnet::NetworkRequesterSelector;
15use crate::Result;
16
17/// A SOCKS5 proxy client connected to the Nym mixnet.
18///
19/// `Socks5MixnetClient` provides a SOCKS5 proxy interface to the Nym mixnet,
20/// allowing HTTP(S) clients and other SOCKS5-compatible applications to route
21/// their traffic through the mixnet without having to modify their networking
22/// code.
23///
24/// Traffic leaves the mixnet through a network requester: a service running on
25/// an exit gateway that makes requests on the client's behalf and enforces the
26/// Nym exit policy. You can let the client discover one for you or name a specific
27/// one; see [`connect_with`](Self::connect_with) and [`NetworkRequesterSelector`].
28///
29/// ## Usage
30///
31/// 1. Connect, either by discovering a requester with
32///    [`connect_with`](Self::connect_with) or naming a known one with
33///    [`connect_new`](Self::connect_new)
34/// 2. Get the SOCKS5 URL via [`socks5_url`](Self::socks5_url)
35/// 3. Point your HTTP client at that SOCKS5 proxy
36///
37/// ## Example
38///
39/// ```rust,no_run
40/// use nym_sdk::mixnet::Socks5MixnetClient;
41///
42/// #[tokio::main]
43/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
44///     // Connect to a known network requester by address
45///     let client = Socks5MixnetClient::connect_new("provider_nym_address...").await?;
46///
47///     // Get the SOCKS5 proxy URL
48///     let socks5_url = client.socks5_url();
49///     println!("Configure your HTTP client to use: {}", socks5_url);
50///
51///     // Your HTTP client can now use the SOCKS5 proxy
52///     // let http_client = reqwest::Client::builder()
53///     //     .proxy(reqwest::Proxy::all(&socks5_url)?)
54///     //     .build()?;
55///
56///     client.disconnect().await;
57///     Ok(())
58/// }
59// ```
60pub struct Socks5MixnetClient {
61    /// The nym address of this connected client.
62    pub(crate) nym_address: Recipient,
63
64    /// The current state of the client that is exposed to the user. This includes things like
65    /// current message send queue length.
66    pub(crate) client_state: ClientState,
67
68    /// The task manager controlling all the spawned tasks the client uses to do its job.
69    pub(crate) task_handle: ShutdownTracker,
70
71    /// SOCKS5 configuration parameters.
72    pub(crate) socks5_config: Socks5,
73}
74
75impl Socks5MixnetClient {
76    /// Create a new client and connect to a network requester over the mixnet via SOCKS5 using
77    /// ephemeral in-memory keys that are discarded at application close.
78    ///
79    /// This is the zero-ceremony path when you already know the requester's
80    /// address; it is shorthand for [`connect_with`](Self::connect_with) with
81    /// [`NetworkRequesterSelector::exact`] and the default listener bind.
82    ///
83    /// Kept for backwards compatibility: it predates [`connect_with`] and overlaps
84    /// with the `exact` case, but existing callers pass an address string directly.
85    ///
86    /// # Examples
87    ///
88    /// ```no_run
89    /// use nym_sdk::mixnet;
90    ///
91    /// #[tokio::main]
92    /// async fn main() {
93    ///     let receiving_client = mixnet::MixnetClient::connect_new().await.unwrap();
94    ///     let mut client = mixnet::Socks5MixnetClient::connect_new(receiving_client.nym_address().to_string()).await;
95    /// }
96    ///
97    /// ```
98    pub async fn connect_new<S: Into<String>>(provider_mix_address: S) -> Result<Self> {
99        MixnetClientBuilder::new_ephemeral()
100            .socks5_config(Socks5::new(provider_mix_address))
101            .build()?
102            .connect_to_mixnet_via_socks5()
103            .await
104    }
105
106    /// Create a new client and connect to a network requester chosen per the
107    /// given [`NetworkRequesterSelector`]: auto-discovered ([`Any`](NetworkRequesterSelector::Any)),
108    /// country-restricted ([`InCountries`](NetworkRequesterSelector::InCountries)), or a
109    /// known address ([`Exact`](NetworkRequesterSelector::Exact)).
110    ///
111    /// The discovered requester enforces the Nym exit policy, so destinations
112    /// outside that policy are refused at the exit regardless of which
113    /// requester is selected.
114    ///
115    /// `bind` sets the local SOCKS5 listener address; pass `None` for the default
116    /// `127.0.0.1:1080`, or `Some(addr)` to move it (for example when 1080 is
117    /// already taken, or to run more than one client at once).
118    ///
119    /// # Examples
120    ///
121    /// ```no_run
122    /// use nym_sdk::mixnet::{NetworkRequesterSelector, Socks5MixnetClient};
123    ///
124    /// #[tokio::main]
125    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
126    ///     // Any requester, weighted by performance, on the default port:
127    ///     let any = Socks5MixnetClient::connect_with(NetworkRequesterSelector::any(), None).await?;
128    ///
129    ///     // Pinned to Switzerland or Germany, listening on 127.0.0.1:1081:
130    ///     let pinned = Socks5MixnetClient::connect_with(
131    ///         NetworkRequesterSelector::in_countries(["CH", "DE"])?,
132    ///         Some("127.0.0.1:1081".parse()?),
133    ///     )
134    ///     .await?;
135    ///     Ok(())
136    /// }
137    /// ```
138    pub async fn connect_with(
139        requester: NetworkRequesterSelector,
140        bind: Option<SocketAddr>,
141    ) -> Result<Self> {
142        let provider = requester.resolve().await?;
143        let mut socks5_config = Socks5::new(provider.to_string());
144        if let Some(addr) = bind {
145            socks5_config.bind_address = addr;
146        }
147        MixnetClientBuilder::new_ephemeral()
148            .socks5_config(socks5_config)
149            .build()?
150            .connect_to_mixnet_via_socks5()
151            .await
152    }
153
154    /// Get the nym address of this client. The nym address is composed of the
155    /// client identity, the client encryption key, and the gateway identity.
156    pub fn nym_address(&self) -> &Recipient {
157        &self.nym_address
158    }
159
160    /// Get the SOCKS5 proxy URL that a HTTP(S) client can connect to.
161    pub fn socks5_url(&self) -> String {
162        format!("socks5h://{}", self.socks5_config.bind_address)
163    }
164
165    /// Get a shallow clone of [`LaneQueueLengths`]. This is useful to manually implement some form
166    /// of backpressure logic.
167    pub fn shared_lane_queue_lengths(&self) -> LaneQueueLengths {
168        self.client_state.shared_lane_queue_lengths.clone()
169    }
170
171    /// Change the network topology used by this client for constructing sphinx packets into the
172    /// provided one.
173    pub async fn manually_overwrite_topology(&self, new_topology: NymTopology) {
174        self.client_state
175            .topology_accessor
176            .manually_change_topology(new_topology)
177            .await
178    }
179
180    /// Restore default topology refreshing behaviour of this client.
181    pub fn restore_automatic_topology_refreshing(&self) {
182        self.client_state.topology_accessor.release_manual_control()
183    }
184
185    /// Disconnect from the mixnet. Currently it is not supported to reconnect a disconnected
186    /// client.
187    pub async fn disconnect(self) {
188        self.task_handle.shutdown().await;
189    }
190
191    /// Gets the current route provider if topology is available.
192    /// Returns `None` if topology is empty/not yet fetched.
193    async fn read_current_route_provider(&self) -> Option<RwLockReadGuard<'_, NymRouteProvider>> {
194        self.client_state
195            .topology_accessor
196            .current_route_provider()
197            .await
198    }
199
200    /// Wait for topology to become available, with a timeout.
201    /// Returns `Ok(())` when topology is ready, or `Err` if timeout is reached.
202    pub async fn wait_for_topology(&self, timeout: Duration) -> Result<(), NymTopologyError> {
203        let deadline = tokio::time::Instant::now() + timeout;
204        loop {
205            if self.read_current_route_provider().await.is_some() {
206                return Ok(());
207            }
208            if tokio::time::Instant::now() >= deadline {
209                return Err(NymTopologyError::EmptyNetworkTopology);
210            }
211            tokio::time::sleep(Duration::from_millis(100)).await;
212        }
213    }
214}