removed dependency on nym-gateway-directory

This commit is contained in:
Jędrzej Stuczyński
2025-10-24 15:06:06 +01:00
committed by mfahampshire
parent db2f3bff05
commit abdd960b20
19 changed files with 91 additions and 1905 deletions
Generated
-26
View File
@@ -5929,31 +5929,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "nym-gateway-directory"
version = "0.0.0"
dependencies = [
"itertools 0.14.0",
"nym-client-core",
"nym-contracts-common",
"nym-crypto",
"nym-http-api-client",
"nym-network-defaults",
"nym-sphinx",
"nym-sphinx-addressing",
"nym-topology",
"nym-validator-client",
"rand 0.8.5",
"serde",
"serde_json",
"strum",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"tracing",
"url",
]
[[package]]
name = "nym-gateway-probe"
version = "1.18.0"
@@ -6912,7 +6887,6 @@ dependencies = [
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-gateway-directory",
"nym-gateway-requests",
"nym-http-api-client",
"nym-ip-packet-requests",
-1
View File
@@ -128,7 +128,6 @@ members = [
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-data-observatory",
"nym-gateway-directory",
"nym-network-monitor",
"nym-node",
"nym-node-status-api/nym-node-status-agent",
-36
View File
@@ -1,36 +0,0 @@
[package]
name = "nym-gateway-directory"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
license.workspace = true
edition.workspace = true
[lints]
workspace = true
[dependencies]
itertools.workspace = true
nym-client-core = { path = "../common/client-core" }
nym-http-api-client = { path = "../common/http-api-client", features = ["network-defaults"] }
nym-sphinx = { path = "../common/nymsphinx" }
nym-topology = { path = "../common/topology" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
nym-crypto = { path = "../common/crypto" }
nym-sphinx-addressing = { path = "../common/nymsphinx/addressing" }
nym-network-defaults = { path = "../common/network-defaults" }
nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
rand.workspace = true
serde.workspace = true
strum.workspace = true
thiserror.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tracing.workspace = true
url.workspace = true
serde_json.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
-3
View File
@@ -1,3 +0,0 @@
# `nym-gateway-directory`
This is a stripped down version of the `nym-gateway-directory` crate from the `nym-vpn-client` repository exposing functions and types related to IPRs. This is for use by the Rust SDK's `stream_wrapper` module.
@@ -1,39 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_sphinx::addressing::clients::Recipient;
use crate::{Error, error::Result};
#[derive(Debug, Copy, Clone)]
pub struct AuthAddress(Recipient);
impl AuthAddress {
pub(crate) fn try_from_base58_string(address: &str) -> Result<Self> {
let recipient = Recipient::try_from_base58_string(address).map_err(|source| {
Error::RecipientFormattingError {
address: address.to_string(),
source,
}
})?;
Ok(AuthAddress(recipient))
}
}
impl std::fmt::Display for AuthAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Recipient> for AuthAddress {
fn from(recipient: Recipient) -> Self {
Self(recipient)
}
}
impl From<AuthAddress> for Recipient {
fn from(auth_address: AuthAddress) -> Self {
auth_address.0
}
}
@@ -1,31 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{Location, NymDirectoryCountry};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Country {
iso_code: String,
}
impl Country {
pub fn iso_code(&self) -> &str {
&self.iso_code
}
}
impl From<NymDirectoryCountry> for Country {
fn from(country: NymDirectoryCountry) -> Self {
Self {
iso_code: country.iso_code().to_string(),
}
}
}
impl From<Location> for Country {
fn from(location: Location) -> Self {
Self {
iso_code: location.two_letter_iso_country_code,
}
}
}
@@ -1,127 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_sphinx::addressing::nodes::NodeIdentity;
use std::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::{
Error, ScoreValue,
entries::gateway::{COUNTRY_WITH_REGION_SELECTOR, Gateway, GatewayFilter, GatewayList},
error::Result,
};
// The entry point is always a gateway identity, or some other entry that can be resolved to a
// gateway identity.
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub enum EntryPoint {
// An explicit entry gateway identity.
Gateway { identity: NodeIdentity },
// Select a random entry gateway in a specific country.
Country { two_letter_iso_country_code: String },
// Select a random entry gateway in a specific region/state.
Region { region: String },
// Select an entry gateway at random.
Random,
}
impl Display for EntryPoint {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
EntryPoint::Gateway { identity } => write!(f, "Gateway: {identity}"),
EntryPoint::Country {
two_letter_iso_country_code,
} => write!(f, "Country: {two_letter_iso_country_code}"),
EntryPoint::Region { region } => write!(f, "Region/state: {region}"),
EntryPoint::Random => write!(f, "Random"),
}
}
}
impl EntryPoint {
pub fn from_base58_string(base58: &str) -> Result<Self> {
let identity = NodeIdentity::from_base58_string(base58).map_err(|source| {
Error::NodeIdentityFormattingError {
identity: base58.to_string(),
source,
}
})?;
Ok(EntryPoint::Gateway { identity })
}
pub fn lookup_gateway(
&self,
gateways: &GatewayList,
min_score: Option<ScoreValue>,
) -> Result<Gateway> {
match &self {
EntryPoint::Gateway { identity } => {
debug!("Selecting gateway by identity: {identity}");
gateways
.gateway_with_identity(identity)
.ok_or_else(|| Error::NoMatchingGateway {
requested_identity: identity.to_string(),
})
.cloned()
}
EntryPoint::Country {
two_letter_iso_country_code,
} => {
debug!("Selecting gateway by country: {two_letter_iso_country_code}");
let filters = Self::build_filters(
vec![GatewayFilter::Country(two_letter_iso_country_code.clone())],
min_score,
);
gateways.choose_random(&filters).ok_or_else(|| {
Error::NoMatchingEntryGatewayForLocation {
requested_location: two_letter_iso_country_code.clone(),
available_countries: gateways.all_iso_codes(),
}
})
}
EntryPoint::Region { region } => {
debug!("Selecting gateway by region/state: {region}");
// Currently only supported in the US
let filters = Self::build_filters(
vec![
GatewayFilter::Country(COUNTRY_WITH_REGION_SELECTOR.to_string()),
GatewayFilter::Region(region.to_string()),
],
min_score,
);
gateways.choose_random(&filters).ok_or_else(|| {
Error::NoMatchingEntryGatewayForLocation {
requested_location: region.clone(),
available_countries: gateways.all_iso_codes(),
}
})
}
EntryPoint::Random => {
debug!("Selecting a random gateway");
let filters = Self::build_filters(vec![], min_score);
gateways
.choose_random(&filters)
.ok_or_else(|| Error::FailedToSelectGatewayRandomly)
}
}
}
#[inline]
fn build_filters(
mut base_filters: Vec<GatewayFilter>,
min_score: Option<ScoreValue>,
) -> Vec<GatewayFilter> {
if let Some(min_score) = min_score {
base_filters.push(GatewayFilter::MinScore(min_score));
}
base_filters
}
}
@@ -1,150 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::addressing::nodes::NodeIdentity;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use crate::{
Error, IpPacketRouterAddress, ScoreValue,
entries::gateway::{COUNTRY_WITH_REGION_SELECTOR, Gateway, GatewayFilter, GatewayList},
error::Result,
};
// The exit point is a nym-address, but if the exit ip-packet-router is running embedded on a
// gateway, we can refer to it by the gateway identity.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum ExitPoint {
// An explicit exit address. This is useful when the exit ip-packet-router is running as a
// standalone entity (private).
Address { address: Box<Recipient> },
// An explicit exit gateway identity. This is useful when the exit ip-packet-router is running
// embedded on a gateway.
Gateway { identity: NodeIdentity },
// Select a random entry gateway in a specific country.
Country { two_letter_iso_country_code: String },
// Select a random entry gateway in a specific region/state.
Region { region: String },
// Select an exit gateway at random.
Random,
}
impl Display for ExitPoint {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ExitPoint::Address { address } => write!(f, "Address: {address}"),
ExitPoint::Gateway { identity } => write!(f, "Gateway: {identity}"),
ExitPoint::Country {
two_letter_iso_country_code,
} => write!(f, "Country: {two_letter_iso_country_code}"),
ExitPoint::Region { region } => write!(f, "Region/state: {region}"),
ExitPoint::Random => write!(f, "Random"),
}
}
}
impl ExitPoint {
pub fn lookup_gateway(
&self,
gateways: &GatewayList,
min_score: Option<ScoreValue>,
residential_exit: bool,
) -> Result<Gateway> {
match &self {
ExitPoint::Address { address } => {
tracing::debug!("Selecting gateway by address: {address}");
// There is no validation done when a ip packet router is specified by address
// since it might be private and not available in any directory.
let ipr_address = IpPacketRouterAddress::from(**address);
let gateway_address = ipr_address.gateway();
// Now fetch the gateway that the IPR is connected to, and override its IPR address
let mut gateway = gateways
.gateway_with_identity(&gateway_address)
.ok_or_else(|| Error::NoMatchingGateway {
requested_identity: gateway_address.to_string(),
})
.cloned()?;
gateway.ipr_address = Some(ipr_address);
Ok(gateway)
}
ExitPoint::Gateway { identity } => {
tracing::debug!("Selecting gateway by identity: {identity}");
gateways
.gateway_with_identity(identity)
.ok_or_else(|| Error::NoMatchingGateway {
requested_identity: identity.to_string(),
})
.cloned()
}
ExitPoint::Country {
two_letter_iso_country_code,
} => {
tracing::debug!("Selecting gateway by country: {two_letter_iso_country_code}");
let filters = Self::build_filters(
vec![GatewayFilter::Country(two_letter_iso_country_code.clone())],
min_score,
residential_exit,
);
gateways.choose_random(&filters).ok_or_else(|| {
Error::NoMatchingExitGatewayForLocation {
requested_location: two_letter_iso_country_code.clone(),
available_countries: gateways.all_iso_codes(),
}
})
}
ExitPoint::Region { region } => {
tracing::debug!("Selecting gateway by region/state: {region}");
let filters = Self::build_filters(
vec![
// Currently only supported in the US
GatewayFilter::Country(COUNTRY_WITH_REGION_SELECTOR.to_string()),
GatewayFilter::Region(region.to_string()),
],
min_score,
residential_exit,
);
gateways.choose_random(&filters).ok_or_else(|| {
Error::NoMatchingExitGatewayForLocation {
requested_location: region.clone(),
available_countries: gateways.all_iso_codes(),
}
})
}
ExitPoint::Random => {
tracing::debug!("Selecting a random exit gateway");
let filters = Self::build_filters(vec![], min_score, residential_exit);
gateways
.choose_random(&filters)
.ok_or_else(|| Error::FailedToSelectGatewayRandomly)
}
}
}
#[inline]
fn build_filters(
mut base_filters: Vec<GatewayFilter>,
min_score: Option<ScoreValue>,
residential_exit: bool,
) -> Vec<GatewayFilter> {
if let Some(min_score) = min_score {
base_filters.push(GatewayFilter::MinScore(min_score));
}
if residential_exit {
base_filters.push(GatewayFilter::Residential);
base_filters.push(GatewayFilter::Exit);
}
base_filters
}
}
@@ -1,729 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use itertools::Itertools;
use nym_contracts_common::Percent;
use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_topology::{NodeId, RoutingNode};
use nym_validator_client::models::{KeyRotationId, NymNodeDescription};
use rand::seq::IteratorRandom;
use serde::{Deserialize, Serialize};
use std::{
fmt::{self, Display},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
str::FromStr,
};
use tracing::error;
use crate::{
AuthAddress, BridgeInformation, BridgeParameters, Country, Error, IpPacketRouterAddress,
ScoreThresholds,
entries::score::{HIGH_SCORE_THRESHOLD, LOW_SCORE_THRESHOLD, MEDIUM_SCORE_THRESHOLD, Score},
error::Result,
helpers,
};
pub const COUNTRY_WITH_REGION_SELECTOR: &str = "US";
#[derive(Clone)]
pub struct Gateway {
pub identity: NodeIdentity,
pub moniker: String,
pub location: Option<Location>,
pub ipr_address: Option<IpPacketRouterAddress>,
pub authenticator_address: Option<AuthAddress>,
pub bridge_params: Option<BridgeInformation>,
pub last_probe: Option<Probe>,
pub ips: Vec<IpAddr>,
pub host: Option<String>,
pub clients_ws_port: Option<u16>,
pub clients_wss_port: Option<u16>,
pub mixnet_performance: Option<Percent>,
pub mixnet_score: Option<Score>,
pub wg_performance: Option<Performance>,
pub version: Option<String>,
}
impl fmt::Debug for Gateway {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Gateway")
.field("identity", &self.identity.to_base58_string())
.field("location", &self.location)
.field("ipr_address", &self.ipr_address)
.field("authenticator_address", &self.authenticator_address)
.field("last_probe", &self.last_probe)
.field("host", &self.host)
.field("clients_ws_port", &self.clients_ws_port)
.field("clients_wss_port", &self.clients_wss_port)
.field("mixnet_performance", &self.mixnet_performance)
.field("mixnet_score", &self.mixnet_score)
.field("wg_performance", &self.wg_performance)
.field("version", &self.version)
.finish()
}
}
impl Gateway {
pub fn try_from_node_description(
node_description: NymNodeDescription,
current_key_rotation: KeyRotationId,
) -> Result<Self> {
let identity = node_description.description.host_information.keys.ed25519;
let location = node_description
.description
.auxiliary_details
.location
.map(|l| Location {
two_letter_iso_country_code: l.alpha2.to_string(),
..Default::default()
});
let ipr_address = node_description
.description
.ip_packet_router
.as_ref()
.and_then(|ipr| {
IpPacketRouterAddress::try_from_base58_string(&ipr.address)
.inspect_err(|err| error!("Failed to parse IPR address: {err}"))
.ok()
});
let authenticator_address = node_description
.description
.authenticator
.as_ref()
.and_then(|a| {
AuthAddress::try_from_base58_string(&a.address)
.inspect_err(|err| error!("Failed to parse authenticator address: {err}"))
.ok()
});
let version = Some(node_description.version().to_string());
let role = if node_description.description.declared_role.entry {
nym_validator_client::nym_nodes::NodeRole::EntryGateway
} else if node_description.description.declared_role.exit_ipr
|| node_description.description.declared_role.exit_nr
{
nym_validator_client::nym_nodes::NodeRole::ExitGateway
} else {
nym_validator_client::nym_nodes::NodeRole::Inactive
};
let gateway = RoutingNode::try_from(&node_description.to_skimmed_node(
current_key_rotation,
role,
Default::default(),
))
.map_err(|_| Error::MalformedGateway)?;
let host = gateway.ws_entry_address(false);
let entry_info = &gateway.entry;
let clients_ws_port = entry_info.as_ref().map(|g| g.clients_ws_port);
let clients_wss_port = entry_info.as_ref().and_then(|g| g.clients_wss_port);
let ips = node_description.description.host_information.ip_address;
Ok(Gateway {
identity,
moniker: String::new(),
location,
ipr_address,
authenticator_address,
bridge_params: None,
last_probe: None,
ips,
host,
clients_ws_port,
clients_wss_port,
mixnet_performance: None,
mixnet_score: None,
wg_performance: None,
version,
})
}
pub fn identity(&self) -> NodeIdentity {
self.identity
}
pub fn two_letter_iso_country_code(&self) -> Option<&str> {
self.location
.as_ref()
.map(|l| l.two_letter_iso_country_code.as_str())
}
pub fn is_in_country(&self, two_letter_iso_country_code: &str) -> bool {
self.location
.as_ref()
.map(|loc| loc.two_letter_iso_country_code == two_letter_iso_country_code)
.unwrap_or(false)
}
pub fn region(&self) -> Option<&str> {
self.location.as_ref().map(|l| l.region.as_str())
}
pub fn is_in_region(&self, region: &str) -> bool {
self.location
.as_ref()
.map(|loc| loc.region == region)
.unwrap_or(false)
}
pub fn is_residential_asn(&self) -> bool {
self.location
.as_ref()
.and_then(|loc| loc.asn.as_ref())
.map(|asn| asn.kind == AsnKind::Residential)
.unwrap_or(false)
}
pub fn is_exit_node(&self) -> bool {
self.ipr_address.is_some()
}
pub fn is_vpn_node(&self) -> bool {
self.authenticator_address.is_some()
}
pub fn host(&self) -> Option<&String> {
self.host.as_ref()
}
pub fn lookup_ip(&self) -> Option<IpAddr> {
self.ips.first().copied()
}
pub fn split_ips(&self) -> (Vec<Ipv4Addr>, Vec<Ipv6Addr>) {
helpers::split_ips(self.ips.clone())
}
pub fn clients_address_no_tls(&self) -> Option<String> {
match (&self.host, &self.clients_ws_port) {
(Some(host), Some(port)) => Some(format!("ws://{host}:{port}")),
_ => None,
}
}
pub fn clients_address_tls(&self) -> Option<String> {
match (&self.host, &self.clients_wss_port) {
(Some(host), Some(port)) => Some(format!("wss://{host}:{port}")),
_ => None,
}
}
pub fn update_to_new_thresholds(&mut self, mix_thresholds: Option<ScoreThresholds>) {
if let (Some(mix_thresholds), Some(score)) = (mix_thresholds, self.mixnet_score.as_mut()) {
score.update_to_new_thresholds(mix_thresholds);
}
}
pub fn meets_score(&self, gw_type: Option<GatewayType>, min_score: ScoreValue) -> bool {
match gw_type {
Some(GatewayType::MixnetEntry) | Some(GatewayType::MixnetExit) => self
.mixnet_performance
.is_some_and(|p| p.round_to_integer() >= min_score.threshold()),
Some(GatewayType::Wg) => self
.wg_performance
.as_ref()
.is_some_and(|p| p.score >= min_score),
None => false,
}
}
/// Tests whether the gateway matches a specific filter.
pub fn matches_filter(&self, gw_type: Option<GatewayType>, filter: &GatewayFilter) -> bool {
match filter {
GatewayFilter::MinScore(score) => self.meets_score(gw_type, *score),
GatewayFilter::Country(code) => self.is_in_country(code),
GatewayFilter::Region(region) => self.is_in_region(region),
GatewayFilter::Residential => self.is_residential_asn(),
GatewayFilter::Exit => self.is_exit_node(),
GatewayFilter::Vpn => self.is_vpn_node(),
}
}
/// Tests whether the gateway matches all of the filters.
pub fn matches_all_filters(
&self,
gw_type: Option<GatewayType>,
filters: &[GatewayFilter],
) -> bool {
filters
.iter()
.all(|filter| self.matches_filter(gw_type, filter))
}
pub fn get_bridge_params(&self) -> Option<BridgeParameters> {
if let Some(all_params) = &self.bridge_params {
all_params.transports.first().cloned()
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AsnKind {
Residential,
Other,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Asn {
pub asn: String,
pub name: String,
pub kind: AsnKind,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Location {
pub two_letter_iso_country_code: String,
pub latitude: f64,
pub longitude: f64,
pub city: String,
pub region: String,
pub asn: Option<Asn>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScoreValue {
Offline,
Low,
Medium,
High,
}
impl ScoreValue {
fn priority(&self) -> u8 {
match self {
ScoreValue::Offline => 0,
ScoreValue::Low => 1,
ScoreValue::Medium => 2,
ScoreValue::High => 3,
}
}
pub fn threshold(&self) -> u8 {
match self {
ScoreValue::Offline => 0,
ScoreValue::Low => LOW_SCORE_THRESHOLD,
ScoreValue::Medium => MEDIUM_SCORE_THRESHOLD,
ScoreValue::High => HIGH_SCORE_THRESHOLD,
}
}
}
impl PartialOrd for ScoreValue {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.priority().cmp(&other.priority()))
}
}
impl FromStr for ScoreValue {
type Err = crate::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"offline" => Ok(ScoreValue::Offline),
"low" => Ok(ScoreValue::Low),
"medium" => Ok(ScoreValue::Medium),
"high" => Ok(ScoreValue::High),
_ => Err(crate::Error::InvalidScoreValue(s.to_string())),
}
}
}
impl Display for ScoreValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ScoreValue::Offline => "Offline",
ScoreValue::Low => "Low",
ScoreValue::Medium => "Medium",
ScoreValue::High => "High",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Performance {
pub last_updated_utc: String,
pub score: ScoreValue,
pub load: ScoreValue,
pub uptime_percentage_last_24_hours: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Probe {
pub last_updated_utc: String,
pub outcome: ProbeOutcome,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProbeOutcome {
pub as_entry: Entry,
pub as_exit: Option<Exit>,
pub wg: Option<WgProbeResults>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Entry {
pub can_connect: bool,
pub can_route: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Exit {
pub can_connect: bool,
pub can_route_ip_v4: bool,
pub can_route_ip_external_v4: bool,
pub can_route_ip_v6: bool,
pub can_route_ip_external_v6: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WgProbeResults {
pub can_register: bool,
pub can_handshake: bool,
pub can_resolve_dns: bool,
pub ping_hosts_performance: f32,
pub ping_ips_performance: f32,
}
impl From<helpers::AsnKind> for AsnKind {
fn from(value: helpers::AsnKind) -> Self {
match value {
helpers::AsnKind::Residential => AsnKind::Residential,
helpers::AsnKind::Other => AsnKind::Other,
}
}
}
impl From<helpers::Asn> for Asn {
fn from(location: helpers::Asn) -> Self {
Asn {
asn: location.asn,
name: location.name,
kind: location.kind.into(),
}
}
}
impl From<helpers::Location> for Location {
fn from(location: helpers::Location) -> Self {
Location {
two_letter_iso_country_code: location.two_letter_iso_country_code,
latitude: location.latitude,
longitude: location.longitude,
city: location.city,
region: location.region,
asn: location.asn.map(Into::into),
}
}
}
// impl From<nym_vpn_api_client::response::ScoreValue> for ScoreValue {
// fn from(value: nym_vpn_api_client::response::ScoreValue) -> Self {
// match value {
// nym_vpn_api_client::response::ScoreValue::Offline => ScoreValue::Offline,
// nym_vpn_api_client::response::ScoreValue::Low => ScoreValue::Low,
// nym_vpn_api_client::response::ScoreValue::Medium => ScoreValue::Medium,
// nym_vpn_api_client::response::ScoreValue::High => ScoreValue::High,
// }
// }
// }
// impl From<nym_vpn_api_client::response::DVpnGatewayPerformance> for Performance {
// fn from(value: nym_vpn_api_client::response::DVpnGatewayPerformance) -> Self {
// Performance {
// last_updated_utc: value.last_updated_utc,
// score: value.score.into(),
// load: value.load.into(),
// uptime_percentage_last_24_hours: value.uptime_percentage_last_24_hours,
// }
// }
// }
// impl From<nym_vpn_api_client::response::Probe> for Probe {
// fn from(probe: nym_vpn_api_client::response::Probe) -> Self {
// Probe {
// last_updated_utc: probe.last_updated_utc,
// outcome: ProbeOutcome::from(probe.outcome),
// }
// }
// }
impl From<Percent> for Score {
fn from(percent: Percent) -> Self {
let rounded_percent = percent.round_to_integer();
if rounded_percent >= HIGH_SCORE_THRESHOLD {
Score::High(rounded_percent)
} else if rounded_percent >= MEDIUM_SCORE_THRESHOLD {
Score::Medium(rounded_percent)
} else if rounded_percent > LOW_SCORE_THRESHOLD {
Score::Low(rounded_percent)
} else {
Score::None
}
}
}
// impl From<nym_vpn_api_client::response::ProbeOutcome> for ProbeOutcome {
// fn from(outcome: nym_vpn_api_client::response::ProbeOutcome) -> Self {
// ProbeOutcome {
// as_entry: Entry::from(outcome.as_entry),
// as_exit: outcome.as_exit.map(Exit::from),
// wg: outcome.wg.map(WgProbeResults::from),
// }
// }
// }
// impl From<nym_vpn_api_client::response::Entry> for Entry {
// fn from(entry: nym_vpn_api_client::response::Entry) -> Self {
// Entry {
// can_connect: entry.can_connect,
// can_route: entry.can_route,
// }
// }
// }
// impl From<nym_vpn_api_client::response::Exit> for Exit {
// fn from(exit: nym_vpn_api_client::response::Exit) -> Self {
// Exit {
// can_connect: exit.can_connect,
// can_route_ip_v4: exit.can_route_ip_v4,
// can_route_ip_external_v4: exit.can_route_ip_external_v4,
// can_route_ip_v6: exit.can_route_ip_v6,
// can_route_ip_external_v6: exit.can_route_ip_external_v6,
// }
// }
// }
// impl From<nym_vpn_api_client::response::WgProbeResults> for WgProbeResults {
// fn from(results: nym_vpn_api_client::response::WgProbeResults) -> Self {
// WgProbeResults {
// can_register: results.can_register,
// can_handshake: results.can_handshake,
// can_resolve_dns: results.can_resolve_dns,
// ping_hosts_performance: results.ping_hosts_performance,
// ping_ips_performance: results.ping_ips_performance,
// }
// }
// }
// impl TryFrom<nym_vpn_api_client::response::NymDirectoryGateway> for Gateway {
// type Error = Error;
// fn try_from(gateway: nym_vpn_api_client::response::NymDirectoryGateway) -> Result<Self> {
// let identity =
// NodeIdentity::from_base58_string(&gateway.identity_key).map_err(|source| {
// Error::NodeIdentityFormattingError {
// identity: gateway.identity_key,
// source,
// }
// })?;
// let ipr_address = gateway
// .ip_packet_router
// .and_then(|ipr| IpPacketRouterAddress::try_from_base58_string(&ipr.address).ok());
// let authenticator_address = gateway
// .authenticator
// .and_then(|auth| AuthAddress::try_from_base58_string(&auth.address).ok());
// let hostname = gateway.entry.hostname;
// let first_ip_address = gateway
// .ip_addresses
// .first()
// .cloned()
// .map(|ip| ip.to_string());
// let host = hostname.or(first_ip_address);
// Ok(Gateway {
// identity,
// moniker: gateway.name,
// location: Some(gateway.location.into()),
// ipr_address,
// authenticator_address,
// bridge_params: gateway.bridges,
// last_probe: gateway.last_probe.map(Probe::from),
// ips: gateway.ip_addresses,
// host,
// clients_ws_port: Some(gateway.entry.ws_port),
// clients_wss_port: gateway.entry.wss_port,
// mixnet_performance: Some(gateway.performance),
// mixnet_score: Some(Score::from(gateway.performance)),
// wg_performance: gateway.performance_v2.map(Performance::from),
// version: gateway.build_information.map(|info| info.build_version),
// })
// }
// }
pub type NymNodeList = GatewayList;
#[derive(Debug, Clone)]
pub struct GatewayList {
/// If None, then the list contains mixed types.
gw_type: Option<GatewayType>,
gateways: Vec<Gateway>,
}
impl GatewayList {
pub fn new(gw_type: Option<GatewayType>, gateways: Vec<Gateway>) -> Self {
GatewayList { gw_type, gateways }
}
// Returns a list of all locations of the gateways, including duplicates
fn all_locations(&self) -> impl Iterator<Item = &Location> {
self.gateways
.iter()
.filter_map(|gateway| gateway.location.as_ref())
}
pub fn all_countries(&self) -> Vec<Country> {
self.all_locations()
.cloned()
.map(Country::from)
.unique()
.collect()
}
pub fn all_iso_codes(&self) -> Vec<String> {
self.all_countries()
.into_iter()
.map(|country| country.iso_code().to_string())
.collect()
}
pub fn filter(&self, filters: &[GatewayFilter]) -> Vec<Gateway> {
self.gateways
.iter()
.filter(|gateway| gateway.matches_all_filters(self.gw_type, filters))
.cloned()
.collect()
}
pub fn node_with_identity(&self, identity: &NodeIdentity) -> Option<&Gateway> {
// Not using self.filter() here as find() will stop at the first match
self.gateways
.iter()
.find(|node| &node.identity() == identity)
}
pub fn gateway_with_identity(&self, identity: &NodeIdentity) -> Option<&Gateway> {
self.node_with_identity(identity)
}
pub fn choose_random(&self, filters: &[GatewayFilter]) -> Option<Gateway> {
self.filter(filters)
.into_iter()
.choose(&mut rand::thread_rng())
}
pub fn remove_gateway(&mut self, entry_gateway: &Gateway) {
self.gateways
.retain(|gateway| gateway.identity() != entry_gateway.identity());
}
pub fn gw_type(&self) -> Option<GatewayType> {
self.gw_type
}
pub fn len(&self) -> usize {
self.gateways.len()
}
pub fn is_empty(&self) -> bool {
self.gateways.is_empty()
}
pub fn into_exit_gateways(self) -> GatewayList {
Self::new(self.gw_type, self.filter(&[GatewayFilter::Exit]))
}
pub fn into_vpn_gateways(self) -> GatewayList {
Self::new(self.gw_type, self.filter(&[GatewayFilter::Vpn]))
}
pub fn into_inner(self) -> Vec<Gateway> {
self.gateways
}
}
impl IntoIterator for GatewayList {
type Item = Gateway;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.gateways.into_iter()
}
}
impl nym_client_core::init::helpers::ConnectableGateway for Gateway {
#[allow(unconditional_recursion)]
fn node_id(&self) -> NodeId {
self.node_id()
}
fn identity(&self) -> NodeIdentity {
self.identity()
}
fn clients_address(&self, _prefer_ipv6: bool) -> Option<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.
Some(
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()
}
}
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, strum::EnumIter)]
pub enum GatewayType {
MixnetEntry,
MixnetExit,
Wg,
}
impl fmt::Display for GatewayType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GatewayType::MixnetEntry => write!(f, "mixnet entry"),
GatewayType::MixnetExit => write!(f, "mixnet exit"),
GatewayType::Wg => write!(f, "vpn"),
}
}
}
impl From<helpers::GatewayType> for GatewayType {
fn from(gateway_type: helpers::GatewayType) -> Self {
match gateway_type {
helpers::GatewayType::MixnetEntry => GatewayType::MixnetEntry,
helpers::GatewayType::MixnetExit => GatewayType::MixnetExit,
helpers::GatewayType::Wg => GatewayType::Wg,
}
}
}
impl From<GatewayType> for helpers::GatewayType {
fn from(gateway_type: GatewayType) -> Self {
match gateway_type {
GatewayType::MixnetEntry => helpers::GatewayType::MixnetEntry,
GatewayType::MixnetExit => helpers::GatewayType::MixnetExit,
GatewayType::Wg => helpers::GatewayType::Wg,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum GatewayFilter {
MinScore(ScoreValue), // Mixnet or Wg score
Country(String), // Two-letter ISO country code
Region(String), // Region name
Residential, // Has a residential ASN
Exit, // Has an IPR address
Vpn, // Has an authenticator address
}
@@ -1,56 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_validator_client::models::NymNodeData;
use crate::{Error, error::Result};
#[derive(Debug, Copy, Clone)]
pub struct IpPacketRouterAddress(Recipient);
impl IpPacketRouterAddress {
pub fn try_from_base58_string(ip_packet_router_nym_address: &str) -> Result<Self> {
Ok(Self(
Recipient::try_from_base58_string(ip_packet_router_nym_address).map_err(|source| {
Error::RecipientFormattingError {
address: ip_packet_router_nym_address.to_string(),
source,
}
})?,
))
}
pub fn try_from_described_gateway(gateway: &NymNodeData) -> Result<Self> {
let address = gateway
.clone()
.ip_packet_router
.map(|ipr| ipr.address)
.ok_or(Error::MissingIpPacketRouterAddress)?;
Ok(Self(Recipient::try_from_base58_string(&address).map_err(
|source| Error::RecipientFormattingError { address, source },
)?))
}
pub fn gateway(&self) -> NodeIdentity {
self.0.gateway()
}
}
impl std::fmt::Display for IpPacketRouterAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Recipient> for IpPacketRouterAddress {
fn from(recipient: Recipient) -> Self {
Self(recipient)
}
}
impl From<IpPacketRouterAddress> for Recipient {
fn from(ipr_address: IpPacketRouterAddress) -> Self {
ipr_address.0
}
}
-7
View File
@@ -1,7 +0,0 @@
pub(crate) mod auth_addresses;
pub(crate) mod country;
pub(crate) mod entry_point;
pub(crate) mod exit_point;
pub(crate) mod gateway;
pub(crate) mod ipr_addresses;
pub(crate) mod score;
@@ -1,34 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::helpers::ScoreThresholds;
pub(crate) const HIGH_SCORE_THRESHOLD: u8 = 80;
pub(crate) const MEDIUM_SCORE_THRESHOLD: u8 = 60;
pub(crate) const LOW_SCORE_THRESHOLD: u8 = 0;
#[derive(Debug, Clone)]
pub enum Score {
High(u8),
Medium(u8),
Low(u8),
None,
}
impl Score {
pub fn update_to_new_thresholds(&mut self, thresholds: ScoreThresholds) {
let score = match self {
Score::None => return,
Score::High(score) | Score::Medium(score) | Score::Low(score) => *score,
};
*self = if score > thresholds.high {
Score::High(score)
} else if score > thresholds.medium {
Score::Medium(score)
} else if score > thresholds.low {
Score::Low(score)
} else {
Score::None
};
}
}
-123
View File
@@ -1,123 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_http_api_client::HttpClientError;
use nym_validator_client::nym_api::error::NymAPIError;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("identity not formatted correctly: {identity}")]
NodeIdentityFormattingError {
identity: String,
source: nym_crypto::asymmetric::ed25519::Ed25519RecoveryError,
},
#[error("recipient is not formatted correctly: {address}")]
RecipientFormattingError {
address: String,
source: nym_sphinx::addressing::clients::RecipientFormattingError,
},
#[error(transparent)]
ValidatorClientError(#[from] nym_validator_client::ValidatorClientError),
#[error("failed to resolve gateway hostname: {hostname}")]
FailedToDnsResolveGateway {
hostname: String,
source: nym_http_api_client::HickoryDnsError,
},
#[error("resolved hostname {0} but no IP address found")]
ResolvedHostnameButNoIp(String),
#[error("timed out while attempting to resolve hostname: {hostname}")]
HostnameResolutionTimeout { hostname: String },
#[error("failed to lookup described gateways")]
FailedToLookupDescribedGateways(#[source] nym_validator_client::ValidatorClientError),
#[error("failed to lookup skimmed gateways")]
FailedToLookupSkimmedGateways(#[source] nym_validator_client::ValidatorClientError),
#[error("failed to lookup skimmed nodes")]
FailedToLookupSkimmedNodes(#[source] nym_validator_client::ValidatorClientError),
#[error("requested gateway not found in the remote list: {0}")]
RequestedGatewayIdNotFound(String),
#[error("missing ip packet router address for gateway")]
MissingIpPacketRouterAddress,
#[error("missing hostname or ip address for gateway")]
MissingHostnameOrIpAddress { gateway_identity: String },
#[error("no matching gateway found: {requested_identity}")]
NoMatchingGateway { requested_identity: String },
#[error(
"no entry gateway available for location {requested_location}, available countries: {available_countries:?}"
)]
NoMatchingEntryGatewayForLocation {
requested_location: String,
available_countries: Vec<String>,
},
#[error(
"no exit gateway available for location {requested_location}, available countries: {available_countries:?}"
)]
NoMatchingExitGatewayForLocation {
requested_location: String,
available_countries: Vec<String>,
},
#[error("no matching gateway found after selecting low latency: {requested_identity}")]
NoMatchingGatewayAfterSelectingLowLatency { requested_identity: String },
#[error("failed to select gateway randomly")]
FailedToSelectGatewayRandomly,
#[error("gateway {0} doesn't have a description available")]
NoGatewayDescriptionAvailable(String),
#[error("failed to lookup gateway ip for gateway {0}")]
FailedToLookupIp(String),
#[error("the url {url} doesn't parse to a host and/or a port: {reason}")]
UrlError { url: url::Url, reason: String },
#[error("the provided gateway information is malformed")]
MalformedGateway,
#[error("no connectivity")]
Offline,
#[error("HTTP client error: {0}")]
HttpClient(#[from] Box<HttpClientError>),
#[error("Nym API error: {source}")]
NymApi { source: Box<NymAPIError> },
#[error("operation cancelled")]
Cancelled,
#[error("invalid score value: {0}. Valid values are: offline, low, medium, high")]
InvalidScoreValue(String),
#[error("ips vec empty")]
NoIpsAvailable,
}
impl Error {
/// Returns true when no gateways matching the search criteria could be found, except when the gateway is constrained to identity
pub fn is_unmatched_non_specific_gateway(&self) -> bool {
matches!(
self,
Error::NoMatchingEntryGatewayForLocation { .. }
| Error::NoMatchingExitGatewayForLocation { .. }
| Error::FailedToSelectGatewayRandomly
)
}
}
// Result type based on our error type
pub type Result<T> = std::result::Result<T, Error>;
-345
View File
@@ -1,345 +0,0 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
net::{IpAddr, SocketAddr},
};
use crate::{
Error, GatewayMinPerformance, ScoreThresholds,
entries::gateway::{Gateway, GatewayList, GatewayType, NymNodeList},
error::Result,
};
use nym_contracts_common::Percent;
use nym_http_api_client::UserAgent;
use nym_validator_client::{
models::NymNodeDescription, nym_api::NymApiClientExt, nym_nodes::SkimmedNodesWithMetadata,
};
use rand::{prelude::SliceRandom, thread_rng};
use tracing::{debug, error, warn};
use url::Url;
#[derive(Clone, Debug)]
pub struct Config {
pub nyxd_url: Url,
pub api_url: Url,
pub min_gateway_performance: Option<GatewayMinPerformance>,
pub mix_score_thresholds: Option<ScoreThresholds>,
pub wg_score_thresholds: Option<ScoreThresholds>,
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "nyxd_url: {}, api_url: {}", self.nyxd_url, self.api_url,)
}
}
impl Config {
pub fn nyxd_url(&self) -> &Url {
&self.nyxd_url
}
pub fn with_custom_nyxd_url(mut self, nyxd_url: Url) -> Self {
self.nyxd_url = nyxd_url;
self
}
pub fn api_url(&self) -> &Url {
&self.api_url
}
pub fn with_custom_api_url(mut self, api_url: Url) -> Self {
self.api_url = api_url;
self
}
pub fn with_min_gateway_performance(
mut self,
min_gateway_performance: GatewayMinPerformance,
) -> Self {
self.min_gateway_performance = Some(min_gateway_performance);
self
}
}
#[derive(Clone)]
pub struct GatewayClient {
api_client: nym_http_api_client::Client,
nyxd_url: Url,
min_gateway_performance: Option<GatewayMinPerformance>,
mix_score_thresholds: Option<ScoreThresholds>,
wg_score_thresholds: Option<ScoreThresholds>,
}
impl GatewayClient {
pub fn new(config: Config, user_agent: UserAgent) -> Result<Self> {
Self::new_with_resolver_overrides(config, user_agent, None)
}
pub fn new_with_resolver_overrides(
config: Config,
user_agent: UserAgent,
_static_nym_api_ip_addresses: Option<&[SocketAddr]>,
) -> Result<Self> {
let api_client = nym_http_api_client::Client::builder(config.api_url.clone())
.map_err(|e| Error::FailedToLookupDescribedGateways(e.into()))?
.with_user_agent(user_agent.clone())
.build()
.map_err(|e| Error::FailedToLookupDescribedGateways(e.into()))?;
Ok(GatewayClient {
api_client,
nyxd_url: config.nyxd_url,
min_gateway_performance: config.min_gateway_performance,
mix_score_thresholds: config.mix_score_thresholds,
wg_score_thresholds: config.wg_score_thresholds,
})
}
pub fn from_network_with_resolver_overrides(
config: Config,
network_details: &nym_network_defaults::NymNetworkDetails,
user_agent: UserAgent,
_static_nym_api_ip_addresses: Option<&[SocketAddr]>,
) -> Result<Self> {
#[allow(deprecated)]
let api_client = nym_http_api_client::ClientBuilder::from_network(network_details)
.map_err(Box::new)?
.with_user_agent(user_agent.clone())
.build()
.map_err(Box::new)?;
Ok(GatewayClient {
api_client,
nyxd_url: config.nyxd_url,
min_gateway_performance: config.min_gateway_performance,
mix_score_thresholds: config.mix_score_thresholds,
wg_score_thresholds: config.wg_score_thresholds,
})
}
/// Return the config of this instance.
pub fn get_config(&self) -> Config {
Config {
api_url: self.api_client.api_url().clone(),
nyxd_url: self.nyxd_url.clone(),
min_gateway_performance: self.min_gateway_performance,
mix_score_thresholds: self.mix_score_thresholds,
wg_score_thresholds: self.wg_score_thresholds,
}
}
pub fn mixnet_min_performance(&self) -> Option<Percent> {
self.min_gateway_performance
.as_ref()
.and_then(|min_performance| min_performance.mixnet_min_performance)
}
pub fn vpn_min_performance(&self) -> Option<Percent> {
self.min_gateway_performance
.as_ref()
.and_then(|min_performance| min_performance.vpn_min_performance)
}
async fn lookup_described_nodes(&self) -> Result<Vec<NymNodeDescription>> {
debug!("Fetching all described nodes from nym-api...");
self.api_client
.get_all_described_nodes()
.await
.map_err(|e| Error::NymApi {
source: Box::new(e),
})
}
async fn lookup_skimmed_gateways(&self) -> Result<SkimmedNodesWithMetadata> {
debug!("Fetching skimmed entry assigned nodes from nym-api...");
self.api_client
.get_all_basic_entry_assigned_nodes_with_metadata()
.await
.map_err(|e| Error::NymApi {
source: Box::new(e),
})
}
async fn lookup_skimmed_nodes(&self) -> Result<SkimmedNodesWithMetadata> {
debug!("Fetching skimmed entry assigned nodes from nym-api...");
self.api_client
.get_all_basic_nodes_with_metadata()
.await
.map_err(|e| Error::NymApi {
source: Box::new(e),
})
}
pub async fn lookup_gateway_ip_from_nym_api(&self, gateway_identity: &str) -> Result<IpAddr> {
debug!("Fetching gateway ip from nym-api...");
let mut ips = self
.api_client
.get_all_described_nodes()
.await
.map_err(|e| Error::NymApi {
source: Box::new(e),
})?
.iter()
.find_map(|node| {
if node
.description
.host_information
.keys
.ed25519
.to_base58_string()
== gateway_identity
{
Some(node.description.host_information.ip_address.clone())
} else {
None
}
})
.ok_or(Error::RequestedGatewayIdNotFound(
gateway_identity.to_string(),
))?;
if ips.is_empty() {
// nym-api should forbid this from ever happening, but we don't want to accidentally panic
// if this assumption fails
warn!("somehow {gateway_identity} hasn't provided any ip addresses!");
return Err(Error::RequestedGatewayIdNotFound(
gateway_identity.to_string(),
));
}
debug!("found the following ips for {gateway_identity}: {ips:?}");
if ips.len() == 1 {
// SAFETY: the vector is not empty, so unwrap is fine
#[allow(clippy::unwrap_used)]
Ok(ips.pop().unwrap())
} else {
// chose a random one if there's more than one
// SAFETY: the vector is not empty, so unwrap is fine
let mut rng = thread_rng();
let ip = ips.choose(&mut rng).ok_or_else(|| Error::NoIpsAvailable)?;
Ok(*ip)
}
}
pub async fn lookup_all_gateways_from_nym_api(&self) -> Result<GatewayList> {
let skimmed_gateways = self.lookup_skimmed_gateways().await?;
let key_rotation_id = skimmed_gateways.metadata.rotation_id;
let mut gateways = self
.lookup_described_nodes()
.await?
.into_iter()
.filter(|node| node.description.declared_role.entry)
.filter_map(|gw| {
Gateway::try_from_node_description(gw, key_rotation_id)
.inspect_err(|err| error!("Failed to parse gateway: {err}"))
.ok()
})
.collect::<Vec<_>>();
append_performance(&mut gateways, skimmed_gateways.nodes);
filter_on_mixnet_min_performance(&mut gateways, &self.min_gateway_performance);
update_thresholds(&mut gateways, self.mix_score_thresholds);
Ok(GatewayList::new(None, gateways))
}
pub async fn lookup_all_nymnodes(&self) -> Result<NymNodeList> {
let skimmed_nodes = self.lookup_skimmed_nodes().await?;
let key_rotation_id = skimmed_nodes.metadata.rotation_id;
let mut nodes = self
.lookup_described_nodes()
.await?
.into_iter()
.filter_map(|gw| {
Gateway::try_from_node_description(gw, key_rotation_id)
.inspect_err(|err| error!("Failed to parse node: {err}"))
.ok()
})
.collect::<Vec<_>>();
append_performance(&mut nodes, skimmed_nodes.nodes);
filter_on_mixnet_min_performance(&mut nodes, &self.min_gateway_performance);
update_thresholds(&mut nodes, self.mix_score_thresholds);
Ok(GatewayList::new(None, nodes))
}
pub async fn lookup_gateways_from_nym_api(&self, gw_type: GatewayType) -> Result<GatewayList> {
match gw_type {
GatewayType::MixnetEntry => self.lookup_entry_gateways_from_nym_api().await,
GatewayType::MixnetExit => self.lookup_exit_gateways_from_nym_api().await,
GatewayType::Wg => self.lookup_vpn_gateways_from_nym_api().await,
}
}
async fn lookup_entry_gateways_from_nym_api(&self) -> Result<GatewayList> {
self.lookup_all_gateways_from_nym_api().await
}
async fn lookup_exit_gateways_from_nym_api(&self) -> Result<GatewayList> {
self.lookup_all_gateways_from_nym_api()
.await
.map(GatewayList::into_exit_gateways)
}
async fn lookup_vpn_gateways_from_nym_api(&self) -> Result<GatewayList> {
self.lookup_all_gateways_from_nym_api()
.await
.map(GatewayList::into_vpn_gateways)
}
pub async fn lookup_gateway_ip(&self, gateway_identity: &str) -> Result<IpAddr> {
self.lookup_gateway_ip_from_nym_api(gateway_identity).await
}
pub async fn lookup_all_gateways(&self) -> Result<GatewayList> {
self.lookup_all_gateways_from_nym_api().await
}
pub async fn lookup_gateways(&self, gw_type: GatewayType) -> Result<GatewayList> {
self.lookup_gateways_from_nym_api(gw_type).await
}
}
fn append_performance(
gateways: &mut [Gateway],
basic_gw: Vec<nym_validator_client::nym_nodes::SkimmedNode>,
) {
debug!("Appending mixnet_performance to gateways");
for gateway in gateways.iter_mut() {
if let Some(basic_gw) = basic_gw
.iter()
.find(|bgw| bgw.ed25519_identity_pubkey == gateway.identity())
{
gateway.mixnet_performance = Some(basic_gw.performance);
} else {
tracing::warn!(
"Failed to append mixnet_performance, node {} not found among the skimmed nodes",
gateway.identity()
);
}
}
}
fn filter_on_mixnet_min_performance(
gateways: &mut Vec<Gateway>,
min_gateway_performance: &Option<GatewayMinPerformance>,
) {
if let Some(min_performance) = min_gateway_performance
&& let Some(mixnet_min_performance) = min_performance.mixnet_min_performance
{
tracing::debug!(
"Filtering gateways based on mixnet_min_performance: {:?}",
min_performance
);
gateways.retain(|gateway| {
gateway.mixnet_performance.unwrap_or_default() >= mixnet_min_performance
});
}
}
fn update_thresholds(gateways: &mut [Gateway], mix_score_thresholds: Option<ScoreThresholds>) {
for gateway in gateways.iter_mut() {
gateway.update_to_new_thresholds(mix_score_thresholds);
}
}
-110
View File
@@ -1,110 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_contracts_common::Percent;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
pub fn split_ips(ips: Vec<IpAddr>) -> (Vec<Ipv4Addr>, Vec<Ipv6Addr>) {
ips.into_iter()
.fold((vec![], vec![]), |(mut v4, mut v6), ip| {
match ip {
IpAddr::V4(ipv4_addr) => v4.push(ipv4_addr),
IpAddr::V6(ipv6_addr) => v6.push(ipv6_addr),
}
(v4, v6)
})
}
// Types copied in from nym-vpn-client/nym-vpn-core/crates/nym-vpn-api-client
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NymDirectoryCountry(String);
impl NymDirectoryCountry {
pub fn iso_code(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl From<String> for NymDirectoryCountry {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScoreThresholds {
pub high: u8,
pub medium: u8,
pub low: u8,
}
#[derive(Clone, Debug)]
pub enum GatewayType {
MixnetEntry,
MixnetExit,
Wg,
}
#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
pub struct GatewayMinPerformance {
pub mixnet_min_performance: Option<Percent>,
pub vpn_min_performance: Option<Percent>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BridgeInformation {
pub version: String,
pub transports: Vec<BridgeParameters>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "transport_type", content = "args")]
#[serde(rename_all = "snake_case")]
pub enum BridgeParameters {
QuicPlain(QuicClientOptions),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct QuicClientOptions {
/// Address describing the remote transport server. This is a vec to support multiple addresses
/// so as to support both IPv4 and IPv6. These addresses are meant to describe a single bridge
/// as the key material should not be used across multiple instances.
pub addresses: Vec<std::net::SocketAddr>,
/// Override hostname used for certificate verification
pub host: Option<String>,
/// Use identity public key to verify server self signed certificate
pub id_pubkey: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AsnKind {
Residential,
Other,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Asn {
pub asn: String,
pub name: String,
pub kind: AsnKind,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Location {
pub two_letter_iso_country_code: String,
pub latitude: f64,
pub longitude: f64,
pub city: String,
pub region: String,
pub asn: Option<Asn>,
}
-28
View File
@@ -1,28 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
mod entries;
mod error;
mod gateway_client;
mod helpers;
pub use crate::{
entries::{
auth_addresses::AuthAddress,
country::Country,
entry_point::EntryPoint,
exit_point::ExitPoint,
gateway::{
Asn, AsnKind, Entry, Exit, Gateway, GatewayFilter, GatewayList, GatewayType, Location,
Performance, Probe, ProbeOutcome, ScoreValue,
},
ipr_addresses::IpPacketRouterAddress,
score::Score,
},
error::Error,
gateway_client::{Config, GatewayClient},
helpers::{
BridgeInformation, BridgeParameters, GatewayMinPerformance, NymDirectoryCountry,
QuicClientOptions, ScoreThresholds, split_ips,
},
};
+3 -2
View File
@@ -2,6 +2,7 @@ use nym_validator_client::nyxd::error::NyxdError;
use std::path::PathBuf;
use nym_ip_packet_requests::v8::response::{ConnectFailureReason, IpPacketResponseData};
use nym_validator_client::nym_api::error::NymAPIError;
/// Top-level Error enum for the mixnet client and its relevant types.
#[derive(Debug, thiserror::Error)]
@@ -132,8 +133,8 @@ pub enum Error {
#[error("connect denied: {0:?}")]
ConnectDenied(ConnectFailureReason),
#[error("gateway directory error: {0}")]
GatewayDirectoryError(#[from] nym_gateway_directory::Error),
#[error("api directory error: {0}")]
GatewayDirectoryError(#[from] NymAPIError),
#[error("did not receive Validator endpoint details")]
NoValidatorDetailsAvailable,
@@ -7,7 +7,6 @@ pub use crate::mixnet::{
InputMessage, MixnetClient, MixnetClientSender, MixnetMessageSender, Recipient,
TransmissionLane,
};
use nym_gateway_directory::IpPacketRouterAddress;
use nym_ip_packet_requests::IpPair;
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
@@ -59,10 +58,7 @@ impl IprClientConnect {
}
}
pub async fn connect(
&mut self,
ip_packet_router_address: IpPacketRouterAddress,
) -> Result<IpPair> {
pub async fn connect(&mut self, ip_packet_router_address: Recipient) -> Result<IpPair> {
if self.connected != ConnectionState::Disconnected {
return Err(Error::AlreadyConnected);
}
@@ -83,20 +79,14 @@ impl IprClientConnect {
}
}
async fn connect_inner(
&mut self,
ip_packet_router_address: IpPacketRouterAddress,
) -> Result<IpPair> {
async fn connect_inner(&mut self, ip_packet_router_address: Recipient) -> Result<IpPair> {
let request_id = self.send_connect_request(ip_packet_router_address).await?;
debug!("Waiting for reply...");
self.listen_for_connect_response(request_id).await
}
async fn send_connect_request(
&mut self,
ip_packet_router_address: IpPacketRouterAddress,
) -> Result<u64> {
async fn send_connect_request(&mut self, ip_packet_router_address: Recipient) -> Result<u64> {
let (request, request_id) = IpPacketRequest::new_connect_request(None);
// We use 20 surbs for the connect request because typically the IPR is configured to have
@@ -7,11 +7,9 @@ use crate::ip_packet_client::{
};
use crate::UserAgent;
use crate::{mixnet::Recipient, Error};
use std::collections::HashMap;
use bytes::Bytes;
use nym_gateway_directory::{
Config as GatewayConfig, GatewayClient, GatewayType, IpPacketRouterAddress,
};
use nym_ip_packet_requests::{
v8::{
request::IpPacketRequest,
@@ -22,6 +20,9 @@ use nym_ip_packet_requests::{
use nym_sphinx::receiver::ReconstructedMessageCodec;
use futures::StreamExt;
use nym_crypto::asymmetric::ed25519;
use nym_network_defaults::ApiUrl;
use nym_validator_client::nym_api::NymApiClientExt;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
@@ -33,6 +34,13 @@ use tracing::{debug, error, info};
const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Clone)]
pub struct IprWithPerformance {
pub(crate) address: Recipient,
pub(crate) identity: ed25519::PublicKey,
pub(crate) performance: u8,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
Disconnected,
@@ -40,7 +48,7 @@ pub enum ConnectionState {
Connected,
}
fn create_gateway_client() -> Result<GatewayClient, Error> {
fn create_nym_api_client(nym_api_urls: Vec<ApiUrl>) -> Result<nym_http_api_client::Client, Error> {
// TODO do something proper with this
let user_agent = UserAgent {
application: "nym-ipr-streamer".to_string(),
@@ -49,55 +57,81 @@ fn create_gateway_client() -> Result<GatewayClient, Error> {
git_commit: "max/sdk-streamer".to_string(),
};
let mainnet_network_defaults = crate::NymNetworkDetails::default();
let api_url = mainnet_network_defaults
.endpoints
.first()
.ok_or_else(|| Error::NoValidatorDetailsAvailable)?
.api_url()
.ok_or_else(|| Error::NoValidatorAPIUrl)?;
let urls = nym_api_urls
.into_iter()
.map(|url| url.url.parse())
.collect::<Result<Vec<nym_http_api_client::Url>, _>>()
.map_err(|err| {
error!("malformed nym-api url: {err}");
Error::NoNymAPIUrl
})?;
let nyxd_url = mainnet_network_defaults
.endpoints
.first()
.ok_or_else(|| Error::NoValidatorDetailsAvailable)?
.nyxd_url();
if urls.is_empty() {
return Err(Error::NoNymAPIUrl);
}
let config = GatewayConfig {
nyxd_url,
api_url,
min_gateway_performance: None,
mix_score_thresholds: None,
wg_score_thresholds: None,
};
let client = nym_http_api_client::ClientBuilder::new_with_urls(urls)
.with_user_agent(user_agent)
.build()?;
Ok(GatewayClient::new(config, user_agent)?)
Ok(client)
}
async fn get_ipr_addr(client: GatewayClient) -> Result<IpPacketRouterAddress, Error> {
let exit_gateways = client.lookup_gateways(GatewayType::MixnetExit).await?;
info!("Found {} Exit Gateways", exit_gateways.len());
let selected_gateway = exit_gateways
async fn retrieve_exit_nodes_with_performance(
client: nym_http_api_client::Client,
) -> Result<Vec<IprWithPerformance>, Error> {
// retrieve all nym-nodes on the network
let all_nodes = client
.get_all_described_nodes()
.await?
.into_iter()
.filter(|gw| gw.ipr_address.is_some())
.max_by_key(|gw| {
gw.mixnet_performance
.map(|p| p.round_to_integer())
.unwrap_or(0)
})
.map(|described| (described.ed25519_identity_key(), described))
.collect::<HashMap<_, _>>();
// annoyingly there's no convenient way of doing this in a single query
// retrieve performance scores of all exit gateways
let exit_gateways = client
.get_all_basic_exit_assigned_nodes_with_metadata()
.await?
.nodes;
let mut described = Vec::new();
for exit in exit_gateways {
if let Some(ipr_info) = all_nodes
.get(&exit.ed25519_identity_pubkey)
.and_then(|n| n.description.ip_packet_router.clone())
{
if let Ok(parsed_address) = ipr_info.address.parse() {
described.push(IprWithPerformance {
address: parsed_address,
identity: exit.ed25519_identity_pubkey,
performance: exit.performance.round_to_integer(),
})
}
}
}
Ok(described)
}
async fn get_random_ipr(client: nym_http_api_client::Client) -> Result<Recipient, Error> {
let nodes = retrieve_exit_nodes_with_performance(client).await?;
info!("Found {} Exit Gateways", nodes.len());
// JS: I'm leaving the old behaviour here of choosing node with the highest performance,
// but I think you should reconsider making a pseudorandom selection weighted by some scaled performance
// otherwise all clients might end up choosing exactly the same node (I will leave this as PR comment when I get here : D)
let selected_gateway = nodes
.into_iter()
.max_by_key(|gw| gw.performance)
.ok_or_else(|| Error::NoGatewayAvailable)?;
let ipr_address = selected_gateway
.ipr_address
.ok_or_else(|| Error::NoIPRAvailable)?;
let ipr_address = selected_gateway.address;
info!(
"Using IPR: {} (Gateway: {}, Performance: {:?})",
ipr_address,
selected_gateway.identity(),
selected_gateway.mixnet_performance
ipr_address, selected_gateway.identity, selected_gateway.performance
);
Ok(ipr_address)
@@ -106,7 +140,7 @@ async fn get_ipr_addr(client: GatewayClient) -> Result<IpPacketRouterAddress, Er
/// Unlike the non-IPR MixStream, we do not start with a Socket and then 'connect' to a Stream; seemed too many layers of abstraction for little trade off.
pub struct IpMixStream {
stream: MixStream,
ipr_address: IpPacketRouterAddress,
ipr_address: Recipient,
listener: IprListener,
allocated_ips: Option<IpPair>,
connection_state: ConnectionState,
@@ -115,8 +149,14 @@ pub struct IpMixStream {
impl IpMixStream {
// TODO be able to pass in DisconnectedMixnetClient to use as MixStream inner client.
pub async fn new() -> Result<Self, Error> {
let gw_client = create_gateway_client()?;
let ipr_address = get_ipr_addr(gw_client).await?;
// JS: I think you need some bootstrapping here, something would need to be passed to `new()`
// to indicate what endpoints to use
// again, for now I'm default to mainnet as you did before
let mainnet_network_defaults = crate::NymNetworkDetails::new_mainnet();
let api_client =
create_nym_api_client(mainnet_network_defaults.nym_api_urls.unwrap_or_default())?;
let ipr_address = get_random_ipr(api_client).await?;
let stream = MixStream::new(None, Recipient::from(ipr_address)).await;
Ok(Self {