diff --git a/.github/workflows/publish-nym-binaries.yml b/.github/workflows/publish-nym-binaries.yml index 5ceb1477b20..276987e522e 100644 --- a/.github/workflows/publish-nym-binaries.yml +++ b/.github/workflows/publish-nym-binaries.yml @@ -30,13 +30,11 @@ jobs: release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }} client_hash: ${{ steps.binary-hashes.outputs.client_hash }} nymvisor_hash: ${{ steps.binary-hashes.outputs.nymvisor_hash }} - nymnode_hash: ${{ steps.binary-hashes.outputs.nymnode_hash }} socks5_hash: ${{ steps.binary-hashes.outputs.socks5_hash }} netreq_hash: ${{ steps.binary-hashes.outputs.netreq_hash }} cli_hash: ${{ steps.binary-hashes.outputs.cli_hash }} client_version: ${{ steps.binary-versions.outputs.client_version }} nymvisor_version: ${{ steps.binary-versions.outputs.nymvisor_version }} - nymnode_version: ${{ steps.binary-versions.outputs.nymnode_version }} socks5_version: ${{ steps.binary-versions.outputs.socks5_version }} netreq_version: ${{ steps.binary-versions.outputs.netreq_version }} cli_version: ${{ steps.binary-versions.outputs.cli_version }} @@ -56,7 +54,7 @@ jobs: - name: Install Rust stable uses: actions-rs/toolchain@v1 with: - toolchain: 1.86.0 + toolchain: 1.88.0 override: true - name: Build all binaries @@ -76,7 +74,6 @@ jobs: target/release/nym-network-requester target/release/nym-cli target/release/nymvisor - target/release/nym-node retention-days: 30 - id: create-release @@ -91,7 +88,6 @@ jobs: target/release/nym-network-requester target/release/nym-cli target/release/nymvisor - target/release/nym-node push-release-data-client: if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e14815cea..759005e547a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,48 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2025.16-halloumi] (2025-09-16) + +- Backport metadata endpoint ([#6010]) +- bugfix: make sure tables are removed in correct order to not trigger FK constraint issue ([#5987]) +- chore: move authenticator into gateway crate ([#5982]) +- Fix the ns api ci workflow ([#5981]) +- Remove freshness check on testrun submit ([#5977]) +- Update sysinfo to the latest ([#5976]) +- bugfix: manually calculate per node work on rewarded set changes ([#5972]) +- fixing the ci for ns agent ([#5965]) +- Feature/testing utils ([#5963]) +- bugfix: fix ci-build for linux (and use updated runner) ([#5958]) +- chore: updated refs to cheddar rev of nym repo ([#5955]) +- http api client adjustment ([#5953]) +- chore: fix rust 1.89 clippy issues ([#5944]) +- Wireguard metadata client library ([#5943]) +- chore: remove unused import ([#5942]) +- feat: introduce additional checks when attempting to send to bounded channels ([#5941]) +- Move credential verifier in peer controller ([#5938]) +- change PK/FK on expiration date signatures tables ([#5934]) +- Wireguard private metadata ([#5915]) + +[#6010]: https://github.com/nymtech/nym/pull/6010 +[#5987]: https://github.com/nymtech/nym/pull/5987 +[#5982]: https://github.com/nymtech/nym/pull/5982 +[#5981]: https://github.com/nymtech/nym/pull/5981 +[#5977]: https://github.com/nymtech/nym/pull/5977 +[#5976]: https://github.com/nymtech/nym/pull/5976 +[#5972]: https://github.com/nymtech/nym/pull/5972 +[#5965]: https://github.com/nymtech/nym/pull/5965 +[#5963]: https://github.com/nymtech/nym/pull/5963 +[#5958]: https://github.com/nymtech/nym/pull/5958 +[#5955]: https://github.com/nymtech/nym/pull/5955 +[#5953]: https://github.com/nymtech/nym/pull/5953 +[#5944]: https://github.com/nymtech/nym/pull/5944 +[#5943]: https://github.com/nymtech/nym/pull/5943 +[#5942]: https://github.com/nymtech/nym/pull/5942 +[#5941]: https://github.com/nymtech/nym/pull/5941 +[#5938]: https://github.com/nymtech/nym/pull/5938 +[#5934]: https://github.com/nymtech/nym/pull/5934 +[#5915]: https://github.com/nymtech/nym/pull/5915 + ## [2025.15-gruyere] (2025-08-20) - Migrate strum to 0.27.2 ([#5960]) diff --git a/Cargo.lock b/Cargo.lock index 7c2db6de8a2..360ca7476ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4825,7 +4825,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.64" +version = "1.1.65" dependencies = [ "anyhow", "async-trait", @@ -5016,7 +5016,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.61" +version = "1.1.62" dependencies = [ "anyhow", "base64 0.22.1", @@ -5098,7 +5098,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.61" +version = "1.1.62" dependencies = [ "bs58", "clap", @@ -5718,6 +5718,7 @@ dependencies = [ "nym-types", "nym-validator-client", "nym-wireguard", + "nym-wireguard-private-metadata-server", "nym-wireguard-types", "rand 0.8.5", "serde", @@ -6159,7 +6160,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.62" +version = "1.1.63" dependencies = [ "addr", "anyhow", @@ -6209,7 +6210,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.17.0" +version = "1.18.0" dependencies = [ "anyhow", "arc-swap", @@ -6674,7 +6675,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.61" +version = "1.1.62" dependencies = [ "bs58", "clap", @@ -7310,6 +7311,68 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-wireguard-private-metadata-client" +version = "1.0.0" +dependencies = [ + "async-trait", + "nym-http-api-client", + "nym-wireguard-private-metadata-shared", + "tracing", +] + +[[package]] +name = "nym-wireguard-private-metadata-server" +version = "1.0.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.7.9", + "futures", + "nym-credential-verification", + "nym-credentials-interface", + "nym-http-api-common", + "nym-wireguard", + "nym-wireguard-private-metadata-shared", + "tokio", + "tokio-util", + "tower-http 0.5.2", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "nym-wireguard-private-metadata-shared" +version = "1.0.0" +dependencies = [ + "axum 0.7.9", + "bincode", + "nym-credentials-interface", + "schemars 0.8.22", + "serde", + "thiserror 2.0.12", + "utoipa", +] + +[[package]] +name = "nym-wireguard-private-metadata-tests" +version = "1.0.0" +dependencies = [ + "async-trait", + "axum 0.7.9", + "nym-credential-verification", + "nym-credentials-interface", + "nym-http-api-client", + "nym-http-api-common", + "nym-wireguard", + "nym-wireguard-private-metadata-client", + "nym-wireguard-private-metadata-server", + "nym-wireguard-private-metadata-shared", + "tokio", + "tower-http 0.5.2", + "utoipa", +] + [[package]] name = "nym-wireguard-types" version = "0.1.0" @@ -7327,7 +7390,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.26" +version = "0.1.27" dependencies = [ "anyhow", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 6cf7625af6e..95b45ef28b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,10 @@ members = [ "common/wasm/storage", "common/wasm/utils", "common/wireguard", + "common/wireguard-private-metadata/client", + "common/wireguard-private-metadata/server", + "common/wireguard-private-metadata/shared", + "common/wireguard-private-metadata/tests", "common/wireguard-types", "common/zulip-client", "documentation/autodoc", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index a27a046e0ba..3736fc047c2 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.61" +version = "1.1.62" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 8f69f57566c..a4b1af15bec 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.61" +version = "1.1.62" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql b/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql index 042e65eed22..3c83cc0aaa4 100644 --- a/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql +++ b/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql @@ -110,14 +110,14 @@ FROM ecash_ticketbook; -- 6. finally swap out the old tables -- drop old tables -DROP TABLE expiration_date_signatures; DROP TABLE pending_issuance; DROP TABLE ecash_ticketbook; +DROP TABLE expiration_date_signatures; -- rename new tables -ALTER TABLE expiration_date_signatures_new - RENAME TO expiration_date_signatures; ALTER TABLE pending_issuance_new RENAME TO pending_issuance; ALTER TABLE ecash_ticketbook_new - RENAME TO ecash_ticketbook; \ No newline at end of file + RENAME TO ecash_ticketbook; +ALTER TABLE expiration_date_signatures_new + RENAME TO expiration_date_signatures; diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 2164c255145..61f9a1b30c5 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::ecash::traits::EcashManager; +use async_trait::async_trait; use bandwidth_storage_manager::BandwidthStorageManager; use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType}; @@ -139,3 +140,18 @@ impl CredentialVerifier { .await) } } + +#[async_trait] +pub trait TicketVerifier { + /// Verify that the ticket is valid and cryptographically correct. + /// If the verification succeeds, also increase the bandwidth with the ticket's + /// amount and return the latest available bandwidth + async fn verify(&mut self) -> Result; +} + +#[async_trait] +impl TicketVerifier for CredentialVerifier { + async fn verify(&mut self) -> Result { + self.verify().await + } +} diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 64359c96e79..60122efaf12 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -136,6 +136,7 @@ //! ``` #![warn(missing_docs)] +pub use reqwest::ClientBuilder as ReqwestClientBuilder; pub use reqwest::StatusCode; use crate::path::RequestPath; diff --git a/common/network-defaults/src/constants.rs b/common/network-defaults/src/constants.rs index e9b70f54e3b..1153fd902d6 100644 --- a/common/network-defaults/src/constants.rs +++ b/common/network-defaults/src/constants.rs @@ -47,7 +47,8 @@ pub mod nyx { pub mod wireguard { use std::net::{Ipv4Addr, Ipv6Addr}; - pub const WG_PORT: u16 = 51822; + pub const WG_TUNNEL_PORT: u16 = 51822; + pub const WG_METADATA_PORT: u16 = 51830; // The interface used to route traffic pub const WG_TUN_BASE_NAME: &str = "nymwg"; diff --git a/common/wireguard-private-metadata/client/Cargo.toml b/common/wireguard-private-metadata/client/Cargo.toml new file mode 100644 index 00000000000..d7d23ce4450 --- /dev/null +++ b/common/wireguard-private-metadata/client/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "nym-wireguard-private-metadata-client" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +tracing = { workspace = true } +nym-http-api-client = { path = "../../http-api-client" } +nym-wireguard-private-metadata-shared = { path = "../shared" } + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/client/src/lib.rs b/common/wireguard-private-metadata/client/src/lib.rs new file mode 100644 index 00000000000..3f683efe5e7 --- /dev/null +++ b/common/wireguard-private-metadata/client/src/lib.rs @@ -0,0 +1,58 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use tracing::instrument; + +use nym_http_api_client::{ApiClient, Client, HttpClientError, NO_PARAMS}; + +use nym_wireguard_private_metadata_shared::{ + routes, Version, {ErrorResponse, Request, Response}, +}; + +pub type WireguardMetadataApiClientError = HttpClientError; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait WireguardMetadataApiClient: ApiClient { + #[instrument(level = "debug", skip(self))] + async fn version(&self) -> Result { + let version: u64 = self + .get_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::VERSION], + NO_PARAMS, + ) + .await?; + Ok(version.into()) + } + + #[instrument(level = "debug", skip(self))] + async fn available_bandwidth( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::AVAILABLE], + NO_PARAMS, + request_body, + ) + .await + } + + #[instrument(level = "debug", skip(self, request_body))] + async fn topup_bandwidth( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::TOPUP], + NO_PARAMS, + request_body, + ) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl WireguardMetadataApiClient for Client {} diff --git a/common/wireguard-private-metadata/server/Cargo.toml b/common/wireguard-private-metadata/server/Cargo.toml new file mode 100644 index 00000000000..fa850181f13 --- /dev/null +++ b/common/wireguard-private-metadata/server/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "nym-wireguard-private-metadata-server" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +futures = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tokio-util = { workspace = true } +tower-http = { workspace = true, features = [ + "cors", + "trace", + "compression-br", + "compression-deflate", + "compression-gzip", + "compression-zstd", +] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } + +nym-credentials-interface = { path = "../../credentials-interface" } +nym-credential-verification = { path = "../../credential-verification" } +nym-http-api-common = { path = "../../http-api-common", features = [ + "middleware", + "utoipa", + "output", +] } +nym-wireguard = { path = "../../wireguard" } +nym-wireguard-private-metadata-shared = { path = "../shared" } + +[dev-dependencies] +async-trait = { workspace = true } + + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/server/src/http/mod.rs b/common/wireguard-private-metadata/server/src/http/mod.rs new file mode 100644 index 00000000000..e3d502d8b5d --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/mod.rs @@ -0,0 +1,46 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; + +use nym_wireguard::WgApiWrapper; + +pub(crate) mod openapi; +pub(crate) mod router; +pub(crate) mod state; + +/// Shutdown goes 2 directions: +/// 1. signal background tasks to gracefully finish +/// 2. signal server itself +/// +/// These are done through separate shutdown handles. Of course, shut down server +/// AFTER you have shut down BG tasks (or past their grace period). +#[allow(unused)] +pub struct ShutdownHandles { + axum_shutdown_button: CancellationToken, + /// Tokio JoinHandle for axum server's task + axum_join_handle: AxumJoinHandle, + /// Wireguard API for kernel interactions + wg_api: Arc, +} + +impl ShutdownHandles { + /// Cancellation token is given to Axum server constructor. When the token + /// receives a shutdown signal, Axum server will shut down gracefully. + pub fn new( + axum_join_handle: AxumJoinHandle, + wg_api: Arc, + axum_shutdown_button: CancellationToken, + ) -> Self { + Self { + axum_shutdown_button, + axum_join_handle, + wg_api, + } + } +} + +type AxumJoinHandle = JoinHandle>; diff --git a/common/wireguard-private-metadata/server/src/http/openapi.rs b/common/wireguard-private-metadata/server/src/http/openapi.rs new file mode 100644 index 00000000000..5e912665820 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/openapi.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use utoipa::OpenApi; + +use nym_wireguard_private_metadata_shared::{Request, Response}; + +#[derive(OpenApi)] +#[openapi( + info(title = "Nym Wireguard Private Metadata"), + tags(), + components(schemas(Request, Response)) +)] +pub(crate) struct ApiDoc; diff --git a/common/wireguard-private-metadata/server/src/http/router.rs b/common/wireguard-private-metadata/server/src/http/router.rs new file mode 100644 index 00000000000..c5936f1f4cf --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/router.rs @@ -0,0 +1,101 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::anyhow; +use axum::response::Redirect; +use axum::routing::get; +use axum::Router; +use core::net::SocketAddr; +use nym_http_api_common::middleware::logging::log_request_info; +use tokio::net::TcpListener; +use tokio_util::sync::WaitForCancellationFutureOwned; +use tower_http::cors::CorsLayer; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::http::openapi::ApiDoc; +use crate::http::state::AppState; +use crate::network::bandwidth_routes; + +/// Wrapper around `axum::Router` which ensures correct [order of layers][order]. +/// Add new routes as if you were working directly with `axum`. +/// +/// Why? Middleware like logger, CORS, TLS which need to handle request before other +/// layers should be added last. Using this builder pattern ensures that. +/// +/// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering +pub struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + /// All routes should be, if possible, added here. Exceptions are e.g. + /// routes which are added conditionally in other places based on some `if`. + pub fn with_default_routes() -> Self { + let default_routes = Router::new() + .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/", get(|| async { Redirect::to("/swagger") })) + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())); + Self { + unfinished_router: default_routes, + } + } + + /// Invoke this as late as possible before constructing HTTP server + /// (after all routes were added). + pub fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + /// Middleware added here intercepts the request before it gets to other routes. + fn finalize_routes(self) -> Router { + self.unfinished_router + .layer(setup_cors()) + .layer(axum::middleware::from_fn(log_request_info)) + } +} + +fn setup_cors() -> CorsLayer { + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([axum::http::Method::GET, axum::http::Method::POST]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} + +pub struct RouterWithState { + pub router: Router, +} + +impl RouterWithState { + pub async fn build_server(self, bind_address: &SocketAddr) -> anyhow::Result { + let listener = tokio::net::TcpListener::bind(bind_address) + .await + .map_err(|err| anyhow!("Couldn't bind to address {} due to {}", bind_address, err))?; + + Ok(ApiHttpServer { + router: self.router, + listener, + }) + } +} + +pub struct ApiHttpServer { + router: Router, + listener: TcpListener, +} + +impl ApiHttpServer { + pub async fn run(self, receiver: WaitForCancellationFutureOwned) -> Result<(), std::io::Error> { + // into_make_service_with_connect_info allows us to see client ip address + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(receiver) + .await + } +} diff --git a/common/wireguard-private-metadata/server/src/http/state.rs b/common/wireguard-private-metadata/server/src/http/state.rs new file mode 100644 index 00000000000..06916bb35fa --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/state.rs @@ -0,0 +1,35 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::IpAddr; + +use nym_credentials_interface::CredentialSpendingData; + +use crate::transceiver::PeerControllerTransceiver; +use nym_wireguard_private_metadata_shared::error::MetadataError; + +#[derive(Clone, axum::extract::FromRef)] +pub struct AppState { + transceiver: PeerControllerTransceiver, +} + +impl AppState { + pub fn new(transceiver: PeerControllerTransceiver) -> Self { + Self { transceiver } + } + + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + self.transceiver.query_bandwidth(ip).await + } + + // Top up with a credential and return the afterwards available bandwidth + pub async fn topup_bandwidth( + &self, + ip: IpAddr, + credential: CredentialSpendingData, + ) -> Result { + self.transceiver + .topup_bandwidth(ip, Box::new(credential)) + .await + } +} diff --git a/common/wireguard-private-metadata/server/src/lib.rs b/common/wireguard-private-metadata/server/src/lib.rs new file mode 100644 index 00000000000..3d772ac1e05 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod http; +mod network; +mod transceiver; + +pub use http::{ + router::{ApiHttpServer, RouterBuilder, RouterWithState}, + state::AppState, + ShutdownHandles, +}; +pub use transceiver::PeerControllerTransceiver; diff --git a/common/wireguard-private-metadata/server/src/network.rs b/common/wireguard-private-metadata/server/src/network.rs new file mode 100644 index 00000000000..36b22ba32b8 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/network.rs @@ -0,0 +1,111 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::SocketAddr; + +use axum::{ + extract::{ConnectInfo, Query, State}, + Json, Router, +}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_wireguard_private_metadata_shared::{ + interface::{RequestData, ResponseData}, + latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, +}; +use tower_http::compression::CompressionLayer; + +use crate::http::state::AppState; + +pub(crate) fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) +} + +#[utoipa::path( + tag = "bandwidth", + get, + path = "/v1/bandwidth/version", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) +} + +#[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/available", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn available_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct( + ResponseData::AvailableBandwidth(available_bandwidth), + version, + ) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} + +#[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/topup", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn topup_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(credential), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .topup_bandwidth(addr.ip(), *credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} diff --git a/common/wireguard-private-metadata/server/src/transceiver.rs b/common/wireguard-private-metadata/server/src/transceiver.rs new file mode 100644 index 00000000000..cbe77126cf7 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/transceiver.rs @@ -0,0 +1,307 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::IpAddr; + +use futures::channel::oneshot; +use tokio::sync::mpsc; + +use nym_credential_verification::ClientBandwidth; +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard::peer_controller::PeerControlRequest; +use nym_wireguard_private_metadata_shared::error::MetadataError; + +#[derive(Clone)] +pub struct PeerControllerTransceiver { + request_tx: mpsc::Sender, +} + +impl PeerControllerTransceiver { + pub fn new(request_tx: mpsc::Sender) -> Self { + Self { request_tx } + } + + async fn get_client_bandwidth(&self, ip: IpAddr) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::GetClientBandwidthByIp { ip, response_tx }; + self.request_tx + .send(msg) + .await + .map_err(|_| MetadataError::PeerInteractionStopped)?; + + response_rx + .await + .map_err(|_| MetadataError::NoResponse)? + .map_err(|err| MetadataError::Unsuccessful { + reason: err.to_string(), + }) + } + + pub(crate) async fn query_bandwidth(&self, ip: IpAddr) -> Result { + Ok(self.get_client_bandwidth(ip).await?.available().await) + } + + // Top up with a credential and return the afterwards available bandwidth + pub(crate) async fn topup_bandwidth( + &self, + ip: IpAddr, + credential: Box, + ) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::GetVerifierByIp { + ip, + credential, + response_tx, + }; + self.request_tx + .send(msg) + .await + .map_err(|_| MetadataError::PeerInteractionStopped)?; + + let mut verifier = response_rx + .await + .map_err(|_| MetadataError::NoResponse)? + .map_err(|err| MetadataError::Unsuccessful { + reason: err.to_string(), + })?; + let available_bandwidth = + verifier + .verify() + .await + .map_err(|err| MetadataError::CredentialVerification { + message: err.to_string(), + })?; + + Ok(available_bandwidth) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use nym_credential_verification::TicketVerifier; + use nym_wireguard::CONTROL_CHANNEL_SIZE; + + use super::*; + + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + pub(crate) struct MockVerifier { + ret: i64, + } + + impl MockVerifier { + pub(crate) fn new(ret: i64) -> MockVerifier { + Self { ret } + } + } + + #[async_trait::async_trait] + impl TicketVerifier for MockVerifier { + async fn verify(&mut self) -> nym_credential_verification::Result { + Ok(self.ret) + } + } + + #[tokio::test] + async fn get_bandwidth() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let bw = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap(); + assert_eq!(bw, 0); + } + + #[tokio::test] + async fn stop_peer() { + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + drop(request_rx); + let err = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap_err(); + assert_eq!(err, MetadataError::PeerInteractionStopped); + } + + #[tokio::test] + async fn unresponsive_peer() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { + ip: _, + response_tx: _, + } => {} + _ => panic!("Not expected"), + } + }); + + let err = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap_err(); + assert_eq!(err, MetadataError::NoResponse); + } + + #[tokio::test] + async fn unsuccessful_query_bandwidth() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Err(nym_wireguard::error::Error::Internal( + "testing".to_owned(), + ))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let ret = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await; + assert!(ret.is_err()); + } + + #[tokio::test] + async fn topup() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let verifier_bw = 42; + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new(verifier_bw)))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let bw = transceiver + .topup_bandwidth("10.0.0.42".parse().unwrap(), Box::new(credential)) + .await + .unwrap(); + assert_eq!(bw, verifier_bw); + } + + #[tokio::test] + async fn unsuccessful_topup() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Err(nym_wireguard::error::Error::Internal( + "testing".to_owned(), + ))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let ret = transceiver + .topup_bandwidth("10.0.0.42".parse().unwrap(), Box::new(credential)) + .await; + assert!(ret.is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/Cargo.toml b/common/wireguard-private-metadata/shared/Cargo.toml new file mode 100644 index 00000000000..26b3cc6cd6d --- /dev/null +++ b/common/wireguard-private-metadata/shared/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nym-wireguard-private-metadata-shared" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +axum = { workspace = true } +bincode = { workspace = true } +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true } +thiserror = { workspace = true } +utoipa = { workspace = true } + +nym-credentials-interface = { path = "../../credentials-interface" } + +[features] +testing = [] + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/shared/src/error.rs b/common/wireguard-private-metadata/shared/src/error.rs new file mode 100644 index 00000000000..3783462a4d7 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/error.rs @@ -0,0 +1,28 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum MetadataError { + #[error("peers can't be interacted with anymore")] + PeerInteractionStopped, + + #[error("no response received")] + NoResponse, + + #[error("query was not successful: {reason}")] + Unsuccessful { reason: String }, + + #[error("Models error: {message}")] + Models { message: String }, + + #[error("Credential verification error: {message}")] + CredentialVerification { message: String }, +} + +impl From for MetadataError { + fn from(value: crate::models::error::Error) -> Self { + Self::Models { + message: value.to_string(), + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs new file mode 100644 index 00000000000..de791578746 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -0,0 +1,20 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod error; +mod models; +pub mod routes; + +#[cfg(feature = "testing")] +pub use models::v0; +pub use models::{ + error::Error as ModelError, interface, latest, v1, AxumErrorResponse, AxumResult, Construct, + ErrorResponse, Extract, Request, Response, Version, +}; + +fn make_bincode_serializer() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/wireguard-private-metadata/shared/src/models/error.rs b/common/wireguard-private-metadata/shared/src/models/error.rs new file mode 100644 index 00000000000..45dc88617d4 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/error.rs @@ -0,0 +1,30 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::models::Version; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Bincode(#[from] bincode::Error), + + #[error("trying to deserialize from version {source_version:?} into {target_version:?}")] + InvalidVersion { + source_version: Version, + target_version: Version, + }, + + #[error( + "trying to deserialize from query type {source_query_type} query type {target_query_type}" + )] + InvalidQueryType { + source_query_type: String, + target_query_type: String, + }, + + #[error("update not possible from {from:?} to {to:?}")] + UpdateNotPossible { from: Version, to: Version }, + + #[error("downgrade not possible from {from:?} to {to:?}")] + DowngradeNotPossible { from: Version, to: Version }, +} diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs new file mode 100644 index 00000000000..3f4537f170f --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -0,0 +1,145 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; + +#[cfg(feature = "testing")] +use crate::models::v0; +use crate::models::{v1, Construct, Extract, Request, Response, Version}; + +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(Box), +} + +impl From for RequestData { + fn from(value: super::latest::interface::RequestData) -> Self { + match value { + super::latest::interface::RequestData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + super::latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for super::latest::interface::RequestData { + fn from(value: RequestData) -> Self { + match value { + RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for ResponseData { + fn from(value: super::latest::interface::ResponseData) -> Self { + match value { + super::latest::interface::ResponseData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + super::latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for super::latest::interface::ResponseData { + fn from(value: ResponseData) -> Self { + match value { + ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl Construct for Request { + fn construct(info: RequestData, version: Version) -> Result { + match version { + #[cfg(feature = "testing")] + Version::V0 => { + let translate_info = super::latest::interface::RequestData::from(info); + let downgrade_info = v0::interface::RequestData::try_from(translate_info)?; + let versioned_request = v0::VersionedRequest::construct(downgrade_info, version)?; + Ok(versioned_request.try_into()?) + } + Version::V1 => { + let versioned_request = v1::VersionedRequest::construct(info.into(), version)?; + Ok(versioned_request.try_into()?) + } + } + } +} + +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), crate::models::Error> { + match self.version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let versioned_request = v0::VersionedRequest::try_from(self.clone())?; + let (request, version) = versioned_request.extract()?; + + let upgrade_request = super::latest::interface::RequestData::try_from(request)?; + + Ok((upgrade_request.into(), version)) + } + super::Version::V1 => { + let versioned_request = v1::VersionedRequest::try_from(self.clone())?; + let (extracted, version) = versioned_request.extract()?; + Ok((extracted.into(), version)) + } + } + } +} + +pub enum ResponseData { + AvailableBandwidth(i64), + TopUpBandwidth(i64), +} + +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let translate_response = super::latest::interface::ResponseData::from(info); + let downgrade_response = v0::interface::ResponseData::try_from(translate_response)?; + let versioned_response = + v0::VersionedResponse::construct(downgrade_response, version)?; + Ok(versioned_response.try_into()?) + } + Version::V1 => { + let versioned_response = v1::VersionedResponse::construct(info.into(), version)?; + Ok(versioned_response.try_into()?) + } + } + } +} + +impl Extract for Response { + fn extract(&self) -> Result<(ResponseData, Version), super::error::Error> { + match self.version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let versioned_response = v0::VersionedResponse::try_from(self.clone())?; + let (response, version) = versioned_response.extract()?; + + let upgrade_response = super::latest::interface::ResponseData::try_from(response)?; + + Ok((upgrade_response.into(), version)) + } + super::Version::V1 => { + let versioned_response = v1::VersionedResponse::try_from(self.clone())?; + let (extracted, version) = versioned_response.extract()?; + Ok((extracted.into(), version)) + } + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/mod.rs b/common/wireguard-private-metadata/shared/src/models/mod.rs new file mode 100644 index 00000000000..e408d7c5dfb --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{Display, Formatter}; + +use axum::http::StatusCode; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub(crate) mod error; +pub mod interface; +#[cfg(feature = "testing")] +pub mod v0; // dummy version, only for filling boilerplate code for update/downgrade and testing +pub mod v1; + +pub use v1 as latest; + +use crate::models::error::Error; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum Version { + #[cfg(feature = "testing")] + /// only used for testing purposes, don't include it in your matching arms + V0, + V1, +} + +impl From for Version { + fn from(value: u64) -> Self { + #[cfg(feature = "testing")] + let zero_version = Version::V0; + #[cfg(not(feature = "testing"))] + let zero_version = latest::VERSION; + match value { + 0 => zero_version, + 1 => Version::V1, + _ => latest::VERSION, // if unknown, it means we're behind, so we can use the latest we know about + } + } +} + +impl From for u64 { + fn from(value: Version) -> Self { + // remember to modify the above match if you're bumping the version + match value { + #[cfg(feature = "testing")] + Version::V0 => 0, + Version::V1 => 1, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct Request { + pub version: Version, + pub(crate) inner: Vec, +} + +#[derive(Clone, Serialize, Deserialize, ToSchema)] +pub struct Response { + pub version: Version, + pub(crate) inner: Vec, +} + +pub trait Extract { + fn extract(&self) -> Result<(T, Version), Error>; +} + +pub trait Construct: Sized { + fn construct(info: T, version: Version) -> Result; +} + +pub type AxumResult = Result; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +pub struct ErrorResponse { + pub message: String, +} + +impl Display for ErrorResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.message.fmt(f) + } +} + +pub struct AxumErrorResponse { + message: ErrorResponse, + status: StatusCode, +} + +impl AxumErrorResponse { + pub fn bad_request(msg: impl Display) -> Self { + Self { + message: ErrorResponse { + message: msg.to_string(), + }, + status: StatusCode::BAD_REQUEST, + } + } +} + +impl axum::response::IntoResponse for AxumErrorResponse { + fn into_response(self) -> axum::response::Response { + (self.status, self.message.message.to_string()).into_response() + } +} + +mod tests { + #[allow(dead_code)] + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs new file mode 100644 index 00000000000..78dfdec7b57 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs new file mode 100644 index 00000000000..5243e312ee8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse {} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse {}; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn empty_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs new file mode 100644 index 00000000000..2f8fd2805cd --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs @@ -0,0 +1,73 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + QueryType, VersionedRequest, VersionedResponse, VERSION, +}; +use crate::models::{error::Error, Construct, Extract, Version}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth(_) => Ok(InnerTopUpRequest {}.try_into()?), + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + Ok((RequestData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let _req = InnerTopUpRequest::try_from(self.clone())?; + Ok((RequestData::TopUpBandwidth(()), VERSION)) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth(()) => { + Ok(InnerAvailableBandwidthResponse {}.try_into()?) + } + ResponseData::TopUpBandwidth(()) => Ok(InnerTopUpResponse {}.try_into()?), + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + Ok((ResponseData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let _resp = InnerTopUpResponse::try_from(self.clone())?; + Ok((ResponseData::TopUpBandwidth(()), VERSION)) + } + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs new file mode 100644 index 00000000000..997fbf6f572 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use bincode::Options; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + make_bincode_serializer, + models::{Request, Response, Version}, +}; + +pub(crate) mod available_bandwidth; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V0; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopupBandwidth, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + Ok(Request { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: Request) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + Ok(Response { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: Response) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[cfg(test)] +mod tests { + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + + use super::*; + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest {}) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs new file mode 100644 index 00000000000..9c333478d2e --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs @@ -0,0 +1,84 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest {} + +impl TryFrom for InnerTopUpRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerTopUpRequest) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest {}; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs new file mode 100644 index 00000000000..cd934b6e7e8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs @@ -0,0 +1,84 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse {} + +impl TryFrom for InnerTopUpResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerTopUpResponse) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse {}; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn empty_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs new file mode 100644 index 00000000000..78dfdec7b57 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs new file mode 100644 index 00000000000..f5addd609f3 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs @@ -0,0 +1,90 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse { + pub available_bandwidth: i64, +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse { + available_bandwidth: 42, + }; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs new file mode 100644 index 00000000000..46cafddc7da --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; + +#[cfg(feature = "testing")] +use super::super::v0 as previous; + +use super::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + QueryType, VersionedRequest, VersionedResponse, VERSION, +}; +use crate::models::{error::Error, Construct, Extract, Version}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(Box), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth(i64), + TopUpBandwidth(i64), +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth(credential) => Ok(InnerTopUpRequest { + credential: *credential, + } + .try_into()?), + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + Ok((RequestData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let req = InnerTopUpRequest::try_from(self.clone())?; + Ok(( + RequestData::TopUpBandwidth(Box::new(req.credential)), + VERSION, + )) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth(available_bandwidth) => { + Ok(InnerAvailableBandwidthResponse { + available_bandwidth, + } + .try_into()?) + } + ResponseData::TopUpBandwidth(available_bandwidth) => Ok(InnerTopUpResponse { + available_bandwidth, + } + .try_into()?), + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + Ok(( + ResponseData::AvailableBandwidth(resp.available_bandwidth), + VERSION, + )) + } + QueryType::TopupBandwidth => { + let resp = InnerTopUpResponse::try_from(self.clone())?; + Ok(( + ResponseData::TopUpBandwidth(resp.available_bandwidth), + VERSION, + )) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for RequestData { + type Error = super::Error; + + fn try_from(value: previous::interface::RequestData) -> Result { + match value { + previous::interface::RequestData::AvailableBandwidth(inner) => { + Ok(Self::AvailableBandwidth(inner)) + } + previous::interface::RequestData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for previous::interface::RequestData { + type Error = super::Error; + + fn try_from(value: RequestData) -> Result { + match value { + RequestData::AvailableBandwidth(inner) => Ok(Self::AvailableBandwidth(inner)), + RequestData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())), + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for ResponseData { + type Error = super::Error; + + fn try_from(value: previous::interface::ResponseData) -> Result { + match value { + previous::interface::ResponseData::AvailableBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + previous::interface::ResponseData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for previous::interface::ResponseData { + type Error = super::Error; + + fn try_from(value: ResponseData) -> Result { + match value { + ResponseData::AvailableBandwidth(_) => Ok(Self::AvailableBandwidth(())), + ResponseData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())), + } + } +} + +#[cfg(test)] +mod test { + use crate::models::tests::CREDENTIAL_BYTES; + + use super::*; + + #[test] + fn request_upgrade() { + assert_eq!( + RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) + .unwrap(), + RequestData::AvailableBandwidth(()) + ); + assert!( + RequestData::try_from(previous::interface::RequestData::TopUpBandwidth(())).is_err(), + ); + } + + #[test] + fn response_upgrade() { + assert!( + ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(())) + .is_err() + ); + assert!( + ResponseData::try_from(previous::interface::ResponseData::TopUpBandwidth(())).is_err() + ); + } + + #[test] + fn request_downgrade() { + assert_eq!( + previous::interface::RequestData::try_from(RequestData::AvailableBandwidth(())) + .unwrap(), + previous::interface::RequestData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::RequestData::try_from(RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))) + .unwrap(), + previous::interface::RequestData::TopUpBandwidth(()) + ); + } + + #[test] + fn response_downgrade() { + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth(42)) + .unwrap(), + previous::interface::ResponseData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::TopUpBandwidth(42)).unwrap(), + previous::interface::ResponseData::TopUpBandwidth(()) + ); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs new file mode 100644 index 00000000000..787a74f671b --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use bincode::Options; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + make_bincode_serializer, + models::{Request, Response, Version}, +}; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +pub(crate) mod available_bandwidth; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V1; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopupBandwidth, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + Ok(Request { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: Request) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + Ok(Response { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: Response) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[cfg(test)] +mod tests { + + use nym_credentials_interface::CredentialSpendingData; + + use crate::models::tests::CREDENTIAL_BYTES; + + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + + use super::*; + + #[test] + fn mismatched_request_version() { + let version = Version::V0; + let future_bw = Request { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedRequest::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn mismatched_response_version() { + let version = Version::V0; + let future_bw = Response { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedResponse::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse { + available_bandwidth: 42, + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse { + available_bandwidth: 42, + }) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs new file mode 100644 index 00000000000..871cc127ef8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs @@ -0,0 +1,92 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use nym_credentials_interface::CredentialSpendingData; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest { + /// Ecash credential + pub credential: CredentialSpendingData, +} + +impl TryFrom for InnerTopUpRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerTopUpRequest) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::models::tests::CREDENTIAL_BYTES; + + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs new file mode 100644 index 00000000000..08e0ef111f4 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs @@ -0,0 +1,88 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse { + pub available_bandwidth: i64, +} + +impl TryFrom for InnerTopUpResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerTopUpResponse) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse { + available_bandwidth: 42, + }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/routes.rs b/common/wireguard-private-metadata/shared/src/routes.rs new file mode 100644 index 00000000000..bda615fe1c8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/routes.rs @@ -0,0 +1,10 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const V1_API_VERSION: &str = "v1"; + +pub const BANDWIDTH: &str = "bandwidth"; + +pub const VERSION: &str = "version"; +pub const AVAILABLE: &str = "available"; +pub const TOPUP: &str = "topup"; diff --git a/common/wireguard-private-metadata/tests/Cargo.toml b/common/wireguard-private-metadata/tests/Cargo.toml new file mode 100644 index 00000000000..6827c16f602 --- /dev/null +++ b/common/wireguard-private-metadata/tests/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "nym-wireguard-private-metadata-tests" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +nym-credential-verification = { path = "../../credential-verification" } +nym-credentials-interface = { path = "../../credentials-interface" } +nym-http-api-client = { path = "../../http-api-client" } +nym-http-api-common = { path = "../../http-api-common", features = [ + "middleware", + "utoipa", + "output", +] } +nym-wireguard = { path = "../../wireguard" } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tower-http = { workspace = true, features = [ + "cors", + "trace", + "compression-br", + "compression-deflate", + "compression-gzip", + "compression-zstd", +] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } + +nym-wireguard-private-metadata-client = { path = "../client" } +nym-wireguard-private-metadata-shared = { path = "../shared", features = [ + "testing", +] } +nym-wireguard-private-metadata-server = { path = "../server" } + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/tests/src/lib.rs b/common/wireguard-private-metadata/tests/src/lib.rs new file mode 100644 index 00000000000..c1a981e484f --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/lib.rs @@ -0,0 +1,217 @@ +#[cfg(test)] +mod v0; + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use nym_credential_verification::{ClientBandwidth, TicketVerifier}; + use nym_credentials_interface::CredentialSpendingData; + use nym_http_api_client::Client; + use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; + use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; + use nym_wireguard_private_metadata_server::{ + AppState, PeerControllerTransceiver, RouterBuilder, + }; + use nym_wireguard_private_metadata_shared::{latest, v0, v1, ErrorResponse}; + use tokio::{net::TcpListener, sync::mpsc}; + + pub(crate) const VERIFIER_AVAILABLE_BANDWIDTH: i64 = 42; + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + pub(crate) struct MockVerifier { + ret: i64, + } + + impl MockVerifier { + pub(crate) fn new(ret: i64) -> MockVerifier { + Self { ret } + } + } + + #[async_trait::async_trait] + impl TicketVerifier for MockVerifier { + async fn verify(&mut self) -> nym_credential_verification::Result { + Ok(self.ret) + } + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = RouterBuilder::with_default_routes() + .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))) + .router; + + tokio::spawn(async move { + loop { + match request_rx.recv().await { + Some(PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx }) => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + Some(PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + }) => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + None => break, + _ => panic!("Not expected"), + } + } + }); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url::<_, ErrorResponse>(addr.to_string(), None).unwrap() + } + + #[tokio::test] + async fn query_latest_version() { + let client = spawn_server_and_create_client().await; + let version = client.version().await.unwrap(); + assert_eq!(version, latest::VERSION); + } + + #[tokio::test] + async fn query_against_server_v0() { + let client = super::v0::network::test::spawn_server_and_create_client().await; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v0::VERSION); + + // v0 reqwests + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + v0::TopUpResponse::try_from(response).unwrap(); + + // v1 reqwests + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + } + + #[tokio::test] + async fn query_against_server_v1() { + let client = spawn_server_and_create_client().await; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v1::VERSION); + + // v0 reqwests + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + + // v1 reqwests + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, 0); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::TopUpResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, VERIFIER_AVAILABLE_BANDWIDTH); + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/interface.rs b/common/wireguard-private-metadata/tests/src/v0/interface.rs new file mode 100644 index 00000000000..77cd2d12fe4 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/interface.rs @@ -0,0 +1,150 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_wireguard_private_metadata_shared::{ + v0 as latest, Construct, Extract, Request, Response, Version, +}; + +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl From for RequestData { + fn from(value: latest::interface::RequestData) -> Self { + match value { + latest::interface::RequestData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for latest::interface::RequestData { + fn from(value: RequestData) -> Self { + match value { + RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for ResponseData { + fn from(value: latest::interface::ResponseData) -> Self { + match value { + latest::interface::ResponseData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for latest::interface::ResponseData { + fn from(value: ResponseData) -> Self { + match value { + ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl Construct for Request { + fn construct( + info: RequestData, + version: Version, + ) -> Result { + match version { + Version::V0 => { + let translate_info = latest::interface::RequestData::from(info); + let versioned_request = + latest::VersionedRequest::construct(translate_info, latest::VERSION)?; + Ok(versioned_request.try_into()?) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { + from: version, + to: Version::V0, + }, + ), + } + } +} + +impl Extract for Request { + fn extract( + &self, + ) -> Result<(RequestData, Version), nym_wireguard_private_metadata_shared::ModelError> { + match self.version { + Version::V0 => { + let versioned_request = latest::VersionedRequest::try_from(self.clone())?; + let (request, version) = versioned_request.extract()?; + + Ok((request.into(), version)) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { + from: self.version, + to: Version::V0, + }, + ), + } + } +} + +pub enum ResponseData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl Construct for Response { + fn construct( + info: ResponseData, + version: Version, + ) -> Result { + match version { + Version::V0 => { + let translate_response = latest::interface::ResponseData::from(info); + let versioned_response = + latest::VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { + from: version, + to: Version::V0, + }, + ), + } + } +} + +impl Extract for Response { + fn extract( + &self, + ) -> Result<(ResponseData, Version), nym_wireguard_private_metadata_shared::ModelError> { + match self.version { + Version::V0 => { + let versioned_response = latest::VersionedResponse::try_from(self.clone())?; + let (response, version) = versioned_response.extract()?; + + Ok((response.into(), version)) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { + from: self.version, + to: Version::V0, + }, + ), + } + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/mod.rs b/common/wireguard-private-metadata/tests/src/v0/mod.rs new file mode 100644 index 00000000000..7338cc5ae23 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod interface; +pub(crate) mod network; diff --git a/common/wireguard-private-metadata/tests/src/v0/network.rs b/common/wireguard-private-metadata/tests/src/v0/network.rs new file mode 100644 index 00000000000..6a847ecc368 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/network.rs @@ -0,0 +1,146 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use std::net::SocketAddr; + + use crate::{ + tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}, + v0::interface::{RequestData, ResponseData}, + }; + use axum::{extract::Query, Json, Router}; + use nym_credential_verification::ClientBandwidth; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::ErrorResponse; + use nym_wireguard_private_metadata_shared::{ + v0 as latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, + }; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + use nym_wireguard_private_metadata_server::AppState; + + fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) + } + + #[utoipa::path( + tag = "bandwidth", + get, + path = "/v1/bandwidth/version", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + #[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/available", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn available_bandwidth( + Query(output): Query, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let response = Response::construct(ResponseData::AvailableBandwidth(()), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + #[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/topup", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn topup_bandwidth( + Query(output): Query, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let response = Response::construct(ResponseData::TopUpBandwidth(()), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = Router::new() + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) + .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url::<_, ErrorResponse>(addr.to_string(), None).unwrap() + } +} diff --git a/common/wireguard-types/src/config.rs b/common/wireguard-types/src/config.rs index fda6fc70f36..e322904d2e4 100644 --- a/common/wireguard-types/src/config.rs +++ b/common/wireguard-types/src/config.rs @@ -17,9 +17,13 @@ pub struct Config { /// default: `fc01::1` pub private_ipv6: Ipv6Addr, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. - pub announced_port: u16, + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_port: u16, /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. /// The maximum value for IPv4 is 32 diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 7716880fdd5..9fe3371fdea 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -18,11 +18,13 @@ use tokio::sync::mpsc::{self, Receiver, Sender}; #[cfg(target_os = "linux")] use nym_network_defaults::constants::WG_TUN_BASE_NAME; -pub(crate) mod error; +pub mod error; pub mod peer_controller; pub mod peer_handle; pub mod peer_storage_manager; +pub const CONTROL_CHANNEL_SIZE: usize = 256; + pub struct WgApiWrapper { inner: WGApi, } @@ -126,7 +128,7 @@ pub struct WireguardGatewayData { impl WireguardGatewayData { pub fn new(config: Config, keypair: Arc) -> (Self, Receiver) { - let (peer_tx, peer_rx) = mpsc::channel(1); + let (peer_tx, peer_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); ( WireguardGatewayData { config, @@ -178,10 +180,16 @@ pub async fn start_wireguard( let mut peer_bandwidth_managers = HashMap::with_capacity(peers.len()); for peer in peers.iter() { - let bandwidth_manager = Arc::new(RwLock::new( - PeerController::generate_bandwidth_manager(ecash_manager.storage(), &peer.public_key) + let bandwidth_manager = peer_handle::SharedBandwidthStorageManager::new( + Arc::new(RwLock::new( + PeerController::generate_bandwidth_manager( + ecash_manager.storage(), + &peer.public_key, + ) .await?, - )); + )), + peer.allowed_ips.clone(), + ); peer_bandwidth_managers.insert(peer.public_key.clone(), (bandwidth_manager, peer.clone())); } @@ -190,7 +198,7 @@ pub async fn start_wireguard( name: ifname.clone(), prvkey: BASE64_STANDARD.encode(wireguard_data.inner.keypair().private_key().to_bytes()), address: wireguard_data.inner.config().private_ipv4.to_string(), - port: wireguard_data.inner.config().announced_port as u32, + port: wireguard_data.inner.config().announced_tunnel_port as u32, peers, mtu: None, }; @@ -233,6 +241,7 @@ pub async fn start_wireguard( let host = wg_api.read_interface_data()?; let wg_api = std::sync::Arc::new(WgApiWrapper::new(wg_api)); + let mut controller = PeerController::new( ecash_manager, metrics, diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index 8708a9c6dfe..7da4a2336ed 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -10,15 +10,18 @@ use futures::channel::oneshot; use log::info; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ecash::traits::EcashManager, - BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, + BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, TicketVerifier, }; use nym_credentials_interface::CredentialSpendingData; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::traits::BandwidthGatewayStorage; use nym_node_metrics::NymNodeMetrics; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; -use std::time::{Duration, SystemTime}; use std::{collections::HashMap, sync::Arc}; +use std::{ + net::IpAddr, + time::{Duration, SystemTime}, +}; use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; @@ -41,22 +44,31 @@ pub enum PeerControlRequest { key: Key, response_tx: oneshot::Sender, }, - GetClientBandwidth { + GetClientBandwidthByKey { key: Key, response_tx: oneshot::Sender, }, - GetVerifier { + GetClientBandwidthByIp { + ip: IpAddr, + response_tx: oneshot::Sender, + }, + GetVerifierByKey { key: Key, credential: Box, response_tx: oneshot::Sender, }, + GetVerifierByIp { + ip: IpAddr, + credential: Box, + response_tx: oneshot::Sender, + }, } pub type AddPeerControlResponse = Result<()>; pub type RemovePeerControlResponse = Result<()>; pub type QueryPeerControlResponse = Result>; pub type GetClientBandwidthControlResponse = Result; -pub type QueryVerifierControlResponse = Result; +pub type QueryVerifierControlResponse = Result>; pub struct PeerController { ecash_verifier: Arc, @@ -77,7 +89,7 @@ pub struct PeerController { impl PeerController { #[allow(clippy::too_many_arguments)] - pub fn new( + pub(crate) fn new( ecash_verifier: Arc, metrics: NymNodeMetrics, wg_api: Arc, @@ -165,10 +177,13 @@ impl PeerController { async fn handle_add_request(&mut self, peer: &Peer) -> Result<()> { self.wg_api.configure_peer(peer)?; - let bandwidth_storage_manager = Arc::new(RwLock::new( - Self::generate_bandwidth_manager(self.ecash_verifier.storage(), &peer.public_key) - .await?, - )); + let bandwidth_storage_manager = SharedBandwidthStorageManager::new( + Arc::new(RwLock::new( + Self::generate_bandwidth_manager(self.ecash_verifier.storage(), &peer.public_key) + .await?, + )), + peer.allowed_ips.clone(), + ); let cached_peer_manager = CachedPeerManager::new(peer); let mut handle = PeerHandle::new( peer.public_key.clone(), @@ -192,7 +207,20 @@ impl PeerController { Ok(()) } - async fn handle_query_peer(&self, key: &Key) -> Result> { + async fn ip_to_key(&self, ip: IpAddr) -> Result> { + Ok(self + .bw_storage_managers + .iter() + .find_map(|(key, bw_manager)| { + bw_manager + .allowed_ips() + .iter() + .find(|ip_mask| ip_mask.ip == ip) + .and(Some(key.clone())) + })) + } + + async fn handle_query_peer_by_key(&self, key: &Key) -> Result> { Ok(self .ecash_verifier .storage() @@ -202,20 +230,32 @@ impl PeerController { .transpose()?) } - async fn handle_get_client_bandwidth(&self, key: &Key) -> Result { + async fn handle_get_client_bandwidth_by_key(&self, key: &Key) -> Result { let bandwidth_storage_manager = self .bw_storage_managers .get(key) .ok_or(Error::MissingClientBandwidthEntry)?; - Ok(bandwidth_storage_manager.read().await.client_bandwidth()) + Ok(bandwidth_storage_manager + .inner() + .read() + .await + .client_bandwidth()) + } + + async fn handle_get_client_bandwidth_by_ip(&self, ip: IpAddr) -> Result { + let Some(key) = self.ip_to_key(ip).await? else { + return Err(Error::MissingClientKernelEntry(ip.to_string())); + }; + + self.handle_get_client_bandwidth_by_key(&key).await } - async fn handle_query_verifier( + async fn handle_query_verifier_by_key( &self, key: &Key, credential: CredentialSpendingData, - ) -> Result { + ) -> Result> { let storage = self.ecash_verifier.storage(); let client_id = storage .get_wireguard_peer(&key.to_string()) @@ -225,7 +265,11 @@ impl PeerController { let Some(bandwidth_storage_manager) = self.bw_storage_managers.get(key) else { return Err(Error::MissingClientBandwidthEntry); }; - let client_bandwidth = bandwidth_storage_manager.read().await.client_bandwidth(); + let client_bandwidth = bandwidth_storage_manager + .inner() + .read() + .await + .client_bandwidth(); let verifier = CredentialVerifier::new( CredentialSpendingRequest::new(credential), self.ecash_verifier.clone(), @@ -237,7 +281,19 @@ impl PeerController { true, ), ); - Ok(verifier) + Ok(Box::new(verifier)) + } + + async fn handle_query_verifier_by_ip( + &self, + ip: IpAddr, + credential: CredentialSpendingData, + ) -> Result> { + let Some(key) = self.ip_to_key(ip).await? else { + return Err(Error::MissingClientKernelEntry(ip.to_string())); + }; + + self.handle_query_verifier_by_key(&key, credential).await } async fn update_metrics(&self, new_host: &Host) { @@ -340,18 +396,23 @@ impl PeerController { response_tx.send(self.remove_peer(&key).await).ok(); } Some(PeerControlRequest::QueryPeer { key, response_tx }) => { - response_tx.send(self.handle_query_peer(&key).await).ok(); + response_tx.send(self.handle_query_peer_by_key(&key).await).ok(); } - Some(PeerControlRequest::GetClientBandwidth { key, response_tx }) => { - response_tx.send(self.handle_get_client_bandwidth(&key).await).ok(); + Some(PeerControlRequest::GetClientBandwidthByKey { key, response_tx }) => { + response_tx.send(self.handle_get_client_bandwidth_by_key(&key).await).ok(); } - Some(PeerControlRequest::GetVerifier { key, credential, response_tx }) => { - response_tx.send(self.handle_query_verifier(&key, *credential).await).ok(); + Some(PeerControlRequest::GetClientBandwidthByIp { ip, response_tx }) => { + response_tx.send(self.handle_get_client_bandwidth_by_ip(ip).await).ok(); + } + Some(PeerControlRequest::GetVerifierByKey { key, credential, response_tx }) => { + response_tx.send(self.handle_query_verifier_by_key(&key, *credential).await).ok(); + } + Some(PeerControlRequest::GetVerifierByIp { ip, credential, response_tx }) => { + response_tx.send(self.handle_query_verifier_by_ip(ip, *credential).await).ok(); } None => { log::trace!("PeerController [main loop]: stopping since channel closed"); break; - } } } diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 014ec00fc5f..9eda055b2ec 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -4,7 +4,7 @@ use crate::error::Error; use crate::peer_controller::PeerControlRequest; use crate::peer_storage_manager::{CachedPeerManager, PeerInformation}; -use defguard_wireguard_rs::{host::Host, key::Key}; +use defguard_wireguard_rs::{host::Host, key::Key, net::IpAddrMask}; use futures::channel::oneshot; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; use nym_task::TaskClient; @@ -13,7 +13,28 @@ use std::sync::Arc; use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; -pub(crate) type SharedBandwidthStorageManager = Arc>; +#[derive(Clone)] +pub(crate) struct SharedBandwidthStorageManager { + inner: Arc>, + allowed_ips: Vec, +} + +impl SharedBandwidthStorageManager { + pub(crate) fn new( + inner: Arc>, + allowed_ips: Vec, + ) -> Self { + Self { inner, allowed_ips } + } + + pub(crate) fn inner(&self) -> &RwLock { + &self.inner + } + + pub(crate) fn allowed_ips(&self) -> &[IpAddrMask] { + &self.allowed_ips + } +} pub struct PeerHandle { public_key: Key, @@ -26,7 +47,7 @@ pub struct PeerHandle { } impl PeerHandle { - pub fn new( + pub(crate) fn new( public_key: Key, host_information: Arc>, cached_peer: CachedPeerManager, @@ -120,6 +141,7 @@ impl PeerHandle { if spent_bandwidth > 0 && self .bandwidth_storage_manager + .inner() .write() .await .try_use_bandwidth(spent_bandwidth) @@ -182,7 +204,7 @@ impl PeerHandle { _ = self.task_client.recv() => { log::trace!("PeerHandle: Received shutdown"); - if let Err(e) = self.bandwidth_storage_manager.write().await.sync_storage_bandwidth().await { + if let Err(e) = self.bandwidth_storage_manager.inner().write().await.sync_storage_bandwidth().await { log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); } diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md index 1a70eacf1d9..29cf2e2826e 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md @@ -58,8 +58,8 @@ Options: Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false] --wireguard-bind-address Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=] - --wireguard-announced-port - Port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=] + --wireguard-tunnel-announced-port + Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=] --wireguard-private-network-prefix The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=] --verloc-bind-address diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 445bc7a2bed..7c93c020d74 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -72,6 +72,7 @@ nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } nym-node-metrics = { path = "../nym-node/nym-node-metrics" } nym-wireguard = { path = "../common/wireguard" } +nym-wireguard-private-metadata-server = { path = "../common/wireguard-private-metadata/server" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } nym-authenticator-requests = { path = "../common/authenticator-requests" } diff --git a/gateway/src/node/internal_service_providers/authenticator/config/mod.rs b/gateway/src/node/internal_service_providers/authenticator/config/mod.rs index 6a11d8bf678..1a1e5423687 100644 --- a/gateway/src/node/internal_service_providers/authenticator/config/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/config/mod.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use nym_network_defaults::{ - WG_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, WG_TUN_DEVICE_NETMASK_V4, - WG_TUN_DEVICE_NETMASK_V6, + WG_METADATA_PORT, WG_TUNNEL_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, + WG_TUN_DEVICE_NETMASK_V4, WG_TUN_DEVICE_NETMASK_V6, }; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -46,9 +46,9 @@ pub struct Authenticator { /// default: `fc01::1` pub private_ipv6: Ipv6Addr, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. - pub announced_port: u16, + pub tunnel_announced_port: u16, /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. /// The maximum value for IPv4 is 32 @@ -62,10 +62,10 @@ pub struct Authenticator { impl Default for Authenticator { fn default() -> Self { Self { - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_PORT), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_TUNNEL_PORT), private_ipv4: WG_TUN_DEVICE_IP_ADDRESS_V4, private_ipv6: WG_TUN_DEVICE_IP_ADDRESS_V6, - announced_port: WG_PORT, + tunnel_announced_port: WG_TUNNEL_PORT, private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, } @@ -78,7 +78,8 @@ impl From for nym_wireguard_types::Config { bind_address: value.bind_address, private_ipv4: value.private_ipv4, private_ipv6: value.private_ipv6, - announced_port: value.announced_port, + announced_tunnel_port: value.tunnel_announced_port, + announced_metadata_port: WG_METADATA_PORT, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, } diff --git a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs index 3d343f4fbfa..c389055696d 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs @@ -291,7 +291,7 @@ impl MixnetListener { v1::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -304,7 +304,7 @@ impl MixnetListener { v2::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -317,7 +317,7 @@ impl MixnetListener { v3::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -330,7 +330,7 @@ impl MixnetListener { v4::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ips: (allowed_ipv4, allowed_ipv6).into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -343,7 +343,7 @@ impl MixnetListener { v5::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ips: (allowed_ipv4, allowed_ipv6).into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, request_id, ) @@ -374,7 +374,7 @@ impl MixnetListener { let registration_data = RegistrationData { nonce, gateway_data: gateway_data.clone(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }; registred_and_free .registration_in_progres @@ -691,7 +691,7 @@ impl MixnetListener { } else { let mut verifier = self .peer_manager - .query_verifier(msg.pub_key(), msg.credential()) + .query_verifier_by_key(msg.pub_key(), msg.credential()) .await?; let available_bandwidth = verifier.verify().await?; self.seen_credential_cache diff --git a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs index c13fb57b6ba..fce8cf71746 100644 --- a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs +++ b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs @@ -4,7 +4,7 @@ use crate::node::internal_service_providers::authenticator::error::AuthenticatorError; use defguard_wireguard_rs::{host::Peer, key::Key}; use futures::channel::oneshot; -use nym_credential_verification::{ClientBandwidth, CredentialVerifier}; +use nym_credential_verification::{ClientBandwidth, TicketVerifier}; use nym_credentials_interface::CredentialSpendingData; use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData}; use nym_wireguard_types::PeerPublicKey; @@ -99,7 +99,7 @@ impl PeerManager { ) -> Result { let key = Key::new(key.to_bytes()); let (response_tx, response_rx) = oneshot::channel(); - let msg = PeerControlRequest::GetClientBandwidth { key, response_tx }; + let msg = PeerControlRequest::GetClientBandwidthByKey { key, response_tx }; self.wireguard_gateway_data .peer_tx() .send(msg) @@ -120,14 +120,14 @@ impl PeerManager { }) } - pub async fn query_verifier( + pub async fn query_verifier_by_key( &mut self, key: PeerPublicKey, credential: CredentialSpendingData, - ) -> Result { + ) -> Result, AuthenticatorError> { let key = Key::new(key.to_bytes()); let (response_tx, response_rx) = oneshot::channel(); - let msg = PeerControlRequest::GetVerifier { + let msg = PeerControlRequest::GetVerifierByKey { key, credential: Box::new(credential), response_tx, @@ -398,13 +398,13 @@ mod tests { let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); assert!(peer_manager - .query_verifier(public_key, credential.clone()) + .query_verifier_by_key(public_key, credential.clone()) .await .is_err()); helper_add_peer(&storage, &mut peer_manager).await; peer_manager - .query_verifier(public_key, credential) + .query_verifier_by_key(public_key, credential) .await .unwrap(); diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 14d7571811d..a89a68cb239 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -19,7 +19,7 @@ use nym_network_defaults::NymNetworkDetails; use nym_network_requester::NRServiceProviderBuilder; use nym_node_metrics::events::MetricEventsSender; use nym_node_metrics::NymNodeMetrics; -use nym_task::TaskClient; +use nym_task::{ShutdownToken, TaskClient}; use nym_topology::TopologyProvider; use nym_validator_client::nyxd::{Coin, CosmWasmClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; @@ -91,7 +91,9 @@ pub struct GatewayTasksBuilder { mnemonic: Arc>, - shutdown: TaskClient, + legacy_task_client: TaskClient, + + shutdown_token: ShutdownToken, // populated and cached as necessary ecash_manager: Option>, @@ -105,7 +107,7 @@ impl Drop for GatewayTasksBuilder { fn drop(&mut self) { // disarm the shutdown as it was already used to construct relevant tasks and we don't want the builder // to cause shutdown - self.shutdown.disarm(); + self.legacy_task_client.disarm(); } } @@ -119,7 +121,8 @@ impl GatewayTasksBuilder { metrics_sender: MetricEventsSender, metrics: NymNodeMetrics, mnemonic: Arc>, - shutdown: TaskClient, + legacy_task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> GatewayTasksBuilder { GatewayTasksBuilder { config, @@ -133,7 +136,8 @@ impl GatewayTasksBuilder { metrics_sender, metrics, mnemonic, - shutdown, + legacy_task_client, + shutdown_token, ecash_manager: None, wireguard_peers: None, wireguard_networks: None, @@ -228,7 +232,7 @@ impl GatewayTasksBuilder { handler_config, nyxd_client, self.identity_keypair.public_key().to_bytes(), - self.shutdown.fork("ecash_manager"), + self.legacy_task_client.fork("ecash_manager"), self.storage.clone(), ) .await?, @@ -270,7 +274,7 @@ impl GatewayTasksBuilder { self.config.gateway.websocket_bind_address, self.config.debug.maximum_open_connections, shared_state, - self.shutdown.fork("websocket"), + self.legacy_task_client.fork("websocket"), )) } @@ -286,13 +290,14 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("network_requester_message_router"), + self.legacy_task_client + .fork("network_requester_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); let (on_start_tx, on_start_rx) = oneshot::channel(); let mut nr_builder = NRServiceProviderBuilder::new(nr_opts.config.clone()) - .with_shutdown(self.shutdown.fork("network_requester_sp")) + .with_shutdown(self.legacy_task_client.fork("network_requester_sp")) .with_custom_gateway_transceiver(transceiver) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) @@ -321,13 +326,13 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("ipr_message_router"), + self.legacy_task_client.fork("ipr_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); let (on_start_tx, on_start_rx) = oneshot::channel(); let mut ip_packet_router = IpPacketRouter::new(ip_opts.config.clone()) - .with_shutdown(self.shutdown.fork("ipr_sp")) + .with_shutdown(self.legacy_task_client.fork("ipr_sp")) .with_custom_gateway_transceiver(Box::new(transceiver)) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) @@ -427,7 +432,7 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("authenticator_message_router"), + self.legacy_task_client.fork("authenticator_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); @@ -440,7 +445,7 @@ impl GatewayTasksBuilder { ecash_manager, ) .with_custom_gateway_transceiver(transceiver) - .with_shutdown(self.shutdown.fork("authenticator_sp")) + .with_shutdown(self.legacy_task_client.fork("authenticator_sp")) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) .with_custom_topology_provider(topology_provider) @@ -460,7 +465,7 @@ impl GatewayTasksBuilder { pub fn build_stale_messages_cleaner(&self) -> StaleMessagesCleaner { StaleMessagesCleaner::new( &self.storage, - self.shutdown.fork("stale_messages_cleaner"), + self.legacy_task_client.fork("stale_messages_cleaner"), self.config.debug.stale_messages_max_age, self.config.debug.stale_messages_cleaner_run_interval, ) @@ -471,13 +476,17 @@ impl GatewayTasksBuilder { &mut self, ) -> Result, Box> { let _ = self.metrics.clone(); + let _ = self.shutdown_token.clone(); unimplemented!("wireguard is not supported on this platform") } #[cfg(target_os = "linux")] pub async fn try_start_wireguard( &mut self, - ) -> Result, Box> { + ) -> Result< + nym_wireguard_private_metadata_server::ShutdownHandles, + Box, + > { let all_peers = self.get_wireguard_peers().await?; let Some(wireguard_data) = self.wireguard_data.take() else { @@ -492,14 +501,43 @@ impl GatewayTasksBuilder { ); }; + let router = nym_wireguard_private_metadata_server::RouterBuilder::with_default_routes(); + let router = router.with_state(nym_wireguard_private_metadata_server::AppState::new( + nym_wireguard_private_metadata_server::PeerControllerTransceiver::new( + wireguard_data.inner.peer_tx().clone(), + ), + )); + + let bind_address = std::net::SocketAddr::new( + wireguard_data.inner.config().private_ipv4.into(), + wireguard_data.inner.config().announced_metadata_port, + ); + let wg_handle = nym_wireguard::start_wireguard( ecash_manager, self.metrics.clone(), all_peers, - self.shutdown.fork("wireguard"), + self.legacy_task_client.fork("wireguard"), wireguard_data, ) .await?; - Ok(wg_handle) + + let server = router.build_server(&bind_address).await?; + let cancel_token: tokio_util::sync::CancellationToken = (*self.shutdown_token).clone(); + let axum_shutdown_receiver = cancel_token.clone().cancelled_owned(); + let server_handle = tokio::spawn(async move { + { + info!("Started Wireguard Axum HTTP V2 server on {bind_address}"); + server.run(axum_shutdown_receiver).await + } + }); + + let shutdown_handles = nym_wireguard_private_metadata_server::ShutdownHandles::new( + server_handle, + wg_handle, + cancel_token, + ); + + Ok(shutdown_handles) } } diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 018f037dffd..cabca429bae 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.64" +version = "1.1.65" authors.workspace = true edition = "2021" rust-version.workspace = true diff --git a/nym-api/nym-api-requests/src/models/described.rs b/nym-api/nym-api-requests/src/models/described.rs index 666cca19966..36ee97be235 100644 --- a/nym-api/nym-api-requests/src/models/described.rs +++ b/nym-api/nym-api-requests/src/models/described.rs @@ -311,7 +311,10 @@ impl From for AuthenticatorDetails { } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct WireguardDetails { + // NOTE: the port field is deprecated in favour of tunnel_port pub port: u16, + pub tunnel_port: u16, + pub metadata_port: u16, pub public_key: String, } @@ -320,6 +323,8 @@ impl From for WireguardDetails { fn from(value: Wireguard) -> Self { WireguardDetails { port: value.port, + tunnel_port: value.tunnel_port, + metadata_port: value.metadata_port, public_key: value.public_key, } } diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index 79077003844..25fafef7986 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-credential-proxy" -version = "0.1.7" +version = "0.2.0" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 0dceb6aedbe..fcb45a818d4 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.17.0" +version = "1.18.0" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs index 24597847ca9..259bd35efab 100644 --- a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs @@ -16,9 +16,16 @@ pub struct Gateway { #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Wireguard { + #[deprecated(note = "use specific port instead (tunnel or metadata service)")] #[cfg_attr(feature = "openapi", schema(example = 51822, default = 51822))] pub port: u16, + #[cfg_attr(feature = "openapi", schema(example = 51822, default = 51822))] + pub tunnel_port: u16, + + #[cfg_attr(feature = "openapi", schema(example = 51830, default = 51830))] + pub metadata_port: u16, + pub public_key: String, } diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 3ff2388c45f..cf6d18b6831 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -276,13 +276,13 @@ pub(crate) struct WireguardArgs { )] pub(crate) wireguard_bind_address: Option, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. #[clap( long, env = NYMNODE_WG_ANNOUNCED_PORT_ARG )] - pub(crate) wireguard_announced_port: Option, + pub(crate) wireguard_tunnel_announced_port: Option, /// The prefix denoting the maximum number of the clients that can be connected via Wireguard. /// The maximum value for IPv4 is 32 and for IPv6 is 128 @@ -311,8 +311,8 @@ impl WireguardArgs { section.bind_address = bind_address } - if let Some(announced_port) = self.wireguard_announced_port { - section.announced_port = announced_port + if let Some(announced_tunnel_port) = self.wireguard_tunnel_announced_port { + section.announced_tunnel_port = announced_tunnel_port } if let Some(private_network_prefix) = self.wireguard_private_network_prefix { diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 282f26af78b..53769f9d809 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -202,7 +202,8 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { bind_address: config.wireguard.bind_address, private_ipv4: config.wireguard.private_ipv4, private_ipv6: config.wireguard.private_ipv6, - announced_port: config.wireguard.announced_port, + announced_tunnel_port: config.wireguard.announced_tunnel_port, + announced_metadata_port: config.wireguard.announced_metadata_port, private_network_prefix_v4: config.wireguard.private_network_prefix_v4, private_network_prefix_v6: config.wireguard.private_network_prefix_v6, storage_paths: config.wireguard.storage_paths.clone(), diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index baf9c8376d3..e401de4e208 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -10,7 +10,7 @@ use human_repr::HumanCount; use nym_bin_common::logging::LoggingSettings; use nym_config::defaults::{ mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT, - DEFAULT_VERLOC_LISTENING_PORT, WG_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, + DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT, WG_TUNNEL_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, }; use nym_config::defaults::{WG_TUN_DEVICE_NETMASK_V4, WG_TUN_DEVICE_NETMASK_V6}; @@ -931,9 +931,13 @@ pub struct Wireguard { /// default: `fc01::1` pub private_ipv6: Ipv6Addr, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. - pub announced_port: u16, + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the metadata endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_port: u16, /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. /// The maximum value for IPv4 is 32 @@ -951,10 +955,11 @@ impl Wireguard { pub fn new_default>(data_dir: P) -> Self { Wireguard { enabled: false, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_PORT), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_TUNNEL_PORT), private_ipv4: WG_TUN_DEVICE_IP_ADDRESS_V4, private_ipv6: WG_TUN_DEVICE_IP_ADDRESS_V6, - announced_port: WG_PORT, + announced_tunnel_port: WG_TUNNEL_PORT, + announced_metadata_port: WG_METADATA_PORT, private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, storage_paths: persistence::WireguardPaths::new(data_dir), @@ -968,7 +973,8 @@ impl From for nym_wireguard_types::Config { bind_address: value.bind_address, private_ipv4: value.private_ipv4, private_ipv6: value.private_ipv6, - announced_port: value.announced_port, + announced_tunnel_port: value.announced_tunnel_port, + announced_metadata_port: value.announced_metadata_port, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, } @@ -981,7 +987,7 @@ impl From for nym_authenticator::config::Authenticator { bind_address: value.bind_address, private_ipv4: value.private_ipv4, private_ipv6: value.private_ipv6, - announced_port: value.announced_port, + tunnel_announced_port: value.announced_tunnel_port, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, } diff --git a/nym-node/src/config/old_configs/mod.rs b/nym-node/src/config/old_configs/mod.rs index 48c1ede17b1..e67f2a83e9f 100644 --- a/nym-node/src/config/old_configs/mod.rs +++ b/nym-node/src/config/old_configs/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only mod old_config_v1; +mod old_config_v10; mod old_config_v2; mod old_config_v3; mod old_config_v4; @@ -12,6 +13,7 @@ mod old_config_v8; mod old_config_v9; pub use old_config_v1::try_upgrade_config_v1; +pub use old_config_v10::try_upgrade_config_v10; pub use old_config_v2::try_upgrade_config_v2; pub use old_config_v3::try_upgrade_config_v3; pub use old_config_v4::try_upgrade_config_v4; diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs new file mode 100644 index 00000000000..a55b5b11134 --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -0,0 +1,1695 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; +use crate::config::gateway_tasks::{ + ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, +}; +use crate::config::persistence::{ + AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, + NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, +}; +use crate::config::service_providers::{ + IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, +}; +use crate::config::{ + gateway_tasks, service_providers, Config, GatewayTasksConfig, Host, Http, KeyRotation, + KeyRotationDebug, Mixnet, MixnetDebug, NodeModes, ReplayProtection, ReplayProtectionDebug, + ServiceProvidersConfig, Verloc, VerlocDebug, Wireguard, DEFAULT_HTTP_PORT, +}; +use crate::error::NymNodeError; +use celes::Country; +use clap::ValueEnum; +use nym_bin_common::logging::LoggingSettings; +use nym_client_core_config_types::DebugConfig as ClientDebugConfig; +use nym_config::defaults::{DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT}; +use nym_config::helpers::{in6addr_any_init, inaddr_any}; +use nym_config::{ + defaults::TICKETBOOK_VALIDITY_DAYS, + read_config_from_toml_file, + serde_helpers::{de_maybe_port, de_maybe_stringified}, +}; +use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tracing::{debug, instrument}; +use url::Url; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardPathsV10 { + pub private_diffie_hellman_key_file: PathBuf, + pub public_diffie_hellman_key_file: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardV10 { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `[::]:51822` + pub bind_address: SocketAddr, + + /// Private IPv4 address of the wireguard gateway. + /// default: `10.1.0.1` + pub private_ipv4: Ipv4Addr, + + /// Private IPv6 address of the wireguard gateway. + /// default: `fc01::1` + pub private_ipv6: Ipv6Addr, + + /// Port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. + /// The maximum value for IPv4 is 32 + pub private_network_prefix_v4: u8, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. + /// The maximum value for IPv6 is 128 + pub private_network_prefix_v6: u8, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: WireguardPathsV10, +} + +// a temporary solution until all "types" are run at the same time +#[derive(Debug, Default, Serialize, Deserialize, ValueEnum, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum NodeModeV10 { + #[default] + #[clap(alias = "mix")] + Mixnode, + + #[clap(alias = "entry", alias = "gateway")] + EntryGateway, + + // to not break existing behaviour, this means exit capabilities AND entry capabilities + #[clap(alias = "exit")] + ExitGateway, + + // will start only SP needed for exit capabilities WITHOUT entry routing + ExitProvidersOnly, +} + +impl From for NodeModes { + fn from(config: NodeModeV10) -> Self { + match config { + NodeModeV10::Mixnode => *NodeModes::default().with_mixnode(), + NodeModeV10::EntryGateway => *NodeModes::default().with_entry(), + // in old version exit implied entry + NodeModeV10::ExitGateway => *NodeModes::default().with_entry().with_exit(), + NodeModeV10::ExitProvidersOnly => *NodeModes::default().with_exit(), + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] +pub struct NodeModesV10 { + /// Specifies whether this node can operate in a mixnode mode. + pub mixnode: bool, + + /// Specifies whether this node can operate in an entry mode. + pub entry: bool, + + /// Specifies whether this node can operate in an exit mode. + pub exit: bool, + // TODO: would it make sense to also put WG here for completion? +} + +// TODO: this is very much a WIP. we need proper ssl certificate support here +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HostV10 { + /// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. + /// If no values are provided, when this node gets included in the network, + /// its ip addresses will be populated by whatever value is resolved by associated nym-api. + pub public_ips: Vec, + + /// Optional hostname of this node, for example nymtech.net. + // TODO: this is temporary. to be replaced by pulling the data directly from the certs. + #[serde(deserialize_with = "de_maybe_stringified")] + pub hostname: Option, + + /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + #[serde(deserialize_with = "de_maybe_stringified")] + pub location: Option, +} + +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct KeyRotationDebugV10 { + /// Specifies how often the node should poll for any changes in the key rotation global state. + #[serde(with = "humantime_serde")] + pub rotation_state_poling_interval: Duration, +} + +impl KeyRotationDebugV10 { + pub const DEFAULT_ROTATION_STATE_POLLING_INTERVAL: Duration = Duration::from_secs(4 * 60 * 60); +} + +impl Default for KeyRotationDebugV10 { + fn default() -> Self { + KeyRotationDebugV10 { + rotation_state_poling_interval: Self::DEFAULT_ROTATION_STATE_POLLING_INTERVAL, + } + } +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct KeyRotationV10 { + pub debug: KeyRotationDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct MixnetDebugV10 { + /// Specifies the duration of time this node is willing to delay a forward packet for. + #[serde(with = "humantime_serde")] + pub maximum_forward_packet_delay: Duration, + + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_initial_backoff: Duration, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_maximum_backoff: Duration, + + /// Timeout for establishing initial connection when trying to forward a sphinx packet. + #[serde(with = "humantime_serde")] + pub initial_connection_timeout: Duration, + + /// Maximum number of packets that can be stored waiting to get sent to a particular connection. + pub maximum_connection_buffer_size: usize, + + /// Specify whether any framed packets between nodes should use the legacy format (v7) + /// as opposed to the current (v8) one. + /// The legacy format has to be used until sufficient number of nodes on the network has upgraded and understand the new variant. + /// This will allow for optimisations to indicate which [sphinx] key is meant to be used when + /// processing received packets. + pub use_legacy_packet_encoding: bool, + + /// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented) + pub unsafe_disable_noise: bool, +} + +impl MixnetDebugV10 { + // given that genuine clients are using mean delay of 50ms, + // the probability of them delaying for over 10s is 10^-87 + // which for all intents and purposes will never happen + pub(crate) const DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY: Duration = Duration::from_secs(10); + pub(crate) const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = + Duration::from_millis(10_000); + pub(crate) const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = + Duration::from_millis(300_000); + pub(crate) const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); + pub(crate) const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; +} + +impl Default for MixnetDebugV10 { + fn default() -> Self { + MixnetDebugV10 { + maximum_forward_packet_delay: Self::DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY, + packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT, + maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + // TODO: update this in few releases... + use_legacy_packet_encoding: true, + unsafe_disable_noise: false, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnetV10 { + /// Address this node will bind to for listening for mixnet packets + /// default: `[::]:1789` + pub bind_address: SocketAddr, + + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] + pub announce_port: Option, + + /// Addresses to nym APIs from which the node gets the view of the network. + pub nym_api_urls: Vec, + + /// Addresses to nyxd which the node uses to interact with the nyx chain. + pub nyxd_urls: Vec, + + /// Settings for controlling replay detection + pub replay_protection: ReplayProtectionV10, + + #[serde(default)] + pub key_rotation: KeyRotationV10, + + #[serde(default)] + pub debug: MixnetDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct ReplayProtectionV10 { + /// Paths for current bloomfilters + pub storage_paths: ReplayProtectionPathsV10, + + #[serde(default)] + pub debug: ReplayProtectionDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ReplayProtectionPathsV10 { + /// Path to the directory storing currently used bloomfilter(s). + pub current_bloomfilters_directory: PathBuf, +} + +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct ReplayProtectionDebugV10 { + /// Specifies whether this node should **NOT** use replay protection + pub unsafe_disabled: bool, + + /// How long the processing task is willing to skip mutex acquisition before it will block the thread + /// until it actually obtains it + pub maximum_replay_detection_deferral: Duration, + + /// How many packets the processing task is willing to queue before it will block the thread + /// until it obtains the mutex + pub maximum_replay_detection_pending_packets: usize, + + /// Probability of false positives, fraction between 0 and 1 or a number indicating 1-in-p + pub false_positive_rate: f64, + + /// Defines initial expected number of packets this node will process a second, + /// so that an initial bloomfilter could be established. + /// As the node is running and BF are cleared, the value will be adjusted dynamically + pub initial_expected_packets_per_second: usize, + + /// Defines minimum expected number of packets this node will process a second + /// when used for calculating the BF size after reset. + /// This is to avoid degenerate cases where node receives 0 packets (because say it's misconfigured) + /// and it constructs an empty bloomfilter. + pub bloomfilter_minimum_packets_per_second_size: usize, + + /// Specifies the amount the bloomfilter size is going to get multiplied by after each reset. + /// It's performed in case the traffic rates increase before the next bloomfilter update. + pub bloomfilter_size_multiplier: f64, + + /// Specifies how often the bloomfilter is flushed to disk for recovery in case of a crash + #[serde(with = "humantime_serde")] + pub bloomfilter_disk_flushing_rate: Duration, +} + +impl ReplayProtectionDebugV10 { + pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL: Duration = Duration::from_millis(50); + + pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS: usize = 100; + + // 12% (completely arbitrary) + pub const DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER: f64 = 1.12; + + // 10^-5 + pub const DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE: f64 = 1e-5; + + // we must have some reasonable balance between losing values and trashing the disk. + // since on average HDD it would take ~30s to save a 2GB bloomfilter + pub const DEFAULT_BF_DISK_FLUSHING_RATE: Duration = Duration::from_secs(10 * 60); + + // this value will have to be adjusted in the future + pub const DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND: usize = 2000; + + pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200; +} + +impl Default for ReplayProtectionDebugV10 { + fn default() -> Self { + ReplayProtectionDebugV10 { + unsafe_disabled: false, + maximum_replay_detection_deferral: Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL, + maximum_replay_detection_pending_packets: + Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS, + false_positive_rate: Self::DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE, + initial_expected_packets_per_second: Self::DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND, + bloomfilter_minimum_packets_per_second_size: + Self::DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE, + bloomfilter_size_multiplier: Self::DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER, + bloomfilter_disk_flushing_rate: Self::DEFAULT_BF_DISK_FLUSHING_RATE, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct KeysPathsV10 { + /// Path to file containing ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing the primary x25519 sphinx private key. + pub primary_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing the secondary x25519 sphinx private key. + pub secondary_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing x25519 noise private key. + pub private_x25519_noise_key_file: PathBuf, + + /// Path to file containing x25519 noise public key. + pub public_x25519_noise_key_file: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NymNodePathsV10 { + pub keys: KeysPathsV10, + + /// Path to a file containing basic node description: human-readable name, website, details, etc. + pub description: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HttpV10 { + /// Socket address this node will use for binding its http API. + /// default: `[::]:8080` + pub bind_address: SocketAddr, + + /// Path to assets directory of custom landing page of this node. + #[serde(deserialize_with = "de_maybe_stringified")] + pub landing_page_assets_path: Option, + + /// An optional bearer token for accessing certain http endpoints. + /// Currently only used for obtaining mixnode's stats. + #[serde(default)] + pub access_token: Option, + + /// Specify whether basic system information should be exposed. + /// default: true + pub expose_system_info: bool, + + /// Specify whether basic system hardware information should be exposed. + /// This option is superseded by `expose_system_info` + /// default: true + pub expose_system_hardware: bool, + + /// Specify whether detailed system crypto hardware information should be exposed. + /// This option is superseded by `expose_system_hardware` + /// default: true + pub expose_crypto_hardware: bool, + + /// Specify the cache ttl of the node load. + /// default: 30s + #[serde(with = "humantime_serde")] + pub node_load_cache_ttl: Duration, +} + +impl HttpV10 { + pub const DEFAULT_NODE_LOAD_CACHE_TTL: Duration = Duration::from_secs(30); +} + +impl Default for HttpV10 { + fn default() -> Self { + HttpV10 { + bind_address: SocketAddr::new(inaddr_any(), DEFAULT_HTTP_PORT), + landing_page_assets_path: None, + access_token: None, + expose_system_info: true, + expose_system_hardware: true, + expose_crypto_hardware: true, + node_load_cache_ttl: Self::DEFAULT_NODE_LOAD_CACHE_TTL, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodePathsV10 {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DebugV10 { + /// Delay between each subsequent node statistics being logged to the console + #[serde(with = "humantime_serde")] + pub node_stats_logging_delay: Duration, + + /// Delay between each subsequent node statistics being updated + #[serde(with = "humantime_serde")] + pub node_stats_updating_delay: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocDebugV10 { + /// Specifies number of echo packets sent to each node during a measurement run. + pub packets_per_node: usize, + + /// Specifies maximum amount of time to wait for the connection to get established. + #[serde(with = "humantime_serde")] + pub connection_timeout: Duration, + + /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. + #[serde(with = "humantime_serde")] + pub packet_timeout: Duration, + + /// Specifies delay between subsequent test packets being sent (after receiving a reply). + #[serde(with = "humantime_serde")] + pub delay_between_packets: Duration, + + /// Specifies number of nodes being tested at once. + pub tested_nodes_batch_size: usize, + + /// Specifies delay between subsequent test runs. + #[serde(with = "humantime_serde")] + pub testing_interval: Duration, + + /// Specifies delay between attempting to run the measurement again if the previous run failed + /// due to being unable to get the list of nodes. + #[serde(with = "humantime_serde")] + pub retry_timeout: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocV10 { + /// Socket address this node will use for binding its verloc API. + /// default: `[::]:1790` + pub bind_address: SocketAddr, + + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] + pub announce_port: Option, + + #[serde(default)] + pub debug: VerlocDebugV10, +} + +impl VerlocV10 { + pub const DEFAULT_VERLOC_PORT: u16 = DEFAULT_VERLOC_LISTENING_PORT; +} + +impl Default for VerlocV10 { + fn default() -> Self { + VerlocV10 { + bind_address: SocketAddr::new(in6addr_any_init(), Self::DEFAULT_VERLOC_PORT), + announce_port: None, + debug: Default::default(), + } + } +} + +impl VerlocDebugV10 { + const DEFAULT_PACKETS_PER_NODE: usize = 100; + const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); + const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); + const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); + const DEFAULT_BATCH_SIZE: usize = 50; + const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); + const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); +} + +impl Default for VerlocDebugV10 { + fn default() -> Self { + VerlocDebugV10 { + packets_per_node: Self::DEFAULT_PACKETS_PER_NODE, + connection_timeout: Self::DEFAULT_CONNECTION_TIMEOUT, + packet_timeout: Self::DEFAULT_PACKET_TIMEOUT, + delay_between_packets: Self::DEFAULT_DELAY_BETWEEN_PACKETS, + tested_nodes_batch_size: Self::DEFAULT_BATCH_SIZE, + testing_interval: Self::DEFAULT_TESTING_INTERVAL, + retry_timeout: Self::DEFAULT_RETRY_TIMEOUT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodeConfigV10 { + pub storage_paths: MixnodePathsV10, + + pub verloc: VerlocV10, + + #[serde(default)] + pub debug: DebugV10, +} + +impl DebugV10 { + const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000); + const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000); +} + +impl Default for DebugV10 { + fn default() -> Self { + DebugV10 { + node_stats_logging_delay: Self::DEFAULT_NODE_STATS_LOGGING_DELAY, + node_stats_updating_delay: Self::DEFAULT_NODE_STATS_UPDATING_DELAY, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayPathsV10 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys and available client bandwidths. + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, + + pub authenticator: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ZkNymTicketHandlerDebugV10 { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than maximum validity. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebugV10 { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + + // use min(4/5 of max validity, validity - 1), but making sure it's no greater than 1 day + // ASSUMPTION: our validity period is AT LEAST 2 days + // + // this could have been a constant, but it's more readable as a function + pub const fn default_maximum_time_between_redemption() -> Duration { + let desired_secs = TICKETBOOK_VALIDITY_DAYS * (86400 * 4) / 5; + let desired_secs_alt = (TICKETBOOK_VALIDITY_DAYS - 1) * 86400; + + // can't use `min` in const context + let target_secs = if desired_secs < desired_secs_alt { + desired_secs + } else { + desired_secs_alt + }; + + assert!( + target_secs > 86400, + "the maximum time between redemption can't be lower than 1 day!" + ); + Duration::from_secs(target_secs as u64) + } +} + +impl Default for ZkNymTicketHandlerDebugV10 { + fn default() -> Self { + ZkNymTicketHandlerDebugV10 { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::default_maximum_time_between_redemption(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigDebugV10 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, + pub zk_nym_tickets: ZkNymTicketHandlerDebugV10, +} + +impl EntryGatewayConfigDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for EntryGatewayConfigDebugV10 { + fn default() -> Self { + EntryGatewayConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigV10 { + pub storage_paths: EntryGatewayPathsV10, + + /// Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `[::]:9000` + pub bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: EntryGatewayConfigDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NetworkRequesterPathsV10 { + /// Path to file containing network requester ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct IpPacketRouterPathsV10 { + /// Path to file containing ip packet router ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct AuthenticatorPathsV10 { + /// Path to file containing authenticator ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayPathsV10 { + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV10, + + pub ip_packet_router: IpPacketRouterPathsV10, + + pub authenticator: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct AuthenticatorV10 { + #[serde(default)] + pub debug: AuthenticatorDebugV10, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct AuthenticatorDebugV10 { + /// Specifies whether authenticator service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run + /// the authenticator. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for AuthenticatorDebugV10 { + fn default() -> Self { + AuthenticatorDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[allow(clippy::derivable_impls)] +impl Default for AuthenticatorV10 { + fn default() -> Self { + AuthenticatorV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct IpPacketRouterDebugV10 { + /// Specifies whether ip packet routing service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for IpPacketRouterDebugV10 { + fn default() -> Self { + IpPacketRouterDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct IpPacketRouterV10 { + #[serde(default)] + pub debug: IpPacketRouterDebugV10, +} + +#[allow(clippy::derivable_impls)] +impl Default for IpPacketRouterV10 { + fn default() -> Self { + IpPacketRouterV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterDebugV10 { + /// Specifies whether network requester service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for NetworkRequesterDebugV10 { + fn default() -> Self { + NetworkRequesterDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterV10 { + #[serde(default)] + pub debug: NetworkRequesterDebugV10, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequesterV10 { + fn default() -> Self { + NetworkRequesterV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayDebugV10 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl ExitGatewayDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ExitGatewayDebugV10 { + fn default() -> Self { + ExitGatewayDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayConfigV10 { + pub storage_paths: ExitGatewayPathsV10, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequesterV10, + + pub ip_packet_router: IpPacketRouterV10, + + #[serde(default)] + pub debug: ExitGatewayDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksPathsV10 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys, available client bandwidths and wireguard peers. + pub clients_storage: PathBuf, + + /// Path to sqlite database containing all persistent stats data. + pub stats_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct StaleMessageDebugV10 { + /// Specifies how often the clean-up task should check for stale data. + #[serde(with = "humantime_serde")] + pub cleaner_run_interval: Duration, + + /// Specifies maximum age of stored messages before they are removed from the storage + #[serde(with = "humantime_serde")] + pub max_age: Duration, +} + +impl StaleMessageDebugV10 { + const DEFAULT_STALE_MESSAGES_CLEANER_RUN_INTERVAL: Duration = Duration::from_secs(60 * 60); + const DEFAULT_STALE_MESSAGES_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60); +} + +impl Default for StaleMessageDebugV10 { + fn default() -> Self { + StaleMessageDebugV10 { + cleaner_run_interval: Self::DEFAULT_STALE_MESSAGES_CLEANER_RUN_INTERVAL, + max_age: Self::DEFAULT_STALE_MESSAGES_MAX_AGE, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct ClientBandwidthDebugV10 { + /// Defines maximum delay between client bandwidth information being flushed to the persistent storage. + pub max_flushing_rate: Duration, + + /// Defines a maximum change in client bandwidth before it gets flushed to the persistent storage. + pub max_delta_flushing_amount: i64, +} + +impl ClientBandwidthDebugV10 { + const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5); + const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB +} + +impl Default for ClientBandwidthDebugV10 { + fn default() -> Self { + ClientBandwidthDebugV10 { + max_flushing_rate: Self::DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE, + max_delta_flushing_amount: Self::DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(default)] +pub struct GatewayTasksConfigDebugV10 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, + + /// The maximum number of client connections the gateway will keep open at once. + pub maximum_open_connections: usize, + + /// Specifies the minimum performance of mixnodes in the network that are to be used in internal topologies + /// of the services providers + pub minimum_mix_performance: u8, + + /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. + #[serde(alias = "maximum_auth_request_age")] + pub max_request_timestamp_skew: Duration, + + pub stale_messages: StaleMessageDebugV10, + + pub client_bandwidth: ClientBandwidthDebugV10, + + pub zk_nym_tickets: ZkNymTicketHandlerDebugV10, +} + +impl GatewayTasksConfigDebugV10 { + pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); + pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; +} + +impl Default for GatewayTasksConfigDebugV10 { + fn default() -> Self { + GatewayTasksConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, + max_request_timestamp_skew: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW, + minimum_mix_performance: Self::DEFAULT_MINIMUM_MIX_PERFORMANCE, + stale_messages: Default::default(), + client_bandwidth: Default::default(), + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksConfigV10 { + pub storage_paths: GatewayTasksPathsV10, + + /// Indicates whether this gateway is accepting only zk-nym credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `[::]:9000` + pub ws_bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: GatewayTasksConfigDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersPathsV10 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys, available client bandwidths and wireguard peers. + pub clients_storage: PathBuf, + + /// Path to sqlite database containing all persistent stats data. + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV10, + + pub ip_packet_router: IpPacketRouterPathsV10, + + pub authenticator: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfigDebugV10 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl ServiceProvidersConfigDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ServiceProvidersConfigDebugV10 { + fn default() -> Self { + ServiceProvidersConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfigV10 { + pub storage_paths: ServiceProvidersPathsV10, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequesterV10, + + pub ip_packet_router: IpPacketRouterV10, + + pub authenticator: AuthenticatorV10, + + #[serde(default)] + pub debug: ServiceProvidersConfigDebugV10, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsConfigV10 { + #[serde(default)] + pub debug: MetricsDebugV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsDebugV10 { + /// Specify whether running statistics of this node should be logged to the console. + pub log_stats_to_console: bool, + + /// Specify the rate of which the metrics aggregator should call the `on_update` methods of all its registered handlers. + #[serde(with = "humantime_serde")] + pub aggregator_update_rate: Duration, + + /// Specify the target rate of clearing old stale mixnet metrics. + #[serde(with = "humantime_serde")] + pub stale_mixnet_metrics_cleaner_rate: Duration, + + /// Specify the target rate of updating global prometheus counters. + #[serde(with = "humantime_serde")] + pub global_prometheus_counters_update_rate: Duration, + + /// Specify the target rate of updating egress packets pending delivery counter. + #[serde(with = "humantime_serde")] + pub pending_egress_packets_update_rate: Duration, + + /// Specify the rate of updating clients sessions + #[serde(with = "humantime_serde")] + pub clients_sessions_update_rate: Duration, + + /// If console logging is enabled, specify the interval at which that happens + #[serde(with = "humantime_serde")] + pub console_logging_update_interval: Duration, + + /// Specify the update rate of running stats for the legacy `/metrics/mixing` endpoint + #[serde(with = "humantime_serde")] + pub legacy_mixing_metrics_update_rate: Duration, +} + +impl MetricsDebugV10 { + const DEFAULT_CONSOLE_LOGGING_INTERVAL: Duration = Duration::from_millis(60_000); + const DEFAULT_LEGACY_MIXING_UPDATE_RATE: Duration = Duration::from_millis(30_000); + const DEFAULT_AGGREGATOR_UPDATE_RATE: Duration = Duration::from_secs(5); + const DEFAULT_STALE_MIXNET_METRICS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const DEFAULT_CLIENT_SESSIONS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const GLOBAL_PROMETHEUS_COUNTERS_UPDATE_INTERVAL: Duration = Duration::from_secs(30); + const DEFAULT_PENDING_EGRESS_PACKETS_UPDATE_RATE: Duration = Duration::from_secs(30); +} + +impl Default for MetricsDebugV10 { + fn default() -> Self { + MetricsDebugV10 { + log_stats_to_console: true, + console_logging_update_interval: Self::DEFAULT_CONSOLE_LOGGING_INTERVAL, + legacy_mixing_metrics_update_rate: Self::DEFAULT_LEGACY_MIXING_UPDATE_RATE, + aggregator_update_rate: Self::DEFAULT_AGGREGATOR_UPDATE_RATE, + stale_mixnet_metrics_cleaner_rate: Self::DEFAULT_STALE_MIXNET_METRICS_UPDATE_RATE, + global_prometheus_counters_update_rate: + Self::GLOBAL_PROMETHEUS_COUNTERS_UPDATE_INTERVAL, + pending_egress_packets_update_rate: Self::DEFAULT_PENDING_EGRESS_PACKETS_UPDATE_RATE, + clients_sessions_update_rate: Self::DEFAULT_CLIENT_SESSIONS_UPDATE_RATE, + } + } +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LoggingSettingsV10 { + // well, we need to implement something here at some point... +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV10 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current modes of this nym-node. + pub modes: NodeModesV10, + + pub host: HostV10, + + pub mixnet: MixnetV10, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV10, + + #[serde(default)] + pub http: HttpV10, + + #[serde(default)] + pub verloc: VerlocV10, + + pub wireguard: WireguardV10, + + #[serde(alias = "entry_gateway")] + pub gateway_tasks: GatewayTasksConfigV10, + + #[serde(alias = "exit_gateway")] + pub service_providers: ServiceProvidersConfigV10, + + #[serde(default)] + pub metrics: MetricsConfigV10, + + #[serde(default)] + pub logging: LoggingSettingsV10, + + #[serde(default)] + pub debug: DebugV10, +} + +impl ConfigV10 { + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: ConfigV10 = + read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + })?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } +} + +#[instrument(skip_all)] +pub async fn try_upgrade_config_v10>( + path: P, + prev_config: Option, +) -> Result { + debug!("attempting to load v10 config..."); + + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV10::read_from_path(&path)? + }; + + let cfg = Config { + save_path: old_cfg.save_path, + id: old_cfg.id, + modes: NodeModes { + mixnode: old_cfg.modes.mixnode, + entry: old_cfg.modes.entry, + exit: old_cfg.modes.exit, + }, + host: Host { + public_ips: old_cfg.host.public_ips, + hostname: old_cfg.host.hostname, + location: old_cfg.host.location, + }, + mixnet: Mixnet { + bind_address: old_cfg.mixnet.bind_address, + announce_port: old_cfg.mixnet.announce_port, + nym_api_urls: old_cfg.mixnet.nym_api_urls, + nyxd_urls: old_cfg.mixnet.nyxd_urls, + replay_protection: ReplayProtection { + storage_paths: ReplayProtectionPaths { + current_bloomfilters_directory: old_cfg + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory, + }, + debug: ReplayProtectionDebug { + unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, + maximum_replay_detection_deferral: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_deferral, + maximum_replay_detection_pending_packets: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_pending_packets, + false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, + initial_expected_packets_per_second: old_cfg + .mixnet + .replay_protection + .debug + .initial_expected_packets_per_second, + bloomfilter_minimum_packets_per_second_size: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_minimum_packets_per_second_size, + bloomfilter_size_multiplier: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_size_multiplier, + bloomfilter_disk_flushing_rate: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_disk_flushing_rate, + }, + }, + key_rotation: KeyRotation { + debug: KeyRotationDebug { + rotation_state_poling_interval: old_cfg + .mixnet + .key_rotation + .debug + .rotation_state_poling_interval, + }, + }, + debug: MixnetDebug { + maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, + packet_forwarding_initial_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_maximum_backoff, + initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, + maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, + unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, + use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, + }, + }, + storage_paths: NymNodePaths { + keys: KeysPaths { + private_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file, + primary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .primary_x25519_sphinx_key_file, + private_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .private_x25519_noise_key_file, + public_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .public_x25519_noise_key_file, + secondary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .secondary_x25519_sphinx_key_file, + }, + description: old_cfg.storage_paths.description, + }, + http: Http { + bind_address: old_cfg.http.bind_address, + landing_page_assets_path: old_cfg.http.landing_page_assets_path, + access_token: old_cfg.http.access_token, + expose_system_info: old_cfg.http.expose_system_info, + expose_system_hardware: old_cfg.http.expose_system_hardware, + expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, + }, + verloc: Verloc { + bind_address: old_cfg.verloc.bind_address, + announce_port: old_cfg.verloc.announce_port, + debug: VerlocDebug { + packets_per_node: old_cfg.verloc.debug.packets_per_node, + connection_timeout: old_cfg.verloc.debug.connection_timeout, + packet_timeout: old_cfg.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.verloc.debug.testing_interval, + retry_timeout: old_cfg.verloc.debug.retry_timeout, + }, + }, + wireguard: Wireguard { + enabled: old_cfg.wireguard.enabled, + bind_address: old_cfg.wireguard.bind_address, + private_ipv4: old_cfg.wireguard.private_ipv4, + private_ipv6: old_cfg.wireguard.private_ipv6, + announced_tunnel_port: old_cfg.wireguard.announced_port, + announced_metadata_port: WG_METADATA_PORT, + private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, + private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + storage_paths: WireguardPaths { + private_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .private_diffie_hellman_key_file, + public_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .public_diffie_hellman_key_file, + }, + }, + gateway_tasks: GatewayTasksConfig { + storage_paths: GatewayTasksPaths { + clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, + stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, + cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, + }, + enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, + ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, + announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, + announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, + debug: gateway_tasks::Debug { + message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, + maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, + minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, + max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, + stale_messages: StaleMessageDebug { + cleaner_run_interval: old_cfg + .gateway_tasks + .debug + .stale_messages + .cleaner_run_interval, + max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, + }, + client_bandwidth: ClientBandwidthDebug { + max_flushing_rate: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_flushing_rate, + max_delta_flushing_amount: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_delta_flushing_amount, + }, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_api_quorum, + minimum_redemption_tickets: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, + }, + }, + service_providers: ServiceProvidersConfig { + storage_paths: ServiceProvidersPaths { + clients_storage: old_cfg.service_providers.storage_paths.clients_storage, + stats_storage: old_cfg.service_providers.storage_paths.stats_storage, + network_requester: NetworkRequesterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.service_providers.open_proxy, + upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, + network_requester: NetworkRequester { + debug: NetworkRequesterDebug { + enabled: old_cfg.service_providers.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .network_requester + .debug + .client_debug, + }, + }, + ip_packet_router: IpPacketRouter { + debug: IpPacketRouterDebug { + enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .ip_packet_router + .debug + .client_debug, + }, + }, + authenticator: Authenticator { + debug: AuthenticatorDebug { + enabled: old_cfg.service_providers.authenticator.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .authenticator + .debug + .disable_poisson_rate, + client_debug: old_cfg.service_providers.authenticator.debug.client_debug, + }, + }, + debug: service_providers::Debug { + message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, + }, + }, + metrics: Default::default(), + logging: LoggingSettings {}, + debug: Default::default(), + }; + Ok(cfg) +} diff --git a/nym-node/src/config/old_configs/old_config_v9.rs b/nym-node/src/config/old_configs/old_config_v9.rs index 94b95598449..839a54b679a 100644 --- a/nym-node/src/config/old_configs/old_config_v9.rs +++ b/nym-node/src/config/old_configs/old_config_v9.rs @@ -1,29 +1,26 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; -use crate::config::gateway_tasks::{ - ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, +use crate::config::old_configs::old_config_v10::{ + AuthenticatorDebugV10, AuthenticatorPathsV10, AuthenticatorV10, ClientBandwidthDebugV10, + ConfigV10, GatewayTasksConfigDebugV10, GatewayTasksConfigV10, GatewayTasksPathsV10, HostV10, + HttpV10, IpPacketRouterDebugV10, IpPacketRouterPathsV10, IpPacketRouterV10, KeysPathsV10, + LoggingSettingsV10, MixnetDebugV10, MixnetV10, NetworkRequesterDebugV10, + NetworkRequesterPathsV10, NetworkRequesterV10, NodeModesV10, NymNodePathsV10, + ReplayProtectionDebugV10, ReplayProtectionPathsV10, ReplayProtectionV10, + ServiceProvidersConfigDebugV10, ServiceProvidersConfigV10, ServiceProvidersPathsV10, + StaleMessageDebugV10, VerlocDebugV10, VerlocV10, WireguardPathsV10, WireguardV10, + ZkNymTicketHandlerDebugV10, }; use crate::config::persistence::{ - AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, - NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, DEFAULT_PRIMARY_X25519_SPHINX_KEY_FILENAME, DEFAULT_SECONDARY_X25519_SPHINX_KEY_FILENAME, }; -use crate::config::service_providers::{ - IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, -}; -use crate::config::{ - gateway_tasks, service_providers, Config, GatewayTasksConfig, Host, Http, Mixnet, MixnetDebug, - NodeModes, ReplayProtection, ReplayProtectionDebug, ServiceProvidersConfig, Verloc, - VerlocDebug, Wireguard, DEFAULT_HTTP_PORT, -}; +use crate::config::{NodeModes, DEFAULT_HTTP_PORT}; use crate::error::{KeyIOFailure, NymNodeError}; use crate::node::helpers::{get_current_rotation_id, load_key, store_key}; use crate::node::key_rotation::key::SphinxPrivateKey; use celes::Country; use clap::ValueEnum; -use nym_bin_common::logging::LoggingSettings; use nym_client_core_config_types::DebugConfig as ClientDebugConfig; use nym_config::defaults::DEFAULT_VERLOC_LISTENING_PORT; use nym_config::helpers::{in6addr_any_init, inaddr_any}; @@ -1330,7 +1327,7 @@ async fn upgrade_sphinx_key(old_cfg: &ConfigV9) -> Result<(PathBuf, PathBuf), Ny pub async fn try_upgrade_config_v9>( path: P, prev_config: Option, -) -> Result { +) -> Result { debug!("attempting to load v9 config..."); let old_cfg = if let Some(prev_config) = prev_config { @@ -1342,33 +1339,33 @@ pub async fn try_upgrade_config_v9>( let (primary_x25519_sphinx_key_file, secondary_x25519_sphinx_key_file) = upgrade_sphinx_key(&old_cfg).await?; - let cfg = Config { + let cfg = ConfigV10 { save_path: old_cfg.save_path, id: old_cfg.id, - modes: NodeModes { + modes: NodeModesV10 { mixnode: old_cfg.modes.mixnode, entry: old_cfg.modes.entry, exit: old_cfg.modes.exit, }, - host: Host { + host: HostV10 { public_ips: old_cfg.host.public_ips, hostname: old_cfg.host.hostname, location: old_cfg.host.location, }, - mixnet: Mixnet { + mixnet: MixnetV10 { bind_address: old_cfg.mixnet.bind_address, announce_port: old_cfg.mixnet.announce_port, nym_api_urls: old_cfg.mixnet.nym_api_urls, nyxd_urls: old_cfg.mixnet.nyxd_urls, - replay_protection: ReplayProtection { - storage_paths: ReplayProtectionPaths { + replay_protection: ReplayProtectionV10 { + storage_paths: ReplayProtectionPathsV10 { current_bloomfilters_directory: old_cfg .mixnet .replay_protection .storage_paths .current_bloomfilters_directory, }, - debug: ReplayProtectionDebug { + debug: ReplayProtectionDebugV10 { unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, maximum_replay_detection_deferral: old_cfg .mixnet @@ -1404,7 +1401,7 @@ pub async fn try_upgrade_config_v9>( }, }, key_rotation: Default::default(), - debug: MixnetDebug { + debug: MixnetDebugV10 { maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, packet_forwarding_initial_backoff: old_cfg .mixnet @@ -1420,8 +1417,8 @@ pub async fn try_upgrade_config_v9>( ..Default::default() }, }, - storage_paths: NymNodePaths { - keys: KeysPaths { + storage_paths: NymNodePathsV10 { + keys: KeysPathsV10 { private_ed25519_identity_key_file: old_cfg .storage_paths .keys @@ -1443,7 +1440,7 @@ pub async fn try_upgrade_config_v9>( }, description: old_cfg.storage_paths.description, }, - http: Http { + http: HttpV10 { bind_address: old_cfg.http.bind_address, landing_page_assets_path: old_cfg.http.landing_page_assets_path, access_token: old_cfg.http.access_token, @@ -1452,10 +1449,10 @@ pub async fn try_upgrade_config_v9>( expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, }, - verloc: Verloc { + verloc: VerlocV10 { bind_address: old_cfg.verloc.bind_address, announce_port: old_cfg.verloc.announce_port, - debug: VerlocDebug { + debug: VerlocDebugV10 { packets_per_node: old_cfg.verloc.debug.packets_per_node, connection_timeout: old_cfg.verloc.debug.connection_timeout, packet_timeout: old_cfg.verloc.debug.packet_timeout, @@ -1465,7 +1462,7 @@ pub async fn try_upgrade_config_v9>( retry_timeout: old_cfg.verloc.debug.retry_timeout, }, }, - wireguard: Wireguard { + wireguard: WireguardV10 { enabled: old_cfg.wireguard.enabled, bind_address: old_cfg.wireguard.bind_address, private_ipv4: old_cfg.wireguard.private_ipv4, @@ -1473,7 +1470,7 @@ pub async fn try_upgrade_config_v9>( announced_port: old_cfg.wireguard.announced_port, private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, - storage_paths: WireguardPaths { + storage_paths: WireguardPathsV10 { private_diffie_hellman_key_file: old_cfg .wireguard .storage_paths @@ -1484,8 +1481,8 @@ pub async fn try_upgrade_config_v9>( .public_diffie_hellman_key_file, }, }, - gateway_tasks: GatewayTasksConfig { - storage_paths: GatewayTasksPaths { + gateway_tasks: GatewayTasksConfigV10 { + storage_paths: GatewayTasksPathsV10 { clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, @@ -1494,12 +1491,12 @@ pub async fn try_upgrade_config_v9>( ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, - debug: gateway_tasks::Debug { + debug: GatewayTasksConfigDebugV10 { message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, - stale_messages: StaleMessageDebug { + stale_messages: StaleMessageDebugV10 { cleaner_run_interval: old_cfg .gateway_tasks .debug @@ -1507,7 +1504,7 @@ pub async fn try_upgrade_config_v9>( .cleaner_run_interval, max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, }, - client_bandwidth: ClientBandwidthDebug { + client_bandwidth: ClientBandwidthDebugV10 { max_flushing_rate: old_cfg .gateway_tasks .debug @@ -1519,7 +1516,7 @@ pub async fn try_upgrade_config_v9>( .client_bandwidth .max_delta_flushing_amount, }, - zk_nym_tickets: ZkNymTicketHandlerDebug { + zk_nym_tickets: ZkNymTicketHandlerDebugV10 { revocation_bandwidth_penalty: old_cfg .gateway_tasks .debug @@ -1544,11 +1541,11 @@ pub async fn try_upgrade_config_v9>( }, }, }, - service_providers: ServiceProvidersConfig { - storage_paths: ServiceProvidersPaths { + service_providers: ServiceProvidersConfigV10 { + storage_paths: ServiceProvidersPathsV10 { clients_storage: old_cfg.service_providers.storage_paths.clients_storage, stats_storage: old_cfg.service_providers.storage_paths.stats_storage, - network_requester: NetworkRequesterPaths { + network_requester: NetworkRequesterPathsV10 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1585,7 +1582,7 @@ pub async fn try_upgrade_config_v9>( .network_requester .gateway_registrations, }, - ip_packet_router: IpPacketRouterPaths { + ip_packet_router: IpPacketRouterPathsV10 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1622,7 +1619,7 @@ pub async fn try_upgrade_config_v9>( .ip_packet_router .gateway_registrations, }, - authenticator: AuthenticatorPaths { + authenticator: AuthenticatorPathsV10 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1662,8 +1659,8 @@ pub async fn try_upgrade_config_v9>( }, open_proxy: old_cfg.service_providers.open_proxy, upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, - network_requester: NetworkRequester { - debug: NetworkRequesterDebug { + network_requester: NetworkRequesterV10 { + debug: NetworkRequesterDebugV10 { enabled: old_cfg.service_providers.network_requester.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1677,8 +1674,8 @@ pub async fn try_upgrade_config_v9>( .client_debug, }, }, - ip_packet_router: IpPacketRouter { - debug: IpPacketRouterDebug { + ip_packet_router: IpPacketRouterV10 { + debug: IpPacketRouterDebugV10 { enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1692,8 +1689,8 @@ pub async fn try_upgrade_config_v9>( .client_debug, }, }, - authenticator: Authenticator { - debug: AuthenticatorDebug { + authenticator: AuthenticatorV10 { + debug: AuthenticatorDebugV10 { enabled: old_cfg.service_providers.authenticator.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1703,12 +1700,12 @@ pub async fn try_upgrade_config_v9>( client_debug: old_cfg.service_providers.authenticator.debug.client_debug, }, }, - debug: service_providers::Debug { + debug: ServiceProvidersConfigDebugV10 { message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, }, }, metrics: Default::default(), - logging: LoggingSettings {}, + logging: LoggingSettingsV10 {}, debug: Default::default(), }; Ok(cfg) diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index bfe413a62bc..bf8ed72710e 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -145,7 +145,11 @@ private_ipv6 = '{{ wireguard.private_ipv6 }}' # Port announced to external clients wishing to connect to the wireguard interface. # Useful in the instances where the node is behind a proxy. -announced_port = {{ wireguard.announced_port }} +announced_tunnel_port = {{ wireguard.announced_tunnel_port }} + +# Port announced to external clients wishing to connect to the metadata service. +# Useful in the instances where the node is behind a proxy. +announced_metadata_port = {{ wireguard.announced_metadata_port }} # The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. # The maximum value for IPv4 is 32 diff --git a/nym-node/src/config/upgrade_helpers.rs b/nym-node/src/config/upgrade_helpers.rs index 85e2cced9c0..bdb10ec8b75 100644 --- a/nym-node/src/config/upgrade_helpers.rs +++ b/nym-node/src/config/upgrade_helpers.rs @@ -16,7 +16,8 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v6(path, cfg).await.ok(); let cfg = try_upgrade_config_v7(path, cfg).await.ok(); let cfg = try_upgrade_config_v8(path, cfg).await.ok(); - match try_upgrade_config_v9(path, cfg).await { + let cfg = try_upgrade_config_v9(path, cfg).await.ok(); + match try_upgrade_config_v10(path, cfg).await { Ok(cfg) => cfg.save(), Err(e) => { tracing::error!("Failed to finish upgrade: {e}"); diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 6ec9105482f..2fca2469979 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -605,7 +605,8 @@ impl NymNode { metrics_sender: MetricEventsSender, active_clients_store: ActiveClientsStore, mix_packet_sender: MixForwardingSender, - task_client: TaskClient, + legacy_task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Result<(), NymNodeError> { let config = gateway_tasks_config(&self.config); @@ -623,7 +624,8 @@ impl NymNode { metrics_sender, self.metrics.clone(), self.entry_gateway.mnemonic.clone(), - task_client, + legacy_task_client, + shutdown_token, ); // if we're running in entry mode, start the websocket @@ -716,8 +718,11 @@ impl NymNode { // entry gateway info let wireguard = if self.config.wireguard.enabled { + #[allow(deprecated)] Some(api_requests::v1::gateway::models::Wireguard { - port: self.config.wireguard.announced_port, + port: self.config.wireguard.announced_tunnel_port, + tunnel_port: self.config.wireguard.announced_tunnel_port, + metadata_port: self.config.wireguard.announced_metadata_port, public_key: self.x25519_wireguard_key()?.to_string(), }) } else { @@ -1183,6 +1188,7 @@ impl NymNode { active_clients_store, mix_packet_sender, self.shutdown_manager.subscribe_legacy("gateway-tasks"), + self.shutdown_manager.child_token("gateway-tasks"), ) .await?; diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index ad7436e5ded..e9829ca4eff 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.62" +version = "1.1.63" authors.workspace = true edition.workspace = true rust-version = "1.70" diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index a30c16a2558..8b46302ef61 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.61" +version = "1.1.62" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index f7331cda2d9..e2752b40951 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.26" +version = "0.1.27" authors.workspace = true repository.workspace = true homepage.workspace = true