add support for not registered nodes

...
This commit is contained in:
benedettadavico
2026-03-11 09:38:34 +01:00
parent 8aaa8dc8d3
commit a43dd38e76
4 changed files with 258 additions and 37 deletions
+1 -1
View File
@@ -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)
+2 -3
View File
@@ -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::<u16>::new())]
pub port_check_ports: Vec<u16>,
/// 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,
}
+109 -16
View File
@@ -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<TestedNodeDetails>,
network: NymNetworkDetails,
config: &RunPortsConfig,
config_dir: &PathBuf,
credential: CredentialMode,
) -> anyhow::Result<PortCheckResult> {
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<PortCheckResult> {
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,
+146 -17
View File
@@ -1,11 +1,14 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<PathBuf>,
/// 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<NodeIdentity>,
/// 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<String>,
/// 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<NodeIdentity>,
/// 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<String>,
}
impl OptionalCredentialMode {
fn into_required(self) -> anyhow::Result<CredentialMode> {
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 <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<u8>,
/// 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<ProbeOutput> {
}
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<ProbeOutput> {
.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 <ID> or --gateway-ip <IP[:PORT]>"
)
})?;
let entry_details = directory
.entry_gateway(&entry_gateway)?
@@ -306,11 +430,16 @@ pub(crate) async fn run() -> anyhow::Result<ProbeOutput> {
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,