temp commit: got gateway dir dependency working, moving on to vpn-api-client

This commit is contained in:
mfahampshire
2025-07-04 13:44:18 +02:00
parent be8c1191f3
commit 4ea2c3beb3
36 changed files with 5037 additions and 13044 deletions
Generated
-13042
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -129,6 +129,7 @@ members = [
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-data-observatory",
"nym-gateway-directory",
"nym-ip-packet-client",
"nym-network-monitor",
"nym-node",
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "nym-gateway-directory"
# version.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
futures.workspace = true
itertools.workspace = true
nym-client-core = { path = "../common/client-core" }
nym-common = { git = "https://github.com/nymtech/nym-vpn-client/", rev = "c7e38aae038748f7d577a8b1ae02931fff97e868" } # nym-vpn-core 1.11.0
nym-http-api-client = { path = "../common/http-api-client" }
nym-offline-monitor = { git = "https://github.com/nymtech/nym-vpn-client/", rev = "c7e38aae038748f7d577a8b1ae02931fff97e868" }
nym-sdk = { path = "../sdk/rust/nym-sdk" }
nym-topology = { path = "../common/topology" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
# nym-vpn-api-client = { path = "../nym-vpn-api-client" } # pull in remote and see if that works? I wonder if I can sack this off
rand.workspace = true
serde.workspace = true
strum.workspace = true
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
url.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
+329
View File
@@ -0,0 +1,329 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::HashMap,
net::IpAddr,
sync::Arc,
time::{Duration, Instant},
};
use futures::{FutureExt, StreamExt, stream::FuturesUnordered};
use nym_offline_monitor::ConnectivityHandle;
pub use nym_sdk::mixnet::NodeIdentity;
use strum::IntoEnumIterator;
use tokio::sync::Mutex;
use crate::{
Config, Country, Error, Gateway, GatewayClient, GatewayList, GatewayType, error::Result,
};
#[derive(Clone)]
pub struct CachingGatewayClient {
inner: Arc<Mutex<CachingGatewayClientInner>>,
}
impl CachingGatewayClient {
pub fn new(
gateway_client: GatewayClient,
connectivity_handle: Option<ConnectivityHandle>,
) -> Self {
Self {
inner: Arc::new(Mutex::new(CachingGatewayClientInner {
gateway_client,
connectivity_handle,
cached_gateways: Default::default(),
cached_countries: Default::default(),
})),
}
}
pub async fn new_from_existing(existing_client: &CachingGatewayClient) -> Self {
let inner = existing_client.inner.lock().await;
Self {
inner: Arc::new(Mutex::new(CachingGatewayClientInner {
gateway_client: inner.gateway_client.clone(),
connectivity_handle: inner.connectivity_handle.clone(),
cached_gateways: inner.cached_gateways.clone(),
cached_countries: inner.cached_countries.clone(),
})),
}
}
pub async fn update_client(&self, new_client: GatewayClient) {
self.inner.lock().await.gateway_client = new_client;
}
pub async fn set_connectivity_handle(&self, connectivity_handle: ConnectivityHandle) {
self.inner.lock().await.connectivity_handle = Some(connectivity_handle);
}
pub async fn get_config(&self) -> Config {
self.inner.lock().await.gateway_client.get_config()
}
pub async fn refresh_all(&self) {
self.inner.lock().await.refresh_all().await
}
pub async fn force_refresh_all(&self) {
self.inner.lock().await.force_refresh_all().await
}
pub async fn lookup_gateways(&self, gw_type: GatewayType) -> Result<GatewayList> {
self.inner.lock().await.lookup_gateways(gw_type).await
}
pub async fn lookup_countries(&self, gw_type: GatewayType) -> Result<Vec<Country>> {
self.inner.lock().await.lookup_countries(gw_type).await
}
pub async fn lookup_gateway_ip(&self, gateway_identity: &str) -> Result<IpAddr> {
self.inner
.lock()
.await
.lookup_gateway_ip(gateway_identity)
.await
}
}
/// A caching client that wraps around the `GatewayClient` and caches the results of
/// `lookup_gateways` and `lookup_countries` calls.
struct CachingGatewayClientInner {
// The underlying client that actually does the work
gateway_client: GatewayClient,
// The connectivity handle to check if we are online
connectivity_handle: Option<ConnectivityHandle>,
// The cached gateways and their last updated time
cached_gateways: HashMap<GatewayType, (GatewayList, Instant)>,
// The cached countries and their last updated time
cached_countries: HashMap<GatewayType, (Vec<Country>, Instant)>,
}
enum LookupResult {
Gateways(Result<GatewayList>),
Countries(Result<Vec<Country>>),
}
impl CachingGatewayClientInner {
/// The maximum age of the cache before it is considered stale.
const MAX_CACHE_AGE: Duration = Duration::from_secs(5 * 60);
async fn check_offline(&self) -> bool {
if let Some(connectivity_handle) = &self.connectivity_handle {
if connectivity_handle.connectivity().await.is_offline() {
return true;
}
}
false
}
pub async fn refresh_all(&mut self) {
tracing::info!("Refreshing all gateways and countries");
self.refresh(
self.get_stale_gateway_list_types(),
self.get_stale_country_list_types(),
)
.await;
}
pub async fn force_refresh_all(&mut self) {
tracing::info!("Forcing refresh of all gateways and countries");
self.refresh(GatewayType::iter().collect(), GatewayType::iter().collect())
.await;
}
fn get_stale_gateway_list_types(&self) -> Vec<GatewayType> {
let mut stale_gw_types = Vec::new();
for gw_type in GatewayType::iter() {
if !self.is_gateways_current(&gw_type) {
stale_gw_types.push(gw_type.clone());
}
}
stale_gw_types
}
fn get_stale_country_list_types(&self) -> Vec<GatewayType> {
let mut stale_gw_types = Vec::new();
for gw_type in GatewayType::iter() {
if !self.is_countries_current(&gw_type) {
stale_gw_types.push(gw_type.clone());
}
}
stale_gw_types
}
async fn refresh(&mut self, gw_list_types: Vec<GatewayType>, country_types: Vec<GatewayType>) {
if self.check_offline().await {
tracing::warn!("Not refreshing gateways and countries because we are not connected");
return;
}
tracing::info!(
"Refreshing gateway lists: {gw_list_types:?}, country lists: {country_types:?}"
);
let mut tasks = FuturesUnordered::new();
for gw_type in country_types {
let client = self.gateway_client.clone();
tasks.push(
async move {
let res = client.lookup_countries(gw_type.clone()).await;
(gw_type, LookupResult::Countries(res))
}
.boxed(),
);
}
for gw_type in gw_list_types {
let client = self.gateway_client.clone();
tasks.push(
async move {
let res = client.lookup_gateways(gw_type.clone()).await;
(gw_type, LookupResult::Gateways(res))
}
.boxed(),
);
}
while let Some((gw_type, res)) = tasks.next().await {
match res {
LookupResult::Gateways(r) => match r {
Ok(ref refreshed_gateways) => {
tracing::info!("Refreshed gateways for {gw_type:?}");
self.cached_gateways.insert(
gw_type.clone(),
(refreshed_gateways.clone(), Instant::now()),
);
}
Err(err) => {
tracing::warn!("Failed to refresh gateways for {gw_type:?}: {err}");
}
},
LookupResult::Countries(r) => match r {
Ok(ref refreshed_countries) => {
tracing::info!("Refreshed countries for {gw_type:?}");
self.cached_countries.insert(
gw_type.clone(),
(refreshed_countries.clone(), Instant::now()),
);
}
Err(err) => {
tracing::warn!("Failed to refresh countries for {gw_type:?}: {err}");
}
},
}
}
}
fn is_countries_current(&self, gw_type: &GatewayType) -> bool {
if let Some((_, last_updated)) = self.cached_countries.get(gw_type) {
last_updated.elapsed() < Self::MAX_CACHE_AGE
} else {
false
}
}
fn is_gateways_current(&self, gw_type: &GatewayType) -> bool {
if let Some((_, last_updated)) = self.cached_gateways.get(gw_type) {
last_updated.elapsed() < Self::MAX_CACHE_AGE
} else {
false
}
}
async fn refresh_countries(&mut self, gw_type: GatewayType) -> Result<Vec<Country>> {
if let Some((countries, last_updated)) = self.cached_countries.get(&gw_type) {
if last_updated.elapsed() < Self::MAX_CACHE_AGE {
return Ok(countries.clone());
}
}
self.force_refresh_countries(gw_type).await
}
async fn force_refresh_countries(&mut self, gw_type: GatewayType) -> Result<Vec<Country>> {
if self.check_offline().await {
tracing::warn!("Not refreshing countries because we are not connected");
return Err(Error::Offline);
}
let refreshed_countries = self
.gateway_client
.lookup_countries(gw_type.clone())
.await?;
self.cached_countries.insert(
gw_type.clone(),
(refreshed_countries.clone(), Instant::now()),
);
Ok(refreshed_countries)
}
async fn refresh_gateways(&mut self, gw_type: GatewayType) -> Result<GatewayList> {
if let Some((gw_list, last_updated)) = self.cached_gateways.get(&gw_type) {
if last_updated.elapsed() < Self::MAX_CACHE_AGE {
return Ok(gw_list.clone());
}
}
self.force_refresh_gateways(gw_type).await
}
async fn force_refresh_gateways(&mut self, gw_type: GatewayType) -> Result<GatewayList> {
if self.check_offline().await {
tracing::warn!("Not refreshing countries because we are not connected");
return Err(Error::Offline);
}
let refreshed_gateways = self.gateway_client.lookup_gateways(gw_type.clone()).await?;
self.cached_gateways.insert(
gw_type.clone(),
(refreshed_gateways.clone(), Instant::now()),
);
Ok(refreshed_gateways)
}
async fn lookup_gateways(&mut self, gw_type: GatewayType) -> Result<GatewayList> {
let refresh_result = self.refresh_gateways(gw_type.clone()).await;
// Regardless of if we managed to refresh the cache, we return the cached gateways if they
// exist. They should be the most recent one we can muster
if let Some((gateways, _)) = self.cached_gateways.get(&gw_type) {
Ok(gateways.clone())
} else {
refresh_result
}
}
async fn lookup_countries(&mut self, gw_type: GatewayType) -> Result<Vec<Country>> {
let refresh_result = self.refresh_countries(gw_type.clone()).await;
// Regardless of if we managed to refresh the cache, we return the cached countries if they
// exist. They should be the most recent one we can muster
if let Some((countries, _)) = self.cached_countries.get(&gw_type) {
Ok(countries.clone())
} else {
refresh_result
}
}
async fn lookup_gateway_ip(&mut self, gateway_identity: &str) -> Result<IpAddr> {
// If we have a populated list of gateways, we should always be able to find the IP there.
if let Ok(identity) = NodeIdentity::from_base58_string(gateway_identity) {
for (_, (gateways, _)) in self.cached_gateways.iter() {
if let Some(ip) = gateways
.node_with_identity(&identity)
.and_then(Gateway::lookup_ip)
{
return Ok(ip);
}
}
} else {
tracing::warn!("Failed to parse gateway identity: {gateway_identity}");
}
// Fallback
tracing::warn!("Using fallback to lookup gateway IP");
self.gateway_client
.lookup_gateway_ip(gateway_identity)
.await
}
}
@@ -0,0 +1,57 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::fmt::Display;
pub use nym_sdk::mixnet::Recipient;
use crate::{Error, error::Result};
// optional, until we remove the wireguard feature flag
#[derive(Debug, Copy, Clone)]
pub struct AuthAddress(pub Option<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(Some(recipient)))
}
}
#[derive(Debug, Copy, Clone)]
pub struct AuthAddresses {
entry_addr: AuthAddress,
exit_addr: AuthAddress,
}
impl AuthAddresses {
pub fn new(entry_addr: AuthAddress, exit_addr: AuthAddress) -> Self {
AuthAddresses {
entry_addr,
exit_addr,
}
}
pub fn entry(&self) -> AuthAddress {
self.entry_addr
}
pub fn exit(&self) -> AuthAddress {
self.exit_addr
}
}
impl Display for AuthAddresses {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"entry: {:?} exit: {:?}",
self.entry_addr.0, self.exit_addr.0
)
}
}
@@ -0,0 +1,31 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::Location;
#[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<nym_vpn_api_client::response::NymDirectoryCountry> for Country {
fn from(country: nym_vpn_api_client::response::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,
}
}
}
@@ -0,0 +1,78 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::fmt::{Display, Formatter};
pub use nym_sdk::mixnet::NodeIdentity;
use serde::{Deserialize, Serialize};
use tracing::debug;
use super::gateway::{Gateway, GatewayList};
use crate::{Error, 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 location.
Location { location: 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::Location { location } => write!(f, "Location: {location}"),
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 is_location(&self) -> bool {
matches!(self, EntryPoint::Location { .. })
}
pub async fn lookup_gateway(&self, gateways: &GatewayList) -> 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::Location { location } => {
debug!("Selecting gateway by location: {}", location);
gateways
.random_gateway_located_at(location.to_string())
.ok_or_else(|| Error::NoMatchingEntryGatewayForLocation {
requested_location: location.clone(),
available_countries: gateways.all_iso_codes(),
})
}
EntryPoint::Random => {
debug!("Selecting a random gateway");
gateways
.random_gateway()
.ok_or_else(|| Error::FailedToSelectGatewayRandomly)
}
}
}
}
@@ -0,0 +1,92 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::fmt::{Display, Formatter};
pub use nym_sdk::mixnet::{NodeIdentity, Recipient};
use serde::{Deserialize, Serialize};
use super::gateway::{Gateway, GatewayList};
use crate::{Error, IpPacketRouterAddress, 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 },
// NOTE: Consider using a crate with strongly typed country codes instead of strings
Location { location: 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::Location { location } => write!(f, "Location: {location}"),
ExitPoint::Random => write!(f, "Random"),
}
}
}
impl ExitPoint {
pub fn is_location(&self) -> bool {
matches!(self, ExitPoint::Location { .. })
}
pub fn lookup_gateway(&self, gateways: &GatewayList) -> 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::Location { location } => {
tracing::debug!("Selecting gateway by location: {location}");
gateways
.random_gateway_located_at(location.to_string())
.ok_or_else(|| Error::NoMatchingExitGatewayForLocation {
requested_location: location.clone(),
available_countries: gateways.all_iso_codes(),
})
}
ExitPoint::Random => {
tracing::debug!("Selecting a random exit gateway");
gateways
.random_gateway()
.ok_or_else(|| Error::FailedToSelectGatewayRandomly)
}
}
}
}
@@ -0,0 +1,535 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use itertools::Itertools;
pub use nym_sdk::mixnet::NodeIdentity;
use nym_topology::{NodeId, RoutingNode};
use nym_vpn_api_client::types::{NaiveFloat, Percent, ScoreThresholds};
use rand::seq::IteratorRandom;
use std::{fmt, net::IpAddr};
use tracing::error;
use crate::{AuthAddress, Country, Error, IpPacketRouterAddress, error::Result};
use super::score::{HIGH_SCORE_THRESHOLD, LOW_SCORE_THRESHOLD, MEDIUM_SCORE_THRESHOLD, Score};
pub type NymNode = Gateway;
#[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 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 wg_performance: Option<Percent>,
pub wg_score: Option<Score>,
pub mixnet_score: Option<Score>,
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)
.finish()
}
}
impl Gateway {
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_two_letter_iso_country_code(&self, code: &str) -> bool {
self.two_letter_iso_country_code() == Some(code)
}
pub fn has_ipr_address(&self) -> bool {
self.ipr_address.is_some()
}
pub fn has_authenticator_address(&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 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>,
wg_thresholds: Option<ScoreThresholds>,
) {
if let (Some(mix_thresholds), Some(score)) = (mix_thresholds, self.mixnet_score.as_mut()) {
score.update_to_new_thresholds(mix_thresholds);
}
if let (Some(wg_thresholds), Some(score)) = (wg_thresholds, self.wg_score.as_mut()) {
score.update_to_new_thresholds(wg_thresholds);
}
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Location {
pub two_letter_iso_country_code: String,
pub latitude: f64,
pub longitude: f64,
}
#[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<nym_vpn_api_client::response::Location> for Location {
fn from(location: nym_vpn_api_client::response::Location) -> Self {
Location {
two_letter_iso_country_code: location.two_letter_iso_country_code,
latitude: location.latitude,
longitude: location.longitude,
}
}
}
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);
let wg_performance = gateway.last_probe.as_ref().and_then(|probe| {
probe
.outcome
.wg
.as_ref()
.and_then(|p| Percent::naive_try_from_f64(p.ping_hosts_performance as f64).ok())
});
Ok(Gateway {
identity,
moniker: gateway.name,
location: Some(gateway.location.into()),
ipr_address,
authenticator_address,
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,
wg_score: wg_performance.map(Score::from),
version: gateway.build_information.map(|info| info.build_version),
})
}
}
impl TryFrom<nym_validator_client::models::NymNodeDescription> for Gateway {
type Error = Error;
fn try_from(
node_description: nym_validator_client::models::NymNodeDescription,
) -> 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(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,
last_probe: None,
ips,
host,
clients_ws_port,
clients_wss_port,
mixnet_performance: None,
wg_performance: None,
wg_score: None,
mixnet_score: None,
version,
})
}
}
pub type NymNodeList = GatewayList;
#[derive(Debug, Clone)]
pub struct GatewayList {
gateways: Vec<Gateway>,
}
impl GatewayList {
pub fn new(gateways: Vec<Gateway>) -> Self {
GatewayList { 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 node_with_identity(&self, identity: &NodeIdentity) -> Option<&NymNode> {
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 gateways_located_at(&self, code: String) -> impl Iterator<Item = &Gateway> {
self.gateways.iter().filter(move |gateway| {
gateway
.two_letter_iso_country_code()
.is_some_and(|gw_code| gw_code == code)
})
}
pub fn random_gateway(&self) -> Option<Gateway> {
self.gateways
.iter()
.choose(&mut rand::thread_rng())
.cloned()
}
pub fn random_gateway_located_at(&self, code: String) -> Option<Gateway> {
self.gateways_located_at(code)
.choose(&mut rand::thread_rng())
.cloned()
}
pub fn remove_gateway(&mut self, entry_gateway: &Gateway) {
self.gateways
.retain(|gateway| gateway.identity() != entry_gateway.identity());
}
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 {
let gw = self
.gateways
.into_iter()
.filter(Gateway::has_ipr_address)
.collect();
Self::new(gw)
}
pub fn into_vpn_gateways(self) -> GatewayList {
let gw = self
.gateways
.into_iter()
.filter(Gateway::has_authenticator_address)
.collect();
Self::new(gw)
}
pub fn into_countries(self) -> Vec<Country> {
self.all_countries()
}
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) -> nym_sdk::mixnet::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, 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<nym_vpn_api_client::types::GatewayType> for GatewayType {
fn from(gateway_type: nym_vpn_api_client::types::GatewayType) -> Self {
match gateway_type {
nym_vpn_api_client::types::GatewayType::MixnetEntry => GatewayType::MixnetEntry,
nym_vpn_api_client::types::GatewayType::MixnetExit => GatewayType::MixnetExit,
nym_vpn_api_client::types::GatewayType::Wg => GatewayType::Wg,
}
}
}
impl From<GatewayType> for nym_vpn_api_client::types::GatewayType {
fn from(gateway_type: GatewayType) -> Self {
match gateway_type {
GatewayType::MixnetEntry => nym_vpn_api_client::types::GatewayType::MixnetEntry,
GatewayType::MixnetExit => nym_vpn_api_client::types::GatewayType::MixnetExit,
GatewayType::Wg => nym_vpn_api_client::types::GatewayType::Wg,
}
}
}
@@ -0,0 +1,56 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub use nym_sdk::mixnet::{NodeIdentity, Recipient};
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
@@ -0,0 +1,7 @@
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;
@@ -0,0 +1,34 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_vpn_api_client::types::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(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
};
}
}
+93
View File
@@ -0,0 +1,93 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("identity not formatted correctly: {identity}")]
NodeIdentityFormattingError {
identity: String,
source: nym_sdk::mixnet::ed25519::Ed25519RecoveryError,
},
#[error("recipient is not formatted correctly: {address}")]
RecipientFormattingError {
address: String,
source: nym_sdk::mixnet::RecipientFormattingError,
},
#[error(transparent)]
ValidatorClientError(#[from] nym_validator_client::ValidatorClientError),
#[error(transparent)]
VpnApiClientError(#[from] nym_vpn_api_client::VpnApiClientError),
#[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("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,
}
// Result type based on our error type
pub type Result<T> = std::result::Result<T, Error>;
+508
View File
@@ -0,0 +1,508 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
net::{IpAddr, SocketAddr},
};
pub use nym_sdk::UserAgent;
use nym_validator_client::{NymApiClient, models::NymNodeDescription, nym_nodes::SkimmedNode};
use nym_vpn_api_client::types::{GatewayMinPerformance, Percent, ScoreThresholds};
use rand::{prelude::SliceRandom, thread_rng};
use tracing::{debug, error, warn};
use url::Url;
use crate::{
Error, NymNode,
entries::{
country::Country,
gateway::{Gateway, GatewayList, GatewayType, NymNodeList},
},
error::Result,
};
#[derive(Clone, Debug)]
pub struct Config {
pub nyxd_url: Url,
pub api_url: Url,
pub nym_vpn_api_url: Option<Url>,
pub min_gateway_performance: Option<GatewayMinPerformance>,
pub mix_score_thresholds: Option<ScoreThresholds>,
pub wg_score_thresholds: Option<ScoreThresholds>,
}
fn to_string<T: fmt::Display>(value: &Option<T>) -> String {
match value {
Some(value) => value.to_string(),
None => "unset".to_string(),
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"nyxd_url: {}, api_url: {}, nym_vpn_api_url: {}",
self.nyxd_url,
self.api_url,
to_string(&self.nym_vpn_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 nym_vpn_api_url(&self) -> Option<&Url> {
self.nym_vpn_api_url.as_ref()
}
pub fn with_custom_nym_vpn_api_url(mut self, nym_vpn_api_url: Url) -> Self {
self.nym_vpn_api_url = Some(nym_vpn_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(Debug, Clone)]
pub struct ResolvedConfig {
pub nyxd_socket_addrs: Vec<SocketAddr>,
pub api_socket_addrs: Vec<SocketAddr>,
pub nym_vpn_api_socket_addrs: Option<Vec<SocketAddr>>,
}
impl ResolvedConfig {
pub fn all_socket_addrs(&self) -> Vec<SocketAddr> {
let mut socket_addrs = vec![];
socket_addrs.extend(self.nyxd_socket_addrs.iter());
socket_addrs.extend(self.api_socket_addrs.iter());
if let Some(vpn_api_socket_addrs) = &self.nym_vpn_api_socket_addrs {
socket_addrs.extend(vpn_api_socket_addrs.iter());
}
socket_addrs
}
}
#[derive(Clone)]
pub struct GatewayClient {
api_client: NymApiClient,
nym_vpn_api_client: Option<nym_vpn_api_client::VpnApiClient>,
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 = NymApiClient::new_with_user_agent(config.api_url, user_agent.clone());
let nym_vpn_api_client = config
.nym_vpn_api_url
.map(|url| {
nym_vpn_api_client::VpnApiClient::new_with_resolver_overrides(
url,
user_agent.clone(),
static_nym_api_ip_addresses,
)
})
.transpose()?;
Ok(GatewayClient {
api_client,
nym_vpn_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(),
nym_vpn_api_url: self
.nym_vpn_api_client
.as_ref()
.map(|client| client.current_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(Error::FailedToLookupDescribedGateways)
}
async fn lookup_skimmed_gateways(&self) -> Result<Vec<SkimmedNode>> {
debug!("Fetching skimmed entry assigned nodes from nym-api...");
self.api_client
.get_all_basic_entry_assigned_nodes()
.await
.map_err(Error::FailedToLookupSkimmedGateways)
}
async fn lookup_skimmed_nodes(&self) -> Result<Vec<SkimmedNode>> {
debug!("Fetching skimmed entry assigned nodes from nym-api...");
self.api_client
.get_all_basic_nodes()
.await
.map_err(Error::FailedToLookupSkimmedNodes)
}
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?
.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
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).unwrap();
Ok(*ip)
}
}
pub async fn lookup_all_gateways_from_nym_api(&self) -> Result<GatewayList> {
let mut gateways = self
.lookup_described_nodes()
.await?
.into_iter()
.filter(|node| node.description.declared_role.entry)
.filter_map(|gw| {
Gateway::try_from(gw)
.inspect_err(|err| error!("Failed to parse gateway: {err}"))
.ok()
})
.collect::<Vec<_>>();
let skimmed_gateways = self.lookup_skimmed_gateways().await?;
append_performance(&mut gateways, skimmed_gateways);
filter_on_mixnet_min_performance(&mut gateways, &self.min_gateway_performance);
Ok(GatewayList::new(gateways))
}
pub async fn lookup_all_nymnodes(&self) -> Result<NymNodeList> {
let mut nodes = self
.lookup_described_nodes()
.await?
.into_iter()
.filter_map(|gw| {
NymNode::try_from(gw)
.inspect_err(|err| error!("Failed to parse node: {err}"))
.ok()
})
.collect::<Vec<_>>();
let skimmed_nodes = self.lookup_skimmed_nodes().await?;
append_performance(&mut nodes, skimmed_nodes);
filter_on_mixnet_min_performance(&mut nodes, &self.min_gateway_performance);
Ok(GatewayList::new(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,
}
}
// This is currently the same as the set of all gateways, but it doesn't have to be.
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> {
if let Some(nym_vpn_api_client) = &self.nym_vpn_api_client {
debug!("Fetching gateway ip from nym-vpn-api...");
let gateway = nym_vpn_api_client
.get_gateways(None)
.await?
.into_iter()
.find_map(|gw| {
if gw.identity_key != gateway_identity {
None
} else {
Gateway::try_from(gw)
.inspect_err(|err| error!("Failed to parse gateway: {err}"))
.ok()
}
})
.ok_or_else(|| Error::RequestedGatewayIdNotFound(gateway_identity.to_string()))?;
gateway
.lookup_ip()
.ok_or(Error::FailedToLookupIp(gateway_identity.to_string()))
} else {
warn!("OPERATING IN FALLBACK MODE WITHOUT NYM-VPN-API!");
self.lookup_gateway_ip_from_nym_api(gateway_identity).await
}
}
pub async fn lookup_all_gateways(&self) -> Result<GatewayList> {
if let Some(nym_vpn_api_client) = &self.nym_vpn_api_client {
debug!("Fetching all gateways from nym-vpn-api...");
let gateways: Vec<_> = nym_vpn_api_client
.get_gateways(self.min_gateway_performance)
.await?
.into_iter()
.filter_map(|gw| {
Gateway::try_from(gw)
.inspect_err(|err| error!("Failed to parse gateway: {err}"))
.ok()
.map(|mut gw| {
gw.update_to_new_thresholds(
self.mix_score_thresholds,
self.wg_score_thresholds,
);
gw
})
})
.collect();
Ok(GatewayList::new(gateways))
} else {
warn!("OPERATING IN FALLBACK MODE WITHOUT NYM-VPN-API!");
self.lookup_all_gateways_from_nym_api().await
}
}
pub async fn lookup_gateways(&self, gw_type: GatewayType) -> Result<GatewayList> {
if let Some(nym_vpn_api_client) = &self.nym_vpn_api_client {
debug!("Fetching {gw_type} gateways from nym-vpn-api...");
let gateways: Vec<_> = nym_vpn_api_client
.get_gateways_by_type(gw_type.into(), self.min_gateway_performance)
.await?
.into_iter()
.filter_map(|gw| {
Gateway::try_from(gw)
.inspect_err(|err| error!("Failed to parse gateway: {err}"))
.ok()
.map(|mut gw| {
gw.update_to_new_thresholds(
self.mix_score_thresholds,
self.wg_score_thresholds,
);
gw
})
})
.collect();
Ok(GatewayList::new(gateways))
} else {
warn!("OPERATING IN FALLBACK MODE WITHOUT NYM-VPN-API!");
self.lookup_gateways_from_nym_api(gw_type).await
}
}
pub async fn lookup_countries(&self, gw_type: GatewayType) -> Result<Vec<Country>> {
if let Some(nym_vpn_api_client) = &self.nym_vpn_api_client {
debug!("Fetching entry countries from nym-vpn-api...");
Ok(nym_vpn_api_client
.get_gateway_countries_by_type(gw_type.into(), self.min_gateway_performance)
.await?
.into_iter()
.map(Country::from)
.collect())
} else {
warn!("OPERATING IN FALLBACK MODE WITHOUT NYM-VPN-API!");
self.lookup_gateways_from_nym_api(gw_type)
.await
.map(GatewayList::into_countries)
}
}
}
// Append the performance to the gateways. This is a temporary hack until the nymvpn.com endpoints
// are updated to also include this field.
fn append_performance(
gateways: &mut [Gateway],
basic_gw: Vec<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 {
if 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
});
}
}
}
#[cfg(test)]
mod test {
use nym_sdk::UserAgent;
use super::*;
fn user_agent() -> UserAgent {
UserAgent {
application: "test".to_string(),
version: "0.0.1".to_string(),
platform: "test".to_string(),
git_commit: "test".to_string(),
}
}
fn new_mainnet() -> Config {
let mainnet_network_defaults = nym_sdk::NymNetworkDetails::default();
let default_nyxd_url = mainnet_network_defaults
.endpoints
.first()
.expect("rust sdk mainnet default incorrectly configured")
.nyxd_url();
let default_api_url = mainnet_network_defaults
.endpoints
.first()
.expect("rust sdk mainnet default incorrectly configured")
.api_url()
.expect("rust sdk mainnet default api_url not parseable");
let default_nym_vpn_api_url = mainnet_network_defaults
.nym_vpn_api_url()
.expect("rust sdk mainnet default nym-vpn-api url not parseable");
Config {
nyxd_url: default_nyxd_url,
api_url: default_api_url,
nym_vpn_api_url: Some(default_nym_vpn_api_url),
min_gateway_performance: None,
mix_score_thresholds: None,
wg_score_thresholds: None,
}
}
#[tokio::test]
async fn lookup_described_gateways() {
let config = new_mainnet();
let client = GatewayClient::new(config, user_agent()).unwrap();
let gateways = client.lookup_described_nodes().await.unwrap();
assert!(!gateways.is_empty());
}
#[tokio::test]
async fn lookup_gateways_in_nym_vpn_api() {
let config = new_mainnet();
let client = GatewayClient::new(config, user_agent()).unwrap();
let gateways = client
.lookup_gateways(GatewayType::MixnetExit)
.await
.unwrap();
assert!(!gateways.is_empty());
}
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::net::{IpAddr, SocketAddr};
use nym_common::trace_err_chain;
use nym_http_api_client::HickoryDnsResolver;
use crate::{Config, Error, error::Result, gateway_client::ResolvedConfig};
async fn try_resolve_hostname(hostname: &str) -> Result<Vec<IpAddr>> {
tracing::debug!("Trying to resolve hostname: {hostname}");
let resolver = HickoryDnsResolver::default();
let addrs = resolver.resolve_str(hostname).await.map_err(|err| {
trace_err_chain!(err, "Failed to resolve gateway hostname");
Error::FailedToDnsResolveGateway {
hostname: hostname.to_string(),
source: err,
}
})?;
tracing::debug!("Resolved to: {addrs:?}");
let ips = addrs.iter().collect::<Vec<_>>();
if ips.is_empty() {
return Err(Error::ResolvedHostnameButNoIp(hostname.to_string()));
}
Ok(ips)
}
async fn url_to_socket_addr(unresolved_url: &url::Url) -> Result<Vec<SocketAddr>> {
let port = unresolved_url
.port_or_known_default()
.ok_or(Error::UrlError {
url: unresolved_url.clone(),
reason: "missing port".to_string(),
})?;
let hostname = unresolved_url.host_str().ok_or(Error::UrlError {
url: unresolved_url.clone(),
reason: "missing hostname".to_string(),
})?;
Ok(try_resolve_hostname(hostname)
.await?
.into_iter()
.map(|ip| SocketAddr::new(ip, port))
.collect())
}
pub async fn resolve_config(config: &Config) -> Result<ResolvedConfig> {
let nyxd_socket_addrs = url_to_socket_addr(config.nyxd_url()).await?;
let api_socket_addrs = url_to_socket_addr(config.api_url()).await?;
let nym_vpn_api_socket_addrs = if let Some(vpn_api_url) = config.nym_vpn_api_url() {
Some(url_to_socket_addr(vpn_api_url).await?)
} else {
None
};
Ok(ResolvedConfig {
nyxd_socket_addrs,
api_socket_addrs,
nym_vpn_api_socket_addrs,
})
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
mod caching_client;
mod entries;
mod error;
mod gateway_client;
mod helpers;
pub use nym_sdk::mixnet::{NodeIdentity, Recipient};
pub use nym_vpn_api_client::types::{GatewayMinPerformance, Percent};
pub use crate::{
caching_client::CachingGatewayClient,
entries::{
auth_addresses::{AuthAddress, AuthAddresses},
country::Country,
entry_point::EntryPoint,
exit_point::ExitPoint,
gateway::{
Entry, Exit, Gateway, GatewayList, GatewayType, Location, NymNode, Probe, ProbeOutcome,
},
ipr_addresses::IpPacketRouterAddress,
score::Score,
},
error::Error,
gateway_client::{Config, GatewayClient, ResolvedConfig},
helpers::resolve_config,
};
+3
View File
@@ -16,6 +16,9 @@ workspace = true
bincode.workspace = true
bytes.workspace = true
futures.workspace = true
nym-gateway-directory = { path = "../nym-gateway-directory" }
nym-ip-packet-requests = { path = "../common/ip-packet-requests" }
nym-sdk = {path = "../sdk/rust/nym-sdk" }
thiserror.workspace = true
tokio-util.workspace = true
tokio.workspace = true
+7
View File
@@ -18,6 +18,9 @@ pub enum Error {
)]
ReceivedResponseWithNewVersion { expected: u8, received: u8 },
#[error("got reply for connect request, but it appears intended for the wrong address?")]
GotReplyIntendedForWrongAddress,
#[error("unexpected connect response")]
UnexpectedConnectResponse,
@@ -41,6 +44,10 @@ pub enum Error {
#[error(transparent)]
Bincode(#[from] bincode::Error),
#[error("failed to create connect request")]
FailedToCreateConnectRequest {
source: nym_ip_packet_requests::sign::SignatureError,
},
}
// Result type based on our error type
+1 -1
View File
@@ -4,7 +4,7 @@
use bytes::Bytes;
use futures::StreamExt;
use nym_ip_packet_requests::{codec::MultiIpPacketCodec, v8::response::ControlResponse};
use nym_sdk::mixnet::ReconstructedMessage;
pub use nym_sdk::mixnet::ReconstructedMessage;
use tokio_util::codec::FramedRead;
use tracing::{debug, error, info, warn};
+40
View File
@@ -0,0 +1,40 @@
[package]
name = "nym-vpn-api-client"
version.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
backon.workspace = true
base64-url.workspace = true
bip39 = { workspace = true, features = ["zeroize"] }
bs58.workspace = true
chrono = { workspace = true, features = ["serde"] }
itertools.workspace = true
nym-compact-ecash.workspace = true
nym-config.workspace = true
nym-contracts-common.workspace = true
nym-credential-proxy-requests.workspace = true
nym-crypto = { workspace = true, features = ["asymmetric", "stream_cipher"] }
nym-http-api-client.workspace = true
nym-validator-client.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sha2.workspace = true
strum.workspace = true
thiserror.workspace = true
time = { workspace = true, features = [
"serde-human-readable",
"serde-well-known",
] }
tokio = { workspace = true, features = [] }
tracing.workspace = true
url.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
bip39 = { workspace = true, features = ["zeroize"] }
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_http_api_client::{ApiClient, NO_PARAMS};
use url::Url;
use crate::{
client::NYM_VPN_API_TIMEOUT,
error::{Result, VpnApiClientError},
response::{NymWellknownDiscoveryItemResponse, RegisteredNetworksResponse},
routes,
};
/// Bootstrapping Environments and Network Discovery
pub struct BootstrapVpnApiClient {
inner: nym_http_api_client::Client,
}
impl BootstrapVpnApiClient {
/// Returns a VpnApiClient Based on locally set well known url and empty user agent.
///
/// THIS SHOULD ONLY BE USED FOR BOOTSTRAPPING.
pub fn new(base_url: Url) -> Result<Self> {
nym_http_api_client::Client::builder(base_url)
.map(|builder| builder.with_timeout(NYM_VPN_API_TIMEOUT))
.and_then(|builder| builder.build())
.map(|c| Self { inner: c })
.map_err(VpnApiClientError::CreateVpnApiClient)
}
pub async fn get_wellknown_envs(&self) -> Result<RegisteredNetworksResponse> {
self.inner
.get_json(
&[
routes::PUBLIC,
routes::V1,
routes::WELLKNOWN,
routes::ENVS_FILE,
],
NO_PARAMS,
)
.await
.map_err(VpnApiClientError::GetNetworkEnvs)
}
pub async fn get_wellknown_discovery(
&self,
network_name: &str,
) -> Result<NymWellknownDiscoveryItemResponse> {
self.inner
.get_json(
&[
routes::PUBLIC,
routes::V1,
routes::WELLKNOWN,
network_name,
routes::DISCOVERY_FILE,
],
NO_PARAMS,
)
.await
.map_err(VpnApiClientError::GetDiscoveryInfo)
}
}
File diff suppressed because it is too large Load Diff
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::error::Error;
pub use nym_http_api_client::HttpClientError;
use nym_contracts_common::ContractsCommonError;
use crate::response::{ErrorMessage, NymErrorResponse, UnexpectedError};
#[derive(Debug, thiserror::Error)]
pub enum VpnApiClientError {
#[error("failed tp create vpn api client")]
CreateVpnApiClient(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get account")]
GetAccount(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get account summary")]
GetAccountSummary(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get devices")]
GetDevices(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to register device")]
RegisterDevice(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get active devices")]
GetActiveDevices(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get device by id")]
GetDeviceById(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get device zk-nym")]
GetDeviceZkNyms(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to update device")]
UpdateDevice(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to request zk-nym")]
RequestZkNym(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get active zk-nym")]
GetActiveZkNym(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get zk-nym by id")]
GetZkNymById(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to confirm zk-nym download")]
ConfirmZkNymDownloadById(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get free passes")]
GetFreePasses(#[source] HttpClientError<ErrorMessage>),
#[error("failed to apply free pass")]
ApplyFreepass(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get subscriptions")]
GetSubscriptions(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to create subscription")]
CreateSubscription(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get active subscription")]
GetActiveSubscriptions(#[source] HttpClientError<NymErrorResponse>),
#[error("failed to get gateways")]
GetGateways(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get gateway countries")]
GetGatewayCountries(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get entry gateways")]
GetEntryGateways(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get entry gateway countries")]
GetEntryGatewayCountries(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get exit gateways")]
GetExitGateways(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get exit gateway countries")]
GetExitGatewayCountries(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get vpn gateways")]
GetVpnGateways(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get vpn gateway countries")]
GetVpnGatewayCountries(#[source] HttpClientError<UnexpectedError>),
#[error("invalud percent value")]
InvalidPercentValue(#[source] ContractsCommonError),
#[error("failed to derive from path")]
CosmosDeriveFromPath(
#[source] nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError,
),
#[error("failed to get directory zk-nym ticketbook partial verification keys")]
GetDirectoryZkNymsTicketbookPartialVerificationKeys(#[source] HttpClientError<ErrorMessage>),
#[error("failed to get health")]
GetHealth(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get usage")]
GetUsage(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get registered network environments")]
GetNetworkEnvs(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get discovery info")]
GetDiscoveryInfo(#[source] HttpClientError<UnexpectedError>),
#[error("failed to get vpn network Details")]
GetVpnNetworkDetails(#[source] HttpClientError<UnexpectedError>),
#[error("failed to post account")]
PostAccount(#[source] HttpClientError<UnexpectedError>),
#[error("create account")]
CreateAccount(#[source] crate::types::AccountError),
}
pub type Result<T> = std::result::Result<T, VpnApiClientError>;
impl TryFrom<VpnApiClientError> for NymErrorResponse {
type Error = VpnApiClientError;
fn try_from(response: VpnApiClientError) -> std::result::Result<Self, Self::Error> {
crate::response::extract_error_response(&response).ok_or(response)
}
}
impl VpnApiClientError {
pub fn http_client_error<T>(&self) -> Option<&HttpClientError<T>>
where
T: std::fmt::Display + std::fmt::Debug + 'static,
{
self.source()
.and_then(|source| source.downcast_ref::<HttpClientError<T>>())
}
}
+247
View File
@@ -0,0 +1,247 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::fmt;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::{DirectSecp256k1HdWallet, signing::signer::OfflineSigner};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use crate::types::VpnApiTime;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct JwtHeader {
/// Type that is always "jwt"
typ: String,
/// Either "ES256K" or "ECDSA"
///
/// NOTE: These JWTs are not meant to be used outside NymVPN API, so the encoding of their signatures
/// serialisation formats do not follow any RFCs, because NymVPN software creates and consumes them
///
/// Elliptic curve signatures using secp256k1 scheme, sadly not in the table of standard algorithms in
/// https://www.rfc-editor.org/rfc/rfc7518#section-3.1. This scheme is chosen to match the signatures used
/// in the Nyx chain based on the Cosmos SDK.
///
alg: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct JwtPayload {
/// Issued at, as a Unix epoch, timezone is UTC
iat: u128,
/// The number of seconds the token is valid for, after the issued at UTC epoch
exp: u8,
/// The base58 public key of the account (for signature verification)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pubkey: Option<String>,
/// The subject is the Cosmos account id of the user, or the public key of the device
sub: String,
}
#[allow(unused)]
#[derive(Debug, Clone)]
pub(crate) struct Jwt {
header: JwtHeader,
payload: JwtPayload,
signature: String,
jwt: String,
}
impl Jwt {
pub fn new_secp256k1(wallet: &DirectSecp256k1HdWallet) -> Jwt {
let timestamp = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs() as u128;
tracing::debug!("timestamp: {}", timestamp);
Jwt::new_secp256k1_with_now(wallet, timestamp)
}
pub fn new_secp256k1_synced(wallet: &DirectSecp256k1HdWallet, remote_time: VpnApiTime) -> Jwt {
Jwt::new_secp256k1_with_now(wallet, remote_time.estimate_remote_now_unix())
}
pub fn new_secp256k1_with_now(wallet: &DirectSecp256k1HdWallet, now: u128) -> Jwt {
let account = wallet.get_accounts().unwrap(); // TODO: result
let address = account[0].address();
let public_key = account[0].public_key().to_bytes();
let header = JwtHeader {
typ: "JWT".to_string(),
alg: "ES256K".to_string(),
};
let payload = JwtPayload {
iat: now,
exp: 30,
pubkey: Some(bs58::encode(&public_key).into_string()),
sub: address.to_string(),
};
let header_base64 = base64_url::encode(&json!(header.clone()).to_string());
let payload_base64 = base64_url::encode(&json!(payload.clone()).to_string());
let message = format!("{header_base64}.{payload_base64}").into_bytes();
let signature = wallet.sign_raw(address, message).unwrap(); // TODO: result
let signature_bytes = signature.to_bytes().to_vec();
let signature_base64 = base64_url::encode(&signature_bytes);
let jwt = format!("{header_base64}.{payload_base64}.{signature_base64}");
Jwt {
header,
payload,
signature: signature_base64,
jwt,
}
}
pub fn new_ecdsa(key_pair: &ed25519::KeyPair) -> Jwt {
let timestamp = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs() as u128;
Jwt::new_ecdsa_with_now(key_pair, timestamp)
}
pub fn new_ecdsa_synced(key_pair: &ed25519::KeyPair, remote_time: VpnApiTime) -> Jwt {
Jwt::new_ecdsa_with_now(key_pair, remote_time.estimate_remote_now_unix())
}
pub fn new_ecdsa_with_now(key_pair: &ed25519::KeyPair, now: u128) -> Jwt {
let header = JwtHeader {
typ: "JWT".to_string(),
alg: "ECDSA".to_string(),
};
let payload = JwtPayload {
iat: now,
exp: 30,
pubkey: None,
sub: key_pair.public_key().to_base58_string(),
};
let header_base64 = base64_url::encode(&json!(header.clone()).to_string());
let payload_base64 = base64_url::encode(&json!(payload.clone()).to_string());
let message = format!("{header_base64}.{payload_base64}").into_bytes();
let to_sign = Sha256::digest(&message);
let signature = key_pair.private_key().sign(to_sign);
let signature_bytes = signature.to_bytes();
let signature_base64 = base64_url::encode(&signature_bytes);
let jwt = format!("{header_base64}.{payload_base64}.{signature_base64}");
Jwt {
header,
payload,
signature: signature_base64,
jwt,
}
}
}
impl fmt::Display for Jwt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.jwt)
}
}
#[cfg(test)]
mod tests {
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::DirectSecp256k1HdWallet;
use super::*;
fn get_secp256k1_keypair() -> DirectSecp256k1HdWallet {
let mnemonic = "kiwi ketchup mix canvas curve ribbon congress method feel frozen act annual aunt comfort side joy mesh palace tennis cannon orange name tortoise piece";
let mnemonic = bip39::Mnemonic::parse(mnemonic).unwrap();
DirectSecp256k1HdWallet::from_mnemonic("n", mnemonic)
}
#[test]
fn secp256k1_jwt_matches_javascript() {
let now = 1722535718u128;
let jwt_expected_from_js_snapshot = "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjMwLCJpYXQiOjE3MjI1MzU3MTgsInB1YmtleSI6IndneFpTM25CbWJBd2Nud0FpTnlDTjE5dWNTZHo5cVdkUXlidDJyYWtQVUhyIiwic3ViIjoibjE4Y2phemx4dTd0ODZzODV3YWwzbTdobms4ZXE3cGNmYWtoZHE1MyJ9.qxwY96D-vzMxHWZ840_l6YVDeuZeEkYqz2FaPS8ROztEqipXWCYUi8M1YTH1ZUuNyjgDAMS3NAM3hYvY09ODWQ";
let jwt_expected_from_js_snapshot_components: Vec<&str> =
jwt_expected_from_js_snapshot.split('.').collect();
let wallet = get_secp256k1_keypair();
let jwt = Jwt::new_secp256k1_with_now(&wallet, now);
let jwt_str = jwt.to_string();
let jwt_components: Vec<&str> = jwt_str.split('.').collect();
if jwt_str != jwt_expected_from_js_snapshot {
println!("== secp256k1 / ED256K1 ==");
println!(
"jwt_expected_from_js_snapshot = {}",
jwt_expected_from_js_snapshot
);
println!("jwt_str = {}", jwt_str);
}
assert_eq!(
jwt_expected_from_js_snapshot_components[0],
jwt_components[0]
); // header
assert_eq!(
jwt_expected_from_js_snapshot_components[1],
jwt_components[1]
); // payload
assert_eq!(
jwt_expected_from_js_snapshot_components[2],
jwt_components[2]
); // signature
assert_eq!(jwt_expected_from_js_snapshot, jwt_str); // whole JWT strings
}
fn get_ed25519_keypair() -> ed25519::KeyPair {
// let mnemonic = "kiwi ketchup mix canvas curve ribbon congress method feel frozen act annual aunt comfort side joy mesh palace tennis cannon orange name tortoise piece";
let private_key_base58 = "9JqXnPvTrWkq1Yq66d8GbXrcz5eryAhPZvZ46cEsBPUY";
let public_key_base58 = "4SPdxfBYsuARBw6REQQa5vFiKcvmYiet9sSWqb751i3Z";
let private_key = bs58::decode(private_key_base58).into_vec().unwrap();
let public_key = bs58::decode(public_key_base58).into_vec().unwrap();
ed25519::KeyPair::from_bytes(&private_key, &public_key).unwrap()
}
#[test]
fn ed25519_ecdsa_jwt_matches_javascript() {
let now = 1722535718u128;
let jwt_expected_from_js_snapshot = "eyJhbGciOiJFQ0RTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjMwLCJpYXQiOjE3MjI1MzU3MTgsInN1YiI6IjRTUGR4ZkJZc3VBUkJ3NlJFUVFhNXZGaUtjdm1ZaWV0OXNTV3FiNzUxaTNaIn0.wSd8y1QdqOVYLf2uTMlnymmiIPQwpxXWd2QvPZ-XqV8O1PNiurQO5JPU65SnaOfggJVA5pnAgZLbj9ciOJKIDg";
let jwt_expected_from_js_snapshot_components: Vec<&str> =
jwt_expected_from_js_snapshot.split('.').collect();
let key_pair = get_ed25519_keypair();
let jwt = Jwt::new_ecdsa_with_now(&key_pair, now);
let jwt_str = jwt.to_string();
let jwt_components: Vec<&str> = jwt_str.split('.').collect();
if jwt_str != jwt_expected_from_js_snapshot {
println!("== ed25519 / ECDSA ==");
println!(
"jwt_expected_from_js_snapshot = {}",
jwt_expected_from_js_snapshot
);
println!("jwt_str = {}", jwt_str);
}
assert_eq!(
jwt_expected_from_js_snapshot_components[0],
jwt_components[0]
); // header
assert_eq!(
jwt_expected_from_js_snapshot_components[1],
jwt_components[1]
); // payload
assert_eq!(
jwt_expected_from_js_snapshot_components[2],
jwt_components[2]
); // signature
assert_eq!(jwt_expected_from_js_snapshot, jwt_str); // whole JWT strings
}
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub mod response;
pub mod types;
pub(crate) mod jwt;
mod bootstrap;
mod client;
mod error;
mod network_compatibility;
mod request;
mod routes;
pub use bootstrap::BootstrapVpnApiClient;
pub use client::VpnApiClient;
pub use error::{HttpClientError, VpnApiClientError};
pub use network_compatibility::NetworkCompatibility;
@@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::response::NetworkCompatibilityResponse;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetworkCompatibility {
pub core: String,
pub ios: String,
pub macos: String,
pub tauri: String,
pub android: String,
}
impl fmt::Display for NetworkCompatibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"core: {}, ios: {}, macos: {}, tauri: {}, android: {}",
self.core, self.ios, self.macos, self.tauri, self.android
)
}
}
impl From<NetworkCompatibilityResponse> for NetworkCompatibility {
fn from(response: NetworkCompatibilityResponse) -> Self {
NetworkCompatibility {
core: response.core,
ios: response.ios,
macos: response.macos,
tauri: response.tauri,
android: response.android,
}
}
}
+99
View File
@@ -0,0 +1,99 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateAccountRequestBody {
pub account_addr: String,
pub pub_key: String,
pub signature_base64: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegisterDeviceRequestBody {
pub device_identity_key: String,
pub signature: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestZkNymRequestBody {
pub withdrawal_request: String,
pub ecash_pubkey: String,
pub expiration_date: String,
pub ticketbook_type: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApplyFreepassRequestBody {
pub code: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateSubscriptionInvoicesRequestBody {
pub subscription: String,
pub date: String,
pub status: CreateSubscriptionInvoicesStatus,
pub invoice_no: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CreateSubscriptionInvoicesStatus {
Unpaid,
Paid,
Cancelled,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateSubscriptionRequestBody {
pub valid_from_utc: String,
pub subscription_kind: CreateSubscriptionKind,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CreateSubscriptionKind {
OneMonth,
OneYear,
TwoYears,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestRefundRequestBody {
subscription_invoice: String,
status: RequestRefundRequestStatus,
user_reason: RequestRefundRequestUserReason,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RequestRefundRequestStatus {
Pending,
Complete,
Rejected,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RequestRefundRequestUserReason {
SubscriptionInError,
PoorPerformance,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateDeviceRequestBody {
pub status: UpdateDeviceRequestStatus,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UpdateDeviceRequestStatus {
Active,
Inactive,
DeleteMe,
}
+658
View File
@@ -0,0 +1,658 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::{collections::HashSet, fmt, net::IpAddr};
use itertools::Itertools;
use nym_contracts_common::Percent;
use nym_credential_proxy_requests::api::v1::ticketbook::models::TicketbookWalletSharesResponse;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::network_compatibility::NetworkCompatibility;
const MAX_PROBE_RESULT_AGE_MINUTES: i64 = 60;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NymVpnRegisterAccountResponse {
pub created_on_utc: String,
pub last_updated_utc: String,
pub account_addr: String,
pub status: NymVpnRegisterAccountStatusResponse,
pub account_token: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NymVpnRegisterAccountStatusResponse {
Active,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NymVpnAccountResponse {
pub created_on_utc: String,
pub last_updated_utc: String,
pub account_addr: String,
pub status: NymVpnAccountStatusResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NymVpnAccountStatusResponse {
Active,
Inactive,
DeleteMe,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnAccountSummaryResponse {
pub account: NymVpnAccountResponse,
pub subscription: NymVpnAccountSummarySubscription,
pub devices: NymVpnAccountSummaryDevices,
pub fair_usage: NymVpnAccountSummaryFairUsage,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnAccountSummarySubscription {
pub is_active: bool,
pub active: Option<NymVpnSubscription>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnAccountSummaryDevices {
pub active: u64,
pub max: u64,
pub remaining: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[allow(non_snake_case)]
// These fields have the substring 'GB' in them, meaning we can't use `rename_all = "camelCase"`
// like for the other structs
pub struct NymVpnAccountSummaryFairUsage {
pub usedGB: u64,
pub limitGB: u64,
pub resetsOnUtc: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnHealthResponse {
pub status: String,
#[serde(with = "time::serde::rfc3339")]
pub timestamp_utc: OffsetDateTime,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NymVpnDevice {
pub created_on_utc: String,
pub last_updated_utc: String,
pub device_identity_key: String,
pub status: NymVpnDeviceStatus,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NymVpnDeviceStatus {
Active,
Inactive,
DeleteMe,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnDevicesResponse {
pub total_items: u64,
pub page: u64,
pub page_size: u64,
pub items: Vec<NymVpnDevice>,
}
impl fmt::Display for NymVpnDevicesResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
self.items
.iter()
.format_with(", ", |item, f| f(&format_args!("{item:?}")))
)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnRefundsResponse {
pub total_items: u64,
pub page: u64,
pub page_size: u64,
pub items: Vec<NymVpnRefund>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NymVpnRefund {
pub created_on_utc: String,
pub last_updated_utc: String,
pub subscription_invoice: String,
pub status: NymVpnRefundStatus,
pub user_reason: NymVpnRefundUserReason,
pub data: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NymVpnRefundStatus {
Pending,
Complete,
Rejected,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NymVpnRefundUserReason {
SubscriptionInError,
PoorPerformance,
Other,
}
// Legacy type, because the blinded_shares response for the POST seems to be different than the GET
// Remove once it's not needed anymore
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NymVpnZkNymPost {
pub created_on_utc: String,
pub last_updated_utc: String,
pub id: String,
pub ticketbook_type: String,
pub valid_until_utc: String,
pub valid_from_utc: String,
pub issued_bandwidth_in_gb: f64,
pub blinded_shares: Option<Vec<Option<TicketbookWalletSharesResponse>>>,
pub status: NymVpnZkNymStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NymVpnZkNym {
pub created_on_utc: String,
pub last_updated_utc: String,
pub id: String,
pub ticketbook_type: String,
pub valid_until_utc: String,
pub valid_from_utc: String,
pub issued_bandwidth_in_gb: f64,
pub blinded_shares: Option<TicketbookWalletSharesResponse>,
pub status: NymVpnZkNymStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, strum::Display)]
#[serde(rename_all = "snake_case")]
pub enum NymVpnZkNymStatus {
Pending,
Active,
Revoking,
Revoked,
Error,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnZkNymResponse {
pub total_items: u64,
pub page: u64,
pub page_size: u64,
pub items: Vec<NymVpnZkNym>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NymVpnSubscription {
pub created_on_utc: String,
pub last_updated_utc: String,
pub id: String,
pub valid_until_utc: String,
pub valid_from_utc: String,
pub status: NymVpnSubscriptionStatus,
pub kind: NymVpnSubscriptionKind,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NymVpnSubscriptionStatus {
Pending,
Complete,
Active,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NymVpnSubscriptionKind {
OneMonth,
OneYear,
TwoYears,
Freepass,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnSubscriptionResponse {
pub is_subscription_active: bool,
pub subscription: Option<NymVpnSubscription>,
pub remaining_allowance_in_gb: f64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnSubscriptionsResponse {
pub total_items: u64,
pub page: u64,
pub page_size: u64,
pub items: Vec<NymVpnSubscription>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NymVpnUsagesResponse {
pub total_items: u64,
pub page: u64,
pub page_size: u64,
pub items: Vec<NymVpnUsage>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct NymVpnUsage {
pub created_on_utc: String,
pub last_updated_utc: String,
pub id: String,
pub subscription_id: String,
pub valid_until_utc: String,
pub valid_from_utc: String,
pub bandwidth_allowance_gb: f64,
pub bandwidth_used_gb: f64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NymDirectoryGatewaysResponse(Vec<NymDirectoryGateway>);
impl NymDirectoryGatewaysResponse {
pub fn into_inner(self) -> Vec<NymDirectoryGateway> {
self.0
}
}
impl IntoIterator for NymDirectoryGatewaysResponse {
type Item = NymDirectoryGateway;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NymDirectoryGateway {
pub identity_key: String,
pub name: String,
pub ip_packet_router: Option<IpPacketRouter>,
pub authenticator: Option<Authenticator>,
pub location: Location,
pub last_probe: Option<Probe>,
pub ip_addresses: Vec<IpAddr>,
pub mix_port: u16,
pub role: Role,
pub entry: EntryInformation,
// The performance data here originates from the nym-api, and is effectively mixnet performance
// at the time of writing this
pub performance: Percent,
pub build_information: Option<BuildInformation>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EntryInformation {
pub hostname: Option<String>,
pub ws_port: u16,
pub wss_port: Option<u16>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UxScore {
pub max_score: u8,
pub current_score: u8,
pub color: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct IpPacketRouter {
pub address: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Authenticator {
pub address: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum Role {
// a properly active mixnode
Mixnode {
layer: u8,
},
#[serde(alias = "entry", alias = "gateway")]
EntryGateway,
#[serde(alias = "exit")]
ExitGateway,
// equivalent of node that's in rewarded set but not in the inactive set
Standby,
Inactive,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BuildInformation {
pub build_version: String,
pub commit_branch: String,
pub commit_sha: String,
}
impl NymDirectoryGateway {
pub fn is_fully_operational_entry(&self) -> bool {
self.last_probe
.as_ref()
.map(|probe| probe.is_fully_operational_entry())
.unwrap_or(false)
}
pub fn is_fully_operational_exit(&self) -> bool {
self.last_probe
.as_ref()
.map(|probe| probe.is_fully_operational_exit())
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Location {
pub two_letter_iso_country_code: String,
pub latitude: f64,
pub longitude: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Probe {
pub last_updated_utc: String,
pub outcome: ProbeOutcome,
}
impl Probe {
pub fn is_fully_operational_entry(&self) -> bool {
if !is_recently_updated(&self.last_updated_utc) {
return false;
}
self.outcome.is_fully_operational_entry()
}
pub fn is_fully_operational_exit(&self) -> bool {
if !is_recently_updated(&self.last_updated_utc) {
return false;
}
self.outcome.is_fully_operational_exit()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeOutcome {
pub as_entry: Entry,
pub as_exit: Option<Exit>,
pub wg: Option<WgProbeResults>,
}
impl ProbeOutcome {
pub fn is_fully_operational_entry(&self) -> bool {
self.as_entry.can_connect && self.as_entry.can_route
}
pub fn is_fully_operational_exit(&self) -> bool {
self.as_entry.can_connect
&& self.as_entry.can_route
&& self.as_exit.as_ref().is_some_and(|exit| {
exit.can_connect
&& exit.can_route_ip_v4
&& exit.can_route_ip_external_v4
&& exit.can_route_ip_v6
&& exit.can_route_ip_external_v6
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entry {
pub can_connect: bool,
pub can_route: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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, Serialize, Deserialize, Default)]
#[serde(rename = "wg")]
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,
}
fn is_recently_updated(last_updated_utc: &str) -> bool {
if let Ok(last_updated) = last_updated_utc.parse::<chrono::DateTime<chrono::Utc>>() {
let now = chrono::Utc::now();
let duration = now - last_updated;
duration.num_minutes() < MAX_PROBE_RESULT_AGE_MINUTES
} else {
false
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NymDirectoryGatewayCountriesResponse(Vec<NymDirectoryCountry>);
impl NymDirectoryGatewayCountriesResponse {
pub fn into_inner(self) -> Vec<NymDirectoryCountry> {
self.0
}
}
impl IntoIterator for NymDirectoryGatewayCountriesResponse {
type Item = NymDirectoryCountry;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[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(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NymErrorResponse {
pub message: String,
pub message_id: Option<String>,
pub code_reference_id: Option<String>,
pub status: String,
}
impl fmt::Display for NymErrorResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fields = [
Some(format!("message: {}", self.message)),
self.message_id
.as_deref()
.map(|x| format!("message_id: {x}")),
self.code_reference_id
.as_deref()
.map(|x| format!("code_reference_id: {x}")),
Some(format!("status: {}", self.status)),
]
.iter()
.filter_map(|x| x.clone())
.collect::<Vec<_>>();
write!(f, "{}", fields.join(", "))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorMessage {
message: String,
}
impl fmt::Display for ErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UnexpectedError {
pub message: String,
}
impl fmt::Display for UnexpectedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StatusOk {
status: String,
}
impl fmt::Display for StatusOk {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.status)
}
}
pub fn extract_error_response<E>(err: &E) -> Option<NymErrorResponse>
where
E: std::error::Error + 'static,
{
let mut source = err.source();
while let Some(err) = source {
if let Some(status) = err
.downcast_ref::<nym_http_api_client::HttpClientError<NymErrorResponse>>()
.and_then(extract_error_response_inner::<NymErrorResponse>)
{
return Some(status);
}
source = err.source();
}
None
}
fn extract_error_response_inner<E>(err: &nym_http_api_client::HttpClientError<E>) -> Option<E>
where
E: Clone + std::fmt::Display,
{
match err {
nym_http_api_client::HttpClientError::EndpointFailure { error, .. } => Some(error.clone()),
_ => None,
}
}
// The response type we fetch from the discovery endpoint
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct NymWellknownDiscoveryItemResponse {
pub network_name: String,
pub nym_api_url: String,
pub nym_vpn_api_url: String,
pub account_management: Option<AccountManagementResponse>,
pub feature_flags: Option<serde_json::Value>,
pub system_messages: Option<Vec<SystemMessageResponse>>,
pub system_configuration: Option<SystemConfigurationResponse>,
pub network_compatibility: Option<NetworkCompatibilityResponse>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct AccountManagementResponse {
pub url: String,
pub paths: AccountManagementPathsResponse,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct AccountManagementPathsResponse {
pub sign_up: String,
pub sign_in: String,
pub account: String,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SystemMessageResponse {
pub name: String,
pub display_from: String,
pub display_until: String,
pub message: String,
pub properties: serde_json::Value,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct SystemConfigurationResponse {
pub mix_thresholds: ScoreThresholdsResponse,
pub wg_thresholds: ScoreThresholdsResponse,
pub statistics_recipient: Option<String>,
pub min_supported_app_versions: Option<NetworkCompatibility>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct NetworkCompatibilityResponse {
pub core: String,
pub macos: String,
pub ios: String,
pub tauri: String,
pub android: String,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct ScoreThresholdsResponse {
pub high: u8,
pub medium: u8,
pub low: u8,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct NymWellknownDiscoveryItem {
pub network_name: String,
pub nym_api_url: String,
pub nym_vpn_api_url: String,
}
pub type RegisteredNetworksResponse = HashSet<String>;
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub(crate) const PUBLIC: &str = "public";
pub(crate) const V1: &str = "v1";
pub(crate) const ACCOUNT: &str = "account";
pub(crate) const APPLE: &str = "apple";
pub(crate) const HEALTH: &str = "health";
pub(crate) const SUMMARY: &str = "summary";
pub(crate) const DEVICE: &str = "device";
pub(crate) const ACTIVE: &str = "active";
pub(crate) const ZKNYM: &str = "zknym";
pub(crate) const AVAILABLE: &str = "available";
pub(crate) const FREEPASS: &str = "freepass";
pub(crate) const SUBSCRIPTION: &str = "subscription";
pub(crate) const USAGE: &str = "usage";
pub(crate) const DIRECTORY: &str = "directory";
pub(crate) const GATEWAYS: &str = "gateways";
pub(crate) const COUNTRIES: &str = "countries";
pub(crate) const ENTRY: &str = "entry";
pub(crate) const EXIT: &str = "exit";
pub(crate) const ZK_NYMS: &str = "zk-nyms";
pub(crate) const TICKETBOOK: &str = "ticketbook";
pub(crate) const PARTIAL_VERIFICATION_KEYS: &str = "partial-verification-keys";
pub(crate) const SHOW_VPN_ONLY: &str = "show_vpn_only";
pub(crate) const VPN_MIN_PERFORMANCE: &str = "vpn_min_performance";
pub(crate) const MIXNET_MIN_PERFORMANCE: &str = "mixnet_min_performance";
pub(crate) const WELLKNOWN: &str = ".wellknown";
pub(crate) const ENVS_FILE: &str = "envs.json";
pub(crate) const DISCOVERY_FILE: &str = "discovery.json";
pub(crate) const CURRENT_ENV: &str = "current-env.json";
+248
View File
@@ -0,0 +1,248 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::fmt;
use nym_compact_ecash::scheme::keygen::KeyPairUser;
use nym_validator_client::{
DirectSecp256k1HdWallet, nyxd::bip32::DerivationPath, signing::signer::OfflineSigner as _,
};
use time::{Duration, OffsetDateTime};
use crate::{VpnApiClientError, error::Result, jwt::Jwt};
const MAX_ACCEPTABLE_SKEW_SECONDS: i64 = 60;
const SKEW_SECONDS_CONSIDERED_SAME: i64 = 2;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("wallet error")]
Wallet(#[from] nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError),
#[error("no accounts in wallet")]
NoAccounts,
}
#[derive(Clone, Debug)]
pub struct VpnApiAccount {
wallet: DirectSecp256k1HdWallet,
id: String,
pub_key: String,
signature_base64: String,
}
impl VpnApiAccount {
fn derive_from_wallet(wallet: DirectSecp256k1HdWallet) -> std::result::Result<Self, Error> {
let accounts = wallet.get_accounts()?;
let address = accounts.first().ok_or(Error::NoAccounts)?.address();
let id = address.to_string();
let pub_key = bs58::encode(
accounts
.first()
.ok_or(Error::NoAccounts)?
.public_key()
.to_bytes(),
)
.into_string();
let message = id.clone().into_bytes();
let signature = wallet.sign_raw(address, message)?;
let signature_bytes = signature.to_bytes().to_vec();
let signature_base64 = base64_url::encode(&signature_bytes);
Ok(Self {
wallet,
id,
pub_key,
signature_base64,
})
}
pub fn random() -> Result<(Self, bip39::Mnemonic)> {
let mnemonic = bip39::Mnemonic::generate(24).unwrap();
let wallet = DirectSecp256k1HdWallet::from_mnemonic("n", mnemonic.clone());
let account = Self::derive_from_wallet(wallet).map_err(VpnApiClientError::CreateAccount)?;
Ok((account, mnemonic))
}
pub fn id(&self) -> &str {
&self.id
}
pub fn pub_key(&self) -> &str {
&self.pub_key
}
pub fn signature_base64(&self) -> &str {
&self.signature_base64
}
pub(crate) fn jwt(&self, remote_time: Option<VpnApiTime>) -> Jwt {
match remote_time {
Some(remote_time) => Jwt::new_secp256k1_synced(&self.wallet, remote_time),
None => Jwt::new_secp256k1(&self.wallet),
}
}
pub fn create_ecash_keypair(&self) -> Result<KeyPairUser> {
let hd_path = cosmos_derivation_path();
let extended_private_key = self
.wallet
.derive_extended_private_key(&hd_path)
.map_err(VpnApiClientError::CosmosDeriveFromPath)?;
Ok(KeyPairUser::new_seeded(
extended_private_key.private_key().to_bytes(),
))
}
pub fn get_mnemonic(&self) -> String {
self.wallet.mnemonic()
}
}
impl TryFrom<bip39::Mnemonic> for VpnApiAccount {
type Error = VpnApiClientError;
fn try_from(mnemonic: bip39::Mnemonic) -> std::result::Result<Self, Self::Error> {
let wallet = DirectSecp256k1HdWallet::from_mnemonic("n", mnemonic.clone());
Self::derive_from_wallet(wallet).map_err(VpnApiClientError::CreateAccount)
}
}
fn cosmos_derivation_path() -> DerivationPath {
nym_config::defaults::COSMOS_DERIVATION_PATH
.parse()
.unwrap()
}
#[derive(Clone, Copy, Debug)]
pub struct VpnApiTime {
// The local time on the client.
pub local_time: OffsetDateTime,
// The estimated time on the remote server. Based on RTT, it's not guaranteed to be accurate.
pub estimated_remote_time: OffsetDateTime,
}
impl VpnApiTime {
pub fn from_estimated_remote_time(
local_time: OffsetDateTime,
estimated_remote_time: OffsetDateTime,
) -> Self {
Self {
local_time,
estimated_remote_time,
}
}
pub fn from_remote_timestamp(
local_time_before_request: OffsetDateTime,
remote_timestamp: OffsetDateTime,
local_time_after_request: OffsetDateTime,
) -> Self {
let rtt = local_time_after_request - local_time_before_request;
let estimated_remote_time = remote_timestamp + (rtt / 2);
Self {
local_time: local_time_after_request,
estimated_remote_time,
}
}
// Local time minus remote time. Meaning if the value is positive, the local time is ahead
// of the remote time.
pub fn local_time_ahead_skew(&self) -> Duration {
self.local_time - self.estimated_remote_time
}
pub fn is_almost_same(&self) -> bool {
self.local_time_ahead_skew().abs().whole_seconds() < SKEW_SECONDS_CONSIDERED_SAME
}
pub fn is_acceptable_synced(&self) -> bool {
self.local_time_ahead_skew().abs().whole_seconds() < MAX_ACCEPTABLE_SKEW_SECONDS
}
pub fn is_synced(&self) -> VpnApiTimeSynced {
if self.is_almost_same() {
VpnApiTimeSynced::AlmostSame
} else if self.is_acceptable_synced() {
VpnApiTimeSynced::AcceptableSynced
} else {
VpnApiTimeSynced::NotSynced
}
}
pub fn estimate_remote_now(&self) -> OffsetDateTime {
tracing::debug!(
"Estimating remote now using (local time ahead) skew: {}",
self.local_time_ahead_skew()
);
let local_time_now = OffsetDateTime::now_utc();
local_time_now - self.local_time_ahead_skew()
}
pub fn estimate_remote_now_unix(&self) -> u128 {
self.estimate_remote_now().unix_timestamp() as u128
}
}
impl fmt::Display for VpnApiTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Local time: {}, Remote time: {}, Skew: {}",
self.local_time,
self.estimated_remote_time,
self.local_time_ahead_skew(),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VpnApiTimeSynced {
AlmostSame,
AcceptableSynced,
NotSynced,
}
impl VpnApiTimeSynced {
pub fn is_synced(&self) -> bool {
matches!(
self,
VpnApiTimeSynced::AlmostSame | VpnApiTimeSynced::AcceptableSynced
)
}
pub fn is_not_synced(&self) -> bool {
!self.is_synced()
}
}
#[cfg(test)]
mod tests {
use crate::types::test_fixtures::{TEST_DEFAULT_MNEMONIC, TEST_DEFAULT_MNEMONIC_ID};
use super::*;
#[test]
fn create_account_from_mnemonic() {
let account =
VpnApiAccount::try_from(bip39::Mnemonic::parse(TEST_DEFAULT_MNEMONIC).unwrap())
.unwrap();
assert_eq!(account.id(), TEST_DEFAULT_MNEMONIC_ID);
}
#[test]
fn create_random_account() {
let (_, mnemonic) = VpnApiAccount::random().unwrap();
assert_eq!(mnemonic.word_count(), 24);
}
#[test]
fn derive_wallets() {
for word_count in [12, 24] {
let wallet = DirectSecp256k1HdWallet::generate("n", word_count).unwrap();
VpnApiAccount::derive_from_wallet(wallet).unwrap();
}
}
}
+222
View File
@@ -0,0 +1,222 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::{fmt, sync::Arc};
use nym_crypto::asymmetric::ed25519;
use sha2::Digest as _;
use crate::{jwt::Jwt, request::UpdateDeviceRequestStatus};
use super::VpnApiTime;
#[derive(Clone)]
pub struct Device {
keypair: Arc<ed25519::KeyPair>,
}
impl Device {
pub fn identity_key(&self) -> &ed25519::PublicKey {
self.keypair.public_key()
}
pub(crate) fn jwt(&self, remote_time: Option<VpnApiTime>) -> Jwt {
match remote_time {
Some(remote_time) => Jwt::new_ecdsa_synced(&self.keypair, remote_time),
None => Jwt::new_ecdsa(&self.keypair),
}
}
pub fn sign<M: AsRef<[u8]>>(&self, message: M) -> DeviceSignature {
let digest = {
let mut hasher = sha2::Sha256::new();
hasher.update(message);
hasher.finalize()
};
DeviceSignature(self.keypair.private_key().sign(digest))
}
pub fn sign_identity_key(&self) -> DeviceSignature {
self.sign(self.identity_key().to_base58_string())
}
}
impl fmt::Display for Device {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Device {{ {} }}", self.identity_key().to_base58_string())
}
}
impl fmt::Debug for Device {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Device {{ identity_key: {} }}",
self.identity_key().to_base58_string()
)
}
}
impl From<Arc<ed25519::KeyPair>> for Device {
fn from(keypair: Arc<ed25519::KeyPair>) -> Self {
Self { keypair }
}
}
impl From<ed25519::KeyPair> for Device {
fn from(keypair: ed25519::KeyPair) -> Self {
Self {
keypair: Arc::new(keypair),
}
}
}
// In tests we create from a mnemonic, in production these are always created directly from
// the keypair
#[cfg(test)]
impl From<bip39::Mnemonic> for Device {
fn from(mnemonic: bip39::Mnemonic) -> Self {
let (entropy, _) = mnemonic.to_entropy_array();
// Entropy is statically >= 32 bytes, so we can safely extract the first 32
// bytes
let seed = &entropy[0..32];
let signing_key = ed25519::PrivateKey::from_bytes(seed).unwrap();
let verifying_key = signing_key.public_key();
let privkey = signing_key.to_bytes().to_vec();
let pubkey = verifying_key.to_bytes().to_vec();
let keypair = ed25519::KeyPair::from_bytes(&privkey, &pubkey).unwrap();
Self {
keypair: Arc::new(keypair),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeviceStatus {
Active,
Inactive,
DeleteMe,
}
impl From<DeviceStatus> for UpdateDeviceRequestStatus {
fn from(status: DeviceStatus) -> Self {
match status {
DeviceStatus::Active => UpdateDeviceRequestStatus::Active,
DeviceStatus::Inactive => UpdateDeviceRequestStatus::Inactive,
DeviceStatus::DeleteMe => UpdateDeviceRequestStatus::DeleteMe,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceSignature(ed25519::Signature);
impl DeviceSignature {
pub fn to_bytes(&self) -> [u8; 64] {
self.0.to_bytes()
}
pub fn to_base64_url_string(&self) -> String {
base64_url::encode(&self.to_bytes())
}
pub fn to_base64_string(&self) -> String {
base64_url::unescape(&self.to_base64_url_string()).to_string()
}
}
#[cfg(test)]
mod tests {
use crate::types::test_fixtures::{
TEST_DEFAULT_DEVICE_IDENTITY_KEY, TEST_DEFAULT_DEVICE_MNEMONIC,
};
use super::*;
fn ed25519_keypair_fixture() -> ed25519::KeyPair {
// The mnemonic used to generate the keypair
let _mnemonic = "kiwi ketchup mix canvas curve ribbon congress method feel frozen act annual aunt comfort side joy mesh palace tennis cannon orange name tortoise piece";
// The corresponding keypair generated from the mnemonic
let private_key_base58 = "9JqXnPvTrWkq1Yq66d8GbXrcz5eryAhPZvZ46cEsBPUY";
let public_key_base58 = "4SPdxfBYsuARBw6REQQa5vFiKcvmYiet9sSWqb751i3Z";
let private_key = bs58::decode(private_key_base58).into_vec().unwrap();
let public_key = bs58::decode(public_key_base58).into_vec().unwrap();
ed25519::KeyPair::from_bytes(&private_key, &public_key).unwrap()
}
#[test]
fn verify_ed25519_keypair_fixture() {
let device = Device::from(
bip39::Mnemonic::parse("kiwi ketchup mix canvas curve ribbon congress method feel frozen act annual aunt comfort side joy mesh palace tennis cannon orange name tortoise piece").unwrap()
);
let expected_keypair = ed25519_keypair_fixture();
assert_eq!(
device.keypair.private_key().to_base58_string(),
expected_keypair.private_key().to_base58_string()
);
assert_eq!(
device.keypair.public_key().to_base58_string(),
expected_keypair.public_key().to_base58_string()
);
}
#[test]
fn create_device_from_mnemonic_1() {
let device = Device::from(bip39::Mnemonic::parse(TEST_DEFAULT_DEVICE_MNEMONIC).unwrap());
assert_eq!(
device.identity_key().to_base58_string(),
TEST_DEFAULT_DEVICE_IDENTITY_KEY
);
}
#[test]
fn create_device_from_mnemonic_2() {
let device = Device::from(
bip39::Mnemonic::parse("kiwi ketchup mix canvas curve ribbon congress method feel frozen act annual aunt comfort side joy mesh palace tennis cannon orange name tortoise piece").unwrap()
);
assert_eq!(
device.identity_key().to_base58_string(),
"4SPdxfBYsuARBw6REQQa5vFiKcvmYiet9sSWqb751i3Z",
);
assert_eq!(
device.keypair.private_key().to_base58_string(),
"9JqXnPvTrWkq1Yq66d8GbXrcz5eryAhPZvZ46cEsBPUY",
);
}
#[test]
fn create_device_from_keypair() {
let device = Device::from(ed25519_keypair_fixture());
assert_eq!(
device.keypair.public_key().to_base58_string(),
"4SPdxfBYsuARBw6REQQa5vFiKcvmYiet9sSWqb751i3Z",
);
assert_eq!(
device.keypair.private_key().to_base58_string(),
"9JqXnPvTrWkq1Yq66d8GbXrcz5eryAhPZvZ46cEsBPUY",
);
}
#[test]
fn sign_identity_key() {
let device = Device::from(bip39::Mnemonic::parse(TEST_DEFAULT_DEVICE_MNEMONIC).unwrap());
assert_eq!(
device.identity_key().to_base58_string(),
TEST_DEFAULT_DEVICE_IDENTITY_KEY
);
let signature = device.sign_identity_key().to_base64_string();
assert_eq!(
signature,
"W5Zv1QhG37Al0QQH/9tqOmv1MU9IjfWP1xDq116GGSu/1Z6cnAW0sOyfrIiqdEleUKJB9wC/HjcsifaogymWAw=="
);
}
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub use nym_contracts_common::Percent;
use crate::VpnApiClientError;
#[derive(Clone, Copy, Default, Debug)]
pub struct GatewayMinPerformance {
pub mixnet_min_performance: Option<Percent>,
pub vpn_min_performance: Option<Percent>,
}
impl GatewayMinPerformance {
pub fn from_percentage_values(
mixnet_min_performance: Option<u64>,
vpn_min_performance: Option<u64>,
) -> Result<Self, VpnApiClientError> {
let mixnet_min_performance = mixnet_min_performance
.map(Percent::from_percentage_value)
.transpose()
.map_err(VpnApiClientError::InvalidPercentValue)?;
let vpn_min_performance = vpn_min_performance
.map(Percent::from_percentage_value)
.transpose()
.map_err(VpnApiClientError::InvalidPercentValue)?;
Ok(Self {
mixnet_min_performance,
vpn_min_performance,
})
}
pub(crate) fn to_param(self) -> Vec<(String, String)> {
let mut params = vec![];
if let Some(threshold) = self.mixnet_min_performance {
params.push((
crate::routes::MIXNET_MIN_PERFORMANCE.to_string(),
threshold.to_string(),
));
};
if let Some(threshold) = self.vpn_min_performance {
params.push((
crate::routes::VPN_MIN_PERFORMANCE.to_string(),
threshold.to_string(),
));
};
params
}
}
#[derive(Clone, Debug)]
pub enum GatewayType {
MixnetEntry,
MixnetExit,
Wg,
}
#[derive(Clone, Copy, Debug)]
pub struct ScoreThresholds {
pub high: u8,
pub medium: u8,
pub low: u8,
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
mod account;
mod device;
mod gateway;
mod platform;
#[cfg(test)]
mod test_fixtures;
pub use account::{Error as AccountError, VpnApiAccount, VpnApiTime, VpnApiTimeSynced};
pub use device::{Device, DeviceStatus};
pub use gateway::{GatewayMinPerformance, GatewayType, ScoreThresholds};
pub use platform::Platform;
pub use nym_contracts_common::{NaiveFloat, Percent};
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
#[derive(Clone, Copy, Debug)]
pub enum Platform {
Apple,
Unspecified,
}
impl Platform {
pub fn api_path_component(&self) -> &'static str {
match self {
Platform::Apple => crate::routes::APPLE,
Platform::Unspecified => "",
}
}
}
@@ -0,0 +1,11 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
// The default acccount mnemonic, the same as in the js integration tests
pub(super) const TEST_DEFAULT_MNEMONIC: &str = "range mystery picture decline olympic acoustic lesson quick rebuild panda royal fold start leader egg hammer width olympic worry length crawl couch link mobile";
pub(super) const TEST_DEFAULT_MNEMONIC_ID: &str = "n1sslaag27wfydyrvyua72hg5e0vteglxrs8nw3c";
// The default device mnemonic, the same as in the js integration tests
pub(super) const TEST_DEFAULT_DEVICE_MNEMONIC: &str = "pitch deputy proof fire movie put bread ribbon what chef zebra car vacuum gadget steak board state oyster layer glory barely thrive nice box";
pub(super) const TEST_DEFAULT_DEVICE_IDENTITY_KEY: &str =
"FJDUECYAeosXhNGjxf8w5MJM7N2DfDwQznvWwTxJz6ft";
+1 -1
View File
@@ -50,7 +50,7 @@ nym-bin-common = { workspace = true, features = [
"basic_tracing",
] }
nym-ip-packet-client = { git = "https://github.com/nymtech/nym-vpn-client", rev = "5c3aacfce158aac73cbd3fd905d90269f8bacc01" } # max/re-export-test https://github.com/nymtech/nym-vpn-client/commit/5c3aacfce158aac73cbd3fd905d90269f8bacc01
# nym-ip-packet-client = { git = "https://github.com/nymtech/nym-vpn-client", rev = "5c3aacfce158aac73cbd3fd905d90269f8bacc01" }
# nym-gateway-directory
bytecodec = { workspace = true }