diff --git a/nym-vpn-core/Cargo.lock b/nym-vpn-core/Cargo.lock index bebfd87953..f14f159cf3 100644 --- a/nym-vpn-core/Cargo.lock +++ b/nym-vpn-core/Cargo.lock @@ -3135,7 +3135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.4", ] [[package]] @@ -3802,8 +3802,8 @@ dependencies = [ "time", "tokio", "tokio-stream", - "tokio-tungstenite", - "tungstenite", + "tokio-tungstenite 0.20.1", + "tungstenite 0.20.1", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4324,8 +4324,8 @@ dependencies = [ "time", "tokio", "tokio-stream", - "tokio-tungstenite", - "tungstenite", + "tokio-tungstenite 0.20.1", + "tungstenite 0.20.1", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4339,13 +4339,11 @@ version = "0.1.0" dependencies = [ "async-trait", "chrono", + "futures", "hickory-resolver", "itertools 0.13.0", - "log", "nym-client-core", "nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=dff82f9)", - "nym-explorer-client", - "nym-harbour-master-client", "nym-sdk", "nym-topology", "nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?rev=dff82f9)", @@ -4354,7 +4352,9 @@ dependencies = [ "serde", "thiserror", "tokio", + "tokio-tungstenite 0.23.1", "tracing", + "tungstenite 0.23.0", "url", ] @@ -4409,7 +4409,7 @@ dependencies = [ "thiserror", "tokio", "tracing", - "tungstenite", + "tungstenite 0.20.1", "wasmtimer", "zeroize", ] @@ -8132,10 +8132,22 @@ dependencies = [ "rustls 0.21.10", "tokio", "tokio-rustls 0.24.1", - "tungstenite", + "tungstenite 0.20.1", "webpki-roots 0.25.4", ] +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.23.0", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -8646,6 +8658,24 @@ dependencies = [ "webpki-roots 0.24.0", ] +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "tunnel-obfuscation" version = "0.0.0" @@ -9081,7 +9111,7 @@ dependencies = [ "gloo-net", "gloo-utils", "js-sys", - "tungstenite", + "tungstenite 0.20.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", diff --git a/nym-vpn-core/crates/nym-gateway-directory/Cargo.toml b/nym-vpn-core/crates/nym-gateway-directory/Cargo.toml index 48e1c29682..8b2b863f07 100644 --- a/nym-vpn-core/crates/nym-gateway-directory/Cargo.toml +++ b/nym-vpn-core/crates/nym-gateway-directory/Cargo.toml @@ -6,14 +6,12 @@ license.workspace = true [dependencies] async-trait.workspace = true -chrono = "0.4.38" +chrono.workspace = true +futures.workspace = true hickory-resolver.workspace = true itertools.workspace = true -log.workspace = true nym-client-core.workspace = true nym-config.workspace = true -nym-explorer-client.workspace = true -nym-harbour-master-client = { path = "../nym-harbour-master-client" } nym-sdk.workspace = true nym-topology.workspace = true nym-validator-client.workspace = true @@ -21,6 +19,9 @@ nym-vpn-api-client = { path = "../nym-vpn-api-client" } rand.workspace = true serde.workspace = true thiserror.workspace = true +tokio = { workspace = true } +tokio-tungstenite = { version = "0.23" } +tungstenite = { version = "0.23" } tracing.workspace = true url.workspace = true diff --git a/nym-vpn-core/crates/nym-gateway-directory/src/entries/entry_point.rs b/nym-vpn-core/crates/nym-gateway-directory/src/entries/entry_point.rs index fd843274e6..0cad6e42ed 100644 --- a/nym-vpn-core/crates/nym-gateway-directory/src/entries/entry_point.rs +++ b/nym-vpn-core/crates/nym-gateway-directory/src/entries/entry_point.rs @@ -51,7 +51,7 @@ impl EntryPoint { matches!(self, EntryPoint::Location { .. }) } - pub fn lookup_gateway(&self, gateways: &GatewayList) -> Result { + pub async fn lookup_gateway(&self, gateways: &GatewayList) -> Result { match &self { EntryPoint::Gateway { identity } => { debug!("Selecting gateway by identity: {}", identity); @@ -73,7 +73,7 @@ impl EntryPoint { } EntryPoint::RandomLowLatency => { debug!("Selecting a random low latency gateway"); - todo!("Need to add client address to Gateway type"); + gateways.random_low_latency_gateway().await } EntryPoint::Random => { debug!("Selecting a random gateway"); diff --git a/nym-vpn-core/crates/nym-gateway-directory/src/entries/exit_point.rs b/nym-vpn-core/crates/nym-gateway-directory/src/entries/exit_point.rs index b910766078..e69a936ce6 100644 --- a/nym-vpn-core/crates/nym-gateway-directory/src/entries/exit_point.rs +++ b/nym-vpn-core/crates/nym-gateway-directory/src/entries/exit_point.rs @@ -6,7 +6,7 @@ use std::fmt::{Display, Formatter}; use crate::{error::Result, Error, IpPacketRouterAddress}; use nym_sdk::mixnet::{NodeIdentity, Recipient}; use serde::{Deserialize, Serialize}; -use tracing::debug; +use tracing::{debug, info}; use super::gateway::{Gateway, GatewayList}; @@ -82,7 +82,7 @@ impl ExitPoint { }) } ExitPoint::Random => { - log::info!("Selecting a random exit gateway"); + info!("Selecting a random exit gateway"); gateways .random_gateway() .ok_or_else(|| Error::FailedToSelectGatewayRandomly) diff --git a/nym-vpn-core/crates/nym-gateway-directory/src/entries/gateway.rs b/nym-vpn-core/crates/nym-gateway-directory/src/entries/gateway.rs index b845e46c02..6d0c560385 100644 --- a/nym-vpn-core/crates/nym-gateway-directory/src/entries/gateway.rs +++ b/nym-vpn-core/crates/nym-gateway-directory/src/entries/gateway.rs @@ -9,6 +9,9 @@ use tracing::error; use crate::{error::Result, AuthAddress, Country, Error, IpPacketRouterAddress}; +// Decimal between 0 and 1 representing the performance of a gateway, measured over 24h. +type Perfomance = f64; + #[derive(Clone, Debug)] pub struct Gateway { pub identity: NodeIdentity, @@ -16,6 +19,10 @@ pub struct Gateway { pub ipr_address: Option, pub authenticator_address: Option, pub last_probe: Option, + pub host: Option, + pub clients_ws_port: Option, + pub clients_wss_port: Option, + pub performance: Option, } impl Gateway { @@ -37,6 +44,20 @@ impl Gateway { pub fn has_ipr_address(&self) -> bool { self.ipr_address.is_some() } + + pub fn clients_address_no_tls(&self) -> Option { + match (&self.host, &self.clients_ws_port) { + (Some(host), Some(port)) => Some(format!("ws://{}:{}", host, port)), + _ => None, + } + } + + pub fn clients_address_tls(&self) -> Option { + match (&self.host, &self.clients_wss_port) { + (Some(host), Some(port)) => Some(format!("wss://{}:{}", host, port)), + _ => None, + } + } } #[derive(Debug, Default, Clone, PartialEq)] @@ -139,6 +160,10 @@ impl TryFrom for Gateway { ipr_address: None, authenticator_address: None, last_probe: gateway.last_probe.map(Probe::from), + host: None, + clients_ws_port: None, + clients_wss_port: None, + performance: None, }) } } @@ -179,12 +204,20 @@ impl TryFrom for Gateway { .inspect_err(|err| error!("Failed to parse authenticator address: {err}")) .ok() }); + let gateway = nym_topology::gateway::Node::try_from(gateway).ok(); + let host = gateway.clone().map(|g| g.host); + let clients_ws_port = gateway.as_ref().map(|g| g.clients_ws_port); + let clients_wss_port = gateway.and_then(|g| g.clients_wss_port); Ok(Gateway { identity, location, ipr_address, authenticator_address, last_probe: None, + host, + clients_ws_port, + clients_wss_port, + performance: None, }) } } @@ -277,6 +310,13 @@ impl GatewayList { pub fn into_inner(self) -> Vec { self.gateways } + + pub(crate) async fn random_low_latency_gateway(&self) -> Result { + let mut rng = rand::rngs::OsRng; + nym_client_core::init::helpers::choose_gateway_by_latency(&mut rng, &self.gateways, false) + .await + .map_err(|err| Error::FailedToSelectGatewayBasedOnLowLatency { source: err }) + } } impl IntoIterator for GatewayList { @@ -287,3 +327,21 @@ impl IntoIterator for GatewayList { self.gateways.into_iter() } } + +impl nym_client_core::init::helpers::ConnectableGateway for Gateway { + fn identity(&self) -> &nym_sdk::mixnet::NodeIdentity { + self.identity() + } + + fn clients_address(&self) -> String { + // This is a bit of a sharp edge, but temporary until we can remove Option from host + // and tls port when we add these to the vpn API endpoints. + self.clients_address_tls() + .or(self.clients_address_no_tls()) + .unwrap_or("ws://".to_string()) + } + + fn is_wss(&self) -> bool { + self.clients_address_tls().is_some() + } +} diff --git a/nym-vpn-core/crates/nym-gateway-directory/src/error.rs b/nym-vpn-core/crates/nym-gateway-directory/src/error.rs index 51b2d45d8e..7d67c37791 100644 --- a/nym-vpn-core/crates/nym-gateway-directory/src/error.rs +++ b/nym-vpn-core/crates/nym-gateway-directory/src/error.rs @@ -19,22 +19,11 @@ pub enum Error { ValidatorClientError(#[from] nym_validator_client::ValidatorClientError), #[error(transparent)] - ExplorerApiError(#[from] nym_explorer_client::ExplorerApiError), - - #[error(transparent)] - HarbourMasterError(#[from] nym_harbour_master_client::HarbourMasterError), - - #[error(transparent)] - HarbourMasterApiError(#[from] nym_harbour_master_client::HarbourMasterApiError), + NymHttpApiError(#[from] nym_vpn_api_client::VpnApiError), #[error(transparent)] NymVpnApiClientError(#[from] nym_vpn_api_client::VpnApiClientError), - #[error("failed to fetch location data from explorer-api: {error}")] - FailedFetchLocationData { - error: nym_explorer_client::ExplorerApiError, - }, - #[error("failed to resolve gateway hostname: {hostname}: {source}")] FailedToDnsResolveGateway { hostname: String, @@ -44,10 +33,11 @@ pub enum Error { #[error("resolved hostname {0} but no IP address found")] ResolvedHostnameButNoIp(String), - #[error("failed to lookup described gateways: {source}")] - FailedToLookupDescribedGateways { - source: nym_validator_client::ValidatorClientError, - }, + #[error("failed to lookup described gateways: {0}")] + FailedToLookupDescribedGateways(#[source] nym_validator_client::ValidatorClientError), + + #[error("failed to lookup skimmed gateways: {0}")] + FailedToLookupSkimmedGateways(#[source] nym_validator_client::ValidatorClientError), #[error("requested gateway not found in the remote list: {0}")] RequestedGatewayIdNotFound(String), diff --git a/nym-vpn-core/crates/nym-gateway-directory/src/gateway_client.rs b/nym-vpn-core/crates/nym-gateway-directory/src/gateway_client.rs index f4f52e536d..5b5bb07463 100644 --- a/nym-vpn-core/crates/nym-gateway-directory/src/gateway_client.rs +++ b/nym-vpn-core/crates/nym-gateway-directory/src/gateway_client.rs @@ -7,12 +7,12 @@ use crate::{ gateway::{Gateway, GatewayList}, }, error::Result, - helpers::{select_random_low_latency_described_gateway, try_resolve_hostname}, + helpers::try_resolve_hostname, AuthAddress, Error, IpPacketRouterAddress, }; use nym_sdk::{mixnet::Recipient, UserAgent}; use nym_topology::IntoGatewayNode; -use nym_validator_client::{models::DescribedGateway, NymApiClient}; +use nym_validator_client::{models::DescribedGateway, nym_nodes::SkimmedNode, NymApiClient}; use nym_vpn_api_client::VpnApiClientExt; use std::{fmt, net::IpAddr, time::Duration}; use tracing::{debug, error, info}; @@ -139,27 +139,25 @@ impl GatewayClient { } async fn lookup_described_gateways(&self) -> Result> { - info!("Fetching gateways from nym-api..."); + info!("Fetching described gateways from nym-api..."); self.api_client .get_cached_described_gateways() .await - .map_err(|source| Error::FailedToLookupDescribedGateways { source }) + .map_err(Error::FailedToLookupDescribedGateways) + } + + async fn lookup_skimmed_gateways(&self) -> Result> { + info!("Fetching skimmed gateways from nym-api..."); + self.api_client + .get_basic_gateways(None) + .await + .map_err(Error::FailedToLookupSkimmedGateways) } pub async fn lookup_low_latency_entry_gateway(&self) -> Result { debug!("Fetching low latency entry gateway..."); - let gateways = self.lookup_described_gateways().await?; - let low_latency_gateway: Gateway = select_random_low_latency_described_gateway(&gateways) - .await - .cloned()? - .try_into()?; - let gateway_list = self.lookup_entry_gateways().await?; - gateway_list - .gateway_with_identity(low_latency_gateway.identity()) - .ok_or_else(|| Error::NoMatchingGateway { - requested_identity: low_latency_gateway.identity().to_string(), - }) - .cloned() + let gateways = self.lookup_entry_gateways().await?; + gateways.random_low_latency_gateway().await } pub async fn lookup_gateway_ip(&self, gateway_identity: &str) -> Result { @@ -191,7 +189,7 @@ impl GatewayClient { } pub async fn lookup_all_gateways_from_nym_api(&self) -> Result { - let gateways = self + let mut gateways = self .lookup_described_gateways() .await? .into_iter() @@ -200,7 +198,9 @@ impl GatewayClient { .inspect_err(|err| error!("Failed to parse gateway: {err}")) .ok() }) - .collect(); + .collect::>(); + let skimmed_gateways = self.lookup_skimmed_gateways().await?; + append_performance(&mut gateways, skimmed_gateways); Ok(GatewayList::new(gateways)) } @@ -232,7 +232,9 @@ impl GatewayClient { // Lookup the IPR and authenticator addresses from the nym-api as a temporary hack until // the nymvpn.com endpoints are updated to also include these fields. let described_gateways = self.lookup_described_gateways().await?; + let basic_gw = self.api_client.get_basic_gateways(None).await.unwrap(); append_ipr_and_authenticator_addresses(&mut gateways, described_gateways); + append_performance(&mut gateways, basic_gw); Ok(GatewayList::new(gateways)) } else { self.lookup_all_gateways_from_nym_api().await @@ -256,7 +258,9 @@ impl GatewayClient { // Lookup the IPR and authenticator addresses from the nym-api as a temporary hack until // the nymvpn.com endpoints are updated to also include these fields. let described_gateways = self.lookup_described_gateways().await?; + let basic_gw = self.api_client.get_basic_gateways(None).await.unwrap(); append_ipr_and_authenticator_addresses(&mut entry_gateways, described_gateways); + append_performance(&mut entry_gateways, basic_gw); Ok(GatewayList::new(entry_gateways)) } else { self.lookup_entry_gateways_from_nym_api().await @@ -280,7 +284,9 @@ impl GatewayClient { // Lookup the IPR and authenticator addresses from the nym-api as a temporary hack until // the nymvpn.com endpoints are updated to also include these fields. let described_gateways = self.lookup_described_gateways().await?; + let basic_gw = self.api_client.get_basic_gateways(None).await.unwrap(); append_ipr_and_authenticator_addresses(&mut exit_gateways, described_gateways); + append_performance(&mut exit_gateways, basic_gw); Ok(GatewayList::new(exit_gateways)) } else { self.lookup_exit_gateways_from_nym_api().await @@ -343,7 +349,37 @@ fn append_ipr_and_authenticator_addresses( .and_then(|d| d.authenticator) .map(|auth| auth.address) .and_then(|address| Recipient::try_from_base58_string(address).ok()) - .map(|r| AuthAddress(Some(r))) + .map(|r| AuthAddress(Some(r))); + let gateway_node = nym_topology::gateway::Node::try_from(described_gateway).unwrap(); + gateway.host = Some(gateway_node.host); + gateway.clients_ws_port = Some(gateway_node.clients_ws_port); + gateway.clients_wss_port = gateway_node.clients_wss_port; + } else { + error!( + "Failed to find described gateway for gateway with identity {}", + gateway.identity() + ); + } + } +} + +// Append the performance to the gateways. This is a temporary hack until the nymvpn.com endpoints +// are updated to also include this field. +fn append_performance( + gateways: &mut [Gateway], + basic_gw: Vec, +) { + for gateway in gateways.iter_mut() { + if let Some(basic_gw) = basic_gw + .iter() + .find(|bgw| bgw.ed25519_identity_pubkey == gateway.identity().to_base58_string()) + { + gateway.performance = Some(basic_gw.performance.round_to_integer() as f64 / 100.0); + } else { + error!( + "Failed to find skimmed node for gateway with identity {}", + gateway.identity() + ); } } } diff --git a/nym-vpn-core/crates/nym-gateway-directory/src/helpers.rs b/nym-vpn-core/crates/nym-gateway-directory/src/helpers.rs index 6b12bea20d..1a89fff2d8 100644 --- a/nym-vpn-core/crates/nym-gateway-directory/src/helpers.rs +++ b/nym-vpn-core/crates/nym-gateway-directory/src/helpers.rs @@ -1,50 +1,14 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{error::Result, Error, FORCE_TLS_FOR_GATEWAY_SELECTION}; +use crate::{error::Result, Error}; use hickory_resolver::{ config::{ResolverConfig, ResolverOpts}, TokioAsyncResolver, }; -use nym_client_core::init::helpers::choose_gateway_by_latency; -use nym_sdk::mixnet::NodeIdentity; -use nym_topology::IntoGatewayNode; -use nym_validator_client::{client::GatewayBond, models::DescribedGateway}; use std::net::IpAddr; use tracing::debug; -pub(crate) async fn select_random_low_latency_gateway_node( - gateways: &[GatewayBond], -) -> Result { - let mut rng = rand::rngs::OsRng; - let must_use_tls = FORCE_TLS_FOR_GATEWAY_SELECTION; - let gateway_nodes: Vec = gateways - .iter() - .filter_map(|gateway| nym_topology::gateway::Node::try_from(gateway).ok()) - .collect(); - let gateway = choose_gateway_by_latency(&mut rng, &gateway_nodes, must_use_tls) - .await - .map(|gateway| *gateway.identity()) - .map_err(|err| Error::FailedToSelectGatewayBasedOnLowLatency { source: err })?; - Ok(gateway) -} - -pub(crate) async fn select_random_low_latency_described_gateway( - gateways: &[DescribedGateway], -) -> Result<&DescribedGateway> { - let gateway_nodes = gateways - .iter() - .map(|gateway| gateway.bond.clone()) - .collect::>(); - let low_latency_gateway = select_random_low_latency_gateway_node(&gateway_nodes).await?; - gateways - .iter() - .find(|gateway| gateway.identity() == low_latency_gateway.to_string()) - .ok_or_else(|| Error::NoMatchingGatewayAfterSelectingLowLatency { - requested_identity: low_latency_gateway.to_string(), - }) -} - pub(crate) async fn try_resolve_hostname(hostname: &str) -> Result { debug!("Trying to resolve hostname: {hostname}"); let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()); diff --git a/nym-vpn-core/crates/nym-gateway-directory/src/lib.rs b/nym-vpn-core/crates/nym-gateway-directory/src/lib.rs index c168749760..b63209f5ff 100644 --- a/nym-vpn-core/crates/nym-gateway-directory/src/lib.rs +++ b/nym-vpn-core/crates/nym-gateway-directory/src/lib.rs @@ -21,5 +21,3 @@ pub use crate::{ pub use nym_sdk::mixnet::{NodeIdentity, Recipient}; pub use nym_validator_client::models::DescribedGateway; - -const FORCE_TLS_FOR_GATEWAY_SELECTION: bool = false; diff --git a/nym-vpn-core/crates/nym-gateway-probe/src/lib.rs b/nym-vpn-core/crates/nym-gateway-probe/src/lib.rs index 6670492590..56e45d9581 100644 --- a/nym-vpn-core/crates/nym-gateway-probe/src/lib.rs +++ b/nym-vpn-core/crates/nym-gateway-probe/src/lib.rs @@ -44,7 +44,7 @@ pub async fn fetch_gateways_with_ipr() -> anyhow::Result { pub async fn probe(entry_point: EntryPoint) -> anyhow::Result { // Setup the entry gateways let gateways = lookup_gateways().await?; - let entry_gateway = entry_point.lookup_gateway(&gateways)?; + let entry_gateway = entry_point.lookup_gateway(&gateways).await?; let exit_router_address = entry_gateway.ipr_address; let entry_gateway_id = entry_gateway.identity(); diff --git a/nym-vpn-core/nym-vpn-lib/src/tunnel_setup.rs b/nym-vpn-core/nym-vpn-lib/src/tunnel_setup.rs index 85bf762dac..4ba50ec987 100644 --- a/nym-vpn-core/nym-vpn-lib/src/tunnel_setup.rs +++ b/nym-vpn-core/nym-vpn-lib/src/tunnel_setup.rs @@ -443,6 +443,7 @@ async fn select_gateways( let entry_gateway = nym_vpn .entry_point() .lookup_gateway(&entry_gateways) + .await .map_err(|source| match source { nym_gateway_directory::Error::NoMatchingEntryGatewayForLocation { requested_location, @@ -460,18 +461,24 @@ async fn select_gateways( info!("Found {} entry gateways", entry_gateways.len()); info!("Found {} exit gateways", exit_gateways.len()); info!( - "Using entry gateway: {}, location: {}", + "Using entry gateway: {}, location: {}, performane: {}", *entry_gateway.identity(), entry_gateway .two_letter_iso_country_code() - .map_or_else(|| "unknown".to_string(), |code| code.to_string()) + .map_or_else(|| "unknown".to_string(), |code| code.to_string()), + entry_gateway + .performance + .map_or_else(|| "unknown".to_string(), |perf| perf.to_string()), ); info!( - "Using exit gateway: {}, location: {}", + "Using exit gateway: {}, location: {}, performance: {}", *exit_gateway.identity(), exit_gateway .two_letter_iso_country_code() - .map_or_else(|| "unknown".to_string(), |code| code.to_string()) + .map_or_else(|| "unknown".to_string(), |code| code.to_string()), + entry_gateway + .performance + .map_or_else(|| "unknown".to_string(), |perf| perf.to_string()), ); info!( "Using exit router address {}",