add support for not registered nodes

...
This commit is contained in:
benedettadavico
2026-03-11 09:38:34 +01:00
parent 802417ccec
commit 2874f26f57
5 changed files with 348 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)
+50
View File
@@ -16,6 +16,7 @@ use nym_lp::packet::version;
use nym_lp::peer::{DHPublicKey, LpRemotePeer};
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_node_requests::api::client::NymNodeApiClientExt;
use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy;
use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails;
use nym_sdk::mixnet::NodeIdentity;
use nym_sdk::mixnet::Recipient;
@@ -361,6 +362,55 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
Err(last_error.unwrap_or_else(|| anyhow!("Failed to connect to gateway at {}", address)))
}
/// Query only the exit policy from a gateway HTTP API by address.
pub async fn query_exit_policy_by_ip(address: &str) -> anyhow::Result<UsedExitPolicy> {
let addresses_to_try = if address.contains(':') {
vec![format!("http://{address}"), format!("https://{address}")]
} else {
vec![
format!("http://{address}:{DEFAULT_NYM_NODE_HTTP_PORT}"),
format!("https://{address}"),
format!("http://{address}"),
]
};
let user_agent: UserAgent = nym_bin_common::bin_info_local_vergen!().into();
let mut last_error = None;
for base in addresses_to_try {
let client = match nym_node_requests::api::Client::builder(base.clone()) {
Ok(builder) => match builder
.with_timeout(Duration::from_secs(5))
.no_hickory_dns()
.with_user_agent(user_agent.clone())
.build()
{
Ok(c) => c,
Err(e) => {
warn!("Failed to build client for {}: {}", base, e);
last_error = Some(e.into());
continue;
}
},
Err(e) => {
warn!("Failed to create client builder for {}: {}", base, e);
last_error = Some(e.into());
continue;
}
};
match client.get_exit_policy().await {
Ok(policy) => return Ok(policy),
Err(e) => {
debug!("Failed to query exit policy at {}: {}", base, e);
last_error = Some(e.into());
}
}
}
Err(last_error.unwrap_or_else(|| anyhow!("Failed to query exit policy at {}", address)))
}
pub struct NymApiDirectory {
// nodes: HashMap<NodeIdentity, DescribedNodeWithPerformance>,
nodes: HashMap<NodeIdentity, DirectoryNode>,
+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,
}
+149 -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;
@@ -46,7 +47,47 @@ pub struct Probe {
topology: Option<NymTopology>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectPortCheckProtocol {
Auto,
Tcp,
Udp,
}
#[derive(Debug, Clone)]
pub struct RunPortsConfig {
pub min_gateway_mixnet_performance: Option<u8>,
pub ignore_egress_epoch_role: bool,
pub netstack_args: NetstackArgs,
}
impl Probe {
async fn check_tcp_socket(socket: SocketAddr, timeout_duration: std::time::Duration) -> bool {
matches!(
tokio::time::timeout(timeout_duration, tokio::net::TcpStream::connect(socket)).await,
Ok(Ok(_))
)
}
async fn check_udp_socket(socket: SocketAddr, timeout_duration: std::time::Duration) -> bool {
let Ok(udp_socket) = tokio::net::UdpSocket::bind("0.0.0.0:0").await else {
return false;
};
if udp_socket.connect(socket).await.is_err() {
return false;
}
if udp_socket.send(&[0]).await.is_err() {
return false;
}
let mut recv_buf = [0u8; 1];
match tokio::time::timeout(timeout_duration, udp_socket.recv(&mut recv_buf)).await {
Ok(Ok(_)) => true,
Ok(Err(_)) => false,
Err(_) => true,
}
}
/// Create a probe with pre-queried gateway nodes
pub fn new(
entry_node: TestedNodeDetails,
@@ -258,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
@@ -278,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!(
@@ -301,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()),
));
@@ -339,7 +383,7 @@ impl Probe {
.await?;
let bandwidth_provider = build_bandwidth_controller(
&self.network,
&network,
storage.credential_store().clone(),
credential.use_mock_ecash,
)?;
@@ -348,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
@@ -411,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(),
@@ -427,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,
@@ -469,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::{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,