- force route construction
- use same entry = exit
This commit is contained in:
dynco-nym
2025-12-19 14:19:50 +01:00
parent 3ea25c40f4
commit a6ea280102
6 changed files with 145 additions and 37 deletions
+82 -7
View File
@@ -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<TestedNodeDetails>,
/// Localnet exit gateway info (used when --exit-gateway-identity is specified)
localnet_exit: Option<TestedNodeDetails>,
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<TestedNodeDetails>,
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<ProbeResult> {
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<Socks5ProbeResults> {
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::<Recipient>() {
let nr_recipient = match network_requester_address.parse::<Recipient>() {
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
+11 -1
View File
@@ -389,6 +389,16 @@ impl NymApiDirectory {
.map(|(id, _)| *id)
}
pub fn random_exit_with_nr(&self) -> anyhow::Result<NodeIdentity> {
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<NodeIdentity> {
info!("Selecting random entry gateway");
self.nodes
@@ -418,7 +428,7 @@ impl NymApiDirectory {
pub fn exit_gateway_nr(&self, identity: &NodeIdentity) -> anyhow::Result<DirectoryNode> {
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")
+18 -5
View File
@@ -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<ProbeResult> {
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<ProbeResult> {
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<ProbeResult> {
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 {
+21 -15
View File
@@ -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());
@@ -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
+5 -1
View File
@@ -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,