diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index d8992617ee..5a2df84c49 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -7,7 +7,7 @@ use std::{ time::Duration, }; -use crate::socks5_test::{HttpsConnectivityResult, HttpsConnectivityTest}; +use crate::socks5_test::HttpsConnectivityTest; use crate::types::Entry; use anyhow::bail; use base64::{Engine as _, engine::general_purpose}; @@ -19,6 +19,10 @@ use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, v2, v3, v4, v5, v6, }; +use nym_client_core::TopologyProvider; +use nym_client_core::client::topology_control::{ + NymApiTopologyProvider, NymApiTopologyProviderConfig, +}; use nym_client_core::config::ForgetMe; use nym_config::defaults::{ NymNetworkDetails, @@ -35,10 +39,15 @@ use nym_ip_packet_requests::{ ControlResponse, DataResponse, InfoLevel, IpPacketResponse, IpPacketResponseData, }, }; -use nym_sdk::mixnet::{ - CredentialStorage, Ephemeral, KeyStore, MixnetClient, MixnetClientBuilder, MixnetClientStorage, - NodeIdentity, Recipient, ReconstructedMessage, Socks5, StoragePaths, +use nym_sdk::{ + DebugConfig, + mixnet::{ + CredentialStorage, Ephemeral, KeyStore, MixnetClient, MixnetClientBuilder, + MixnetClientStorage, NodeIdentity, Recipient, ReconstructedMessage, Socks5, StoragePaths, + }, }; +use nym_topology::NymTopology; +use nym_topology::provider_trait::HardcodedTopologyProvider; use nym_validator_client::models::NetworkRequesterDetails; use rand::rngs::OsRng; use std::path::PathBuf; @@ -130,7 +139,7 @@ impl CredentialArgs { #[derive(Args)] pub struct Socks5Args { - #[arg(long, value_delimiter = ',')] + #[arg(long, value_delimiter = ';')] socks5_json_rpc_url_list: Vec, #[arg(long, default_value_t = 30)] @@ -320,44 +329,6 @@ impl Probe { self } - pub async fn test_socks5_only( - self, - directory: NymApiDirectory, - gateway_key: Option, - network_details: NymNetworkDetails, - ) -> anyhow::Result { - let exit_gateway = match gateway_key { - Some(gateway_key) => NodeIdentity::from_base58_string(gateway_key)?, - None => directory.random_exit_with_nr()?, - }; - info!("Testing SOCKS5 only on exit gateway {}", exit_gateway); - let node_info = directory - .exit_gateway_nr(&exit_gateway)? - .to_testable_node()?; - - let socks5_outcome = self - .test_socks5_if_possible( - network_details, - &node_info.network_requester_details, - &directory, - ) - .await; - - let probe_result = ProbeResult { - node: exit_gateway.to_base58_string(), - used_entry: exit_gateway.to_base58_string(), - outcome: ProbeOutcome { - as_entry: Entry::NotTested, - as_exit: None, - socks5: socks5_outcome, - wg: None, - lp: None, - }, - }; - - Ok(probe_result) - } - #[allow(clippy::too_many_arguments)] pub async fn probe( self, @@ -395,6 +366,15 @@ impl Probe { let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await; + // Extract topology from the connected client (if successful) to reuse for SOCKS5 test + let topology = match &mixnet_client { + Ok(client) => client + .read_current_route_provider() + .await + .map(|rp| rp.topology.clone()), + Err(_) => None, + }; + // Convert legacy flags to TestMode let has_exit = self.exit_gateway_node.is_some() || self.localnet_exit.is_some(); let test_mode = @@ -412,6 +392,7 @@ impl Probe { only_wireguard, false, // Not using mock ecash in regular probe mode network_details, + topology, ) .await } @@ -471,6 +452,7 @@ impl Probe { only_wireguard, use_mock_ecash, network_details, + None, // No topology (no mixnet client in localnet mode) ) .await; } @@ -519,6 +501,7 @@ impl Probe { only_wireguard, use_mock_ecash, network_details, + None, // No topology (no mixnet client in direct gateway mode) ) .await; } @@ -594,6 +577,15 @@ impl Probe { let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await; + // extract topology from the connected client (if any) to reuse for SOCKS5 test + let topology = match &mixnet_client { + Ok(client) => client + .read_current_route_provider() + .await + .map(|rp| rp.topology.clone()), + Err(_) => None, + }; + // Convert legacy flags to TestMode let has_exit = self.exit_gateway_node.is_some() || self.localnet_exit.is_some(); let test_mode = @@ -611,6 +603,7 @@ impl Probe { only_wireguard, use_mock_ecash, network_details, + topology, ) .await } @@ -703,6 +696,7 @@ impl Probe { network_details: NymNetworkDetails, network_requester_details: &Option, directory: &NymApiDirectory, + topology: Option, ) -> Option { if let Some(nr_details) = network_requester_details { match do_socks5_connectivity_test( @@ -713,6 +707,7 @@ impl Probe { self.socks5_args.mixnet_client_timeout_sec, self.socks5_args.test_count, self.socks5_args.failure_count_cutoff, + topology, ) .await { @@ -801,6 +796,7 @@ impl Probe { only_wireguard: bool, use_mock_ecash: bool, network_details: NymNetworkDetails, + topology: Option, ) -> anyhow::Result where T: MixnetClientStorage + Clone + 'static, @@ -1072,6 +1068,7 @@ impl Probe { network_details, &node_info.network_requester_details, directory, + topology, ) .await; @@ -1668,6 +1665,7 @@ async fn do_ping_exit( /// Creates a SOCKS5 proxy connection through the mixnet to the exit GW /// and performs necessary tests. +#[allow(clippy::too_many_arguments)] #[instrument(level = "info", name = "socks5_test", skip_all)] async fn do_socks5_connectivity_test( network_requester_address: &str, @@ -1677,6 +1675,7 @@ async fn do_socks5_connectivity_test( mixnet_client_timeout: u64, test_run_count: u64, failure_count_cutoff: usize, + topology: Option, ) -> anyhow::Result { info!( "Starting SOCKS5 test through Network Requester: {}", @@ -1686,16 +1685,16 @@ async fn do_socks5_connectivity_test( bail!("You need to define JSON RPC URLs in order to test SOCKS5") } - let mut results = Socks5ProbeResults::default(); - // parse the network requester address let nr_recipient = match network_requester_address.parse::() { Ok(addr) => addr, Err(e) => { error!("Invalid Network Requester address: {}", e); - results.https_connectivity = - HttpsConnectivityResult::with_error(format!("Invalid NR address: {}", e)); - return Ok(results); + + return Ok(Socks5ProbeResults::error_before_connecting(format!( + "Invalid NR address: {}", + e + ))); } }; @@ -1711,23 +1710,18 @@ async fn do_socks5_connectivity_test( // create ephemeral SOCKS5 client let socks5_config = Socks5::new(network_requester_address.to_string()); - // Create debug config similar to main probe - let mut debug_config = nym_client_core::config::DebugConfig::default(); - debug_config - .traffic - .disable_main_poisson_packet_distribution = true; - debug_config.cover_traffic.disable_loop_cover_traffic_stream = true; - debug_config.topology.ignore_egress_epoch_role = true; // since we define both entry & exit gateways to be the same tested GW, // this shouldn't negatively affect mixnet layers but it will force route - // construction in case GW would get filtered out on topology refresh - debug_config.topology.minimum_gateway_performance = 0; + // construction in case GW would get filtered out of topology + let min_gw_performance = Some(0); + + // debug config similar to main probe + let debug_config = mixnet_debug_config(min_gw_performance, true); // Verify the NR gateway exists in the directory with exit_nr role let nr_gateway_id = nr_recipient.gateway(); if let Err(e) = directory.exit_gateway_nr(&nr_gateway_id) { - results.https_connectivity = HttpsConnectivityResult::with_error(e.to_string()); - return Ok(results); + return Ok(Socks5ProbeResults::error_before_connecting(e.to_string())); } else { info!("✔️ Network Requester gateway found in directory with exit_nr role"); } @@ -1735,12 +1729,28 @@ async fn do_socks5_connectivity_test( // use intended exit as entry as well let entry_gateway = nr_gateway_id; + // use existing topology if available, otherwise fetch it + let topology_provider: Box = match topology { + Some(t) => { + info!("✔️ Reusing topology from main mixnet client"); + Box::new(HardcodedTopologyProvider::new(t)) + } + None => { + info!("Fetching topology for SOCKS5 client..."); + match hardcoded_topology(&network_details, &debug_config).await { + Ok(provider) => provider, + Err(e) => return Ok(Socks5ProbeResults::error_before_connecting(e)), + } + } + }; + let socks5_client_builder = MixnetClientBuilder::new_ephemeral() // Specify entry gateway explicitly .request_gateway(entry_gateway.to_base58_string()) .socks5_config(socks5_config) .network_details(network_details) .debug_config(debug_config) + .custom_topology_provider(topology_provider) .build()?; // connect to mixnet via SOCKS5 @@ -1751,43 +1761,68 @@ async fn do_socks5_connectivity_test( "Connected via entry gateway: {}", client.nym_address().gateway().to_base58_string() ); - results.can_connect_socks5 = true; client } Err(e) => { error!("Failed to establish SOCKS5 connection: {}", e); - results.https_connectivity = - HttpsConnectivityResult::with_error(format!("SOCKS5 connection failed: {}", e)); - return Ok(results); + return Ok(Socks5ProbeResults::error_before_connecting(format!( + "SOCKS5 connection failed: {}", + e + ))); } }; - info!("Waiting for network topology to be ready..."); - let topology_timeout = Duration::from_secs(60); - if let Err(e) = socks5_client.wait_for_topology(topology_timeout).await { - error!( - "Topology not available after {}s: {}", - topology_timeout.as_secs(), - e - ); - results.https_connectivity = - HttpsConnectivityResult::with_error(format!("Topology timeout: {}", e)); - socks5_client.disconnect().await; - return Ok(results); - } else { - info!("Network topology is ready") - } - let test = HttpsConnectivityTest::new(test_run_count, mixnet_client_timeout, json_rpc_endpoints); - results.https_connectivity = test + let result = test .run_tests(socks5_client.socks5_url(), failure_count_cutoff) .await; - // cleanup socks5_client.disconnect().await; - Ok(results) + Ok(Socks5ProbeResults::with_http_result(result)) +} + +async fn hardcoded_topology( + network_details: &NymNetworkDetails, + debug_config: &DebugConfig, +) -> Result, String> { + // get Nym API URLs from network_details + let nym_api_urls: Vec = network_details + .nym_api_urls + .as_ref() + .map(|urls| urls.iter().filter_map(|u| u.url.parse().ok()).collect()) + .or_else(|| { + network_details + .endpoints + .first() + .and_then(|e| e.api_url()) + .map(|url| vec![url]) + }) + .unwrap_or_default(); + + if nym_api_urls.is_empty() { + return Err(String::from("No nym-api URLs available to fetch topology")); + } + + let topology_config = NymApiTopologyProviderConfig { + min_mixnode_performance: debug_config.topology.minimum_mixnode_performance, + min_gateway_performance: debug_config.topology.minimum_gateway_performance, + use_extended_topology: debug_config.topology.use_extended_topology, + ignore_egress_epoch_role: debug_config.topology.ignore_egress_epoch_role, + }; + + let api_client = nym_http_api_client::Client::new_url(nym_api_urls[0].clone(), None) + .map_err(|e| e.to_string())?; + let mut provider = NymApiTopologyProvider::new(topology_config, nym_api_urls, api_client); + + match provider.get_new_topology().await { + Some(topology) => { + info!("Fetched network topology"); + Ok(Box::new(HardcodedTopologyProvider::new(topology))) + } + None => Err(String::from("Failed to fetch network topology")), + } } async fn send_icmp_pings( diff --git a/nym-gateway-probe/src/socks5_test.rs b/nym-gateway-probe/src/socks5_test.rs index 4832b9e4d9..eaa7be015a 100644 --- a/nym-gateway-probe/src/socks5_test.rs +++ b/nym-gateway-probe/src/socks5_test.rs @@ -228,6 +228,27 @@ impl HttpsConnectivityResult { }, } } + + pub fn https_success(&self) -> bool { + self.https_success + } + + pub fn https_status_code(&self) -> Option<&u16> { + self.https_status_code.as_ref() + } + + pub fn https_latency_ms(&self) -> Option<&u64> { + self.https_latency_ms.as_ref() + } + + pub fn endpoint_used(&self) -> Option<&String> { + self.endpoint_used.as_ref() + } + + pub fn error(&self) -> Option<&String> { + self.error.as_ref() + } + } /// https://www.jsonrpc.org/specification diff --git a/nym-gateway-probe/src/types.rs b/nym-gateway-probe/src/types.rs index f3c3ab28c6..449969eadc 100644 --- a/nym-gateway-probe/src/types.rs +++ b/nym-gateway-probe/src/types.rs @@ -1,7 +1,7 @@ use nym_connection_monitor::ConnectionStatusEvent; use serde::{Deserialize, Serialize}; -use crate::socks5_test::HttpsConnectivityResult; +pub use super::socks5_test::HttpsConnectivityResult; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProbeResult { @@ -138,10 +138,33 @@ impl Exit { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Socks5ProbeResults { /// whether we could establish a SOCKS5 proxy connection - pub can_connect_socks5: bool, + can_connect_socks5: bool, /// HTTPS connectivity test - pub https_connectivity: HttpsConnectivityResult, + https_connectivity: HttpsConnectivityResult, +} + +impl Socks5ProbeResults { + pub fn with_http_result(https_connectivity: HttpsConnectivityResult) -> Self { + Self { + can_connect_socks5: true, + https_connectivity, + } + } + + pub fn error_before_connecting(error: impl Into) -> Self { + Self { + can_connect_socks5: false, + https_connectivity: HttpsConnectivityResult::with_error(error.into()), + } + } + + pub fn error_after_connecting(error: impl Into) -> Self { + Self { + can_connect_socks5: true, + https_connectivity: HttpsConnectivityResult::with_error(error.into()), + } + } } #[derive(Debug, Clone, Default)] diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index c9b821acab..20e562e662 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -601,7 +601,6 @@ where ); let available_gateways = self.available_gateways().await?; - debug!("Listing all available gateways in topology:"); for node in available_gateways.iter() { debug!( "node_id={}, identity_key={}", diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 1875924616..d3c920669a 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -1,13 +1,9 @@ -use std::time::Duration; - use nym_client_core::client::base_client::ClientState; 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 nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; +use nym_topology::NymTopology; use crate::mixnet::client::MixnetClientBuilder; use crate::Result; @@ -88,28 +84,4 @@ impl Socks5MixnetClient { pub async fn disconnect(self) { self.task_handle.shutdown().await; } - - /// Gets the current route provider if topology is available. - /// Returns `None` if topology is empty/not yet fetched. - async fn read_current_route_provider(&self) -> Option> { - self.client_state - .topology_accessor - .current_route_provider() - .await - } - - /// Wait for topology to become available, with a timeout. - /// Returns `Ok(())` when topology is ready, or `Err` if timeout is reached. - pub async fn wait_for_topology(&self, timeout: Duration) -> Result<(), NymTopologyError> { - let deadline = tokio::time::Instant::now() + timeout; - loop { - if self.read_current_route_provider().await.is_some() { - return Ok(()); - } - if tokio::time::Instant::now() >= deadline { - return Err(NymTopologyError::EmptyNetworkTopology); - } - tokio::time::sleep(Duration::from_millis(100)).await; - } - } }