wip - dvpn directory cache

This commit is contained in:
Mark Sinclair
2025-05-23 18:24:43 +01:00
committed by dynco-nym
parent 8ba58ba11e
commit 23f86b6c1e
3 changed files with 249 additions and 0 deletions
@@ -0,0 +1,110 @@
use axum::{
extract::{Path, Query, State},
Json, Router,
};
use itertools::Itertools;
use serde::Deserialize;
use tracing::instrument;
use utoipa::IntoParams;
use crate::http::{error::{HttpError, HttpResult}, models::{Gateway}, state::AppState};
use crate::http::models::DVpnGateway;
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/", axum::routing::get(gateways))
.route(
"/country/:two_letter_country_code",
axum::routing::get(gateways_by_country),
)
}
async fn get_gateways_from_cache(
State(state): State<AppState>,
) -> Vec<DVpnGateway> {
let db = state.db_pool();
let res = state.cache().get_gateway_list(db).await;
// TODO: parse
let MINIMUM_NYM_NODE_VERSION = "1.6.2";
// TODO: cache the output of this filter
let output: Vec<DVpnGateway> = res.iter()
.filter(|g| {
// gateways must be bonded and not blacklisted
if !g.bonded {
return false;
}
// gateways must meet minimum semver
// if g.self_described.something_something < MINIMUM_NYM_NODE_VERSION {
// return false;
// }
true
})
.map(|d| d.try_into())
.filter_map(Result::ok)
.filter(|g: &DVpnGateway| {
// gateways must have a country
g.location.two_letter_iso_country_code.len() == 2
})
// TODO: sort by two-letter country code, then by identity key
// .sorted_by(...)
.collect();
output
}
#[utoipa::path(
tag = "dVPN Directory Cache",
get,
path = "/directory/gateways",
summary = "Gets available entry and exit gateways from the Nym network directory",
context_path = "/dvpn/v1",
responses(
(status = 200, body = Vec<DVpnGateway>)
)
)]
#[instrument(level = tracing::Level::INFO, skip_all)]
async fn gateways(
state: State<AppState>,
) -> HttpResult<Json<Vec<DVpnGateway>>> {
let res = get_gateways_from_cache(state).await;
Ok(Json(res))
}
#[allow(dead_code)] // clippy doesn't detect usage in utoipa macros
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Path)]
struct TwoLetterCountryCodeParam {
#[param(minimum = 2, maximum = 2)]
two_letter_country_code: String,
}
#[utoipa::path(
tag = "dVPN Directory Cache",
get,
params(
TwoLetterCountryCodeParam
),
path = "/directory/gateways/country/{two_letter_country_code}",
summary = "Gets available gateways from the Nym network directory by country",
context_path = "/dvpn/v1",
responses(
(status = 200, body = Vec<DVpnGateway>)
)
)]
#[instrument(level = tracing::Level::INFO, skip(state), fields(two_letter_country_code = two_letter_country_code))]
async fn gateways_by_country(
Path(TwoLetterCountryCodeParam { two_letter_country_code}): Path<TwoLetterCountryCodeParam>,
state: State<AppState>,
) -> HttpResult<Json<Vec<DVpnGateway>>> {
match two_letter_country_code.len() {
2 => {
let res = get_gateways_from_cache(state).await;
Ok(Json(res))
}
_ => Err(HttpError::invalid_input("Only two letter country code is allowed")),
}
}
@@ -15,6 +15,7 @@ pub(crate) mod nym_nodes;
pub(crate) mod services;
pub(crate) mod summary;
pub(crate) mod testruns;
pub(crate) mod dvpn;
pub(crate) struct RouterBuilder {
unfinished_router: Router<AppState>,
@@ -44,6 +45,10 @@ impl RouterBuilder {
"/explorer/v3",
Router::new().nest("/nym-nodes", nym_nodes::routes()),
)
.nest(
"/dvpn/v1",
Router::new().nest("/directory/gateways", dvpn::routes()),
)
.nest(
"/internal",
Router::new().nest("/testruns", testruns::routes()),
@@ -23,6 +23,140 @@ pub struct Gateway {
pub config_score: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
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, ToSchema)]
pub struct BuildInformation {
pub build_version: String,
pub commit_branch: String,
pub commit_sha: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct IpPacketRouter {
pub address: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct Authenticator {
pub address: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct EntryInformation {
pub hostname: Option<String>,
pub ws_port: u16,
pub wss_port: Option<u16>,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct Location {
pub two_letter_iso_country_code: String,
pub latitude: f64,
pub longitude: f64,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct DVpnGateway {
pub identity_key: String,
pub name: String,
pub ip_packet_router: Option<IpPacketRouter>,
pub authenticator: Option<Authenticator>,
pub location: Location,
pub last_probe: Option<serde_json::Value>,
pub ip_addresses: Vec<String>,
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: u8,
pub build_information: Option<BuildInformation>,
}
impl TryFrom<&Gateway> for DVpnGateway
{
type Error = ();
fn try_from(value: &Gateway) -> Result<Self, Self::Error> {
// TODO: try to parse out values from gateway and fail when unable to parse
// TODO: polyfill `last_probe`, see below from VPN API
/**
const last_testrun_utc = item.last_testrun_utc;
const last_probe: DirectoryGatewayProbe | undefined =
last_testrun_utc && item.last_probe_result?.outcome
? {
last_updated_utc: last_testrun_utc,
outcome: item.last_probe_result.outcome,
}
: undefined;
//
// reshape test probe
//
if (
last_probe?.outcome?.wg &&
isDirectoryGatewayProbeOutcome_WG_V2(last_probe?.outcome?.wg) &&
last_probe?.outcome?.wg?.can_handshake === undefined
) {
last_probe.outcome.wg = {
...last_probe.outcome.wg,
can_handshake: last_probe.outcome.wg.can_handshake_v4,
can_resolve_dns: last_probe.outcome.wg.can_resolve_dns_v4,
ping_hosts_performance:
last_probe.outcome.wg.ping_hosts_performance_v4,
ping_ips_performance: last_probe.outcome.wg.ping_ips_performance_v4,
};
}
*/
Ok(Self {
identity_key: value.gateway_identity_key.clone(),
name: value.description.moniker.clone(),
ip_packet_router: None, // value.ip_packet_router,
authenticator: None, // value.authenticator,
location: Location {
latitude: 0.0f64,
longitude: 0.0f64,
two_letter_iso_country_code: "".to_string(),
},
last_probe: value.last_probe_result.clone(),
ip_addresses: vec![], // value.ip_addresses,
mix_port: 0u16, // value.mix_port,
role: Role::EntryGateway, // value.role,
entry: EntryInformation {
ws_port: 0u16,
wss_port: None,
hostname: None,
},
performance: value.performance,
build_information: None, // value.build_information,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct GatewaySkinny {
pub gateway_identity_key: String,