Endpoint for exit GW IPs (#6418)

This commit is contained in:
dynco-nym
2026-02-04 22:14:46 +01:00
committed by GitHub
parent cfcf804b47
commit 660eff45dc
4 changed files with 95 additions and 10 deletions
@@ -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
@@ -17,6 +17,7 @@ pub(crate) fn routes() -> Router<AppState> {
"/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<AppState>) -> HttpResult<J
))
}
#[utoipa::path(
tag = "dVPN Directory Cache",
get,
path = "/exit/ips",
summary = "Gets IP addresses of all exit gateways currently on the network",
context_path = "/dvpn/v1/directory/gateways",
operation_id = "getExitGatewayIps",
responses(
(status = 200, body = Vec<String>)
)
)]
#[instrument(level = tracing::Level::INFO, skip(state))]
pub async fn get_exit_gateway_ips(state: State<AppState>) -> HttpResult<Json<Vec<String>>> {
Ok(Json(
state
.cache()
.get_exit_gateway_ips(state.db_pool(), &MIN_SUPPORTED_VERSION)
.await,
))
}
#[utoipa::path(
tag = "dVPN Directory Cache",
get,
@@ -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<String, Arc<RwLock<Vec<Gateway>>>>,
dvpn_gateways: Cache<String, Arc<RwLock<Vec<DVpnGateway>>>>,
exit_gateway_ips: Cache<String, Arc<RwLock<Vec<String>>>>,
nym_nodes: Cache<String, Arc<RwLock<Vec<ExtendedNymNode>>>>,
mixstats: Cache<String, Arc<RwLock<Vec<DailyStats>>>>,
history: Cache<String, Arc<RwLock<Vec<SummaryHistory>>>>,
@@ -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<String> {
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<String> = 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<String>,
) -> Entry<String, Arc<RwLock<Vec<String>>>> {
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<ExtendedNymNode>,