diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 393429246d..d2b37b5a84 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -17,7 +17,7 @@ use nym_client_core::client::base_client::storage::gateway_details::{ }; use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup; -use nym_client_core::config::{GatewayEndpointConfig, TopologyStructure}; +use nym_client_core::config::{GatewayEndpointConfig, GroupBy, TopologyStructure}; use nym_client_core::error::ClientCoreError; use nym_config::OptionalSet; use nym_sphinx::params::{PacketSize, PacketType}; @@ -101,9 +101,17 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { let secondary_packet_size = args.medium_toggle.then_some(PacketSize::ExtendedPacket16); let no_per_hop_delays = args.medium_toggle; - let topology_structure = if args.medium_toggle || args.geo_routing.is_some() { - // TODO: rethink the default group. I just picked one for now. - TopologyStructure::GeoAware(args.geo_routing.unwrap_or(CountryGroup::Europe)) + let topology_structure = if args.medium_toggle { + // Use the location of the network-requester + let address = config + .core + .socks5 + .provider_mix_address + .parse() + .expect("failed to parse provider mix address"); + TopologyStructure::GeoAware(GroupBy::NymAddress(address)) + } else if let Some(code) = args.geo_routing { + TopologyStructure::GeoAware(GroupBy::CountryGroup(code)) } else { TopologyStructure::default() }; diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 2f2e7d0072..02615f2b95 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -354,11 +354,13 @@ where nym_api_urls, env!("CARGO_PKG_VERSION").to_string(), )), - config::TopologyStructure::GeoAware(group) => Box::new(GeoAwareTopologyProvider::new( - nym_api_urls, - env!("CARGO_PKG_VERSION").to_string(), - group, - )), + config::TopologyStructure::GeoAware(group_by) => { + Box::new(GeoAwareTopologyProvider::new( + nym_api_urls, + env!("CARGO_PKG_VERSION").to_string(), + group_by, + )) + } }) } diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index ece6e967a2..dabece05e5 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -1,20 +1,36 @@ use std::{collections::HashMap, fmt}; use log::{debug, error, info}; -use nym_explorer_api_requests::PrettyDetailedMixNodeBond; +use nym_explorer_api_requests::{PrettyDetailedGatewayBond, PrettyDetailedMixNodeBond}; use nym_network_defaults::var_names::EXPLORER_API; use nym_topology::{ nym_topology_from_detailed, provider_trait::{async_trait, TopologyProvider}, NymTopology, }; -use nym_validator_client::client::MixId; +use nym_validator_client::client::{IdentityKey, MixId}; use rand::{prelude::SliceRandom, thread_rng}; use serde::{Deserialize, Serialize}; +use tap::TapOptional; use url::Url; +use crate::config::GroupBy; + const MIN_NODES_PER_LAYER: usize = 1; +#[cfg(target_arch = "wasm32")] +fn reqwest_client() -> Option { + reqwest::Client::builder().build().ok() +} + +#[cfg(not(target_arch = "wasm32"))] +fn reqwest_client() -> Option { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .ok() +} + // TODO: create a explorer-api-client async fn fetch_mixnodes_from_explorer_api() -> Option> { let explorer_api_url = std::env::var(EXPLORER_API).ok()?; @@ -24,20 +40,7 @@ async fn fetch_mixnodes_from_explorer_api() -> Option Option Option> { + let explorer_api_url = std::env::var(EXPLORER_API).ok()?; + let explorer_api_url = Url::parse(&explorer_api_url) + .ok()? + .join("v1/gateways") + .ok()?; + + debug!("Fetching: {}", explorer_api_url); + reqwest_client()? + .get(explorer_api_url) + .send() + .await + .ok()? + .json::>() + .await + .ok() +} + #[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)] pub enum CountryGroup { Europe, @@ -213,6 +235,23 @@ fn group_mixnodes_by_country_code( }) } +fn group_gateways_by_country_code( + gateways: Vec, +) -> HashMap> { + gateways.into_iter().fold( + HashMap::>::new(), + |mut acc, g| { + if let Some(ref location) = g.location { + let country_code = location.two_letter_iso_country_code.clone(); + let group_code = CountryGroup::new(country_code.as_str()); + let gateways = acc.entry(group_code).or_insert_with(Vec::new); + gateways.push(g.gateway.identity_key) + } + acc + }, + ) +} + fn log_mixnode_distribution(mixnodes: &HashMap>) { let mixnode_distribution = mixnodes .iter() @@ -222,6 +261,15 @@ fn log_mixnode_distribution(mixnodes: &HashMap>) { debug!("Mixnode distribution - {}", mixnode_distribution); } +fn log_gateway_distribution(gateways: &HashMap>) { + let gateway_distribution = gateways + .iter() + .map(|(k, v)| format!("{}: {}", k, v.len())) + .collect::>() + .join(", "); + debug!("Gateway distribution - {}", gateway_distribution); +} + fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { let mixes = topology.mixes(); if mixes.keys().len() < 3 { @@ -244,7 +292,7 @@ fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { pub struct GeoAwareTopologyProvider { validator_client: nym_validator_client::client::NymApiClient, - filter_on: CountryGroup, + filter_on: GroupBy, client_version: String, } @@ -252,7 +300,7 @@ impl GeoAwareTopologyProvider { pub fn new( mut nym_api_urls: Vec, client_version: String, - filter_on: CountryGroup, + filter_on: GroupBy, ) -> GeoAwareTopologyProvider { log::info!( "Creating geo-aware topology provider with filter on {:?}", @@ -294,6 +342,38 @@ impl GeoAwareTopologyProvider { return None; }; + debug!("Fetching gateways from explorer-api..."); + let Some(gateways_from_explorer_api) = fetch_gateways_from_explorer_api().await else { + error!("failed to get mixnodes from explorer-api"); + return None; + }; + + // Determine what we should filter around + let filter_on = match self.filter_on { + GroupBy::CountryGroup(group) => group, + GroupBy::NymAddress(recipient) => { + // Convert recipient into a country group by extracting out the gateway part and + // using that as the country code. + let gateway = recipient.gateway().to_base58_string(); + + // Lookup the location of this gateway by using the location data from the + // explorer-api + let gateway_location = gateways_from_explorer_api + .iter() + .find(|g| g.gateway.identity_key == gateway) + .and_then(|g| g.location.clone()) + .map(|location| location.two_letter_iso_country_code) + .tap_none(|| error!("No location found for the gateway: {}", gateway))?; + debug!( + "Filtering on nym-address: {}, with location: {}", + recipient, gateway_location + ); + + CountryGroup::new(&gateway_location) + } + }; + debug!("Filter group: {}", filter_on); + // Partition mixnodes_from_explorer_api according to the value of // two_letter_iso_country_code. // NOTE: we construct the full distribution here, but only use the one we're interested in. @@ -302,8 +382,16 @@ impl GeoAwareTopologyProvider { let mixnode_distribution = group_mixnodes_by_country_code(mixnodes_from_explorer_api); log_mixnode_distribution(&mixnode_distribution); - let Some(filtered_mixnode_ids) = mixnode_distribution.get(&self.filter_on) else { - error!("no mixnodes found for: {}", self.filter_on); + let gateway_distribution = group_gateways_by_country_code(gateways_from_explorer_api); + log_gateway_distribution(&gateway_distribution); + + let Some(filtered_mixnode_ids) = mixnode_distribution.get(&filter_on) else { + error!("no mixnodes found for: {}", filter_on); + return None; + }; + + let Some(filtered_gateway_ids) = gateway_distribution.get(&filter_on) else { + error!("no gateways found for: {}", filter_on); return None; }; @@ -312,6 +400,11 @@ impl GeoAwareTopologyProvider { .filter(|m| filtered_mixnode_ids.contains(&m.mix_id())) .collect::>(); + let gateways = gateways + .into_iter() + .filter(|g| filtered_gateway_ids.contains(g.identity())) + .collect::>(); + let topology = nym_topology_from_detailed(mixnodes, gateways) .filter_system_version(&self.client_version); diff --git a/common/client-core/src/config/mod.rs b/common/client-core/src/config/mod.rs index 465c227e19..f3cee8200f 100644 --- a/common/client-core/src/config/mod.rs +++ b/common/client-core/src/config/mod.rs @@ -3,7 +3,10 @@ use nym_config::defaults::NymNetworkDetails; use nym_crypto::asymmetric::identity; -use nym_sphinx::params::{PacketSize, PacketType}; +use nym_sphinx::{ + addressing::clients::Recipient, + params::{PacketSize, PacketType}, +}; use serde::{Deserialize, Serialize}; use std::time::Duration; use url::Url; @@ -480,11 +483,19 @@ pub struct Topology { pub topology_structure: TopologyStructure, } +#[allow(clippy::large_enum_variant)] #[derive(Default, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum TopologyStructure { #[default] NymApi, - GeoAware(CountryGroup), + GeoAware(GroupBy), +} + +#[allow(clippy::large_enum_variant)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum GroupBy { + CountryGroup(CountryGroup), + NymAddress(Recipient), } impl Default for Topology { diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index df5097ad1c..e915498a01 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -3884,6 +3884,7 @@ version = "1.1.15" dependencies = [ "async-trait", "base64 0.21.2", + "cfg-if", "dashmap", "dirs 4.0.0", "futures", diff --git a/nym-connect/desktop/src-tauri/src/tasks.rs b/nym-connect/desktop/src-tauri/src/tasks.rs index 06b0809c2f..4af366ca48 100644 --- a/nym-connect/desktop/src-tauri/src/tasks.rs +++ b/nym-connect/desktop/src-tauri/src/tasks.rs @@ -1,12 +1,9 @@ use futures::{channel::mpsc, StreamExt}; use nym_client_core::{ - client::{ - base_client::storage::{ - gateway_details::GatewayDetailsStore, MixnetClientStorage, OnDiskPersistent, - }, - topology_control::geo_aware_provider::CountryGroup, + client::base_client::storage::{ + gateway_details::GatewayDetailsStore, MixnetClientStorage, OnDiskPersistent, }, - config::{GatewayEndpointConfig, TopologyStructure}, + config::{GatewayEndpointConfig, GroupBy, TopologyStructure}, error::ClientCoreStatusMessage, }; use nym_socks5_client_core::{NymClient as Socks5NymClient, Socks5ControlMessageSender}; @@ -54,12 +51,17 @@ fn override_config_from_env(config: &mut Config, privacy_level: &PrivacyLevel) { config.core.base.set_no_per_hop_delays(); // TODO: selectable in the UI - let default_country_group = CountryGroup::Europe; - log::warn!("Using geo-aware mixnode selection: {default_country_group}"); + let address = config + .core + .socks5 + .provider_mix_address + .parse() + .expect("failed to parse provider mix address"); + log::warn!("Using geo-aware mixnode selection baseon the location of: {address}"); config .core .base - .set_topology_structure(TopologyStructure::GeoAware(default_country_group)); + .set_topology_structure(TopologyStructure::GeoAware(GroupBy::NymAddress(address))); } }