Additional ticket for agent (#6551)

* Additional ticket type for LP tests

* Remove hardcoded comments

* bump cargo version

* Nuke fallback edge case in the probe

* Cleanup unused code

* Bump API & agent versions
- agent bump required due to probe changes
This commit is contained in:
dynco-nym
2026-03-12 14:49:03 +01:00
committed by GitHub
parent 3524089ad8
commit 80370b98ec
11 changed files with 21 additions and 445 deletions
-70
View File
@@ -5,14 +5,12 @@ use crate::config::NetstackArgs;
use anyhow::Context;
use serde::Deserialize;
use std::ffi::{CStr, CString};
use std::net::SocketAddr;
mod sys {
use std::ffi::{c_char, c_void};
unsafe extern "C" {
pub unsafe fn wgPing(req: *const c_char) -> *const c_char;
pub unsafe fn wgPingTwoHop(req: *const c_char) -> *const c_char;
pub unsafe fn wgFreePtr(ptr: *mut c_void);
}
}
@@ -213,71 +211,3 @@ pub fn ping(req: &NetstackRequestGo) -> anyhow::Result<NetstackResult> {
result
}
/// Request structure for two-hop WireGuard ping test.
/// Matches TwoHopNetstackRequest in Go.
// This struct is serialized to JSON and passed to wgPingTwoHop() via CGO.
// The Go side creates: entry tunnel -> UDP forwarder -> exit tunnel, then runs tests.
#[derive(Clone, Debug, serde::Serialize)]
pub struct TwoHopNetstackRequestGo {
// Entry tunnel configuration (connects directly to entry gateway)
pub entry_wg_ip: String,
pub entry_private_key: String,
pub entry_public_key: String,
pub entry_endpoint: SocketAddr,
pub entry_awg_args: String,
// Exit tunnel configuration (connects via forwarder through entry)
pub exit_wg_ip: String,
pub exit_private_key: String,
pub exit_public_key: String,
pub exit_endpoint: SocketAddr,
pub exit_awg_args: String,
// Test parameters
pub dns: String,
pub ip_version: u8,
pub ping_hosts: Vec<String>,
pub ping_ips: Vec<String>,
pub num_ping: u8,
pub send_timeout_sec: u64,
pub recv_timeout_sec: u64,
pub download_timeout_sec: u64,
}
/// Perform a two-hop WireGuard ping test through entry and exit gateways.
///
/// This creates two nested WireGuard tunnels with a UDP forwarder:
/// - Entry tunnel connects directly to entry gateway (reachable from host)
/// - UDP forwarder listens on localhost and forwards via entry tunnel
/// - Exit tunnel connects to forwarder, traffic flows: exit -> forwarder -> entry -> exit gateway
/// - Tests run through the exit tunnel
pub fn ping_two_hop(req: &TwoHopNetstackRequestGo) -> anyhow::Result<NetstackResult> {
let req_json = serde_json::to_string_pretty(req)?;
let req_json_cstr = CString::new(req_json)?;
// SAFETY: safety guarantees are upheld by CGO
let response_str_ptr = unsafe { sys::wgPingTwoHop(req_json_cstr.as_ptr()) };
if response_str_ptr.is_null() {
return Err(anyhow::anyhow!("wgPingTwoHop() returned null"));
}
// SAFETY: safety guarantees are upheld by CGO
let response_cstr = unsafe { CStr::from_ptr(response_str_ptr) };
let result = match response_cstr.to_str() {
Ok(response_str) => {
let mut de = serde_json::Deserializer::from_str(response_str);
let response = NetstackResult::deserialize(&mut de);
response.context("Failed to deserialize ffi response")
}
Err(err) => Err(anyhow::anyhow!(
"Failed to convert ffi response to utf8 string: {err}"
)),
};
// SAFETY: freeing the pointer returned by CGO
unsafe { sys::wgFreePtr(response_str_ptr as _) };
result
}
+3 -199
View File
@@ -2,14 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
use crate::common::helpers::mixnet_debug_config;
use crate::common::nodes::{TestedNodeDetails, TestedNodeLpDetails};
use crate::common::nodes::TestedNodeLpDetails;
use crate::common::socks5_test::HttpsConnectivityTest;
use crate::common::types::{
Entry, Exit, IpPingReplies, LpProbeResults, ProbeOutcome, Socks5ProbeResults, WgProbeResults,
};
use crate::common::wireguard::{
TwoHopWgTunnelConfig, WgTunnelConfig, run_tunnel_tests, run_two_hop_tunnel_tests,
};
use crate::common::wireguard::{WgTunnelConfig, run_tunnel_tests};
use crate::common::{helpers, icmp};
use crate::config::{NetstackArgs, Socks5Args};
use anyhow::bail;
@@ -25,11 +23,10 @@ use nym_bandwidth_controller::BandwidthTicketProvider;
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_lp::peer::DHKeyPair;
use nym_registration_client::{LpRegistrationClient, NestedLpSession};
use nym_registration_client::LpRegistrationClient;
use nym_sdk::NymNetworkDetails;
use nym_sdk::mixnet::{MixnetClient, MixnetClientBuilder, NodeIdentity, Recipient, Socks5};
use nym_topology::{HardcodedTopologyProvider, NymTopology};
@@ -247,199 +244,6 @@ pub async fn lp_registration_probe(
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(
entry_gateway: &TestedNodeDetails,
exit_gateway: &TestedNodeDetails,
bandwidth_controller: &dyn BandwidthTicketProvider,
awg_args: Option<String>,
netstack_args: NetstackArgs,
) -> anyhow::Result<WgProbeResults> {
// 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_lp_version = entry_lp_data.lp_version;
let exit_lp_version = exit_lp_data.lp_version;
let entry_lp_ciphersuite = entry_lp_data.ciphersuite;
let exit_lp_ciphersuite = exit_lp_data.ciphersuite;
info!("Starting LP-based WireGuard probe (entry→exit via forwarding)");
let mut wg_outcome = WgProbeResults::default();
// Generate x25519 keypairs for LP protocol
let mut rng09 = rand09::rngs::StdRng::from_os_rng();
let entry_lp_keypair = Arc::new(DHKeyPair::new(&mut rng09));
let exit_lp_keypair = Arc::new(DHKeyPair::new(&mut rng09));
// Generate WireGuard keypairs for VPN registration
let mut rng = rand::rngs::OsRng;
let entry_wg_keypair = x25519::KeyPair::new(&mut rng);
let exit_wg_keypair = x25519::KeyPair::new(&mut rng);
let entry_peer = entry_lp_data.into_remote_peer();
let exit_peer = exit_lp_data.into_remote_peer();
// 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,
entry_lp_ciphersuite,
entry_lp_version,
);
// 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,
exit_lp_keypair,
exit_peer,
exit_lp_ciphersuite,
exit_lp_version,
);
let exit_gateway_pubkey = exit_gateway.identity;
// Perform handshake and registration with exit gateway via forwarding
if let Err(err) = nested_session.perform_handshake(&mut entry_client).await {
error!("Failed to perform handshake with exit gateway: {err}");
return Ok(wg_outcome);
};
let exit_gateway_data = match nested_session
.register_dvpn(
&mut entry_client,
&mut rng09,
&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 = match entry_client
.register_dvpn(
&mut rng09,
&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 = entry_gateway_data.endpoint;
// Exit endpoint uses exit_ip + port from registration (forwarded via entry)
let exit_wg_endpoint = exit_gateway_data.endpoint;
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().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.unwrap_or_default(), // 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,
+1 -139
View File
@@ -7,13 +7,10 @@
//! that is shared between different test modes (authenticator-based and LP-based).
use nym_config::defaults::{WG_METADATA_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4};
use std::net::SocketAddr;
use tracing::{error, info};
use crate::NetstackArgs;
use crate::common::netstack::{
NetstackRequest, NetstackRequestGo, NetstackResult, TwoHopNetstackRequestGo,
};
use crate::common::netstack::{NetstackRequest, NetstackRequestGo, NetstackResult};
use crate::common::types::WgProbeResults;
/// Safe division that returns 0.0 when divisor is 0 (instead of NaN/Inf)
@@ -171,138 +168,3 @@ pub fn run_tunnel_tests(
}
}
}
/// Two-hop WireGuard tunnel configuration for nested tunnel testing.
///
/// Traffic flows: Exit tunnel -> UDP Forwarder -> Entry tunnel -> Exit Gateway -> Internet
// This is used for LP two-hop mode where traffic must go through entry gateway
// to reach exit gateway. The forwarder bridges the two netstack tunnels on localhost.
pub struct TwoHopWgTunnelConfig {
// Entry tunnel (outer, connects directly to entry gateway)
/// Entry client's private IPv4 address in the tunnel
pub entry_private_ipv4: String,
/// Entry client's WireGuard private key (hex encoded)
pub entry_private_key_hex: String,
/// Entry gateway's WireGuard public key (hex encoded)
pub entry_public_key_hex: String,
/// Entry WireGuard endpoint address (entry_gateway_ip:port)
pub entry_endpoint: SocketAddr,
/// Entry Amnezia WG args (empty for standard WG)
pub entry_awg_args: String,
// Exit tunnel (inner, connects via forwarder through entry)
/// Exit client's private IPv4 address in the tunnel
pub exit_private_ipv4: String,
/// Exit client's WireGuard private key (hex encoded)
pub exit_private_key_hex: String,
/// Exit gateway's WireGuard public key (hex encoded)
pub exit_public_key_hex: String,
/// Exit WireGuard endpoint address (exit_gateway_ip:port, forwarded via entry)
pub exit_endpoint: SocketAddr,
/// Exit Amnezia WG args (empty for standard WG)
pub exit_awg_args: String,
}
impl TwoHopWgTunnelConfig {
/// Create a new two-hop tunnel configuration.
#[allow(clippy::too_many_arguments)]
pub fn new(
entry_private_ipv4: impl Into<String>,
entry_private_key_hex: impl Into<String>,
entry_public_key_hex: impl Into<String>,
entry_endpoint: SocketAddr,
entry_awg_args: impl Into<String>,
exit_private_ipv4: impl Into<String>,
exit_private_key_hex: impl Into<String>,
exit_public_key_hex: impl Into<String>,
exit_endpoint: SocketAddr,
exit_awg_args: impl Into<String>,
) -> Self {
Self {
entry_private_ipv4: entry_private_ipv4.into(),
entry_private_key_hex: entry_private_key_hex.into(),
entry_public_key_hex: entry_public_key_hex.into(),
entry_endpoint,
entry_awg_args: entry_awg_args.into(),
exit_private_ipv4: exit_private_ipv4.into(),
exit_private_key_hex: exit_private_key_hex.into(),
exit_public_key_hex: exit_public_key_hex.into(),
exit_endpoint,
exit_awg_args: exit_awg_args.into(),
}
}
}
/// Run two-hop WireGuard tunnel connectivity tests using netstack.
///
/// This function tests connectivity through nested WireGuard tunnels:
/// - Entry tunnel connects directly to entry gateway
/// - UDP forwarder bridges entry and exit tunnels on localhost
/// - Exit tunnel sends traffic via forwarder -> entry tunnel -> exit gateway
/// - Tests (DNS, ping, download) run through the exit tunnel
///
/// # Arguments
/// * `config` - Two-hop WireGuard tunnel configuration
/// * `netstack_args` - Netstack test parameters (DNS, hosts to ping, timeouts, etc.)
/// * `wg_outcome` - Mutable reference to write test results into
// Currently only tests IPv4. IPv6 support can be added later if needed.
pub fn run_two_hop_tunnel_tests(
config: &TwoHopWgTunnelConfig,
netstack_args: &NetstackArgs,
wg_outcome: &mut WgProbeResults,
) {
// Build the two-hop netstack request for IPv4
let request = TwoHopNetstackRequestGo {
// Entry tunnel config
entry_wg_ip: config.entry_private_ipv4.clone(),
entry_private_key: config.entry_private_key_hex.clone(),
entry_public_key: config.entry_public_key_hex.clone(),
entry_endpoint: config.entry_endpoint,
entry_awg_args: config.entry_awg_args.clone(),
// Exit tunnel config
exit_wg_ip: config.exit_private_ipv4.clone(),
exit_private_key: config.exit_private_key_hex.clone(),
exit_public_key: config.exit_public_key_hex.clone(),
exit_endpoint: config.exit_endpoint,
exit_awg_args: config.exit_awg_args.clone(),
// Test parameters (use IPv4 config)
dns: netstack_args.netstack_v4_dns.clone(),
ip_version: 4,
ping_hosts: netstack_args.netstack_ping_hosts_v4.clone(),
ping_ips: netstack_args.netstack_ping_ips_v4.clone(),
num_ping: netstack_args.netstack_num_ping,
send_timeout_sec: netstack_args.netstack_send_timeout_sec,
recv_timeout_sec: netstack_args.netstack_recv_timeout_sec,
download_timeout_sec: netstack_args.netstack_download_timeout_sec,
};
info!("Testing two-hop IPv4 tunnel connectivity...");
info!(" Entry endpoint: {}", config.entry_endpoint);
info!(" Exit endpoint (via forwarder): {}", config.exit_endpoint);
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;
wg_outcome.can_resolve_dns_v4 = response.can_resolve_dns;
wg_outcome.ping_hosts_performance_v4 =
safe_ratio(response.received_hosts, response.sent_hosts);
wg_outcome.ping_ips_performance_v4 =
safe_ratio(response.received_ips, response.sent_ips);
wg_outcome.download_duration_sec_v4 = response.download_duration_sec;
wg_outcome.download_duration_milliseconds_v4 = response.download_duration_milliseconds;
wg_outcome.downloaded_file_size_bytes_v4 = response.downloaded_file_size_bytes;
wg_outcome.downloaded_file_v4 = response.downloaded_file;
wg_outcome.download_error_v4 = response.download_error;
}
Ok(NetstackResult::Error { error }) => {
error!("Two-hop netstack runtime error (IPv4): {error}")
}
Err(error) => {
error!("Two-hop internal error (IPv4): {error}")
}
}
}
+1 -23
View File
@@ -5,7 +5,7 @@ use crate::common::bandwidth_helpers::build_bandwidth_controller;
use crate::common::helpers;
use crate::common::nodes::TestedNodeDetails;
use crate::common::probe_tests::{
do_ping, do_socks5_connectivity_test, lp_registration_probe, wg_probe, wg_probe_lp,
do_ping, do_socks5_connectivity_test, lp_registration_probe, wg_probe,
};
use crate::common::types::{Entry, LpProbeResults};
use crate::config::{CredentialArgs, CredentialMode, NetstackArgs, ProbeConfig};
@@ -375,28 +375,6 @@ impl Probe {
// At this point, any mixnet client MUST be disconnected
// The current probe includes registration as part of the Wireguard result, which makes that a bit awkward
// If we're supposed to run WireGuard tests and LP tests, and Mixnet registration failed, let's try to do it with lp
// This behavior should change in the future
if probe_result.outcome.wg.is_none()
&& self.config.test_mode.wireguard_tests()
&& self.config.test_mode.lp_tests()
{
// Test WireGuard via LP registration (nested session forwarding)
info!("Testing WireGuard via LP registration (no mixnet)");
let outcome = wg_probe_lp(
&self.entry_node,
&exit_node,
&bandwith_provider,
self.config.amnezia_args.clone(),
self.config.netstack_args.clone(),
)
.await
.unwrap_or_default();
probe_result.outcome.wg = Some(outcome);
}
// Test LP registration if node has LP address
if self.config.test_mode.lp_tests() {
if let Some(lp_data) = self.entry_node.lp_data {