shuffling files around in the probe, before improving it (#6391)

This commit is contained in:
Simon Wicky
2026-01-29 10:34:57 +01:00
committed by GitHub
parent 8e4cae2f57
commit 561182ce6b
15 changed files with 1118 additions and 1084 deletions
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::common::nodes::TestedNodeLpDetails;
use nym_crypto::asymmetric::ed25519;
use nym_ip_packet_requests::v8::response::{
ControlResponse, DataResponse, InfoLevel, IpPacketResponse, IpPacketResponseData,
};
use nym_lp::peer::LpRemotePeer;
use nym_sdk::mixnet::ReconstructedMessage;
use tracing::*;
pub fn to_lp_remote_peer(identity: ed25519::PublicKey, data: TestedNodeLpDetails) -> LpRemotePeer {
LpRemotePeer::new(identity, data.x25519).with_key_digests(
data.expected_kem_key_hashes,
data.expected_signing_key_hashes,
)
}
pub fn mixnet_debug_config(
min_gateway_performance: Option<u8>,
ignore_egress_epoch_role: bool,
) -> nym_client_core::config::DebugConfig {
let mut debug_config = nym_client_core::config::DebugConfig::default();
debug_config
.traffic
.disable_main_poisson_packet_distribution = true;
debug_config.cover_traffic.disable_loop_cover_traffic_stream = true;
if let Some(minimum_gateway_performance) = min_gateway_performance {
debug_config.topology.minimum_gateway_performance = minimum_gateway_performance;
}
if ignore_egress_epoch_role {
debug_config.topology.ignore_egress_epoch_role = ignore_egress_epoch_role;
}
debug_config
}
pub fn unpack_data_response(reconstructed_message: &ReconstructedMessage) -> Option<DataResponse> {
match IpPacketResponse::from_reconstructed_message(reconstructed_message) {
Ok(response) => match response.data {
IpPacketResponseData::Data(data_response) => Some(data_response),
IpPacketResponseData::Control(control) => match *control {
ControlResponse::Info(info) => {
let msg = format!("Received info response from the mixnet: {}", info.reply);
match info.level {
InfoLevel::Info => info!("{msg}"),
InfoLevel::Warn => warn!("{msg}"),
InfoLevel::Error => error!("{msg}"),
}
None
}
_ => {
info!("Ignoring: {:?}", control);
None
}
},
},
Err(err) => {
warn!("Failed to parse mixnet message: {err}");
None
}
}
}
+8 -5
View File
@@ -6,8 +6,11 @@
//! This module contains shared functionality used by multiple test modes:
//! - WireGuard tunnel testing via netstack
pub mod wireguard;
pub use wireguard::{
TwoHopWgTunnelConfig, WgTunnelConfig, run_tunnel_tests, run_two_hop_tunnel_tests,
};
pub(crate) mod bandwidth_helpers;
pub(crate) mod helpers;
pub(crate) mod icmp;
pub(crate) mod netstack;
pub(crate) mod nodes;
pub(crate) mod probe_tests;
pub(crate) mod types;
pub(crate) mod wireguard;
@@ -15,7 +15,7 @@ mod sys {
}
}
use crate::NetstackArgs;
use crate::config::NetstackArgs;
#[derive(serde::Serialize)]
pub struct NetstackRequest {
@@ -1,7 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{TestedNodeDetails, TestedNodeLpDetails};
use anyhow::{Context, anyhow, bail};
use nym_api_requests::models::{
AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2, HostInformationV2,
@@ -10,16 +9,20 @@ use nym_api_requests::models::{
};
use nym_authenticator_requests::AuthenticatorVersion;
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_crypto::asymmetric::x25519;
use nym_http_api_client::UserAgent;
use nym_kkt_ciphersuite::SignatureScheme;
use nym_kkt_ciphersuite::{KEM, KEMKeyDigests};
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_node_requests::api::client::NymNodeApiClientExt;
use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails;
use nym_sdk::mixnet::NodeIdentity;
use nym_sdk::mixnet::Recipient;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::models::NymNodeDescriptionV2;
use rand::prelude::IteratorRandom;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use time::OffsetDateTime;
use tracing::{debug, info, warn};
@@ -437,3 +440,69 @@ impl NymApiDirectory {
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 authenticator_address: Option<Recipient>,
pub authenticator_version: AuthenticatorVersion,
pub ip_address: Option<IpAddr>,
pub lp_data: Option<TestedNodeLpDetails>,
}
#[derive(Debug, Clone)]
pub struct TestedNodeLpDetails {
pub address: SocketAddr,
pub expected_kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
pub expected_signing_key_hashes: HashMap<SignatureScheme, KEMKeyDigests>,
pub x25519: x25519::PublicKey,
}
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),
// 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()
}
}
+697
View File
@@ -0,0 +1,697 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::common::nodes::{TestedNodeDetails, TestedNodeLpDetails};
use crate::common::types::{
Entry, Exit, IpPingReplies, LpProbeResults, ProbeOutcome, WgProbeResults,
};
use crate::common::wireguard::{
TwoHopWgTunnelConfig, WgTunnelConfig, run_tunnel_tests, run_two_hop_tunnel_tests,
};
use crate::common::{bandwidth_helpers, helpers, icmp};
use crate::config::NetstackArgs;
use anyhow::bail;
use base64::{Engine, engine::general_purpose};
use bytes::BytesMut;
use futures::StreamExt;
use nym_authenticator_client::AuthenticatorClient;
use nym_authenticator_requests::{
AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, v2, v3,
v4, v5, v6,
};
use nym_config::defaults::mixnet_vpn::{NYM_TUN_DEVICE_ADDRESS_V4, NYM_TUN_DEVICE_ADDRESS_V6};
use nym_connection_monitor::self_ping_and_wait;
use nym_credentials_interface::{CredentialSpendingData, TicketType};
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::mixnet::{MixnetClient, NodeIdentity, Recipient};
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
time::Duration,
};
use tokio::net::TcpStream;
use tokio_util::{codec::Decoder, sync::CancellationToken};
use tracing::*;
pub async fn wg_probe(
mut auth_client: AuthenticatorClient,
gateway_ip: IpAddr,
auth_version: AuthenticatorVersion,
awg_args: String,
netstack_args: NetstackArgs,
// TODO: update type
credential: CredentialSpendingData,
) -> anyhow::Result<WgProbeResults> {
info!("attempting to use authenticator version {auth_version:?}");
let mut rng = rand::thread_rng();
// that's a long conversion chain
// (it should be simplified later...)
// nym x25519 -> dalek x25519 -> wireguard wrapper x25519
let private_key = nym_crypto::asymmetric::encryption::PrivateKey::new(&mut rng);
let public_key = private_key.public_key();
let authenticator_pub_key = public_key.inner().into();
let init_message = match auth_version {
AuthenticatorVersion::V2 => ClientMessage::Initial(Box::new(
v2::registration::InitMessage::new(authenticator_pub_key),
)),
AuthenticatorVersion::V3 => ClientMessage::Initial(Box::new(
v3::registration::InitMessage::new(authenticator_pub_key),
)),
AuthenticatorVersion::V4 => ClientMessage::Initial(Box::new(
v4::registration::InitMessage::new(authenticator_pub_key),
)),
AuthenticatorVersion::V5 => ClientMessage::Initial(Box::new(
v5::registration::InitMessage::new(authenticator_pub_key),
)),
AuthenticatorVersion::V6 => ClientMessage::Initial(Box::new(
v6::registration::InitMessage::new(authenticator_pub_key),
)),
AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => bail!("unknown version number"),
};
let mut wg_outcome = WgProbeResults::default();
info!(
"connecting to authenticator: {}...",
auth_client.auth_recipient
);
let response = auth_client
.send_and_wait_for_response(&init_message)
.await?;
let registered_data = match response {
AuthenticatorResponse::PendingRegistration(pending_registration_response) => {
// Unwrap since we have already checked that we have the keypair.
debug!("Verifying data");
pending_registration_response.verify(&private_key)?;
let credential = credential
.try_into()
.inspect_err(|err| error!("invalid zk-nym data: {err}"))
.ok();
let finalized_message =
pending_registration_response.finalise_registration(&private_key, credential);
let client_message = ClientMessage::Final(finalized_message);
let response = auth_client
.send_and_wait_for_response(&client_message)
.await?;
let AuthenticatorResponse::Registered(registered_response) = response else {
bail!("Unexpected response");
};
registered_response
}
AuthenticatorResponse::Registered(registered_response) => registered_response,
_ => bail!("Unexpected response"),
};
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 public_key_hex = hex::encode(peer_public.as_bytes());
info!("WG connection details");
info!("Peer public key: {}", public_key_bs64);
info!(
"ips {}(v4) {}(v6), port {}",
registered_data.private_ips().ipv4,
registered_data.private_ips().ipv6,
registered_data.wg_port(),
);
let wg_endpoint = format!("{gateway_ip}:{}", registered_data.wg_port());
info!("Successfully registered with the gateway");
wg_outcome.can_register = true;
// Run tunnel connectivity tests using shared helper
let tunnel_config = WgTunnelConfig::new(
registered_data.private_ips().ipv4.to_string(),
registered_data.private_ips().ipv6.to_string(),
private_key_hex,
public_key_hex,
wg_endpoint,
);
run_tunnel_tests(&tunnel_config, &netstack_args, &awg_args, &mut wg_outcome);
Ok(wg_outcome)
}
pub async fn lp_registration_probe<St>(
gateway_identity: NodeIdentity,
gateway_lp_data: TestedNodeLpDetails,
bandwidth_controller: &nym_bandwidth_controller::BandwidthController<
nym_validator_client::nyxd::NyxdClient<nym_validator_client::HttpRpcClient>,
St,
>,
use_mock_ecash: bool,
) -> anyhow::Result<LpProbeResults>
where
St: nym_sdk::mixnet::CredentialStorage + Clone + Send + Sync + 'static,
<St as nym_sdk::mixnet::CredentialStorage>::StorageError: Send + Sync,
{
use nym_crypto::asymmetric::ed25519;
use nym_registration_client::LpRegistrationClient;
let lp_address = gateway_lp_data.address;
let peer = helpers::to_lp_remote_peer(gateway_identity, gateway_lp_data);
info!("Starting LP registration probe for gateway at {lp_address}",);
let mut lp_outcome = LpProbeResults::default();
// Generate Ed25519 keypair for this connection (X25519 will be derived internally by LP)
let mut rng = rand::thread_rng();
let client_ed25519_keypair = std::sync::Arc::new(ed25519::KeyPair::new(&mut rng));
// Step 0: Derive X25519 keys from Ed25519 for the gateways
// Create LP registration client (uses Ed25519 keys directly, derives X25519 internally)
let mut client = LpRegistrationClient::<TcpStream>::new_with_default_config(
client_ed25519_keypair,
peer,
lp_address,
);
// Step 1: Perform handshake (connection is implicit in packet-per-connection model)
// LpRegistrationClient uses packet-per-connection model - connect() is gone,
// connection is established during handshake and registration automatically.
info!("Performing LP handshake at {lp_address}...");
match client.perform_handshake().await {
Ok(_) => {
info!("LP handshake completed successfully");
lp_outcome.can_connect = true; // Connection succeeded if handshake succeeded
lp_outcome.can_handshake = true;
}
Err(e) => {
let error_msg = format!("LP handshake failed: {}", e);
error!("{}", error_msg);
lp_outcome.error = Some(error_msg);
return Ok(lp_outcome);
}
}
// Step 2: Register with gateway (send request + receive response in one call)
info!("Sending LP registration request...");
// Generate WireGuard keypair for dVPN registration
let mut rng = rand::thread_rng();
let wg_keypair = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rng);
// Convert gateway identity to ed25519 public key
let gateway_ed25519_pubkey = match nym_crypto::asymmetric::ed25519::PublicKey::from_bytes(
&gateway_identity.to_bytes(),
) {
Ok(key) => key,
Err(e) => {
let error_msg = format!("Failed to convert gateway identity: {}", e);
error!("{}", error_msg);
lp_outcome.error = Some(error_msg);
return Ok(lp_outcome);
}
};
// Register using the new packet-per-connection API (returns GatewayData directly)
let ticket_type = TicketType::V1WireguardEntry;
let gateway_data = if use_mock_ecash {
info!("Using mock ecash credential for LP registration");
let credential = bandwidth_helpers::create_dummy_credential(
&gateway_ed25519_pubkey.to_bytes(),
ticket_type,
);
match client
.register_with_credential(&mut rng, &wg_keypair, credential, ticket_type)
.await
{
Ok(data) => data,
Err(e) => {
let error_msg = format!("LP registration failed (mock ecash): {}", e);
error!("{}", error_msg);
lp_outcome.error = Some(error_msg);
return Ok(lp_outcome);
}
}
} else {
info!("Using real bandwidth controller for LP registration");
match client
.register(
&mut rng,
&wg_keypair,
&gateway_ed25519_pubkey,
bandwidth_controller,
ticket_type,
)
.await
{
Ok(data) => data,
Err(e) => {
let error_msg = format!("LP registration failed: {}", e);
error!("{}", error_msg);
lp_outcome.error = Some(error_msg);
return Ok(lp_outcome);
}
}
};
info!("LP registration successful! Received gateway data:");
info!(" - Gateway public key: {:?}", gateway_data.public_key);
info!(" - Private IPv4: {}", gateway_data.private_ipv4);
info!(" - Private IPv6: {}", gateway_data.private_ipv6);
info!(" - Endpoint: {}", gateway_data.endpoint);
lp_outcome.can_register = true;
Ok(lp_outcome)
}
/// LP-based WireGuard probe: Tests LP nested session registration + WireGuard tunnel connectivity
///
/// This function tests the full VPN flow using LP registration instead of mixnet+authenticator:
/// 1. Connects to entry gateway (outer LP session)
/// 2. Registers with exit gateway via entry forwarding (nested LP session)
/// 3. Receives WireGuard configuration from both gateways
/// 4. Tests WireGuard tunnel connectivity (IPv4/IPv6)
///
/// This validates that IP hiding works (exit sees entry IP, not client IP) and that the
/// full VPN tunnel operates correctly after LP registration.
///
// Known issue in localnet mode - After this probe runs, container networking
// to the external internet becomes unstable while internal container-to-container traffic
// continues to work. The two-hop WireGuard tunnel itself succeeds (handshake completes),
// but subsequent DNS/ping tests may timeout. This appears to be related to Apple Container
// Runtime networking quirks combined with our NAT/iptables configuration. Tracked in
// beads issue nym-vbdo. Workaround: restart the localnet containers between probe runs.
pub async fn wg_probe_lp<St>(
entry_gateway: &TestedNodeDetails,
exit_gateway: &TestedNodeDetails,
bandwidth_controller: &nym_bandwidth_controller::BandwidthController<
nym_validator_client::nyxd::NyxdClient<nym_validator_client::HttpRpcClient>,
St,
>,
use_mock_ecash: bool,
awg_args: String,
netstack_args: NetstackArgs,
) -> anyhow::Result<WgProbeResults>
where
St: nym_sdk::mixnet::CredentialStorage + Clone + Send + Sync + 'static,
<St as nym_sdk::mixnet::CredentialStorage>::StorageError: Send + Sync,
{
// Validate that both gateways have required information
let entry_lp_data = entry_gateway
.lp_data
.clone()
.ok_or_else(|| anyhow::anyhow!("Entry gateway missing LP data"))?;
let exit_lp_data = exit_gateway
.lp_data
.clone()
.ok_or_else(|| anyhow::anyhow!("Exit gateway missing LP data"))?;
let entry_address = entry_lp_data.address;
let exit_address = exit_lp_data.address;
let entry_ip = entry_address.ip();
let exit_ip = exit_address.ip();
info!("Starting LP-based WireGuard probe (entry→exit via forwarding)");
let mut wg_outcome = WgProbeResults::default();
// Generate Ed25519 keypairs for LP protocol
let mut rng = rand::thread_rng();
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng));
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng));
// Generate WireGuard keypairs for VPN registration
let entry_wg_keypair = x25519::KeyPair::new(&mut rng);
let exit_wg_keypair = x25519::KeyPair::new(&mut rng);
let entry_peer = helpers::to_lp_remote_peer(entry_gateway.identity, entry_lp_data);
let exit_peer = helpers::to_lp_remote_peer(exit_gateway.identity, exit_lp_data);
// STEP 1: Establish outer LP session with entry gateway
// LpRegistrationClient uses packet-per-connection model - connect() is gone,
// connection is established automatically during handshake.
info!("Establishing outer LP session with entry gateway...");
let mut entry_client = LpRegistrationClient::<TcpStream>::new_with_default_config(
entry_lp_keypair,
entry_peer,
entry_address,
);
// Perform handshake with entry gateway (connection is implicit)
if let Err(e) = entry_client.perform_handshake().await {
error!("Failed to handshake with entry gateway: {}", e);
return Ok(wg_outcome);
}
info!("Outer LP session with entry gateway established");
// STEP 2: Use nested session to register with exit gateway via forwarding
info!("Registering with exit gateway via entry forwarding...");
let mut nested_session =
NestedLpSession::new(exit_address.to_string(), exit_lp_keypair, exit_peer);
// 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))?;
// Perform handshake and registration with exit gateway via forwarding
let exit_gateway_data = if use_mock_ecash {
info!("Using mock ecash credential for exit gateway registration");
let credential = bandwidth_helpers::create_dummy_credential(
&exit_gateway_pubkey.to_bytes(),
TicketType::V1WireguardExit,
);
match nested_session
.handshake_and_register_with_credential(
&mut entry_client,
&mut rng,
&exit_wg_keypair,
credential,
TicketType::V1WireguardExit,
)
.await
{
Ok(data) => data,
Err(e) => {
error!("Failed to register with exit gateway (mock ecash): {}", e);
return Ok(wg_outcome);
}
}
} else {
match nested_session
.handshake_and_register(
&mut entry_client,
&mut rng,
&exit_wg_keypair,
&exit_gateway_pubkey,
bandwidth_controller,
TicketType::V1WireguardExit,
)
.await
{
Ok(data) => data,
Err(e) => {
error!("Failed to register with exit gateway: {}", e);
return Ok(wg_outcome);
}
}
};
info!("Exit gateway registration successful via forwarding");
// STEP 3: Register with entry gateway
info!("Registering with entry gateway...");
let entry_gateway_pubkey =
ed25519::PublicKey::from_bytes(&entry_gateway.identity.to_bytes())
.map_err(|e| anyhow::anyhow!("Invalid entry gateway identity: {}", e))?;
// Use packet-per-connection register() which returns GatewayData directly
let entry_gateway_data = if use_mock_ecash {
info!("Using mock ecash credential for entry gateway registration");
let credential = bandwidth_helpers::create_dummy_credential(
&entry_gateway_pubkey.to_bytes(),
TicketType::V1WireguardEntry,
);
match entry_client
.register_with_credential(
&mut rng,
&entry_wg_keypair,
credential,
TicketType::V1WireguardEntry,
)
.await
{
Ok(data) => data,
Err(e) => {
error!("Failed to register with entry gateway (mock ecash): {}", e);
return Ok(wg_outcome);
}
}
} else {
match entry_client
.register(
&mut rng,
&entry_wg_keypair,
&entry_gateway_pubkey,
bandwidth_controller,
TicketType::V1WireguardEntry,
)
.await
{
Ok(data) => data,
Err(e) => {
error!("Failed to register with entry gateway: {}", e);
return Ok(wg_outcome);
}
}
};
info!("Entry gateway registration successful");
info!("LP registration successful for both gateways!");
wg_outcome.can_register = true;
// STEP 4: Test WireGuard tunnels using two-hop configuration
// Traffic flows: Exit tunnel -> UDP Forwarder -> Entry tunnel -> Exit Gateway -> Internet
// The exit gateway endpoint is not directly reachable from the host in localnet.
// We must tunnel through the entry gateway using the UDP forwarder pattern.
// Convert keys to hex for netstack
let entry_private_key_hex = hex::encode(entry_wg_keypair.private_key().to_bytes());
let entry_public_key_hex = hex::encode(entry_gateway_data.public_key.to_bytes());
let exit_private_key_hex = hex::encode(exit_wg_keypair.private_key().to_bytes());
let exit_public_key_hex = hex::encode(exit_gateway_data.public_key.to_bytes());
// Build WireGuard endpoint addresses
// Entry endpoint uses entry_ip (host-reachable) + port from registration
let entry_wg_endpoint = format!("{}:{}", entry_ip, entry_gateway_data.endpoint.port());
// Exit endpoint uses exit_ip + port from registration (forwarded via entry)
let exit_wg_endpoint = format!("{}:{}", exit_ip, exit_gateway_data.endpoint.port());
info!("Two-hop WireGuard configuration:");
info!(" Entry gateway:");
info!(" Private IPv4: {}", entry_gateway_data.private_ipv4);
info!(" Endpoint: {}", entry_wg_endpoint);
info!(" Exit gateway:");
info!(" Private IPv4: {}", exit_gateway_data.private_ipv4);
info!(" Endpoint (via forwarder): {}", exit_wg_endpoint);
// Build two-hop tunnel configuration
let two_hop_config = TwoHopWgTunnelConfig::new(
entry_gateway_data.private_ipv4.to_string(),
entry_private_key_hex,
entry_public_key_hex,
entry_wg_endpoint,
awg_args.clone(), // 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
);
// Run two-hop tunnel connectivity tests
run_two_hop_tunnel_tests(&two_hop_config, &netstack_args, &mut wg_outcome);
info!("LP-based two-hop WireGuard probe completed");
Ok(wg_outcome)
}
pub async fn do_ping(
mut mixnet_client: MixnetClient,
our_address: Recipient,
exit_router_address: Option<Recipient>,
tested_entry: bool,
) -> (anyhow::Result<ProbeOutcome>, MixnetClient) {
let entry = do_ping_entry(&mut mixnet_client, our_address, tested_entry).await;
let (exit_result, mixnet_client) = if let Some(exit_router_address) = exit_router_address {
let (maybe_ip_pair, mut mixnet_client) =
connect_exit(mixnet_client, exit_router_address).await;
match maybe_ip_pair {
Some(ip_pair) => (
do_ping_exit(&mut mixnet_client, ip_pair, exit_router_address).await,
mixnet_client,
),
None => (Ok(Some(Exit::fail_to_connect())), mixnet_client),
}
} else {
(Ok(None), mixnet_client)
};
(
exit_result.map(|exit| ProbeOutcome {
as_entry: entry,
as_exit: exit,
wg: None,
lp: None,
}),
mixnet_client,
)
}
async fn do_ping_entry(
mixnet_client: &mut MixnetClient,
our_address: Recipient,
tested_entry: bool,
) -> Entry {
// Step 1: confirm that the entry gateway is routing our mixnet traffic
info!("Sending mixnet ping to ourselves to verify mixnet connection");
if self_ping_and_wait(our_address, mixnet_client)
.await
.is_err()
{
return if tested_entry {
Entry::fail_to_connect()
} else {
Entry::EntryFailure
};
}
info!("Successfully mixnet pinged ourselves");
Entry::success()
}
async fn connect_exit(
mixnet_client: MixnetClient,
exit_router_address: Recipient,
) -> (Option<IpPair>, MixnetClient) {
// Step 2: connect to the exit gateway
info!(
"Connecting to exit gateway: {}",
exit_router_address.gateway().to_base58_string()
);
// The IPR supports cancellation, but it's unused in the gateway probe
let cancel_token = CancellationToken::new();
let mut ipr_client = IprClientConnect::new(mixnet_client, cancel_token);
let maybe_ip_pair = ipr_client.connect(exit_router_address).await;
let mixnet_client = ipr_client.into_mixnet_client();
if let Ok(our_ips) = maybe_ip_pair {
info!("Successfully connected to exit gateway");
info!("Using mixnet VPN IP addresses: {our_ips}");
(Some(our_ips), mixnet_client)
} else {
(None, mixnet_client)
}
}
pub async fn do_ping_exit(
mixnet_client: &mut MixnetClient,
our_ips: IpPair,
exit_router_address: Recipient,
) -> anyhow::Result<Option<Exit>> {
// Step 3: perform ICMP connectivity checks for the exit gateway
send_icmp_pings(mixnet_client, our_ips, exit_router_address).await?;
listen_for_icmp_ping_replies(mixnet_client, our_ips).await
}
async fn send_icmp_pings(
mixnet_client: &MixnetClient,
our_ips: IpPair,
exit_router_address: Recipient,
) -> anyhow::Result<()> {
// ipv4 addresses for testing
let ipr_tun_ip_v4 = NYM_TUN_DEVICE_ADDRESS_V4;
let external_ip_v4 = Ipv4Addr::new(8, 8, 8, 8);
// ipv6 addresses for testing
let ipr_tun_ip_v6 = NYM_TUN_DEVICE_ADDRESS_V6;
let external_ip_v6 = Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888);
info!(
"Sending ICMP echo requests to: {ipr_tun_ip_v4}, {ipr_tun_ip_v6}, {external_ip_v4}, {external_ip_v6}"
);
// send ipv4 pings
for ii in 0..10 {
icmp::send_ping_v4(
mixnet_client,
our_ips,
ii,
ipr_tun_ip_v4,
exit_router_address,
)
.await?;
icmp::send_ping_v4(
mixnet_client,
our_ips,
ii,
external_ip_v4,
exit_router_address,
)
.await?;
}
// send ipv6 pings
for ii in 0..10 {
icmp::send_ping_v6(
mixnet_client,
our_ips,
ii,
ipr_tun_ip_v6,
exit_router_address,
)
.await?;
icmp::send_ping_v6(
mixnet_client,
our_ips,
ii,
external_ip_v6,
exit_router_address,
)
.await?;
}
Ok(())
}
pub async fn listen_for_icmp_ping_replies(
mixnet_client: &mut MixnetClient,
our_ips: IpPair,
) -> anyhow::Result<Option<Exit>> {
let mut multi_ip_packet_decoder = MultiIpPacketCodec::new();
let mut registered_replies = IpPingReplies::new();
loop {
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(2)) => {
info!("Finished waiting for ICMP echo reply from exit gateway");
break;
}
Some(reconstructed_message) = mixnet_client.next() => {
let Some(data_response) = helpers::unpack_data_response(&reconstructed_message) else {
continue;
};
// IP packets are bundled together in a mixnet message
let mut bytes = BytesMut::from(&*data_response.ip_packet);
while let Ok(Some(packet)) = multi_ip_packet_decoder.decode(&mut bytes) {
if let Some(event) = icmp::check_for_icmp_beacon_reply(&packet.into_bytes(), icmp::icmp_identifier(), our_ips) {
info!("Received ICMP echo reply from exit gateway");
info!("Connection event: {event:?}");
registered_replies.register_event(&event);
}
}
}
}
}
Ok(Some(Exit {
can_connect: true,
can_route_ip_v4: registered_replies.ipr_tun_ip_v4,
can_route_ip_external_v4: registered_replies.external_ip_v4,
can_route_ip_v6: registered_replies.ipr_tun_ip_v6,
can_route_ip_external_v6: registered_replies.external_ip_v6,
}))
}
+6 -6
View File
@@ -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::netstack::{
use crate::common::netstack::{
NetstackRequest, NetstackRequestGo, NetstackResult, TwoHopNetstackRequestGo,
};
use crate::types::WgProbeResults;
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 {
@@ -99,7 +99,7 @@ pub fn run_tunnel_tests(
info!("Testing IPv4 tunnel connectivity...");
let ipv4_request = NetstackRequestGo::from_rust_v4(&netstack_request);
match crate::netstack::ping(&ipv4_request) {
match crate::common::netstack::ping(&ipv4_request) {
Ok(NetstackResult::Response(netstack_response_v4)) => {
info!(
"WireGuard probe response for IPv4: {:#?}",
@@ -137,7 +137,7 @@ pub fn run_tunnel_tests(
info!("Testing IPv6 tunnel connectivity...");
let ipv6_request = NetstackRequestGo::from_rust_v6(&netstack_request);
match crate::netstack::ping(&ipv6_request) {
match crate::common::netstack::ping(&ipv6_request) {
Ok(NetstackResult::Response(netstack_response_v6)) => {
info!(
"WireGuard probe response for IPv6: {:#?}",
@@ -281,7 +281,7 @@ pub fn run_two_hop_tunnel_tests(
info!(" Entry endpoint: {}", config.entry_endpoint);
info!(" Exit endpoint (via forwarder): {}", config.exit_endpoint);
match crate::netstack::ping_two_hop(&request) {
match crate::common::netstack::ping_two_hop(&request) {
Ok(NetstackResult::Response(response)) => {
info!("Two-hop WireGuard probe response (IPv4): {:#?}", response);
wg_outcome.can_handshake_v4 = response.can_handshake;
@@ -0,0 +1,29 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Args;
use nym_node_status_client::models::AttachedTicketMaterials;
#[derive(Args)]
pub struct CredentialArgs {
#[arg(long)]
ticket_materials: Option<String>,
#[arg(long, default_value_t = 1)]
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,
self.ticket_materials_revision,
)?)
}
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod credentials;
mod netstack;
mod test_mode;
pub use credentials::CredentialArgs;
pub use netstack::NetstackArgs;
pub use test_mode::TestMode;
+209
View File
@@ -0,0 +1,209 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Args;
#[derive(Args, Clone)]
pub struct NetstackArgs {
#[arg(long, default_value_t = 180)]
pub netstack_download_timeout_sec: u64,
#[arg(long, default_value_t = 30)]
pub metadata_timeout_sec: u64,
#[arg(long, default_value = "1.1.1.1")]
pub netstack_v4_dns: String,
#[arg(long, default_value = "2606:4700:4700::1111")]
pub netstack_v6_dns: String,
#[arg(long, default_value_t = 5)]
pub netstack_num_ping: u8,
#[arg(long, default_value_t = 3)]
pub netstack_send_timeout_sec: u64,
#[arg(long, default_value_t = 3)]
pub netstack_recv_timeout_sec: u64,
#[arg(long, 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()])]
pub netstack_ping_ips_v4: Vec<String>,
#[arg(long, 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()])]
pub netstack_ping_ips_v6: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_netstack_args_default_values() {
// Test that the default values are correctly set in the struct definition
// This validates that our changes to the default values are correct
// Create a default instance to test the values
let args = NetstackArgs {
netstack_download_timeout_sec: 180,
metadata_timeout_sec: 30,
netstack_v4_dns: "1.1.1.1".to_string(),
netstack_v6_dns: "2606:4700:4700::1111".to_string(),
netstack_num_ping: 5,
netstack_send_timeout_sec: 3,
netstack_recv_timeout_sec: 3,
netstack_ping_hosts_v4: vec!["nym.com".to_string()],
netstack_ping_ips_v4: vec!["1.1.1.1".to_string()],
netstack_ping_hosts_v6: vec!["cloudflare.com".to_string()],
netstack_ping_ips_v6: vec![
"2001:4860:4860::8888".to_string(),
"2606:4700:4700::1111".to_string(),
"2620:fe::fe".to_string(),
],
};
// Test IPv4 defaults
assert_eq!(args.netstack_ping_hosts_v4, vec!["nym.com"]);
assert_eq!(args.netstack_ping_ips_v4, vec!["1.1.1.1"]);
assert_eq!(args.netstack_v4_dns, "1.1.1.1");
// Test IPv6 defaults
assert_eq!(args.netstack_ping_hosts_v6, vec!["cloudflare.com"]);
assert_eq!(
args.netstack_ping_ips_v6,
vec![
"2001:4860:4860::8888",
"2606:4700:4700::1111",
"2620:fe::fe"
]
);
assert_eq!(args.netstack_v6_dns, "2606:4700:4700::1111");
// Test other defaults
assert_eq!(args.netstack_download_timeout_sec, 180);
assert_eq!(args.netstack_num_ping, 5);
assert_eq!(args.netstack_send_timeout_sec, 3);
assert_eq!(args.netstack_recv_timeout_sec, 3);
}
#[test]
fn test_netstack_args_custom_construction() {
// Test that we can create instances with custom values
let args = NetstackArgs {
netstack_download_timeout_sec: 300,
metadata_timeout_sec: 30,
netstack_v4_dns: "8.8.8.8".to_string(),
netstack_v6_dns: "2001:4860:4860::8888".to_string(),
netstack_num_ping: 10,
netstack_send_timeout_sec: 5,
netstack_recv_timeout_sec: 5,
netstack_ping_hosts_v4: vec!["example.com".to_string()],
netstack_ping_ips_v4: vec!["8.8.8.8".to_string()],
netstack_ping_hosts_v6: vec!["ipv6.example.com".to_string()],
netstack_ping_ips_v6: vec!["2001:4860:4860::8888".to_string()],
};
assert_eq!(args.netstack_ping_hosts_v4, vec!["example.com"]);
assert_eq!(args.netstack_ping_hosts_v6, vec!["ipv6.example.com"]);
assert_eq!(args.netstack_ping_ips_v4, vec!["8.8.8.8"]);
assert_eq!(args.netstack_ping_ips_v6, vec!["2001:4860:4860::8888"]);
assert_eq!(args.netstack_v4_dns, "8.8.8.8");
assert_eq!(args.netstack_v6_dns, "2001:4860:4860::8888");
assert_eq!(args.netstack_download_timeout_sec, 300);
assert_eq!(args.netstack_num_ping, 10);
assert_eq!(args.netstack_send_timeout_sec, 5);
assert_eq!(args.netstack_recv_timeout_sec, 5);
}
#[test]
fn test_netstack_args_multiple_values() {
// Test that multiple hosts and IPs can be stored
let args = NetstackArgs {
netstack_download_timeout_sec: 180,
metadata_timeout_sec: 30,
netstack_v4_dns: "1.1.1.1".to_string(),
netstack_v6_dns: "2606:4700:4700::1111".to_string(),
netstack_num_ping: 5,
netstack_send_timeout_sec: 3,
netstack_recv_timeout_sec: 3,
netstack_ping_hosts_v4: vec!["nym.com".to_string(), "example.com".to_string()],
netstack_ping_ips_v4: vec!["1.1.1.1".to_string(), "8.8.8.8".to_string()],
netstack_ping_hosts_v6: vec![
"cloudflare.com".to_string(),
"ipv6.example.com".to_string(),
],
netstack_ping_ips_v6: vec![
"2001:4860:4860::8888".to_string(),
"2606:4700:4700::1111".to_string(),
],
};
assert_eq!(args.netstack_ping_hosts_v4, vec!["nym.com", "example.com"]);
assert_eq!(
args.netstack_ping_hosts_v6,
vec!["cloudflare.com", "ipv6.example.com"]
);
assert_eq!(args.netstack_ping_ips_v4, vec!["1.1.1.1", "8.8.8.8"]);
assert_eq!(
args.netstack_ping_ips_v6,
vec!["2001:4860:4860::8888", "2606:4700:4700::1111"]
);
}
#[test]
fn test_netstack_args_edge_cases() {
// Test edge cases like zero values and empty vectors
let args = NetstackArgs {
netstack_download_timeout_sec: 0,
metadata_timeout_sec: 30,
netstack_v4_dns: "1.1.1.1".to_string(),
netstack_v6_dns: "2606:4700:4700::1111".to_string(),
netstack_num_ping: 0,
netstack_send_timeout_sec: 0,
netstack_recv_timeout_sec: 0,
netstack_ping_hosts_v4: vec![],
netstack_ping_ips_v4: vec![],
netstack_ping_hosts_v6: vec![],
netstack_ping_ips_v6: vec![],
};
assert_eq!(args.netstack_num_ping, 0);
assert_eq!(args.netstack_send_timeout_sec, 0);
assert_eq!(args.netstack_recv_timeout_sec, 0);
assert_eq!(args.netstack_download_timeout_sec, 0);
assert!(args.netstack_ping_hosts_v4.is_empty());
assert!(args.netstack_ping_ips_v4.is_empty());
assert!(args.netstack_ping_hosts_v6.is_empty());
assert!(args.netstack_ping_ips_v6.is_empty());
}
#[test]
fn test_netstack_args_domain_validation() {
// Test that our domain choices are reasonable
let args = NetstackArgs {
netstack_download_timeout_sec: 180,
metadata_timeout_sec: 30,
netstack_v4_dns: "1.1.1.1".to_string(),
netstack_v6_dns: "2606:4700:4700::1111".to_string(),
netstack_num_ping: 5,
netstack_send_timeout_sec: 3,
netstack_recv_timeout_sec: 3,
netstack_ping_hosts_v4: vec!["nym.com".to_string()],
netstack_ping_ips_v4: vec!["1.1.1.1".to_string()],
netstack_ping_hosts_v6: vec!["cloudflare.com".to_string()],
netstack_ping_ips_v6: vec!["2001:4860:4860::8888".to_string()],
};
assert!(args.netstack_ping_hosts_v4[0].contains("nym"));
assert!(args.netstack_ping_hosts_v6[0].contains("cloudflare"));
assert_eq!(args.netstack_v4_dns, "1.1.1.1");
assert_eq!(args.netstack_v6_dns, "2606:4700:4700::1111");
}
}
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -6,10 +6,9 @@ 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::nodes::{NymApiDirectory, query_gateway_by_ip};
use nym_gateway_probe::{
CredentialArgs, NetstackArgs, ProbeResult, TestMode, TestedNode, TestedNodeDetails,
TestedNodeLpDetails,
CredentialArgs, NetstackArgs, NymApiDirectory, ProbeResult, TestMode, TestedNode,
TestedNodeDetails, TestedNodeLpDetails, query_gateway_by_ip,
};
use nym_kkt_ciphersuite::{HashFunction, KEM};
use nym_sdk::mixnet::NodeIdentity;