From a43dd38e76fa95385d041c4f59b2fa899dc2e068 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Wed, 11 Mar 2026 09:38:34 +0100 Subject: [PATCH] add support for not registered nodes ... --- nym-gateway-probe/netstack_ping/lib.go | 2 +- nym-gateway-probe/src/config/netstack.rs | 5 +- nym-gateway-probe/src/lib.rs | 125 ++++++++++++++--- nym-gateway-probe/src/run.rs | 163 ++++++++++++++++++++--- 4 files changed, 258 insertions(+), 37 deletions(-) diff --git a/nym-gateway-probe/netstack_ping/lib.go b/nym-gateway-probe/netstack_ping/lib.go index 8ab6b371ce..9beb635d0c 100644 --- a/nym-gateway-probe/netstack_ping/lib.go +++ b/nym-gateway-probe/netstack_ping/lib.go @@ -618,7 +618,7 @@ func checkPorts(target string, ports []uint16, timeoutSec uint64, tnet *netstack // testing all 114 ports in one burst trips it after ~29 connections. // small batches with a cooldown in between keep us under the limit. const batchSize = 20 - const batchDelay = 45 * time.Second + const batchDelay = 25 * time.Second const dialGap = 200 * time.Millisecond results := make(map[string]bool) diff --git a/nym-gateway-probe/src/config/netstack.rs b/nym-gateway-probe/src/config/netstack.rs index d07ba86558..7fae2b4591 100644 --- a/nym-gateway-probe/src/config/netstack.rs +++ b/nym-gateway-probe/src/config/netstack.rs @@ -47,12 +47,11 @@ pub struct NetstackArgs { #[arg(long = "use-target", default_value = "portquiz.net")] pub port_check_target: String, - /// TCP ports to check through the WireGuard tunnel for exit policy verification. - /// Only used with the `run-ports` subcommand. For all exit policy ports, use --check-all-ports instead. + /// List ports to check, separated by a comma. #[arg(long = "check-ports", value_delimiter = ',', default_values_t = Vec::::new())] pub port_check_ports: Vec, - /// Timeout in seconds for each individual TCP port check + /// Timeout in seconds for each individual port check attempt #[arg(long, default_value_t = 5)] pub port_check_timeout_sec: u64, } diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index fdaea8a1bb..0d8e003b42 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -8,7 +8,7 @@ use crate::common::probe_tests::{ do_ping, do_socks5_connectivity_test, lp_registration_probe, wg_probe, }; use crate::common::types::{Entry, LpProbeResults}; -use crate::config::{CredentialArgs, CredentialMode, ProbeConfig}; +use crate::config::{CredentialArgs, CredentialMode, NetstackArgs, ProbeConfig}; use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient}; use nym_bandwidth_controller::BandwidthTicketProvider; use nym_client_core::config::ForgetMe; @@ -21,6 +21,7 @@ use nym_sdk::mixnet::{ use nym_topology::{HardcodedTopologyProvider, NymTopology}; use rand::rngs::OsRng; use std::collections::HashMap; +use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use tokio_util::sync::CancellationToken; @@ -298,12 +299,15 @@ impl Probe { } /// Run a port-check probe against the exit gateway's WG exit policy - pub async fn probe_run_ports( - mut self, + pub async fn run_ports_bonded( + entry_node: TestedNodeDetails, + exit_node: Option, + network: NymNetworkDetails, + config: &RunPortsConfig, config_dir: &PathBuf, credential: CredentialMode, ) -> anyhow::Result { - let exit_node = self.exit_node.take().unwrap_or(self.entry_node.clone()); + let exit_node = exit_node.unwrap_or(entry_node.clone()); let exit_identity = exit_node.identity.to_string(); // need authenticator + IP to be a functional exit @@ -318,8 +322,8 @@ impl Probe { } }; - let ports = self.config.netstack_args.port_check_ports.clone(); - let port_check_target = self.config.netstack_args.port_check_target.clone(); + let ports = config.netstack_args.port_check_ports.clone(); + let port_check_target = config.netstack_args.port_check_target.clone(); if ports.is_empty() { anyhow::bail!( @@ -341,23 +345,23 @@ impl Probe { .await?; let mixnet_debug_config = helpers::mixnet_debug_config( - self.config.min_gateway_mixnet_performance, - self.config.ignore_egress_epoch_role, + config.min_gateway_mixnet_performance, + config.ignore_egress_epoch_role, ); - self.topology = helpers::fetch_topology(&self.network, &mixnet_debug_config) + let topology = helpers::fetch_topology(&network, &mixnet_debug_config) .await .inspect_err(|e| warn!("Failed to fetch topology: {e}")) .ok(); let mut mixnet_client_builder = MixnetClientBuilder::new_with_storage(storage.clone()) - .request_gateway(self.entry_node.identity.to_string()) - .network_details(self.network.clone()) + .request_gateway(entry_node.identity.to_string()) + .network_details(network.clone()) .debug_config(mixnet_debug_config) .with_forget_me(ForgetMe::new_stats()) .credentials_mode(!credential.use_mock_ecash); - if let Some(topology) = &self.topology { + if let Some(topology) = &topology { mixnet_client_builder = mixnet_client_builder.custom_topology_provider(Box::new( HardcodedTopologyProvider::new(topology.clone()), )); @@ -379,7 +383,7 @@ impl Probe { .await?; let bandwidth_provider = build_bandwidth_controller( - &self.network, + &network, storage.credential_store().clone(), credential.use_mock_ecash, )?; @@ -388,7 +392,7 @@ impl Probe { Ok(client) => { info!( "Connected to mixnet via entry gateway: {}", - self.entry_node.identity + entry_node.identity ); info!("Our nym address: {}", *client.nym_address()); client @@ -451,7 +455,7 @@ impl Probe { } }; - let netstack_args = self.config.netstack_args.clone(); + let netstack_args = config.netstack_args.clone(); let mut rng = rand::thread_rng(); let auth_client = AuthenticatorClient::new( mixnet_listener_task.subscribe(), @@ -467,7 +471,7 @@ impl Probe { auth_client, ip_address, exit_node.authenticator_version, - self.config.amnezia_args.clone(), + None, netstack_args, true, // port_check_only credential, @@ -509,6 +513,95 @@ impl Probe { }) } + /// Run a direct TCP port check against an IP-only target gateway. + /// This bypasses mixnet and auth registration. It is intended for unannounced/local gateways + pub async fn probe_run_ports_direct_ip( + gateway_address: &str, + target_ip: IpAddr, + config: &RunPortsConfig, + protocol: DirectPortCheckProtocol, + ) -> anyhow::Result { + let ports = config.netstack_args.port_check_ports.clone(); + if ports.is_empty() { + anyhow::bail!( + "No ports specified. Use --check-ports 80,443,22021 or --check-all-ports" + ); + } + + // Preferred truth source: gateway-declared exit policy. + if let Ok(used_policy) = + crate::common::nodes::query_exit_policy_by_ip(gateway_address).await + { + if used_policy.enabled { + if let Some(policy) = used_policy.policy { + let mut policy_results = HashMap::with_capacity(ports.len()); + let v4_probe = std::net::IpAddr::V4(std::net::Ipv4Addr::new(1, 1, 1, 1)); + let v6_probe = std::net::IpAddr::V6(std::net::Ipv6Addr::new( + 0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111, + )); + + for port in &ports { + let allowed_v4 = policy.allows(&v4_probe, *port).unwrap_or(false); + let allowed_v6 = policy.allows(&v6_probe, *port).unwrap_or(false); + policy_results.insert(port.to_string(), allowed_v4 || allowed_v6); + } + + return Ok(PortCheckResult { + gateway: gateway_address.to_string(), + can_register: true, + port_check_target: gateway_address.to_string(), + ports: policy_results, + error: None, + }); + } + } + } + + let timeout_duration = + std::time::Duration::from_secs(config.netstack_args.port_check_timeout_sec); + let mut port_results = HashMap::with_capacity(ports.len()); + + info!( + "Direct {:?} port check: testing {} ports on {} (timeout {}s per port)", + protocol, + ports.len(), + target_ip, + config.netstack_args.port_check_timeout_sec + ); + + for port in ports { + let socket = SocketAddr::new(target_ip, port); + let is_open = match protocol { + DirectPortCheckProtocol::Auto => { + Probe::check_tcp_socket(socket, timeout_duration).await + || Probe::check_udp_socket(socket, timeout_duration).await + } + DirectPortCheckProtocol::Tcp => { + Probe::check_tcp_socket(socket, timeout_duration).await + } + DirectPortCheckProtocol::Udp => { + Probe::check_udp_socket(socket, timeout_duration).await + } + }; + port_results.insert(port.to_string(), is_open); + } + + let open = port_results.values().filter(|&&is_open| is_open).count(); + info!( + "Direct port check complete: {}/{} ports reachable", + open, + port_results.len() + ); + + Ok(PortCheckResult { + gateway: gateway_address.to_string(), + can_register: true, + port_check_target: gateway_address.to_string(), + ports: port_results, + error: None, + }) + } + #[allow(clippy::too_many_arguments)] pub async fn do_probe_test( self, diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index 00eac13661..8f6535e6ca 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -1,11 +1,14 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use clap::{Args, Parser, Subcommand}; +use clap::{ArgGroup, Args, Parser, Subcommand, ValueEnum}; use nym_bin_common::bin_info; use nym_config::defaults::setup_env; -use nym_gateway_probe::config::{CredentialArgs, CredentialMode, ProbeConfig}; -use nym_gateway_probe::{NymApiDirectory, PortCheckResult, ProbeResult, query_gateway_by_ip}; +use nym_gateway_probe::config::{CredentialArgs, CredentialMode, NetstackArgs, ProbeConfig}; +use nym_gateway_probe::{ + DirectPortCheckProtocol, NymApiDirectory, PortCheckResult, ProbeResult, RunPortsConfig, + query_gateway_by_ip, +}; use nym_sdk::mixnet::NodeIdentity; use serde::Serialize; use std::path::Path; @@ -90,12 +93,20 @@ enum Commands { #[arg(long)] config_dir: Option, - /// Gateway to test (used as both entry and exit unless --exit-gateway is set) - #[arg(long, short = 'g', alias = "gateway")] - entry_gateway: NodeIdentity, + /// Bonded gateway identity. + /// Cannot be used with --gateway-ip. + #[arg(long, short = 'g', alias = "gateway", conflicts_with = "gateway_ip")] + entry_gateway: Option, + + /// Gateway queried directly by IP address (unannounced/local gateways) + /// Cannot be used with --gateway/--entry-gateway. + /// Cannot be used with --mnemonic or --use-mock-ecash. + #[arg(long, conflicts_with = "entry_gateway", conflicts_with = "gateway")] + gateway_ip: Option, /// Separate exit gateway to test (entry_gateway is used for mixnet entry) - #[arg(long)] + /// Cannot be used with --gateway-ip. + #[arg(long, conflicts_with = "gateway_ip")] exit_gateway: Option, /// Test every port in the canonical exit policy (network-tunnel-manager.sh PORT_MAPPINGS). @@ -103,12 +114,17 @@ enum Commands { #[arg(long)] check_all_ports: bool, - /// Arguments to manage credentials + /// Specify the protocol used for tests with --gateway-ip. + #[arg(long, value_enum, default_value_t = PortCheckProtocol::Auto)] + check_protocol: PortCheckProtocol, + + /// Optional credential arguments. + /// Required only in bonded mode (when using --gateway/--entry-gateway). #[command(flatten)] - credential_mode: CredentialMode, + credential_mode: OptionalCredentialMode, #[command(flatten)] - probe_config: ProbeConfig, + probe_config: RunPortsProbeConfig, }, /// Run the probe by NS agents @@ -126,6 +142,60 @@ enum Commands { }, } +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +enum PortCheckProtocol { + Auto, + Tcp, + Udp, +} + +#[derive(Debug, Args, Clone)] +#[command(group( + ArgGroup::new("run_ports_credential_mode") + .args(["use_mock_ecash","mnemonic"]) + .required(false) + .multiple(false) +))] +struct OptionalCredentialMode { + /// Use mock ecash credentials for testing + #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with = "gateway_ip")] + use_mock_ecash: bool, + + /// Mnemonic to get credentials from the blockchain + #[arg(long, conflicts_with = "gateway_ip")] + mnemonic: Option, +} + +impl OptionalCredentialMode { + fn into_required(self) -> anyhow::Result { + if self.use_mock_ecash || self.mnemonic.is_some() { + Ok(CredentialMode { + use_mock_ecash: self.use_mock_ecash, + mnemonic: self.mnemonic, + }) + } else { + anyhow::bail!( + "missing credentials for bonded run-ports mode: provide --mnemonic or --use-mock-ecash" + ) + } + } +} + +#[derive(Debug, Args, Clone)] +struct RunPortsProbeConfig { + /// Only choose gateway with that minimum performance + #[arg(long)] + min_gateway_mixnet_performance: Option, + + /// Ignore egress epoch role constraints + #[arg(long, global = true)] + ignore_egress_epoch_role: bool, + + /// Arguments to manage netstack downloads and port checks + #[command(flatten)] + netstack_args: NetstackArgs, +} + /// CLI output wrapper — either a standard probe result or a port-check result #[derive(Serialize)] #[serde(untagged)] @@ -255,22 +325,71 @@ pub(crate) async fn run() -> anyhow::Result { } Commands::RunPorts { entry_gateway, + gateway_ip, exit_gateway, config_dir, check_all_ports, + check_protocol, credential_mode, - mut probe_config, + probe_config, } => { + let mut run_ports_config = RunPortsConfig { + min_gateway_mixnet_performance: probe_config.min_gateway_mixnet_performance, + ignore_egress_epoch_role: probe_config.ignore_egress_epoch_role, + netstack_args: probe_config.netstack_args, + }; + // --check-all-ports overrides --check-ports with the full exit policy list if check_all_ports { use nym_gateway_probe::config::EXIT_POLICY_PORTS; - probe_config.netstack_args.port_check_ports = EXIT_POLICY_PORTS.to_vec(); + run_ports_config.netstack_args.port_check_ports = EXIT_POLICY_PORTS.to_vec(); info!( "Using full exit policy port list ({} ports)", EXIT_POLICY_PORTS.len() ); } + if let Some(gateway_ip) = gateway_ip { + info!("Using direct IP-only port check mode for gateway: {gateway_ip}"); + if entry_gateway.is_some() { + anyhow::bail!("--gateway/--entry-gateway cannot be used with --gateway-ip"); + } + + let target_ip: std::net::IpAddr = gateway_ip.parse().map_err(|_| { + anyhow::anyhow!( + "invalid --gateway-ip value '{gateway_ip}': expected plain IP address" + ) + })?; + + let direct_protocol = match check_protocol { + PortCheckProtocol::Auto => DirectPortCheckProtocol::Auto, + PortCheckProtocol::Tcp => DirectPortCheckProtocol::Tcp, + PortCheckProtocol::Udp => DirectPortCheckProtocol::Udp, + }; + + return Box::pin(nym_gateway_probe::Probe::probe_run_ports_direct_ip( + &gateway_ip, + target_ip, + &run_ports_config, + direct_protocol, + )) + .await + .map(ProbeOutput::PortCheck); + } + + if check_protocol == PortCheckProtocol::Udp { + anyhow::bail!( + "--check-protocol udp is only supported with --gateway-ip direct mode" + ); + } + if check_protocol == PortCheckProtocol::Auto { + info!( + "Bonded run-ports mode uses TCP checks; treating --check-protocol auto as tcp" + ); + } + + let credential_mode = credential_mode.into_required()?; + let api_url = network .endpoints .first() @@ -278,6 +397,11 @@ pub(crate) async fn run() -> anyhow::Result { .ok_or(anyhow::anyhow!("missing api url"))?; let directory = NymApiDirectory::new(api_url).await?; + let entry_gateway = entry_gateway.ok_or_else(|| { + anyhow::anyhow!( + "missing gateway selection: provide --gateway or --gateway-ip " + ) + })?; let entry_details = directory .entry_gateway(&entry_gateway)? @@ -306,11 +430,16 @@ pub(crate) async fn run() -> anyhow::Result { config_dir.display() ); - let trial = - nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config); - Box::pin(trial.probe_run_ports(&config_dir, credential_mode)) - .await - .map(ProbeOutput::PortCheck) + Box::pin(nym_gateway_probe::Probe::run_ports_bonded( + entry_details, + exit_details, + network, + &run_ports_config, + &config_dir, + credential_mode, + )) + .await + .map(ProbeOutput::PortCheck) } Commands::RunAgent { entry_gateway,