Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-check-ns-api-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: ci-check-ns-api-version
on:
pull_request:
paths:
- "nym-node-status-api/**"
- "nym-node-status-api/nym-node-status-api/**"

env:
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
Expand Down
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions nym-gateway-probe/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ hex.workspace = true
tracing.workspace = true
pnet_packet.workspace = true
rand.workspace = true
reqwest = { workspace = true, features = ["socks"] }
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
Expand Down
209 changes: 206 additions & 3 deletions nym-gateway-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use std::{
time::Duration,
};

use crate::{netstack::NetstackResult, types::Entry};
use crate::{
netstack::NetstackResult,
types::{Entry, HttpsConnectivityTest},
};
use anyhow::bail;
use base64::{Engine as _, engine::general_purpose};
use bytes::BytesMut;
Expand Down Expand Up @@ -36,8 +39,9 @@ use nym_ip_packet_requests::{
};
use nym_sdk::mixnet::{
CredentialStorage, Ephemeral, KeyStore, MixnetClient, MixnetClientBuilder, MixnetClientStorage,
NodeIdentity, Recipient, ReconstructedMessage, StoragePaths,
NodeIdentity, Recipient, ReconstructedMessage, Socks5, StoragePaths,
};
use nym_validator_client::models::NetworkRequesterDetails;
use rand::rngs::OsRng;
use std::path::PathBuf;

Expand All @@ -48,7 +52,7 @@ use url::Url;

use crate::{
icmp::{check_for_icmp_beacon_reply, icmp_identifier, send_ping_v4, send_ping_v6},
types::Exit,
types::{Exit, HttpsConnectivityResult, Socks5ProbeResults},
};

use netstack::{NetstackRequest, NetstackRequestGo};
Expand Down Expand Up @@ -124,6 +128,15 @@ impl CredentialArgs {
}
}

#[derive(Args)]
pub struct Socks5Args {
#[arg(long, default_value_t = 45)]
mixnet_client_timeout_sec: u64,

#[arg(long, default_value_t = 10)]
test_count: u64,
}

#[derive(Default, Debug)]
pub enum TestedNode {
#[default]
Expand Down Expand Up @@ -151,6 +164,7 @@ impl TestedNode {
pub struct TestedNodeDetails {
identity: NodeIdentity,
exit_router_address: Option<Recipient>,
network_requester_details: Option<NetworkRequesterDetails>,
authenticator_address: Option<Recipient>,
authenticator_version: AuthenticatorVersion,
ip_address: Option<IpAddr>,
Expand All @@ -162,6 +176,7 @@ pub struct Probe {
amnezia_args: String,
netstack_args: NetstackArgs,
credentials_args: CredentialArgs,
socks5_args: Socks5Args,
}

impl Probe {
Expand All @@ -170,20 +185,74 @@ impl Probe {
tested_node: TestedNode,
netstack_args: NetstackArgs,
credentials_args: CredentialArgs,
socks5_args: Socks5Args,
) -> Self {
Self {
entrypoint,
tested_node,
amnezia_args: "".into(),
netstack_args,
credentials_args,
socks5_args,
}
}
pub fn with_amnezia(&mut self, args: &str) -> &Self {
self.amnezia_args = args.to_string();
self
}

pub async fn test_socks5_only(
self,
directory: NymApiDirectory,
gateway_key: Option<String>,
network_details: NymNetworkDetails,
) -> anyhow::Result<ProbeResult> {
let exit_gateway = match gateway_key {
Some(gateway_key) => NodeIdentity::from_base58_string(gateway_key)?,
None => directory.random_exit_with_nr()?,
};
info!("Testing SOCKS5 only on exit gateway {}", exit_gateway);
let node_info = directory
.exit_gateway_nr(&exit_gateway)?
.to_testable_node()?;

let socks5_outcome = {
if let Some(ref nr_details) = node_info.network_requester_details {
match do_socks5_connectivity_test(
&nr_details.address,
network_details,
directory,
self.socks5_args.mixnet_client_timeout_sec,
self.socks5_args.test_count,
)
.await
{
Ok(results) => Some(results),
Err(e) => {
error!("SOCKS5 test failed: {}", e);
None
}
}
} else {
info!("No NR available, skipping SOCKS5 tests");
None
}
};

let probe_result = ProbeResult {
node: exit_gateway.to_base58_string(),
used_entry: exit_gateway.to_base58_string(),
outcome: ProbeOutcome {
as_entry: Entry::NotTested,
as_exit: None,
socks5: socks5_outcome,
wg: None,
},
};

Ok(probe_result)
}

pub async fn probe(
self,
directory: NymApiDirectory,
Expand Down Expand Up @@ -225,6 +294,7 @@ impl Probe {
nyxd_url,
tested_entry,
only_wireguard,
directory,
)
.await
}
Expand Down Expand Up @@ -305,6 +375,7 @@ impl Probe {
nyxd_url,
tested_entry,
only_wireguard,
directory,
)
.await
}
Expand Down Expand Up @@ -361,6 +432,7 @@ impl Probe {
nyxd_url: Url,
tested_entry: bool,
only_wireguard: bool,
directory: NymApiDirectory,
) -> anyhow::Result<ProbeResult>
where
T: MixnetClientStorage + Clone + 'static,
Expand All @@ -381,6 +453,7 @@ impl Probe {
Entry::EntryFailure
},
as_exit: None,
socks5: None,
wg: None,
},
});
Expand All @@ -403,6 +476,7 @@ impl Probe {
Entry::NotTested
},
as_exit: None,
socks5: None,
wg: None,
}),
mixnet_client,
Expand Down Expand Up @@ -475,9 +549,34 @@ impl Probe {
WgProbeResults::default()
};

