add support for not registered nodes
...
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user