[LP Gateway Probe] CLI and behavior improvements (#6400)
* attempt to de-spaghettificationize the gateway probe * applying suggestions
This commit is contained in:
@@ -15,8 +15,6 @@ workspace = true
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
bs58.workspace = true
|
||||
bincode.workspace = true
|
||||
bytes.workspace = true
|
||||
clap = { workspace = true, features = ["cargo", "derive"] }
|
||||
futures.workspace = true
|
||||
@@ -27,7 +25,7 @@ rand.workspace = true
|
||||
reqwest = { workspace = true, features = ["socks"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
@@ -39,43 +37,34 @@ tokio-util.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
url = { workspace = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
x25519-dalek = { workspace = true, features = [
|
||||
"reusable_secrets",
|
||||
"static_secrets",
|
||||
] }
|
||||
|
||||
|
||||
nym-api-requests = { path = "../nym-api/nym-api-requests" }
|
||||
nym-authenticator-client = { workspace = true }
|
||||
nym-authenticator-requests = { workspace = true }
|
||||
nym-bandwidth-controller = { workspace = true }
|
||||
nym-bin-common = { workspace = true }
|
||||
nym-client-core = { workspace = true }
|
||||
nym-crypto = { workspace = true }
|
||||
nym-config = { workspace = true }
|
||||
nym-connection-monitor = { path = "../common/nym-connection-monitor" }
|
||||
nym-credentials = { workspace = true }
|
||||
nym-credentials-interface = { workspace = true }
|
||||
nym-credential-utils = { workspace = true }
|
||||
nym-ip-packet-client = { workspace = true }
|
||||
nym-authenticator-client = { workspace = true }
|
||||
nym-ip-packet-requests = { workspace = true }
|
||||
nym-sdk = { workspace = true }
|
||||
nym-validator-client = { workspace = true }
|
||||
nym-credentials = { workspace = true }
|
||||
nym-http-api-client-macro = { path = "../common/http-api-client-macro" }
|
||||
nym-crypto = { workspace = true }
|
||||
nym-http-api-client = { path = "../common/http-api-client" }
|
||||
nym-node-status-client = { path = "../nym-node-status-api/nym-node-status-client" }
|
||||
nym-node-requests = { path = "../nym-node/nym-node-requests" }
|
||||
nym-registration-client = { path = "../nym-registration-client" }
|
||||
nym-lp = { path = "../common/nym-lp" }
|
||||
nym-http-api-client-macro = { path = "../common/http-api-client-macro" }
|
||||
nym-ip-packet-client = { workspace = true }
|
||||
nym-ip-packet-requests = { workspace = true }
|
||||
nym-kkt-ciphersuite = { workspace = true }
|
||||
|
||||
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-lp = { path = "../common/nym-lp" }
|
||||
nym-network-defaults = { path = "../common/network-defaults" }
|
||||
nym-node-requests = { path = "../nym-node/nym-node-requests" }
|
||||
nym-node-status-client = { path = "../nym-node-status-api/nym-node-status-client" }
|
||||
nym-registration-client = { path = "../nym-registration-client" }
|
||||
nym-registration-common = { path = "../common/registration" }
|
||||
time = { workspace = true }
|
||||
|
||||
# TEMP: REMOVE BEFORE PR
|
||||
nym-sdk = { workspace = true }
|
||||
nym-topology = { workspace = true }
|
||||
nym-validator-client = { workspace = true }
|
||||
|
||||
[features]
|
||||
utoipa = ["dep:utoipa"]
|
||||
|
||||
@@ -8,29 +8,38 @@ use nym_bandwidth_controller::mock::MockBandwidthController;
|
||||
use nym_client_core::client::base_client::storage::OnDiskPersistent;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_node_status_client::models::AttachedTicketMaterials;
|
||||
use nym_sdk::NymNetworkDetails;
|
||||
use nym_sdk::bandwidth::BandwidthImporter;
|
||||
use nym_sdk::mixnet::{CredentialStorage, DisconnectedMixnetClient, EphemeralCredentialStorage};
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use std::time::Duration;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub(crate) fn build_bandwidth_controller<S>(
|
||||
rpc_client: QueryHttpRpcNyxdClient,
|
||||
on_disk_storage: S,
|
||||
network: &NymNetworkDetails,
|
||||
storage: S,
|
||||
use_mock_ecash: bool,
|
||||
) -> Box<dyn BandwidthTicketProvider>
|
||||
) -> anyhow::Result<Box<dyn BandwidthTicketProvider>>
|
||||
where
|
||||
S: CredentialStorage + 'static,
|
||||
S::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
if !use_mock_ecash {
|
||||
Box::new(nym_bandwidth_controller::BandwidthController::new(
|
||||
on_disk_storage,
|
||||
rpc_client,
|
||||
let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network)?;
|
||||
|
||||
let nyxd_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
.map(|ep| ep.nyxd_url())
|
||||
.ok_or(anyhow::anyhow!("missing nyxd url"))?;
|
||||
let rpc_client =
|
||||
nym_validator_client::nyxd::NyxdClient::connect(config, nyxd_url.as_str())?;
|
||||
|
||||
Ok(Box::new(
|
||||
nym_bandwidth_controller::BandwidthController::new(storage, rpc_client),
|
||||
))
|
||||
} else {
|
||||
Box::new(MockBandwidthController::default())
|
||||
Ok(Box::new(MockBandwidthController::default()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,13 @@ use nym_ip_packet_requests::v8::response::{
|
||||
ControlResponse, DataResponse, InfoLevel, IpPacketResponse, IpPacketResponseData,
|
||||
};
|
||||
use nym_lp::peer::LpRemotePeer;
|
||||
use nym_sdk::mixnet::ReconstructedMessage;
|
||||
use nym_sdk::{
|
||||
DebugConfig, NymApiTopologyProvider, NymApiTopologyProviderConfig, NymNetworkDetails,
|
||||
TopologyProvider, mixnet::ReconstructedMessage,
|
||||
};
|
||||
use nym_topology::NymTopology;
|
||||
use tracing::*;
|
||||
use url::Url;
|
||||
|
||||
pub fn to_lp_remote_peer(identity: ed25519::PublicKey, data: TestedNodeLpDetails) -> LpRemotePeer {
|
||||
LpRemotePeer::new(identity, data.x25519).with_key_digests(
|
||||
@@ -62,3 +67,45 @@ pub fn unpack_data_response(reconstructed_message: &ReconstructedMessage) -> Opt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_topology(
|
||||
network_details: &NymNetworkDetails,
|
||||
debug_config: &DebugConfig,
|
||||
) -> Result<NymTopology, String> {
|
||||
// get Nym API URLs from network_details
|
||||
let nym_api_urls: Vec<Url> = network_details
|
||||
.nym_api_urls
|
||||
.as_ref()
|
||||
.map(|urls| urls.iter().filter_map(|u| u.url.parse().ok()).collect())
|
||||
.or_else(|| {
|
||||
network_details
|
||||
.endpoints
|
||||
.first()
|
||||
.and_then(|e| e.api_url())
|
||||
.map(|url| vec![url])
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if nym_api_urls.is_empty() {
|
||||
return Err(String::from("No nym-api URLs available to fetch topology"));
|
||||
}
|
||||
|
||||
let topology_config = NymApiTopologyProviderConfig {
|
||||
min_mixnode_performance: debug_config.topology.minimum_mixnode_performance,
|
||||
min_gateway_performance: debug_config.topology.minimum_gateway_performance,
|
||||
use_extended_topology: debug_config.topology.use_extended_topology,
|
||||
ignore_egress_epoch_role: debug_config.topology.ignore_egress_epoch_role,
|
||||
};
|
||||
|
||||
let api_client = nym_http_api_client::Client::new_url(nym_api_urls[0].clone(), None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut provider = NymApiTopologyProvider::new(topology_config, nym_api_urls, api_client);
|
||||
|
||||
match provider.get_new_topology().await {
|
||||
Some(topology) => {
|
||||
info!("Fetched network topology");
|
||||
Ok(topology)
|
||||
}
|
||||
None => Err(String::from("Failed to fetch network topology")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::config::NetstackArgs;
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
use std::ffi::{CStr, CString};
|
||||
@@ -15,8 +16,6 @@ mod sys {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::config::NetstackArgs;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct NetstackRequest {
|
||||
private_key: String,
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use nym_api_requests::models::{
|
||||
AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2, HostInformationV2,
|
||||
IpPacketRouterDetailsV2, LewesProtocolDetailsV1, NetworkRequesterDetailsV1,
|
||||
NetworkRequesterDetailsV2, NymNodeDataV2, OffsetDateTimeJsonSchemaWrapper, WebSocketsV2,
|
||||
WireguardDetailsV2,
|
||||
IpPacketRouterDetailsV2, NetworkRequesterDetailsV2, NymNodeDataV2,
|
||||
OffsetDateTimeJsonSchemaWrapper, WebSocketsV2, WireguardDetailsV2,
|
||||
};
|
||||
use nym_authenticator_requests::AuthenticatorVersion;
|
||||
use nym_bin_common::build_information::BinaryBuildInformationOwned;
|
||||
@@ -27,7 +26,7 @@ use std::collections::HashMap;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use url::Url;
|
||||
// in the old behaviour we were getting all skimmed nodes to retrieve performance
|
||||
// that was ultimately unused
|
||||
@@ -108,6 +107,11 @@ impl DirectoryNode {
|
||||
.as_ref()
|
||||
.map(|ipr| ipr.address.parse().context("malformed ipr address"))
|
||||
.transpose()?;
|
||||
let network_requester_address = description
|
||||
.network_requester
|
||||
.as_ref()
|
||||
.map(|nr| nr.address.parse().context("malformed nr address"))
|
||||
.transpose()?;
|
||||
let authenticator_address = description
|
||||
.authenticator
|
||||
.as_ref()
|
||||
@@ -140,12 +144,11 @@ impl DirectoryNode {
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
let network_requester_details = self.described.description.network_requester.clone();
|
||||
|
||||
Ok(TestedNodeDetails {
|
||||
identity: self.identity(),
|
||||
exit_router_address,
|
||||
network_requester_details,
|
||||
network_requester_address,
|
||||
authenticator_address,
|
||||
authenticator_version,
|
||||
ip_address: Some(ip_address),
|
||||
@@ -217,12 +220,33 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
|
||||
let build_info_result = client.get_build_information().await;
|
||||
let aux_details_result = client.get_auxiliary_details().await;
|
||||
let websockets_result = client.get_mixnet_websockets().await;
|
||||
let lp_result = client.get_lewes_protocol().await;
|
||||
|
||||
// These are optional, so we use ok() to ignore errors
|
||||
let ipr_result = client.get_ip_packet_router().await.ok();
|
||||
let authenticator_result = client.get_authenticator().await.ok();
|
||||
let wireguard_result = client.get_wireguard().await.ok();
|
||||
let ipr_result = client
|
||||
.get_ip_packet_router()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get ipr information : {e}"))
|
||||
.ok();
|
||||
let nr_result = client
|
||||
.get_network_requester()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get nr information : {e}"))
|
||||
.ok();
|
||||
let authenticator_result = client
|
||||
.get_authenticator()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get authenticator information : {e}"))
|
||||
.ok();
|
||||
let wireguard_result = client
|
||||
.get_wireguard()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get wireguard information : {e}"))
|
||||
.ok();
|
||||
let lp_result = client
|
||||
.get_lewes_protocol()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get LP information : {e}"))
|
||||
.ok();
|
||||
|
||||
// Check required fields
|
||||
let host_info = host_info_result.context("Failed to get host information")?;
|
||||
@@ -242,7 +266,11 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
|
||||
}
|
||||
|
||||
// Convert to our internal types
|
||||
let network_requester: Option<NetworkRequesterDetailsV2> = None; // Not needed for LP testing
|
||||
let network_requester: Option<NetworkRequesterDetailsV2> =
|
||||
nr_result.map(|nr| NetworkRequesterDetailsV2 {
|
||||
address: nr.address,
|
||||
uses_exit_policy: false, // Field not availabe, to change if it becomes useful here
|
||||
});
|
||||
let ip_packet_router: Option<IpPacketRouterDetailsV2> =
|
||||
ipr_result.map(|ipr| IpPacketRouterDetailsV2 {
|
||||
address: ipr.address,
|
||||
@@ -260,8 +288,6 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
|
||||
public_key: wg.public_key,
|
||||
});
|
||||
|
||||
let lp: Option<LewesProtocolDetailsV1> = lp_result.ok().map(Into::into);
|
||||
|
||||
// Construct NymNodeData
|
||||
let node_data = NymNodeDataV2 {
|
||||
last_polled: OffsetDateTimeJsonSchemaWrapper(OffsetDateTime::now_utc()),
|
||||
@@ -293,7 +319,7 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
|
||||
ip_packet_router,
|
||||
authenticator,
|
||||
wireguard,
|
||||
lewes_protocol: lp,
|
||||
lewes_protocol: lp_result.map(Into::into),
|
||||
mixnet_websockets: WebSocketsV2 {
|
||||
ws_port: websockets.ws_port,
|
||||
wss_port: websockets.wss_port,
|
||||
@@ -431,45 +457,27 @@ impl NymApiDirectory {
|
||||
Ok(maybe_entry)
|
||||
}
|
||||
|
||||
pub fn exit_gateway_nr(&self, identity: &NodeIdentity) -> anyhow::Result<DirectoryNode> {
|
||||
pub fn exit_gateway(&self, identity: &NodeIdentity) -> anyhow::Result<DirectoryNode> {
|
||||
let Some(maybe_entry) = self.nodes.get(identity).cloned() else {
|
||||
bail!("{identity} not found in directory")
|
||||
bail!("{identity} does not exist")
|
||||
};
|
||||
if !maybe_entry.described.description.declared_role.exit_nr {
|
||||
bail!("{identity} doesn't support exit NR mode")
|
||||
if !maybe_entry
|
||||
.described
|
||||
.description
|
||||
.declared_role
|
||||
.can_operate_exit_gateway()
|
||||
{
|
||||
bail!("{identity} is not an exit node")
|
||||
};
|
||||
Ok(maybe_entry)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub enum TestedNode {
|
||||
#[default]
|
||||
SameAsEntry,
|
||||
Custom {
|
||||
identity: NodeIdentity,
|
||||
shares_entry: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl TestedNode {
|
||||
pub fn is_same_as_entry(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestedNode::SameAsEntry
|
||||
| TestedNode::Custom {
|
||||
shares_entry: true,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestedNodeDetails {
|
||||
pub identity: NodeIdentity,
|
||||
pub exit_router_address: Option<Recipient>,
|
||||
pub network_requester_details: Option<NetworkRequesterDetailsV1>,
|
||||
pub network_requester_address: Option<Recipient>,
|
||||
pub authenticator_address: Option<Recipient>,
|
||||
pub authenticator_version: AuthenticatorVersion,
|
||||
pub ip_address: Option<IpAddr>,
|
||||
@@ -484,29 +492,3 @@ pub struct TestedNodeLpDetails {
|
||||
pub x25519: x25519::PublicKey,
|
||||
pub lp_version: u8,
|
||||
}
|
||||
|
||||
impl TestedNodeDetails {
|
||||
/// Create from CLI args (localnet mode - no HTTP query needed)
|
||||
pub fn from_cli(identity: NodeIdentity, lp_data: TestedNodeLpDetails) -> Self {
|
||||
Self {
|
||||
identity,
|
||||
ip_address: Some(lp_data.address.ip()),
|
||||
lp_data: Some(lp_data),
|
||||
network_requester_details: None,
|
||||
// These are None in localnet mode - only needed for mixnet/authenticator
|
||||
exit_router_address: None,
|
||||
authenticator_address: None,
|
||||
authenticator_version: AuthenticatorVersion::UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this node has sufficient info for LP testing
|
||||
pub fn can_test_lp(&self) -> bool {
|
||||
self.lp_data.is_some()
|
||||
}
|
||||
|
||||
/// Check if this node has sufficient info for mixnet testing
|
||||
pub fn can_test_mixnet(&self) -> bool {
|
||||
self.exit_router_address.is_some() || self.authenticator_address.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::NymApiDirectory;
|
||||
use crate::common::helpers::mixnet_debug_config;
|
||||
use crate::common::nodes::{TestedNodeDetails, TestedNodeLpDetails};
|
||||
use crate::common::socks5_test::HttpsConnectivityTest;
|
||||
@@ -12,7 +11,7 @@ use crate::common::wireguard::{
|
||||
TwoHopWgTunnelConfig, WgTunnelConfig, run_tunnel_tests, run_two_hop_tunnel_tests,
|
||||
};
|
||||
use crate::common::{helpers, icmp};
|
||||
use crate::config::NetstackArgs;
|
||||
use crate::config::{NetstackArgs, Socks5Args};
|
||||
use anyhow::bail;
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use bytes::BytesMut;
|
||||
@@ -30,11 +29,8 @@ use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_ip_packet_client::IprClientConnect;
|
||||
use nym_ip_packet_requests::{IpPair, codec::MultiIpPacketCodec};
|
||||
use nym_registration_client::{LpRegistrationClient, NestedLpSession};
|
||||
use nym_sdk::NymNetworkDetails;
|
||||
use nym_sdk::mixnet::{MixnetClient, MixnetClientBuilder, NodeIdentity, Recipient, Socks5};
|
||||
use nym_sdk::{
|
||||
DebugConfig, NymApiTopologyProvider, NymApiTopologyProviderConfig, NymNetworkDetails,
|
||||
TopologyProvider,
|
||||
};
|
||||
use nym_topology::{HardcodedTopologyProvider, NymTopology};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
@@ -44,13 +40,12 @@ use std::{
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::{codec::Decoder, sync::CancellationToken};
|
||||
use tracing::*;
|
||||
use url::Url;
|
||||
|
||||
pub async fn wg_probe(
|
||||
mut auth_client: AuthenticatorClient,
|
||||
gateway_ip: IpAddr,
|
||||
auth_version: AuthenticatorVersion,
|
||||
awg_args: String,
|
||||
awg_args: Option<String>,
|
||||
netstack_args: NetstackArgs,
|
||||
// TODO: update type
|
||||
credential: CredentialSpendingData,
|
||||
@@ -123,9 +118,8 @@ pub async fn wg_probe(
|
||||
};
|
||||
|
||||
let peer_public = registered_data.pub_key().inner();
|
||||
let static_private = x25519_dalek::StaticSecret::from(private_key.to_bytes());
|
||||
let public_key_bs64 = general_purpose::STANDARD.encode(peer_public.as_bytes());
|
||||
let private_key_hex = hex::encode(static_private.to_bytes());
|
||||
let private_key_hex = hex::encode(private_key.to_bytes());
|
||||
let public_key_hex = hex::encode(peer_public.as_bytes());
|
||||
|
||||
info!("WG connection details");
|
||||
@@ -152,7 +146,12 @@ pub async fn wg_probe(
|
||||
wg_endpoint,
|
||||
);
|
||||
|
||||
run_tunnel_tests(&tunnel_config, &netstack_args, &awg_args, &mut wg_outcome);
|
||||
run_tunnel_tests(
|
||||
&tunnel_config,
|
||||
&netstack_args,
|
||||
&awg_args.unwrap_or_default(),
|
||||
&mut wg_outcome,
|
||||
);
|
||||
|
||||
Ok(wg_outcome)
|
||||
}
|
||||
@@ -166,7 +165,7 @@ pub async fn lp_registration_probe(
|
||||
let lp_version = gateway_lp_data.lp_version;
|
||||
let peer = helpers::to_lp_remote_peer(gateway_identity, gateway_lp_data);
|
||||
|
||||
info!("Starting LP registration probe for gateway at {lp_address}",);
|
||||
info!("Starting LP registration probe for gateway at {lp_address}");
|
||||
|
||||
let mut lp_outcome = LpProbeResults::default();
|
||||
|
||||
@@ -274,7 +273,7 @@ pub async fn wg_probe_lp(
|
||||
entry_gateway: &TestedNodeDetails,
|
||||
exit_gateway: &TestedNodeDetails,
|
||||
bandwidth_controller: &dyn BandwidthTicketProvider,
|
||||
awg_args: String,
|
||||
awg_args: Option<String>,
|
||||
netstack_args: NetstackArgs,
|
||||
) -> anyhow::Result<WgProbeResults> {
|
||||
// Validate that both gateways have required information
|
||||
@@ -340,9 +339,7 @@ pub async fn wg_probe_lp(
|
||||
exit_lp_version,
|
||||
);
|
||||
|
||||
// Convert exit gateway identity to ed25519 public key for registration
|
||||
let exit_gateway_pubkey = ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid exit gateway identity: {}", e))?;
|
||||
let exit_gateway_pubkey = exit_gateway.identity;
|
||||
|
||||
// Perform handshake and registration with exit gateway via forwarding
|
||||
let exit_gateway_data = match nested_session
|
||||
@@ -424,12 +421,12 @@ pub async fn wg_probe_lp(
|
||||
entry_private_key_hex,
|
||||
entry_public_key_hex,
|
||||
entry_wg_endpoint,
|
||||
awg_args.clone(), // Entry AWG args
|
||||
awg_args.clone().unwrap_or_default(), // Entry AWG args
|
||||
exit_gateway_data.private_ipv4.to_string(),
|
||||
exit_private_key_hex,
|
||||
exit_public_key_hex,
|
||||
exit_wg_endpoint,
|
||||
awg_args, // Exit AWG args
|
||||
awg_args.unwrap_or_default(), // Exit AWG args
|
||||
);
|
||||
|
||||
// Run two-hop tunnel connectivity tests
|
||||
@@ -632,39 +629,23 @@ pub async fn listen_for_icmp_ping_replies(
|
||||
|
||||
/// Creates a SOCKS5 proxy connection through the mixnet to the exit GW
|
||||
/// and performs necessary tests.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(level = "info", name = "socks5_test", skip_all)]
|
||||
pub(crate) async fn do_socks5_connectivity_test(
|
||||
network_requester_address: &str,
|
||||
nr_recipient: &Recipient,
|
||||
entry_gateway_id: NodeIdentity,
|
||||
network_details: NymNetworkDetails,
|
||||
directory: &NymApiDirectory,
|
||||
json_rpc_endpoints: Vec<String>,
|
||||
mixnet_client_timeout: u64,
|
||||
test_run_count: u64,
|
||||
failure_count_cutoff: usize,
|
||||
topology: Option<NymTopology>,
|
||||
min_gw_performance: Option<u8>,
|
||||
socks5_args: Socks5Args,
|
||||
maybe_topology: Option<NymTopology>,
|
||||
) -> anyhow::Result<Socks5ProbeResults> {
|
||||
info!(
|
||||
"Starting SOCKS5 test through Network Requester: {}",
|
||||
network_requester_address
|
||||
nr_recipient
|
||||
);
|
||||
if json_rpc_endpoints.is_empty() {
|
||||
if socks5_args.socks5_json_rpc_url_list.is_empty() {
|
||||
bail!("You need to define JSON RPC URLs in order to test SOCKS5")
|
||||
}
|
||||
|
||||
// parse the network requester address
|
||||
let nr_recipient = match network_requester_address.parse::<Recipient>() {
|
||||
Ok(addr) => addr,
|
||||
Err(e) => {
|
||||
error!("Invalid Network Requester address: {}", e);
|
||||
|
||||
return Ok(Socks5ProbeResults::error_before_connecting(format!(
|
||||
"Invalid NR address: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Network Requester gateway: {}",
|
||||
nr_recipient.gateway().to_base58_string()
|
||||
@@ -675,53 +656,30 @@ pub(crate) async fn do_socks5_connectivity_test(
|
||||
);
|
||||
|
||||
// create ephemeral SOCKS5 client
|
||||
let socks5_config = Socks5::new(network_requester_address.to_string());
|
||||
|
||||
// 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 of topology
|
||||
let min_gw_performance = Some(0);
|
||||
let socks5_config = Socks5::new(nr_recipient.to_string());
|
||||
|
||||
// debug config similar to main probe
|
||||
let debug_config = mixnet_debug_config(min_gw_performance, true);
|
||||
|
||||
// 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) {
|
||||
return Ok(Socks5ProbeResults::error_before_connecting(e.to_string()));
|
||||
} 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;
|
||||
|
||||
// use existing topology if available, otherwise fetch it
|
||||
let topology_provider: Box<HardcodedTopologyProvider> = match topology {
|
||||
Some(t) => {
|
||||
info!("✔️ Reusing topology from main mixnet client");
|
||||
Box::new(HardcodedTopologyProvider::new(t))
|
||||
}
|
||||
None => {
|
||||
info!("Fetching topology for SOCKS5 client...");
|
||||
match hardcoded_topology(&network_details, &debug_config).await {
|
||||
Ok(provider) => provider,
|
||||
Err(e) => return Ok(Socks5ProbeResults::error_before_connecting(e)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let socks5_client_builder = MixnetClientBuilder::new_ephemeral()
|
||||
let mut socks5_client_builder = MixnetClientBuilder::new_ephemeral()
|
||||
// Specify entry gateway explicitly
|
||||
.request_gateway(entry_gateway.to_base58_string())
|
||||
.request_gateway(entry_gateway_id.to_base58_string())
|
||||
.socks5_config(socks5_config)
|
||||
.network_details(network_details)
|
||||
.debug_config(debug_config)
|
||||
.custom_topology_provider(topology_provider)
|
||||
.build()?;
|
||||
.debug_config(debug_config);
|
||||
|
||||
if let Some(topology) = maybe_topology {
|
||||
socks5_client_builder = socks5_client_builder
|
||||
.custom_topology_provider(Box::new(HardcodedTopologyProvider::new(topology)));
|
||||
}
|
||||
|
||||
let disconnected_socks5_client = socks5_client_builder.build()?;
|
||||
|
||||
// connect to mixnet via SOCKS5
|
||||
let socks5_client = match socks5_client_builder.connect_to_mixnet_via_socks5().await {
|
||||
let socks5_client = match disconnected_socks5_client
|
||||
.connect_to_mixnet_via_socks5()
|
||||
.await
|
||||
{
|
||||
Ok(client) => {
|
||||
info!("🌐 Successfully connected to mixnet via SOCKS5 proxy");
|
||||
info!(
|
||||
@@ -740,10 +698,10 @@ pub(crate) async fn do_socks5_connectivity_test(
|
||||
};
|
||||
|
||||
let test = match HttpsConnectivityTest::new(
|
||||
test_run_count,
|
||||
mixnet_client_timeout,
|
||||
failure_count_cutoff,
|
||||
json_rpc_endpoints,
|
||||
socks5_args.test_count,
|
||||
socks5_args.mixnet_client_timeout_sec,
|
||||
socks5_args.failure_count_cutoff,
|
||||
socks5_args.socks5_json_rpc_url_list,
|
||||
socks5_client.socks5_url(),
|
||||
) {
|
||||
Ok(test) => test,
|
||||
@@ -762,45 +720,3 @@ pub(crate) async fn do_socks5_connectivity_test(
|
||||
|
||||
Ok(Socks5ProbeResults::with_http_result(result))
|
||||
}
|
||||
|
||||
async fn hardcoded_topology(
|
||||
network_details: &NymNetworkDetails,
|
||||
debug_config: &DebugConfig,
|
||||
) -> Result<Box<HardcodedTopologyProvider>, String> {
|
||||
// get Nym API URLs from network_details
|
||||
let nym_api_urls: Vec<Url> = network_details
|
||||
.nym_api_urls
|
||||
.as_ref()
|
||||
.map(|urls| urls.iter().filter_map(|u| u.url.parse().ok()).collect())
|
||||
.or_else(|| {
|
||||
network_details
|
||||
.endpoints
|
||||
.first()
|
||||
.and_then(|e| e.api_url())
|
||||
.map(|url| vec![url])
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if nym_api_urls.is_empty() {
|
||||
return Err(String::from("No nym-api URLs available to fetch topology"));
|
||||
}
|
||||
|
||||
let topology_config = NymApiTopologyProviderConfig {
|
||||
min_mixnode_performance: debug_config.topology.minimum_mixnode_performance,
|
||||
min_gateway_performance: debug_config.topology.minimum_gateway_performance,
|
||||
use_extended_topology: debug_config.topology.use_extended_topology,
|
||||
ignore_egress_epoch_role: debug_config.topology.ignore_egress_epoch_role,
|
||||
};
|
||||
|
||||
let api_client = nym_http_api_client::Client::new_url(nym_api_urls[0].clone(), None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut provider = NymApiTopologyProvider::new(topology_config, nym_api_urls, api_client);
|
||||
|
||||
match provider.get_new_topology().await {
|
||||
Some(topology) => {
|
||||
info!("Fetched network topology");
|
||||
Ok(Box::new(HardcodedTopologyProvider::new(topology)))
|
||||
}
|
||||
None => Err(String::from("Failed to fetch network topology")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
use nym_config::defaults::{WG_METADATA_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::NetstackArgs;
|
||||
use crate::common::netstack::{
|
||||
NetstackRequest, NetstackRequestGo, NetstackResult, TwoHopNetstackRequestGo,
|
||||
};
|
||||
use crate::common::types::WgProbeResults;
|
||||
use crate::config::NetstackArgs;
|
||||
|
||||
/// Safe division that returns 0.0 when divisor is 0 (instead of NaN/Inf)
|
||||
fn safe_ratio(received: u16, sent: u16) -> f32 {
|
||||
|
||||
@@ -1,29 +1,94 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Args;
|
||||
use clap::{ArgGroup, Args};
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_node_status_client::models::AttachedTicketMaterials;
|
||||
use nym_sdk::mixnet::{
|
||||
CredentialStorage, DisconnectedMixnetClient, Ephemeral, MixnetClientStorage, OnDiskPersistent,
|
||||
};
|
||||
|
||||
#[derive(Args)]
|
||||
use crate::common::bandwidth_helpers::{acquire_bandwidth, import_bandwidth};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CredentialArgs {
|
||||
/// Serialized credential data
|
||||
#[arg(long)]
|
||||
ticket_materials: Option<String>,
|
||||
pub ticket_materials: String,
|
||||
|
||||
/// Version of the serialized credential
|
||||
#[arg(long, default_value_t = 1)]
|
||||
ticket_materials_revision: u8,
|
||||
pub ticket_materials_revision: u8,
|
||||
}
|
||||
|
||||
impl CredentialArgs {
|
||||
pub fn decode_attached_ticket_materials(&self) -> anyhow::Result<AttachedTicketMaterials> {
|
||||
let ticket_materials = self
|
||||
.ticket_materials
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("ticket_materials is required"))?
|
||||
.clone();
|
||||
|
||||
Ok(AttachedTicketMaterials::from_serialised_string(
|
||||
ticket_materials,
|
||||
pub async fn import_credential(
|
||||
self,
|
||||
mixnet_client: &DisconnectedMixnetClient<Ephemeral>,
|
||||
) -> anyhow::Result<()> {
|
||||
let tickets_materials = AttachedTicketMaterials::from_serialised_string(
|
||||
self.ticket_materials,
|
||||
self.ticket_materials_revision,
|
||||
)?)
|
||||
)?;
|
||||
let bandwidth_import = mixnet_client.begin_bandwidth_import();
|
||||
import_bandwidth(bandwidth_import, tickets_materials).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Two ways to inject credentials when not running as agent
|
||||
/// 1. Mnemonic : expected to be used on lower envs
|
||||
/// - mnemonic
|
||||
/// 2. Mock ecash : expected to be used for local setups
|
||||
/// - use_mock_ecash
|
||||
#[derive(Debug, Args)]
|
||||
#[command(group(
|
||||
ArgGroup::new("credential_mode")
|
||||
.args(["use_mock_ecash","mnemonic"])
|
||||
.required(true)
|
||||
.multiple(false)
|
||||
))]
|
||||
pub struct CredentialMode {
|
||||
/// Use mock ecash credentials for testing (requires gateway with --lp-use-mock-ecash)
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
pub use_mock_ecash: bool,
|
||||
|
||||
/// Mnemonic to get credentials from the blockchain. It needs NYMs.
|
||||
#[arg(long)]
|
||||
pub mnemonic: Option<String>,
|
||||
}
|
||||
|
||||
impl CredentialMode {
|
||||
pub async fn acquire(
|
||||
&self,
|
||||
disconnected_mixnet_client: &DisconnectedMixnetClient<OnDiskPersistent>,
|
||||
storage: &OnDiskPersistent,
|
||||
) -> anyhow::Result<()> {
|
||||
// Return immediately as there is nothing to do
|
||||
if self.use_mock_ecash {
|
||||
return Ok(());
|
||||
}
|
||||
let ticketbook_count = storage
|
||||
.credential_store()
|
||||
.get_ticketbooks_info()
|
||||
.await?
|
||||
.len();
|
||||
tracing::info!("Credential store contains {} ticketbooks", ticketbook_count);
|
||||
|
||||
if ticketbook_count < 1 {
|
||||
let mnemonic = self.mnemonic.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"We are not using mock ecash and mnemonic is not set, this should not happen"
|
||||
)
|
||||
})?;
|
||||
for ticketbook_type in [
|
||||
TicketType::V1MixnetEntry,
|
||||
TicketType::V1WireguardEntry,
|
||||
TicketType::V1WireguardExit,
|
||||
] {
|
||||
acquire_bandwidth(mnemonic, disconnected_mixnet_client, ticketbook_type).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,49 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Args;
|
||||
|
||||
mod credentials;
|
||||
mod netstack;
|
||||
mod socks5;
|
||||
mod test_mode;
|
||||
|
||||
pub use credentials::CredentialArgs;
|
||||
pub use credentials::{CredentialArgs, CredentialMode};
|
||||
pub use netstack::NetstackArgs;
|
||||
pub use socks5::Socks5Args;
|
||||
pub use test_mode::TestMode;
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ProbeConfig {
|
||||
/// Only choose gateway with that minimum performance
|
||||
#[arg(long)]
|
||||
pub min_gateway_mixnet_performance: Option<u8>,
|
||||
|
||||
/// Test mode - explicitly specify which tests to run
|
||||
///
|
||||
/// Modes:
|
||||
/// core. - Traditional mixnet testing (entry/exit pings + WireGuard via authenticator)
|
||||
/// wg-mix - Wireguard via authenticator
|
||||
/// wg-lp - Entry LP + Exit LP (nested forwarding) + WireGuard
|
||||
/// lp-only - LP registration only (no WireGuard)
|
||||
/// socks5-only - Socks5 network requester test
|
||||
/// all - Mixnet, wireguard over authenticator and LP registration
|
||||
///
|
||||
#[arg(long, default_value_t = TestMode::default(), verbatim_doc_comment)]
|
||||
pub test_mode: TestMode,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
pub ignore_egress_epoch_role: bool,
|
||||
|
||||
/// Arguments to be appended to the wireguard config enabling amnezia-wg configuration
|
||||
#[arg(long)]
|
||||
pub amnezia_args: Option<String>,
|
||||
|
||||
/// Arguments to manage netstack downloads
|
||||
#[command(flatten)]
|
||||
pub netstack_args: NetstackArgs,
|
||||
|
||||
/// Arguments to configure socks5 probe
|
||||
#[command(flatten)]
|
||||
pub socks5_args: Socks5Args,
|
||||
}
|
||||
|
||||
@@ -3,39 +3,39 @@
|
||||
|
||||
use clap::Args;
|
||||
|
||||
#[derive(Args, Clone)]
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct NetstackArgs {
|
||||
#[arg(long, default_value_t = 180)]
|
||||
#[arg(long, hide = true, default_value_t = 180)]
|
||||
pub netstack_download_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_value_t = 30)]
|
||||
#[arg(long, hide = true, default_value_t = 30)]
|
||||
pub metadata_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_value = "1.1.1.1")]
|
||||
#[arg(long, hide = true, default_value = "1.1.1.1")]
|
||||
pub netstack_v4_dns: String,
|
||||
|
||||
#[arg(long, default_value = "2606:4700:4700::1111")]
|
||||
#[arg(long, hide = true, default_value = "2606:4700:4700::1111")]
|
||||
pub netstack_v6_dns: String,
|
||||
|
||||
#[arg(long, default_value_t = 5)]
|
||||
#[arg(long, hide = true, default_value_t = 5)]
|
||||
pub netstack_num_ping: u8,
|
||||
|
||||
#[arg(long, default_value_t = 3)]
|
||||
#[arg(long, hide = true, default_value_t = 3)]
|
||||
pub netstack_send_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_value_t = 3)]
|
||||
#[arg(long, hide = true, default_value_t = 3)]
|
||||
pub netstack_recv_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_values_t = vec!["nym.com".to_string()])]
|
||||
#[arg(long, hide= true, default_values_t = vec!["nym.com".to_string()])]
|
||||
pub netstack_ping_hosts_v4: Vec<String>,
|
||||
|
||||
#[arg(long, default_values_t = vec!["1.1.1.1".to_string()])]
|
||||
#[arg(long, hide= true, default_values_t = vec!["1.1.1.1".to_string()])]
|
||||
pub netstack_ping_ips_v4: Vec<String>,
|
||||
|
||||
#[arg(long, default_values_t = vec!["cloudflare.com".to_string()])]
|
||||
#[arg(long, hide= true, default_values_t = vec!["cloudflare.com".to_string()])]
|
||||
pub netstack_ping_hosts_v6: Vec<String>,
|
||||
|
||||
#[arg(long, default_values_t = vec!["2001:4860:4860::8888".to_string(), "2606:4700:4700::1111".to_string(), "2620:fe::fe".to_string()])]
|
||||
#[arg(long, hide= true, default_values_t = vec!["2001:4860:4860::8888".to_string(), "2606:4700:4700::1111".to_string(), "2620:fe::fe".to_string()])]
|
||||
pub netstack_ping_ips_v6: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,21 @@ use clap::Args;
|
||||
|
||||
use crate::common::socks5_test::JsonRpcClient;
|
||||
|
||||
#[derive(Args)]
|
||||
const DEFAULT_RPC_ENDPOINT: &str = "https://cloudflare-eth.com";
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct Socks5Args {
|
||||
#[arg(long, value_delimiter = ';')]
|
||||
#[arg(long, hide = true, value_delimiter = ';', default_value = DEFAULT_RPC_ENDPOINT)]
|
||||
pub socks5_json_rpc_url_list: Vec<String>,
|
||||
|
||||
#[arg(long, default_value_t = 30)]
|
||||
#[arg(long, hide = true, default_value_t = 30)]
|
||||
pub mixnet_client_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_value_t = 10)]
|
||||
#[arg(long, hide = true, default_value_t = 10)]
|
||||
pub test_count: u64,
|
||||
|
||||
/// stops socks5 test early after this many failed attempts
|
||||
#[arg(long, default_value_t = 3)]
|
||||
#[arg(long, hide = true, default_value_t = 3)]
|
||||
pub failure_count_cutoff: usize,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,87 +4,72 @@
|
||||
//! Test mode definitions for gateway probe.
|
||||
//!
|
||||
//! This module defines the different test modes supported by the gateway probe:
|
||||
//! - Mixnet: Traditional mixnet path testing
|
||||
//! - SingleHop: LP registration + WireGuard on single gateway
|
||||
//! - TwoHop: Entry LP + Exit LP (nested forwarding) + WireGuard
|
||||
//! - Core: Traditional mixnet path testing and Wireguard via authenticator
|
||||
//! - WgMix: Wireguard via authenticator
|
||||
//! - WgLp: Entry LP + Exit LP (nested forwarding) + WireGuard
|
||||
//! - LpOnly: LP registration only, no WireGuard
|
||||
//! - Socks5Only: Socks5 test
|
||||
//! - All: Mixnet, wireguard over authenticator and LP registration
|
||||
|
||||
/// Test mode for the gateway probe.
|
||||
///
|
||||
/// Determines which tests are performed and how connections are established.
|
||||
// This enum replaces the scattered boolean flags (only_wireguard,
|
||||
// only_lp_registration, test_lp_wg) with explicit, named modes for clarity.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum TestMode {
|
||||
/// Traditional mixnet testing - connects via mixnet, tests entry/exit pings + WireGuard via authenticator
|
||||
/// Mixnet tests + WireGuard via authenticator
|
||||
#[default]
|
||||
Mixnet,
|
||||
/// LP registration + WireGuard on single gateway (no mixnet, no forwarding)
|
||||
SingleHop,
|
||||
/// Entry LP + Exit LP (nested session forwarding) + WireGuard tunnel
|
||||
TwoHop,
|
||||
/// LP registration only - test handshake and registration, skip WireGuard
|
||||
Core,
|
||||
/// Wireguard via authenticator
|
||||
WgMix,
|
||||
/// Wireguard over LP
|
||||
WgLp,
|
||||
/// LP registration only - test handshake and registration
|
||||
LpOnly,
|
||||
/// Socks5 test only
|
||||
Socks5Only,
|
||||
/// Mixnet tests, Wireguard tests, LP tests, Socks5 test
|
||||
All,
|
||||
}
|
||||
|
||||
impl TestMode {
|
||||
/// Infer test mode from legacy boolean flags (backward compatibility)
|
||||
pub fn from_flags(
|
||||
only_wireguard: bool,
|
||||
only_lp_registration: bool,
|
||||
test_lp_wg: bool,
|
||||
has_exit_gateway: bool,
|
||||
) -> Self {
|
||||
if only_lp_registration {
|
||||
TestMode::LpOnly
|
||||
} else if test_lp_wg {
|
||||
if has_exit_gateway {
|
||||
TestMode::TwoHop
|
||||
} else {
|
||||
TestMode::SingleHop
|
||||
}
|
||||
} else if only_wireguard {
|
||||
// WireGuard via authenticator (still uses mixnet path)
|
||||
TestMode::Mixnet
|
||||
} else {
|
||||
TestMode::Mixnet
|
||||
}
|
||||
// Wether we need to run mixnet tests
|
||||
pub fn mixnet_tests(&self) -> bool {
|
||||
matches!(self, TestMode::Core | TestMode::All)
|
||||
}
|
||||
|
||||
// Wether we need to run Wiregurd tests
|
||||
pub fn wireguard_tests(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestMode::Core | TestMode::WgMix | TestMode::WgLp | TestMode::All
|
||||
)
|
||||
}
|
||||
|
||||
// Wether we need to run Lp tests
|
||||
pub fn lp_tests(&self) -> bool {
|
||||
matches!(self, TestMode::WgLp | TestMode::LpOnly | TestMode::All)
|
||||
}
|
||||
|
||||
// Wether we need to run socks5 tests
|
||||
pub fn socks5_tests(&self) -> bool {
|
||||
matches!(self, TestMode::Socks5Only | TestMode::All)
|
||||
}
|
||||
|
||||
/// Whether this mode requires a mixnet client
|
||||
pub fn needs_mixnet(&self) -> bool {
|
||||
matches!(self, TestMode::Mixnet)
|
||||
}
|
||||
|
||||
/// Whether this mode uses LP registration
|
||||
pub fn uses_lp(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestMode::SingleHop | TestMode::TwoHop | TestMode::LpOnly
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this mode tests WireGuard tunnels
|
||||
pub fn tests_wireguard(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestMode::Mixnet | TestMode::SingleHop | TestMode::TwoHop
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this mode requires an exit gateway
|
||||
pub fn needs_exit_gateway(&self) -> bool {
|
||||
matches!(self, TestMode::TwoHop)
|
||||
matches!(self, TestMode::Core | TestMode::All | TestMode::WgMix)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TestMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TestMode::Mixnet => write!(f, "mixnet"),
|
||||
TestMode::SingleHop => write!(f, "single-hop"),
|
||||
TestMode::TwoHop => write!(f, "two-hop"),
|
||||
TestMode::Core => write!(f, "core"),
|
||||
TestMode::WgMix => write!(f, "wg-mix"),
|
||||
TestMode::WgLp => write!(f, "wg-lp"),
|
||||
TestMode::LpOnly => write!(f, "lp-only"),
|
||||
TestMode::Socks5Only => write!(f, "socks5-only"),
|
||||
TestMode::All => write!(f, "all"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,12 +79,14 @@ impl std::str::FromStr for TestMode {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"mixnet" => Ok(TestMode::Mixnet),
|
||||
"single-hop" | "singlehop" | "single_hop" => Ok(TestMode::SingleHop),
|
||||
"two-hop" | "twohop" | "two_hop" => Ok(TestMode::TwoHop),
|
||||
"mixnet" | "core" => Ok(TestMode::Core),
|
||||
"wg-mix" | "wgmix" | "wg_mix" => Ok(TestMode::WgMix),
|
||||
"wg-lp" | "wglp" | "wg_lp" => Ok(TestMode::WgLp),
|
||||
"lp-only" | "lponly" | "lp_only" => Ok(TestMode::LpOnly),
|
||||
"socks5-only" | "socks5only" | "socks5_only" => Ok(TestMode::Socks5Only),
|
||||
"all" => Ok(TestMode::All),
|
||||
_ => Err(format!(
|
||||
"Unknown test mode: '{}'. Valid modes: mixnet, single-hop, two-hop, lp-only",
|
||||
"Unknown test mode: '{}'. Valid modes: core, wg-mix, wg-lp, lp-only, socks5-only, all",
|
||||
s
|
||||
)),
|
||||
}
|
||||
@@ -110,152 +97,80 @@ impl std::str::FromStr for TestMode {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ============ from_flags() tests ============
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_default_is_mixnet() {
|
||||
// All flags false -> Mixnet (default)
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, false, false, false),
|
||||
TestMode::Mixnet
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_only_wireguard_is_mixnet() {
|
||||
// only_wireguard still uses mixnet path (WG via authenticator)
|
||||
assert_eq!(
|
||||
TestMode::from_flags(true, false, false, false),
|
||||
TestMode::Mixnet
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_only_lp_registration() {
|
||||
// only_lp_registration -> LpOnly (takes priority)
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, true, false, false),
|
||||
TestMode::LpOnly
|
||||
);
|
||||
// Even with other flags set, only_lp_registration wins
|
||||
assert_eq!(
|
||||
TestMode::from_flags(true, true, true, true),
|
||||
TestMode::LpOnly
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_test_lp_wg_single_hop() {
|
||||
// test_lp_wg without exit gateway -> SingleHop
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, false, true, false),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_test_lp_wg_two_hop() {
|
||||
// test_lp_wg with exit gateway -> TwoHop
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, false, true, true),
|
||||
TestMode::TwoHop
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_has_exit_gateway_alone_is_mixnet() {
|
||||
// has_exit_gateway alone doesn't change mode
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, false, false, true),
|
||||
TestMode::Mixnet
|
||||
);
|
||||
}
|
||||
|
||||
// ============ Helper method tests ============
|
||||
|
||||
#[test]
|
||||
fn test_needs_mixnet() {
|
||||
assert!(TestMode::Mixnet.needs_mixnet());
|
||||
assert!(!TestMode::SingleHop.needs_mixnet());
|
||||
assert!(!TestMode::TwoHop.needs_mixnet());
|
||||
assert!(TestMode::Core.needs_mixnet());
|
||||
assert!(TestMode::WgMix.needs_mixnet());
|
||||
assert!(!TestMode::WgLp.needs_mixnet());
|
||||
assert!(!TestMode::LpOnly.needs_mixnet());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uses_lp() {
|
||||
assert!(!TestMode::Mixnet.uses_lp());
|
||||
assert!(TestMode::SingleHop.uses_lp());
|
||||
assert!(TestMode::TwoHop.uses_lp());
|
||||
assert!(TestMode::LpOnly.uses_lp());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tests_wireguard() {
|
||||
assert!(TestMode::Mixnet.tests_wireguard());
|
||||
assert!(TestMode::SingleHop.tests_wireguard());
|
||||
assert!(TestMode::TwoHop.tests_wireguard());
|
||||
assert!(!TestMode::LpOnly.tests_wireguard());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_needs_exit_gateway() {
|
||||
assert!(!TestMode::Mixnet.needs_exit_gateway());
|
||||
assert!(!TestMode::SingleHop.needs_exit_gateway());
|
||||
assert!(TestMode::TwoHop.needs_exit_gateway());
|
||||
assert!(!TestMode::LpOnly.needs_exit_gateway());
|
||||
assert!(!TestMode::Socks5Only.needs_mixnet());
|
||||
assert!(TestMode::All.needs_mixnet());
|
||||
}
|
||||
|
||||
// ============ Display tests ============
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(TestMode::Mixnet.to_string(), "mixnet");
|
||||
assert_eq!(TestMode::SingleHop.to_string(), "single-hop");
|
||||
assert_eq!(TestMode::TwoHop.to_string(), "two-hop");
|
||||
assert_eq!(TestMode::Core.to_string(), "core");
|
||||
assert_eq!(TestMode::WgMix.to_string(), "wg-mix");
|
||||
assert_eq!(TestMode::WgLp.to_string(), "wg-lp");
|
||||
assert_eq!(TestMode::LpOnly.to_string(), "lp-only");
|
||||
assert_eq!(TestMode::Socks5Only.to_string(), "socks5-only");
|
||||
assert_eq!(TestMode::All.to_string(), "all");
|
||||
}
|
||||
|
||||
// ============ FromStr tests ============
|
||||
|
||||
#[test]
|
||||
fn test_from_str_canonical() {
|
||||
assert_eq!("mixnet".parse::<TestMode>().unwrap(), TestMode::Mixnet);
|
||||
assert_eq!(
|
||||
"single-hop".parse::<TestMode>().unwrap(),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
assert_eq!("two-hop".parse::<TestMode>().unwrap(), TestMode::TwoHop);
|
||||
assert_eq!("core".parse::<TestMode>().unwrap(), TestMode::Core);
|
||||
assert_eq!("wg-mix".parse::<TestMode>().unwrap(), TestMode::WgMix);
|
||||
assert_eq!("wg-lp".parse::<TestMode>().unwrap(), TestMode::WgLp);
|
||||
assert_eq!("lp-only".parse::<TestMode>().unwrap(), TestMode::LpOnly);
|
||||
assert_eq!(
|
||||
"socks5-only".parse::<TestMode>().unwrap(),
|
||||
TestMode::Socks5Only
|
||||
);
|
||||
assert_eq!("all".parse::<TestMode>().unwrap(), TestMode::All);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_alternate_formats() {
|
||||
// Default aliases
|
||||
assert_eq!("mixnet".parse::<TestMode>().unwrap(), TestMode::Core);
|
||||
|
||||
// snake_case
|
||||
assert_eq!(
|
||||
"single_hop".parse::<TestMode>().unwrap(),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
assert_eq!("two_hop".parse::<TestMode>().unwrap(), TestMode::TwoHop);
|
||||
assert_eq!("wg_mix".parse::<TestMode>().unwrap(), TestMode::WgMix);
|
||||
assert_eq!("wg_lp".parse::<TestMode>().unwrap(), TestMode::WgLp);
|
||||
assert_eq!("lp_only".parse::<TestMode>().unwrap(), TestMode::LpOnly);
|
||||
assert_eq!(
|
||||
"socks5_only".parse::<TestMode>().unwrap(),
|
||||
TestMode::Socks5Only
|
||||
);
|
||||
|
||||
// no separator
|
||||
assert_eq!(
|
||||
"singlehop".parse::<TestMode>().unwrap(),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
assert_eq!("twohop".parse::<TestMode>().unwrap(), TestMode::TwoHop);
|
||||
assert_eq!("wgmix".parse::<TestMode>().unwrap(), TestMode::WgMix);
|
||||
assert_eq!("wglp".parse::<TestMode>().unwrap(), TestMode::WgLp);
|
||||
assert_eq!("lponly".parse::<TestMode>().unwrap(), TestMode::LpOnly);
|
||||
assert_eq!(
|
||||
"socks5only".parse::<TestMode>().unwrap(),
|
||||
TestMode::Socks5Only
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_case_insensitive() {
|
||||
assert_eq!("MIXNET".parse::<TestMode>().unwrap(), TestMode::Mixnet);
|
||||
assert_eq!(
|
||||
"Single-Hop".parse::<TestMode>().unwrap(),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
assert_eq!("TWO_HOP".parse::<TestMode>().unwrap(), TestMode::TwoHop);
|
||||
assert_eq!("cOrE".parse::<TestMode>().unwrap(), TestMode::Core);
|
||||
assert_eq!("WG-MIX".parse::<TestMode>().unwrap(), TestMode::WgMix);
|
||||
assert_eq!("Wg_Lp".parse::<TestMode>().unwrap(), TestMode::WgLp);
|
||||
assert_eq!("LpOnly".parse::<TestMode>().unwrap(), TestMode::LpOnly);
|
||||
assert_eq!(
|
||||
"soCkS5-oNlY".parse::<TestMode>().unwrap(),
|
||||
TestMode::Socks5Only
|
||||
);
|
||||
assert_eq!("ALL".parse::<TestMode>().unwrap(), TestMode::All);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -270,10 +185,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_display_fromstr_roundtrip() {
|
||||
for mode in [
|
||||
TestMode::Mixnet,
|
||||
TestMode::SingleHop,
|
||||
TestMode::TwoHop,
|
||||
TestMode::Core,
|
||||
TestMode::WgMix,
|
||||
TestMode::WgLp,
|
||||
TestMode::LpOnly,
|
||||
TestMode::Socks5Only,
|
||||
TestMode::All,
|
||||
] {
|
||||
let s = mode.to_string();
|
||||
let parsed: TestMode = s.parse().unwrap();
|
||||
|
||||
+346
-794
File diff suppressed because it is too large
Load Diff
+145
-511
@@ -1,20 +1,12 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use anyhow::bail;
|
||||
use clap::{Parser, Subcommand};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_config::defaults::setup_env;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_gateway_probe::config::Socks5Args;
|
||||
use nym_gateway_probe::{
|
||||
CredentialArgs, NetstackArgs, NymApiDirectory, ProbeResult, TestMode, TestedNode,
|
||||
TestedNodeDetails, TestedNodeLpDetails, query_gateway_by_ip,
|
||||
};
|
||||
use nym_kkt_ciphersuite::{HashFunction, KEM};
|
||||
use nym_gateway_probe::config::{CredentialArgs, CredentialMode, ProbeConfig};
|
||||
use nym_gateway_probe::{NymApiDirectory, ProbeResult, query_gateway_by_ip};
|
||||
use nym_sdk::mixnet::NodeIdentity;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
use tracing::*;
|
||||
@@ -23,178 +15,84 @@ fn pretty_build_info_static() -> &'static str {
|
||||
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
|
||||
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||
}
|
||||
|
||||
fn validate_node_identity(s: &str) -> Result<NodeIdentity, String> {
|
||||
match s.parse() {
|
||||
Ok(cg) => Ok(cg),
|
||||
Err(_) => Err(format!("failed to parse country group: {s}")),
|
||||
}
|
||||
}
|
||||
const DEFAULT_CONFIG_DIR: &str = "/tmp/nym-gateway-probe/config/";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
||||
struct CliArgs {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
command: Commands,
|
||||
|
||||
/// Path pointing to an env file describing the network.
|
||||
#[arg(short, long, global = true)]
|
||||
#[arg(short, long)]
|
||||
config_env_file: Option<PathBuf>,
|
||||
|
||||
/// The specific gateway specified by ID.
|
||||
#[arg(long, short = 'g', alias = "gateway", global = true)]
|
||||
entry_gateway: Option<String>,
|
||||
|
||||
/// The address of the gateway to probe directly (bypasses directory lookup)
|
||||
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
|
||||
#[arg(long, global = true)]
|
||||
gateway_ip: Option<String>,
|
||||
|
||||
// ##########
|
||||
// ENTRY
|
||||
// ##########
|
||||
/// Ed25519 identity of the entry gateway (base58 encoded)
|
||||
/// When provided, skips HTTP API query - use for localnet testing
|
||||
#[arg(long, global = true)]
|
||||
entry_gateway_identity: Option<String>,
|
||||
|
||||
/// x25519 key of the entry gateway used for KKT exchange (base58 encoded)
|
||||
#[arg(long, global = true, requires = "entry_gateway_identity")]
|
||||
entry_gateway_x25519_key: Option<String>,
|
||||
|
||||
/// expected kem type of the entry gateway used during KKT exchange
|
||||
#[arg(long, global = true, requires = "entry_gateway_x25519_key")]
|
||||
entry_gateway_kem_key_type: Option<String>,
|
||||
|
||||
/// expected hash function used for the entry gateway kem key digest
|
||||
#[arg(long, global = true, requires = "entry_gateway_kem_key_type")]
|
||||
entry_gateway_kem_key_hash_function: Option<String>,
|
||||
|
||||
/// expected entry gateway kem key digest (base58 encoded)
|
||||
#[arg(long, global = true, requires = "entry_gateway_kem_key_hash_function")]
|
||||
entry_gateway_kem_hey_hash_bs58: Option<String>,
|
||||
|
||||
/// LP listener address for entry gateway (e.g., "192.168.66.6:41264")
|
||||
/// Used with --entry-gateway-identity for localnet mode
|
||||
#[arg(long, global = true)]
|
||||
entry_lp_address: Option<SocketAddr>,
|
||||
|
||||
// ##########
|
||||
// EXIT
|
||||
// ##########
|
||||
/// The address of the exit gateway for LP forwarding tests (used with --test-lp-wg)
|
||||
/// When specified, --gateway-ip becomes the entry gateway and this becomes the exit gateway
|
||||
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
|
||||
#[arg(long, global = true)]
|
||||
exit_gateway_ip: Option<String>,
|
||||
|
||||
/// Ed25519 identity of the exit gateway (base58 encoded)
|
||||
/// When provided, skips HTTP API query - use for localnet testing
|
||||
#[arg(long, global = true)]
|
||||
exit_gateway_identity: Option<String>,
|
||||
|
||||
/// x25519 key of the exit gateway used for KKT exchange (base58 encoded)
|
||||
#[arg(long, global = true, requires = "exit_gateway_identity")]
|
||||
exit_gateway_x25519_key: Option<String>,
|
||||
|
||||
/// expected kem type of the exit gateway used during KKT exchange
|
||||
#[arg(long, global = true, requires = "exit_gateway_x25519_key")]
|
||||
exit_gateway_kem_key_type: Option<String>,
|
||||
|
||||
/// expected hash function used for the exit gateway kem key digest
|
||||
#[arg(long, global = true, requires = "exit_gateway_kem_key_type")]
|
||||
exit_gateway_kem_key_hash_function: Option<String>,
|
||||
|
||||
/// expected exit gateway kem key digest (base58 encoded)
|
||||
#[arg(long, global = true, requires = "exit_gateway_kem_key_hash_function")]
|
||||
exit_gateway_kem_hey_hash_bs58: Option<String>,
|
||||
|
||||
/// LP listener address for exit gateway (e.g., "172.18.0.5:41264")
|
||||
/// This is the address the entry gateway uses to reach exit (for forwarding)
|
||||
/// Used with --exit-gateway-identity for localnet mode
|
||||
#[arg(long, global = true)]
|
||||
exit_lp_address: Option<SocketAddr>,
|
||||
|
||||
/// Default LP control port when deriving LP address from gateway IP
|
||||
#[arg(long, global = true, default_value = "41264")]
|
||||
lp_port: u16,
|
||||
|
||||
/// Identity of the node to test
|
||||
#[arg(long, short, value_parser = validate_node_identity, global = true)]
|
||||
node: Option<NodeIdentity>,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
min_gateway_mixnet_performance: Option<u8>,
|
||||
|
||||
// this was a dead field
|
||||
// #[arg(long, global = true)]
|
||||
// min_gateway_vpn_performance: Option<u8>,
|
||||
#[arg(long, global = true)]
|
||||
only_wireguard: bool,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
only_lp_registration: bool,
|
||||
|
||||
/// Test WireGuard via LP registration (no mixnet) - uses nested session forwarding
|
||||
#[arg(long, global = true)]
|
||||
test_lp_wg: bool,
|
||||
|
||||
/// Test mode - explicitly specify which tests to run
|
||||
///
|
||||
/// Modes:
|
||||
/// mixnet - Traditional mixnet testing (entry/exit pings + WireGuard via authenticator)
|
||||
/// single-hop - LP registration + WireGuard on single gateway (no mixnet)
|
||||
/// two-hop - Entry LP + Exit LP (nested forwarding) + WireGuard tunnel
|
||||
/// lp-only - LP registration only (no WireGuard)
|
||||
///
|
||||
/// If not specified, mode is inferred from other flags:
|
||||
/// --only-lp-registration → lp-only
|
||||
/// --test-lp-wg with exit gateway → two-hop
|
||||
/// --test-lp-wg without exit → single-hop
|
||||
/// otherwise → mixnet
|
||||
#[arg(long, global = true, value_name = "MODE")]
|
||||
mode: Option<String>,
|
||||
|
||||
/// Disable logging during probe
|
||||
#[arg(long, global = true)]
|
||||
ignore_egress_epoch_role: bool,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
#[arg(long)]
|
||||
no_log: bool,
|
||||
|
||||
/// Arguments to be appended to the wireguard config enabling amnezia-wg configuration
|
||||
#[arg(long, short, global = true)]
|
||||
amnezia_args: Option<String>,
|
||||
|
||||
/// Arguments to manage netstack downloads
|
||||
#[command(flatten)]
|
||||
netstack_args: NetstackArgs,
|
||||
|
||||
/// 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/";
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Run the probe locally
|
||||
/// Run the probe on an unannounced gateway. IP must be provided. Bypasses directory lookup
|
||||
RunLocal {
|
||||
/// Provide a mnemonic to get credentials (optional when using --use-mock-ecash)
|
||||
#[arg(long)]
|
||||
mnemonic: Option<String>,
|
||||
|
||||
/// Directory for credential and mixnet storage
|
||||
#[arg(long)]
|
||||
config_dir: Option<PathBuf>,
|
||||
|
||||
/// Use mock ecash credentials for testing (requires gateway with --lp-use-mock-ecash)
|
||||
/// The address of the gateway
|
||||
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
|
||||
#[arg(long)]
|
||||
use_mock_ecash: bool,
|
||||
entry_gateway_ip: String,
|
||||
|
||||
/// The address of the exit gateway. If not provided, entry acts as exit
|
||||
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
|
||||
#[arg(long)]
|
||||
exit_gateway_ip: Option<String>,
|
||||
|
||||
/// Arguments to manage credentials
|
||||
#[command(flatten)]
|
||||
credential_mode: CredentialMode,
|
||||
|
||||
#[command(flatten)]
|
||||
probe_config: ProbeConfig,
|
||||
},
|
||||
|
||||
/// Run the probe on a bonded gateway. Uses directory lookup
|
||||
Run {
|
||||
/// Directory for credential and mixnet storage
|
||||
#[arg(long)]
|
||||
config_dir: Option<PathBuf>,
|
||||
|
||||
/// The specific gateway specified by ID.
|
||||
#[arg(long, short = 'g', alias = "gateway")]
|
||||
entry_gateway: NodeIdentity,
|
||||
|
||||
/// Optional identity of the exit node to test, if not provided, entry_gateway is used
|
||||
#[arg(long)]
|
||||
exit_gateway: Option<NodeIdentity>,
|
||||
|
||||
/// Arguments to manage credentials
|
||||
#[command(flatten)]
|
||||
credential_mode: CredentialMode,
|
||||
|
||||
#[command(flatten)]
|
||||
probe_config: ProbeConfig,
|
||||
},
|
||||
|
||||
/// Run the probe by NS agents
|
||||
RunAgent {
|
||||
/// The specific gateway specified by ID.
|
||||
#[arg(long, short = 'g', alias = "gateway")]
|
||||
entry_gateway: NodeIdentity,
|
||||
|
||||
/// Arguments to manage credentials
|
||||
#[command(flatten)]
|
||||
credential_args: CredentialArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
probe_config: ProbeConfig,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -214,266 +112,66 @@ fn setup_logging() {
|
||||
.init();
|
||||
}
|
||||
|
||||
/// Resolve the test mode from explicit --mode arg or infer from legacy flags
|
||||
fn resolve_test_mode(
|
||||
mode_arg: Option<&str>,
|
||||
only_wireguard: bool,
|
||||
only_lp_registration: bool,
|
||||
test_lp_wg: bool,
|
||||
has_exit_gateway: bool,
|
||||
) -> anyhow::Result<TestMode> {
|
||||
if let Some(mode_str) = mode_arg {
|
||||
// Explicit --mode takes priority
|
||||
mode_str
|
||||
.parse::<TestMode>()
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))
|
||||
} else {
|
||||
// Infer from legacy flags
|
||||
Ok(TestMode::from_flags(
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
has_exit_gateway,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert TestMode back to legacy boolean flags for backward compatibility
|
||||
fn mode_to_flags(mode: TestMode) -> (bool, bool, bool) {
|
||||
match mode {
|
||||
TestMode::Mixnet => (false, false, false), // only_wireguard handled separately
|
||||
TestMode::SingleHop => (false, false, true),
|
||||
TestMode::TwoHop => (false, false, true),
|
||||
TestMode::LpOnly => (false, true, false),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::todo)]
|
||||
#[allow(unreachable_code, unused)]
|
||||
// ^^^^ // NOTE: to be changed by @SW
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
let args = CliArgs::parse();
|
||||
if !args.no_log {
|
||||
setup_logging();
|
||||
}
|
||||
debug!("{:?}", nym_bin_common::bin_info_local_vergen!());
|
||||
setup_env(args.config_env_file.as_ref());
|
||||
|
||||
setup_env(args.config_env_file.as_ref());
|
||||
let network = nym_sdk::NymNetworkDetails::new_from_env();
|
||||
|
||||
let nyxd_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
.map(|ep| ep.nyxd_url())
|
||||
.ok_or(anyhow::anyhow!("missing nyxd url"))?;
|
||||
match args.command {
|
||||
Commands::RunLocal {
|
||||
config_dir,
|
||||
entry_gateway_ip,
|
||||
exit_gateway_ip,
|
||||
credential_mode,
|
||||
probe_config,
|
||||
} => {
|
||||
info!("Using direct IP query mode for gateway: {entry_gateway_ip}");
|
||||
let entry_details = query_gateway_by_ip(entry_gateway_ip)
|
||||
.await?
|
||||
.to_testable_node()?;
|
||||
|
||||
args.socks5_args.validate_socks5_endpoints().await?;
|
||||
|
||||
// Three resolution modes in priority order:
|
||||
// 1. Localnet mode: --entry-gateway-identity provided (no HTTP query)
|
||||
// 2. Direct IP mode: --gateway-ip provided (queries HTTP API)
|
||||
// 3. Directory mode: uses nym-api directory service
|
||||
|
||||
// Localnet mode: identity provided via CLI, skip HTTP queries entirely
|
||||
if let Some(kem_key_digest) = &args.entry_gateway_kem_hey_hash_bs58 {
|
||||
info!("Using localnet mode with CLI-provided gateway identity");
|
||||
|
||||
// SAFETY: if kem key digest is provided, all other LP data must also be present
|
||||
// (enforced by clap)
|
||||
let hash_fn: HashFunction = args
|
||||
.entry_gateway_kem_key_hash_function
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.parse()?;
|
||||
let kem_type: KEM = args.entry_gateway_kem_key_type.as_ref().unwrap().parse()?;
|
||||
let x25519_key: x25519::PublicKey =
|
||||
args.entry_gateway_x25519_key.as_ref().unwrap().parse()?;
|
||||
let identity: ed25519::PublicKey = args.entry_gateway_identity.as_ref().unwrap().parse()?;
|
||||
let digest = bs58::decode(&kem_key_digest).into_vec()?;
|
||||
|
||||
let mut expected_kem_key_hashes = HashMap::new();
|
||||
let mut digests = HashMap::new();
|
||||
digests.insert(hash_fn, digest);
|
||||
expected_kem_key_hashes.insert(kem_type, digests);
|
||||
|
||||
// Entry LP address: explicit or derived from gateway_ip + lp_port
|
||||
let entry_lp_addr: SocketAddr = if let Some(lp_addr) = args.entry_lp_address {
|
||||
lp_addr
|
||||
} else if let Some(gw_ip) = &args.gateway_ip {
|
||||
// Derive LP address from gateway IP
|
||||
let ip: std::net::IpAddr = gw_ip
|
||||
.parse()
|
||||
.map_err(|e| anyhow::anyhow!("Invalid gateway-ip '{gw_ip}': {e}"))?;
|
||||
SocketAddr::new(ip, args.lp_port)
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"--entry-lp-address or --gateway-ip required with --entry-gateway-identity"
|
||||
);
|
||||
};
|
||||
|
||||
let entry_lp_node = TestedNodeLpDetails {
|
||||
address: entry_lp_addr,
|
||||
expected_kem_key_hashes,
|
||||
expected_signing_key_hashes: todo!(),
|
||||
x25519: x25519_key,
|
||||
lp_version: todo!(),
|
||||
};
|
||||
let entry_details = TestedNodeDetails::from_cli(identity, entry_lp_node);
|
||||
|
||||
// Parse exit gateway if provided
|
||||
let exit_details = if let Some(kem_key_digest) = &args.exit_gateway_kem_hey_hash_bs58 {
|
||||
let exit_lp_addr = *args.exit_lp_address.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("--exit-lp-address required with --exit-gateway-identity")
|
||||
})?;
|
||||
|
||||
// SAFETY: if kem key digest is provided, all other LP data must also be present
|
||||
// (enforced by clap)
|
||||
let hash_fn: HashFunction = args
|
||||
.exit_gateway_kem_key_hash_function
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.parse()?;
|
||||
let kem_type: KEM = args.exit_gateway_kem_key_type.as_ref().unwrap().parse()?;
|
||||
let x25519_key: x25519::PublicKey =
|
||||
args.exit_gateway_x25519_key.as_ref().unwrap().parse()?;
|
||||
let identity: ed25519::PublicKey =
|
||||
args.exit_gateway_identity.as_ref().unwrap().parse()?;
|
||||
let digest = bs58::decode(&kem_key_digest).into_vec()?;
|
||||
|
||||
let mut expected_kem_key_hashes = HashMap::new();
|
||||
let mut digests = HashMap::new();
|
||||
digests.insert(hash_fn, digest);
|
||||
expected_kem_key_hashes.insert(kem_type, digests);
|
||||
|
||||
let exit_lp_node = TestedNodeLpDetails {
|
||||
address: exit_lp_addr,
|
||||
expected_kem_key_hashes,
|
||||
expected_signing_key_hashes: todo!(),
|
||||
x25519: x25519_key,
|
||||
lp_version: todo!(),
|
||||
};
|
||||
|
||||
Some(TestedNodeDetails::from_cli(identity, exit_lp_node))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Resolve test mode from --mode arg or infer from flags
|
||||
let has_exit = exit_details.is_some();
|
||||
let test_mode = resolve_test_mode(
|
||||
args.mode.as_deref(),
|
||||
args.only_wireguard,
|
||||
args.only_lp_registration,
|
||||
args.test_lp_wg,
|
||||
has_exit,
|
||||
)?;
|
||||
|
||||
// Validate that two-hop mode has required exit gateway
|
||||
if test_mode.needs_exit_gateway() && !has_exit {
|
||||
bail!(
|
||||
"--mode two-hop requires exit gateway \
|
||||
(use --exit-gateway-identity and --exit-lp-address)"
|
||||
);
|
||||
}
|
||||
|
||||
info!("Test mode: {}", test_mode);
|
||||
|
||||
// Convert back to flags for backward compatibility with existing probe methods
|
||||
// only_wireguard is preserved from args since it's orthogonal to mode
|
||||
// (it means "skip ping tests" in mixnet mode, irrelevant for LP modes)
|
||||
let (_, only_lp_registration, test_lp_wg) = mode_to_flags(test_mode);
|
||||
let only_wireguard = args.only_wireguard;
|
||||
|
||||
let mut trial = nym_gateway_probe::Probe::new_localnet(
|
||||
entry_details,
|
||||
exit_details,
|
||||
args.netstack_args,
|
||||
args.credential_args,
|
||||
args.socks5_args,
|
||||
);
|
||||
|
||||
if let Some(awg_args) = args.amnezia_args {
|
||||
trial.with_amnezia(&awg_args);
|
||||
}
|
||||
|
||||
// Localnet mode doesn't need directory, but nyxd_url is still used for credentials
|
||||
return match &args.command {
|
||||
Some(Commands::RunLocal {
|
||||
mnemonic,
|
||||
config_dir,
|
||||
use_mock_ecash,
|
||||
}) => {
|
||||
let config_dir = config_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_DIR).join(&network.network_name));
|
||||
|
||||
info!(
|
||||
"using the following directory for the probe config: {}",
|
||||
config_dir.display()
|
||||
);
|
||||
|
||||
Box::pin(trial.probe_run_locally(
|
||||
&config_dir,
|
||||
mnemonic.as_deref(),
|
||||
None, // No directory in localnet mode
|
||||
nyxd_url,
|
||||
args.ignore_egress_epoch_role,
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
args.min_gateway_mixnet_performance,
|
||||
*use_mock_ecash,
|
||||
network,
|
||||
))
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
Box::pin(trial.probe(
|
||||
None, // No directory in localnet mode
|
||||
nyxd_url,
|
||||
args.ignore_egress_epoch_role,
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
args.min_gateway_mixnet_performance,
|
||||
network,
|
||||
))
|
||||
.await
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// If gateway IP is provided, query it directly without using the directory
|
||||
let (entry, directory, gateway_node, exit_gateway_node) =
|
||||
if let Some(gateway_ip) = args.gateway_ip.clone() {
|
||||
info!("Using direct IP query mode for gateway: {}", gateway_ip);
|
||||
let gateway_node = query_gateway_by_ip(gateway_ip).await?;
|
||||
let identity = gateway_node.identity();
|
||||
|
||||
// Query exit gateway if provided (for LP forwarding tests)
|
||||
let exit_node = if let Some(exit_gateway_ip) = args.exit_gateway_ip {
|
||||
info!(
|
||||
"Using direct IP query mode for exit gateway: {}",
|
||||
exit_gateway_ip
|
||||
);
|
||||
Some(query_gateway_by_ip(exit_gateway_ip).await?)
|
||||
// Parse exit gateway if provided
|
||||
let exit_details = if let Some(ip_address) = exit_gateway_ip {
|
||||
info!("Using direct IP query mode for exit gateway: {ip_address}");
|
||||
Some(query_gateway_by_ip(ip_address).await?.to_testable_node()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Still create the directory for potential secondary lookups,
|
||||
// but only if API URL is available
|
||||
let directory =
|
||||
if let Some(api_url) = network.endpoints.first().and_then(|ep| ep.api_url()) {
|
||||
Some(NymApiDirectory::new(api_url).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let config_dir = config_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_DIR).join(&network.network_name));
|
||||
|
||||
(identity, directory, Some(gateway_node), exit_node)
|
||||
} else {
|
||||
// Original behavior: use directory service
|
||||
if config_dir.is_file() {
|
||||
anyhow::bail!("provided configuration directory is a file");
|
||||
}
|
||||
|
||||
if !config_dir.exists() {
|
||||
std::fs::create_dir_all(config_dir.clone())?;
|
||||
}
|
||||
|
||||
info!(
|
||||
"using the following directory for the probe config: {}",
|
||||
config_dir.display()
|
||||
);
|
||||
|
||||
let trial =
|
||||
nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config);
|
||||
|
||||
Box::pin(trial.probe_run_locally(&config_dir, credential_mode)).await
|
||||
}
|
||||
Commands::Run {
|
||||
entry_gateway,
|
||||
exit_gateway,
|
||||
config_dir,
|
||||
credential_mode,
|
||||
probe_config,
|
||||
} => {
|
||||
let api_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
@@ -481,121 +179,57 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
.ok_or(anyhow::anyhow!("missing api url"))?;
|
||||
|
||||
let directory = NymApiDirectory::new(api_url).await?;
|
||||
let entry_details = directory
|
||||
.entry_gateway(&entry_gateway)?
|
||||
.to_testable_node()?;
|
||||
let exit_details = exit_gateway
|
||||
.map(|id_key| directory.exit_gateway(&id_key))
|
||||
.transpose()?
|
||||
.map(|node| node.to_testable_node())
|
||||
.transpose()?;
|
||||
|
||||
let entry = if let Some(gateway) = &args.entry_gateway {
|
||||
NodeIdentity::from_base58_string(gateway)?
|
||||
} else {
|
||||
directory.random_exit_with_ipr()?
|
||||
};
|
||||
|
||||
(entry, Some(directory), None, None)
|
||||
};
|
||||
|
||||
let test_point = if let Some(node) = args.node {
|
||||
TestedNode::Custom {
|
||||
identity: node,
|
||||
shares_entry: false,
|
||||
}
|
||||
} else {
|
||||
TestedNode::SameAsEntry
|
||||
};
|
||||
|
||||
// Resolve test mode from --mode arg or infer from flags
|
||||
let has_exit = exit_gateway_node.is_some();
|
||||
let test_mode = resolve_test_mode(
|
||||
args.mode.as_deref(),
|
||||
args.only_wireguard,
|
||||
args.only_lp_registration,
|
||||
args.test_lp_wg,
|
||||
has_exit,
|
||||
)?;
|
||||
info!("Test mode: {}", test_mode);
|
||||
|
||||
// Convert back to flags for backward compatibility with existing probe methods
|
||||
// only_wireguard is preserved from args since it's orthogonal to mode
|
||||
let (_, only_lp_registration, test_lp_wg) = mode_to_flags(test_mode);
|
||||
let only_wireguard = args.only_wireguard;
|
||||
|
||||
let mut trial = if let (Some(entry_node), Some(exit_node)) = (&gateway_node, &exit_gateway_node)
|
||||
{
|
||||
// Both entry and exit gateways provided (for LP telescoping tests)
|
||||
info!("Using both entry and exit gateways for LP forwarding test");
|
||||
nym_gateway_probe::Probe::new_with_gateways(
|
||||
entry,
|
||||
test_point,
|
||||
args.netstack_args,
|
||||
args.credential_args,
|
||||
entry_node.clone(),
|
||||
exit_node.clone(),
|
||||
args.socks5_args,
|
||||
)
|
||||
} else if let Some(gw_node) = gateway_node {
|
||||
// Only entry gateway provided
|
||||
nym_gateway_probe::Probe::new_with_gateway(
|
||||
entry,
|
||||
test_point,
|
||||
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,
|
||||
args.socks5_args,
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(awg_args) = args.amnezia_args {
|
||||
trial.with_amnezia(&awg_args);
|
||||
}
|
||||
|
||||
match args.command {
|
||||
Some(Commands::RunLocal {
|
||||
mnemonic,
|
||||
config_dir,
|
||||
use_mock_ecash,
|
||||
}) => {
|
||||
let config_dir = config_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_DIR).join(&network.network_name));
|
||||
|
||||
if config_dir.is_file() {
|
||||
anyhow::bail!("provided configuration directory is a file");
|
||||
}
|
||||
|
||||
if !config_dir.exists() {
|
||||
std::fs::create_dir_all(config_dir.clone())?;
|
||||
}
|
||||
|
||||
info!(
|
||||
"using the following directory for the probe config: {}",
|
||||
config_dir.display()
|
||||
);
|
||||
|
||||
Box::pin(trial.probe_run_locally(
|
||||
&config_dir,
|
||||
mnemonic.as_deref(),
|
||||
directory,
|
||||
nyxd_url,
|
||||
args.ignore_egress_epoch_role,
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
args.min_gateway_mixnet_performance,
|
||||
use_mock_ecash,
|
||||
network,
|
||||
))
|
||||
.await
|
||||
let trial =
|
||||
nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config);
|
||||
Box::pin(trial.probe_run(&config_dir, credential_mode)).await
|
||||
}
|
||||
None => {
|
||||
Box::pin(trial.probe(
|
||||
directory,
|
||||
nyxd_url,
|
||||
args.ignore_egress_epoch_role,
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
args.min_gateway_mixnet_performance,
|
||||
network,
|
||||
))
|
||||
.await
|
||||
Commands::RunAgent {
|
||||
entry_gateway,
|
||||
credential_args,
|
||||
mut probe_config,
|
||||
} => {
|
||||
let api_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
.and_then(|ep| ep.api_url())
|
||||
.ok_or(anyhow::anyhow!("missing api url"))?;
|
||||
|
||||
let directory = NymApiDirectory::new(api_url).await?;
|
||||
let entry_details = directory
|
||||
.entry_gateway(&entry_gateway)?
|
||||
.to_testable_node()?;
|
||||
|
||||
// Agents run everything
|
||||
probe_config.test_mode = nym_gateway_probe::config::TestMode::All;
|
||||
|
||||
let trial = nym_gateway_probe::Probe::new(entry_details, None, network, probe_config);
|
||||
Box::pin(trial.probe_run_agent(credential_args)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user