diff --git a/Cargo.lock b/Cargo.lock index e53cd9ddcb..86ec8c1c56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7346,7 +7346,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "4.0.14" +version = "4.1.0" dependencies = [ "ammonia", "anyhow", diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 8d414c8817..8991b95e1c 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "4.0.14" +version = "4.1.0" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/exit.rs b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/exit.rs index 5d8b2918f4..02419a71a7 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/exit.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/exit.rs @@ -17,6 +17,7 @@ pub(crate) fn routes() -> Router { "/exit/countries", axum::routing::get(get_entry_gateway_countries), ) + .route("/exit/ips", axum::routing::get(get_exit_gateway_ips)) .route( "/exit/country/:two_letter_country_code", axum::routing::get(get_exit_gateways_by_country), @@ -70,6 +71,27 @@ pub async fn get_entry_gateway_countries(state: State) -> HttpResult) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state))] +pub async fn get_exit_gateway_ips(state: State) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_exit_gateway_ips(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await, + )) +} + #[utoipa::path( tag = "dVPN Directory Cache", get, diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index 14bfc99c8d..88ef86a75b 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -9,10 +9,10 @@ use nym_node_status_client::auth::VerifiableRequest; use nym_validator_client::nym_api::SkimmedNode; use semver::Version; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::{collections::HashMap, net::IpAddr, sync::Arc, time::Duration}; use time::UtcDateTime; use tokio::sync::RwLock; -use tracing::{error, instrument, warn}; +use tracing::{error, instrument, trace, warn}; use utoipa::ToSchema; use super::models::SessionStats; @@ -153,6 +153,7 @@ impl AppState { static GATEWAYS_LIST_KEY: &str = "gateways"; static DVPN_GATEWAYS_LIST_KEY: &str = "dvpn_gateways"; +static DVPN_EXIT_GATEWAY_IPS: &str = "dvpn_exit_gateway_ips"; static NYM_NODES_LIST_KEY: &str = "nym_nodes"; static MIXSTATS_LIST_KEY: &str = "mixstats"; static SUMMARY_HISTORY_LIST_KEY: &str = "summary-history"; @@ -164,6 +165,7 @@ const MIXNODE_STATS_HISTORY_DAYS: usize = 30; pub(crate) struct HttpCache { gateways: Cache>>>, dvpn_gateways: Cache>>>, + exit_gateway_ips: Cache>>>, nym_nodes: Cache>>>, mixstats: Cache>>>, history: Cache>>>, @@ -174,27 +176,31 @@ impl HttpCache { pub async fn new(ttl_seconds: u64) -> Self { HttpCache { gateways: Cache::builder() - .max_capacity(2) + .max_capacity(1) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), dvpn_gateways: Cache::builder() - .max_capacity(6) + .max_capacity(1) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + exit_gateway_ips: Cache::builder() + .max_capacity(1) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), nym_nodes: Cache::builder() - .max_capacity(2) + .max_capacity(1) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), mixstats: Cache::builder() - .max_capacity(2) + .max_capacity(1) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), history: Cache::builder() - .max_capacity(2) + .max_capacity(1) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), session_stats: Cache::builder() - .max_capacity(2) + .max_capacity(1) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), } @@ -436,6 +442,63 @@ impl HttpCache { .collect() } + pub async fn get_exit_gateway_ips( + &self, + db: &DbPool, + min_node_version: &Version, + ) -> Vec { + match self.exit_gateway_ips.get(DVPN_EXIT_GATEWAY_IPS).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + trace!("No exit gateway IPs in cache, refreshing..."); + + let ips: Vec = self + .get_dvpn_gateway_list(db, min_node_version) + .await + .into_iter() + .filter_map(|gw| { + if gw.can_route_exit() { + Some(gw.ip_addresses) + } else { + None + } + }) + .flatten() + .filter(IpAddr::is_ipv4) + .map(|ip| ip.to_string()) + .sorted() + .dedup() + .collect(); + + self.upsert_exit_gateway_ips(ips.clone()).await; + + ips + } + } + } + + pub async fn upsert_exit_gateway_ips( + &self, + ip_list: Vec, + ) -> Entry>>> { + self.exit_gateway_ips + .entry_by_ref(DVPN_EXIT_GATEWAY_IPS) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = ip_list; + v.clone() + } else { + Arc::new(RwLock::new(ip_list)) + } + }) + .await + } + pub async fn upsert_nym_node_list( &self, nym_node_list: Vec,