removed dependency on nym-gateway-directory
This commit is contained in:
committed by
mfahampshire
parent
db2f3bff05
commit
abdd960b20
Generated
-26
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user