From a6ea2801026c0642e0b60dbe087f2c8717cd3a3e Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:19:50 +0100 Subject: [PATCH] Fix bugs - force route construction - use same entry = exit --- nym-gateway-probe/src/lib.rs | 89 +++++++++++++++++-- nym-gateway-probe/src/nodes.rs | 12 ++- nym-gateway-probe/src/run.rs | 23 +++-- nym-gateway-probe/src/types.rs | 36 ++++---- .../nym-node-status-agent/run.sh | 16 ++-- sdk/rust/nym-sdk/src/mixnet/client.rs | 6 +- 6 files changed, 145 insertions(+), 37 deletions(-) diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index 655a06f8da..2a321136c9 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -127,6 +127,15 @@ impl CredentialArgs { } } +#[derive(Args)] +pub struct Socks5Args { + #[arg(long, default_value_t = 45)] + mixnet_client_timeout_sec: u64, + + #[arg(long, default_value_t = 10)] + test_count: u64, +} + #[derive(Default, Debug)] pub enum TestedNode { #[default] @@ -202,6 +211,7 @@ pub struct Probe { localnet_entry: Option, /// Localnet exit gateway info (used when --exit-gateway-identity is specified) localnet_exit: Option, + socks5_args: Socks5Args, } impl Probe { @@ -210,6 +220,7 @@ impl Probe { tested_node: TestedNode, netstack_args: NetstackArgs, credentials_args: CredentialArgs, + socks5_args: Socks5Args, ) -> Self { Self { entrypoint, @@ -221,6 +232,7 @@ impl Probe { exit_gateway_node: None, localnet_entry: None, localnet_exit: None, + socks5_args, } } @@ -231,6 +243,7 @@ impl Probe { netstack_args: NetstackArgs, credentials_args: CredentialArgs, gateway_node: DirectoryNode, + socks5_args: Socks5Args, ) -> Self { Self { entrypoint, @@ -242,6 +255,7 @@ impl Probe { exit_gateway_node: None, localnet_entry: None, localnet_exit: None, + socks5_args, } } @@ -253,6 +267,7 @@ impl Probe { credentials_args: CredentialArgs, entry_gateway_node: DirectoryNode, exit_gateway_node: DirectoryNode, + socks5_args: Socks5Args, ) -> Self { Self { entrypoint, @@ -264,6 +279,7 @@ impl Probe { exit_gateway_node: Some(exit_gateway_node), localnet_entry: None, localnet_exit: None, + socks5_args, } } @@ -274,6 +290,7 @@ impl Probe { exit: Option, netstack_args: NetstackArgs, credentials_args: CredentialArgs, + socks5_args: Socks5Args, ) -> Self { let entrypoint = entry.identity; Self { @@ -286,6 +303,7 @@ impl Probe { exit_gateway_node: None, localnet_entry: Some(entry), localnet_exit: exit, + socks5_args, } } @@ -302,7 +320,7 @@ impl Probe { ) -> anyhow::Result { let exit_gateway = match gateway_key { Some(gateway_key) => NodeIdentity::from_base58_string(gateway_key)?, - None => directory.random_exit_with_ipr()?, + None => directory.random_exit_with_nr()?, }; info!("Testing SOCKS5 only on exit gateway {}", exit_gateway); let node_info = directory @@ -311,7 +329,15 @@ impl Probe { let socks5_outcome = { if let Some(ref nr_details) = node_info.network_requester_details { - match do_socks5_connectivity_test(&nr_details.address, network_details).await { + match do_socks5_connectivity_test( + &nr_details.address, + network_details, + Some(&directory), + self.socks5_args.mixnet_client_timeout_sec, + self.socks5_args.test_count, + ) + .await + { Ok(results) => Some(results), Err(e) => { error!("SOCKS5 test failed: {}", e); @@ -1016,6 +1042,9 @@ impl Probe { match do_socks5_connectivity_test( &nr_details.address, NymNetworkDetails::new_from_env(), + directory.as_deref(), + self.socks5_args.mixnet_client_timeout_sec, + self.socks5_args.test_count, ) .await { @@ -1622,14 +1651,15 @@ async fn do_ping_exit( listen_for_icmp_ping_replies(mixnet_client, our_ips).await } -const TEST_REPEAT_COUNT: usize = 10; - /// Creates a SOCKS5 proxy connection through the mixnet to the exit GW /// and performs necessary tests. #[instrument(level = "info", name = "socks5_test", skip_all)] async fn do_socks5_connectivity_test( network_requester_address: &str, network_details: NymNetworkDetails, + directory: Option<&NymApiDirectory>, + mixnet_client_timeout: u64, + test_run_count: u64, ) -> anyhow::Result { info!( "Starting SOCKS5 test through Network Requester: {}", @@ -1639,7 +1669,7 @@ async fn do_socks5_connectivity_test( let mut results = Socks5ProbeResults::default(); // parse the network requester address - let _nr_recipient = match network_requester_address.parse::() { + let nr_recipient = match network_requester_address.parse::() { Ok(addr) => addr, Err(e) => { error!("Invalid Network Requester address: {}", e); @@ -1649,19 +1679,61 @@ async fn do_socks5_connectivity_test( } }; + info!( + "Network Requester gateway: {}", + nr_recipient.gateway().to_base58_string() + ); + info!( + "Network Requester identity: {}", + nr_recipient.identity().to_base58_string() + ); + // create ephemeral SOCKS5 client let socks5_config = Socks5::new(network_requester_address.to_string()); - // mainnet + // 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; + + let Some(directory) = directory else { + bail!("You need to provide Nym API directory through environment") + }; + // 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); + } else { + info!("✔️ Network Requester gateway found in directory with exit_nr role"); + } + + // use intended exit as entry as well + let entry_gateway = nr_gateway_id; + 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) .build()?; // connect to mixnet via SOCKS5 let socks5_client = match socks5_client_builder.connect_to_mixnet_via_socks5().await { Ok(client) => { info!("Successfully established SOCKS5 proxy connection"); + info!( + "Connected via entry gateway: {}", + client.nym_address().gateway().to_base58_string() + ); results.can_connect_socks5 = true; client } @@ -1673,7 +1745,10 @@ async fn do_socks5_connectivity_test( } }; - let test = HttpsConnectivityTest::new(TEST_REPEAT_COUNT); + info!("Waiting for network topology to be ready..."); + tokio::time::sleep(Duration::from_secs(10)).await; + + let test = HttpsConnectivityTest::new(test_run_count, mixnet_client_timeout); results.https_connectivity = test.run_tests(socks5_client.socks5_url()).await; // cleanup diff --git a/nym-gateway-probe/src/nodes.rs b/nym-gateway-probe/src/nodes.rs index 27ce2fcdbf..cd0367a441 100644 --- a/nym-gateway-probe/src/nodes.rs +++ b/nym-gateway-probe/src/nodes.rs @@ -389,6 +389,16 @@ impl NymApiDirectory { .map(|(id, _)| *id) } + pub fn random_exit_with_nr(&self) -> anyhow::Result { + info!("Selecting random gateway with NR enabled"); + self.nodes + .iter() + .filter(|(_, n)| n.described.description.ip_packet_router.is_some()) + .choose(&mut rand::thread_rng()) + .ok_or(anyhow!("no gateways running NR available")) + .map(|(id, _)| *id) + } + pub fn random_entry_gateway(&self) -> anyhow::Result { info!("Selecting random entry gateway"); self.nodes @@ -418,7 +428,7 @@ impl NymApiDirectory { pub fn exit_gateway_nr(&self, identity: &NodeIdentity) -> anyhow::Result { let Some(maybe_entry) = self.nodes.get(identity).cloned() else { - bail!("{identity} does not exist") + bail!("{identity} not found in directory") }; if !maybe_entry.described.description.declared_role.exit_nr { bail!("{identity} doesn't support exit NR mode") diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index cc3d732e0a..25aebbaa91 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -5,10 +5,10 @@ use anyhow::bail; use clap::{Parser, Subcommand}; use nym_bin_common::bin_info; use nym_config::defaults::setup_env; -use nym_gateway_probe::nodes::{NymApiDirectory, query_gateway_by_ip}; -use nym_gateway_probe::{ - CredentialArgs, NetstackArgs, ProbeResult, TestMode, TestedNode, TestedNodeDetails, -}; +use nym_gateway_probe::nodes::NymApiDirectory; +use nym_gateway_probe::nodes::query_gateway_by_ip; +use nym_gateway_probe::{CredentialArgs, NetstackArgs, ProbeResult, Socks5Args, TestedNode}; +use nym_gateway_probe::{TestMode, TestedNodeDetails}; use nym_sdk::mixnet::NodeIdentity; use std::net::SocketAddr; use std::path::Path; @@ -131,6 +131,10 @@ struct CliArgs { /// Arguments to manage credentials #[command(flatten)] credential_args: CredentialArgs, + + /// Arguments to configure socks5 probe + #[command(flatten)] + socks5_args: Socks5Args, } const DEFAULT_CONFIG_DIR: &str = "/tmp/nym-gateway-probe/config/"; @@ -295,6 +299,7 @@ pub(crate) async fn run() -> anyhow::Result { exit_details, args.netstack_args, args.credential_args, + args.socks5_args, ); if let Some(awg_args) = args.amnezia_args { @@ -429,6 +434,7 @@ pub(crate) async fn run() -> anyhow::Result { args.credential_args, entry_node.clone(), exit_node.clone(), + args.socks5_args, ) } else if let Some(gw_node) = gateway_node { // Only entry gateway provided @@ -438,10 +444,17 @@ pub(crate) async fn run() -> anyhow::Result { args.netstack_args, args.credential_args, gw_node, + args.socks5_args, ) } else { // No direct gateways, use directory lookup - nym_gateway_probe::Probe::new(entry, test_point, args.netstack_args, args.credential_args) + nym_gateway_probe::Probe::new( + entry, + test_point, + args.netstack_args, + args.credential_args, + args.socks5_args, + ) }; if let Some(awg_args) = args.amnezia_args { diff --git a/nym-gateway-probe/src/types.rs b/nym-gateway-probe/src/types.rs index 7f21960c8d..9710d8a0d0 100644 --- a/nym-gateway-probe/src/types.rs +++ b/nym-gateway-probe/src/types.rs @@ -172,19 +172,20 @@ impl HttpsConnectivityResult { } pub struct HttpsConnectivityTest { - test_count: usize, + test_count: u64, + mixnet_client_timeout: Duration, } -/// currently we test against this endpoint +/// endpoint to test against /// https://www.quicknode.com/docs/ethereum/web3_clientVersion const TARGET_URL: &str = "https://docs-demo.quiknode.pro"; const POST_BODY: &str = r#"{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}"#; -const MIXNET_TIMEOUT: Duration = Duration::from_secs(60); impl HttpsConnectivityTest { - pub fn new(test_count: usize) -> Self { + pub fn new(test_count: u64, mixnet_client_timeout: u64) -> Self { Self { test_count: std::cmp::max(test_count, 1), + mixnet_client_timeout: Duration::from_secs(mixnet_client_timeout), } } @@ -201,8 +202,7 @@ impl HttpsConnectivityTest { let client = match reqwest::Client::builder() .proxy(proxy) - // longer timeout for mixnet - .timeout(MIXNET_TIMEOUT) + .timeout(self.mixnet_client_timeout) .build() { Ok(c) => c, @@ -212,9 +212,9 @@ impl HttpsConnectivityTest { } }; - let mut successful_runs = 0; - for i in 0..self.test_count { - info!("Running test {}/{}", i + 1, self.test_count); + let mut successful_runs = 0u64; + for i in 1..self.test_count + 1 { + info!("Running test {}/{}", i, self.test_count); let interim_res = self.perform_https_request(&client).await; if interim_res.https_success && let Some(latency_ms) = interim_res.https_latency_ms @@ -227,11 +227,17 @@ impl HttpsConnectivityTest { ); result.https_success = true; result.https_status_code = interim_res.https_status_code; - info!("{}/{} latency: {}ms", i + 1, self.test_count, latency_ms); + info!("{}/{} latency: {}ms", i, self.test_count, latency_ms); } else if let Some(new_error) = interim_res.error { - result.error = result - .error - .map(|existing| format!("{},{}", existing, new_error)); + result.error = Some(result.error.map_or(new_error.clone(), |existing| { + format!("{},{}", existing, new_error) + })) + } + + // too many failed runs: return early + if successful_runs < 2 && i - successful_runs > 2 { + // if < 2 runs, we don't have to calculate average before returning + return result; } } result.https_latency_ms = result @@ -251,7 +257,7 @@ impl HttpsConnectivityTest { let mut result = HttpsConnectivityResult::default(); let start = Instant::now(); match tokio::time::timeout( - MIXNET_TIMEOUT, + self.mixnet_client_timeout, client .post(TARGET_URL) .header(reqwest::header::CONTENT_TYPE, "application/json") @@ -281,7 +287,7 @@ impl HttpsConnectivityTest { Err(_) => { warn!( "HTTPS request timed out after {}s", - MIXNET_TIMEOUT.as_secs() + self.mixnet_client_timeout.as_secs() ); if result.error.is_none() { result.error = Some("HTTPS request timed out".to_string()); diff --git a/nym-node-status-api/nym-node-status-agent/run.sh b/nym-node-status-api/nym-node-status-agent/run.sh index 1061ad5f95..2a5a0b5e41 100755 --- a/nym-node-status-api/nym-node-status-agent/run.sh +++ b/nym-node-status-api/nym-node-status-agent/run.sh @@ -1,15 +1,16 @@ #!/bin/bash +# used primarily for local testing + set -eu export ENVIRONMENT=${ENVIRONMENT:-"mainnet"} -probe_git_ref="nym-vpn-core-v1.4.0" - crate_root=$(dirname $(realpath "$0")) +echo crate_root=${crate_root} monorepo_root=$(realpath "${crate_root}/../..") +echo monorepo_root=${monorepo_root} -echo "Expecting nym-vpn-client repo at a sibling level of nym monorepo dir" -gateway_probe_src=$(dirname "${monorepo_root}")/nym-vpn-client/nym-vpn-core +gateway_probe_src="${monorepo_root}/nym-gateway-probe" echo "gateway_probe_src=$gateway_probe_src" set -a @@ -25,7 +26,8 @@ export RUST_LOG="info" NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" NODE_STATUS_AGENT_SERVER_PORT="8000" SERVER="${NODE_STATUS_AGENT_SERVER_ADDRESS}|${NODE_STATUS_AGENT_SERVER_PORT}" -export NODE_STATUS_AGENT_AUTH_KEY="BjyC9SsHAZUzPRkQR4sPTvVrp4GgaquTh5YfSJksvvWT" +# hardcoded key used only for LOCAL TESTING +export NODE_STATUS_AGENT_AUTH_KEY=${NODE_STATUS_AGENT_AUTH_KEY_STAGING:-"BjyC9SsHAZUzPRkQR4sPTvVrp4GgaquTh5YfSJksvvWT"} export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" export NODE_STATUS_AGENT_PROBE_EXTRA_ARGS="netstack-download-timeout-sec=30,netstack-num-ping=2,netstack-send-timeout-sec=1,netstack-recv-timeout-sec=1" @@ -35,11 +37,9 @@ echo "Running $workers workers in parallel" # build & copy over GW probe function copy_gw_probe() { pushd $gateway_probe_src - git fetch -a - git checkout $probe_git_ref cargo build --release --package nym-gateway-probe - cp target/release/nym-gateway-probe "$crate_root" + cp "${monorepo_root}/target/release/nym-gateway-probe" "$crate_root" $crate_root/nym-gateway-probe --version popd diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 5020c1bd5c..7fe176b05b 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -8,7 +8,7 @@ use crate::mixnet::{CredentialStorage, MixnetClient, Recipient}; use crate::GatewayTransceiver; use crate::NymNetworkDetails; use crate::{Error, Result}; -use log::{debug, warn}; +use log::{debug, info, warn}; use nym_client_core::client::base_client::storage::gateways_storage::GatewayRegistration; use nym_client_core::client::base_client::storage::helpers::{ get_active_gateway_identity, get_all_registered_identities, has_gateway_details, @@ -601,6 +601,10 @@ where ); let available_gateways = self.available_gateways().await?; + info!("Listing all available gateways in topology:"); + for node in available_gateways.iter() { + info!("{}", node.identity_key.to_base58_string()); + } Ok(GatewaySetup::New { specification: selection_spec,