// test failure doesn't stop further tests
let socks5_outcome = {
if let Some(ref nr_details) = node_info.network_requester_details {
match do_socks5_connectivity_test(
&nr_details.address,
NymNetworkDetails::new_from_env(),
directory,
self.socks5_args.mixnet_client_timeout_sec,
self.socks5_args.test_count,
)
.await
{
Ok(results) => Some(results),
Err(e) => {
error!("SOCKS5 test failed: {}", e);
None
}
}
} else {
info!("No NR available, skipping SOCKS5 tests");
None
}
};

// Disconnect the mixnet client gracefully
outcome.map(|mut outcome| {
outcome.wg = Some(wg_outcome);
outcome.socks5 = socks5_outcome;
ProbeResult {
node: node_info.identity.to_string(),
used_entry: mixnet_entry_gateway_id.to_string(),
Expand Down Expand Up @@ -711,6 +810,7 @@ async fn do_ping(
exit_result.map(|exit| ProbeOutcome {
as_entry: entry,
as_exit: exit,
socks5: None,
wg: None,
}),
mixnet_client,
Expand Down Expand Up @@ -775,6 +875,109 @@ async fn do_ping_exit(
listen_for_icmp_ping_replies(mixnet_client, our_ips).await
}

/// Creates a SOCKS5 proxy connection through the mixnet to the exit GW
/// and performs necessary tests.
#[instrument(level = "info", name = "socks5_test", skip_all)]
async fn do_socks5_connectivity_test(
network_requester_address: &str,
network_details: NymNetworkDetails,
directory: NymApiDirectory,
mixnet_client_timeout: u64,
test_run_count: u64,
) -> anyhow::Result<Socks5ProbeResults> {
info!(
"Starting SOCKS5 test through Network Requester: {}",
network_requester_address
);

let mut results = Socks5ProbeResults::default();

// 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);
results.https_connectivity =
HttpsConnectivityResult::with_error(format!("Invalid NR address: {}", e));
return Ok(results);
}
};

info!(
"Network Requester gateway: {}",
nr_recipient.gateway().to_base58_string()
);
info!(
"Network Requester identity: {}",
nr_recipient.identity().to_base58_string()
);

// create ephemeral SOCKS5 client
let socks5_config = Socks5::new(network_requester_address.to_string());

// Create debug config similar to main probe
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;
debug_config.topology.ignore_egress_epoch_role = true;
// 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 on topology refresh
debug_config.topology.minimum_gateway_performance = 0;

// 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) {
results.https_connectivity = HttpsConnectivityResult::with_error(e.to_string());
return Ok(results);
} 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;

let socks5_client_builder = MixnetClientBuilder::new_ephemeral()
// Specify entry gateway explicitly
.request_gateway(entry_gateway.to_base58_string())
.socks5_config(socks5_config)
.network_details(network_details)
.debug_config(debug_config)
.build()?;

// connect to mixnet via SOCKS5
let socks5_client = match socks5_client_builder.connect_to_mixnet_via_socks5().await {
Ok(client) => {
info!("Successfully established SOCKS5 proxy connection");
info!(
"Connected via entry gateway: {}",
client.nym_address().gateway().to_base58_string()
);
results.can_connect_socks5 = true;
client
}
Err(e) => {
error!("Failed to establish SOCKS5 connection: {}", e);
results.https_connectivity =
HttpsConnectivityResult::with_error(format!("SOCKS5 connection failed: {}", e));
return Ok(results);
}
};

info!("Waiting for network topology to be ready...");
tokio::time::sleep(Duration::from_secs(10)).await;

let test = HttpsConnectivityTest::new(test_run_count, mixnet_client_timeout);
results.https_connectivity = test.run_tests(socks5_client.socks5_url()).await;

// cleanup
socks5_client.disconnect().await;

Ok(results)
}

async fn send_icmp_pings(
mixnet_client: &MixnetClient,
our_ips: IpPair,
Expand Down
Loading
Loading