diff --git a/Cargo.lock b/Cargo.lock index 6ff6f8bb68d..b9bcc7eb58d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4988,6 +4988,7 @@ dependencies = [ "nym-network-defaults", "nym-service-provider-requests-common", "nym-sphinx", + "nym-test-utils", "nym-wireguard-types", "rand 0.8.5", "semver 1.0.26", @@ -4995,6 +4996,7 @@ dependencies = [ "sha2 0.10.9", "strum_macros", "thiserror 2.0.12", + "tracing", "x25519-dalek", ] @@ -5605,12 +5607,13 @@ dependencies = [ "nym-api-requests", "nym-credentials", "nym-credentials-interface", + "nym-crypto", "nym-ecash-contract-common", "nym-gateway-requests", "nym-gateway-storage", "nym-task", + "nym-upgrade-mode-check", "nym-validator-client", - "rand 0.8.5", "si-scale", "thiserror 2.0.12", "time", @@ -5650,6 +5653,7 @@ dependencies = [ "nym-compact-ecash", "nym-ecash-time", "nym-network-defaults", + "nym-upgrade-mode-check", "rand 0.8.5", "serde", "strum", @@ -5800,7 +5804,6 @@ dependencies = [ name = "nym-gateway" version = "1.1.36" dependencies = [ - "anyhow", "async-trait", "bincode", "bip39", @@ -5811,7 +5814,6 @@ dependencies = [ "futures", "ipnetwork", "mock_instant", - "nym-api-requests", "nym-authenticator-requests", "nym-client-core", "nym-credential-verification", @@ -5824,7 +5826,6 @@ dependencies = [ "nym-id", "nym-ip-packet-router", "nym-mixnet-client", - "nym-mixnode-common", "nym-network-defaults", "nym-network-requester", "nym-node-metrics", @@ -5834,20 +5835,18 @@ dependencies = [ "nym-statistics-common", "nym-task", "nym-topology", - "nym-types", + "nym-upgrade-mode-check", "nym-validator-client", "nym-wireguard", "nym-wireguard-private-metadata-server", "nym-wireguard-types", "rand 0.8.5", "serde", - "sha2 0.10.9", "thiserror 2.0.12", "time", "tokio", "tokio-stream", "tokio-tungstenite", - "tokio-util", "tracing", "url", "zeroize", @@ -7574,17 +7573,10 @@ dependencies = [ name = "nym-wireguard" version = "0.1.0" dependencies = [ - "async-trait", "base64 0.22.1", - "bincode", - "chrono", - "dashmap", "defguard_wireguard_rs", - "dyn-clone", "futures", "ip_network", - "log", - "nym-authenticator-requests", "nym-credential-verification", "nym-credentials-interface", "nym-crypto", @@ -7595,11 +7587,9 @@ dependencies = [ "nym-task", "nym-wireguard-types", "thiserror 2.0.12", - "time", "tokio", "tokio-stream", "tracing", - "x25519-dalek", ] [[package]] @@ -7651,15 +7641,20 @@ version = "1.0.0" dependencies = [ "async-trait", "axum", + "futures", "nym-credential-verification", "nym-credentials-interface", + "nym-crypto", "nym-http-api-client", "nym-http-api-common", + "nym-upgrade-mode-check", "nym-wireguard", "nym-wireguard-private-metadata-client", "nym-wireguard-private-metadata-server", "nym-wireguard-private-metadata-shared", + "time", "tokio", + "tower 0.5.2", "tower-http 0.5.2", "utoipa", ] @@ -7669,10 +7664,7 @@ name = "nym-wireguard-types" version = "0.1.0" dependencies = [ "base64 0.22.1", - "log", - "nym-config", "nym-crypto", - "nym-network-defaults", "rand 0.8.5", "serde", "thiserror 2.0.12", diff --git a/Cargo.toml b/Cargo.toml index c07db1f4baa..92f5fdbb7b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,6 +171,7 @@ members = [ default-members = [ "clients/native", "clients/socks5", + "nym-authenticator-client", "nym-api", "nym-credential-proxy/nym-credential-proxy", "nym-node", diff --git a/common/authenticator-requests/Cargo.toml b/common/authenticator-requests/Cargo.toml index 60ff6826ba7..6126a18f803 100644 --- a/common/authenticator-requests/Cargo.toml +++ b/common/authenticator-requests/Cargo.toml @@ -16,6 +16,7 @@ serde = { workspace = true, features = ["derive"] } semver = { workspace = true } strum_macros = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["asymmetric"] } @@ -29,7 +30,13 @@ hmac = { workspace = true, optional = true } sha2 = { workspace = true, optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"] } +[dev-dependencies] +nym-test-utils = { path = "../test-utils" } + [features] default = ["verify"] # this is moved to a separate feature as we really need clients to import it (especially, *cough*, wasm) verify = ["hmac", "sha2"] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/authenticator-requests/src/client_message.rs b/common/authenticator-requests/src/client_message.rs index 06a910b9b93..23bdd13ab05 100644 --- a/common/authenticator-requests/src/client_message.rs +++ b/common/authenticator-requests/src/client_message.rs @@ -6,9 +6,8 @@ use nym_wireguard_types::PeerPublicKey; use crate::{ AuthenticatorVersion, Error, - latest::registration::IpPair, traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable}, - v2, v3, v4, v5, + v2, v3, v4, v5, v6, }; // This is very redundant with AuthenticatorRequest and I reckon they could be smooshed. @@ -21,6 +20,272 @@ pub enum ClientMessage { TopUp(Box), } +pub struct SerialisedRequest { + pub bytes: Vec, + pub request_id: u64, +} + +impl SerialisedRequest { + pub fn new(bytes: Vec, request_id: u64) -> Self { + Self { bytes, request_id } + } +} + +impl ClientMessage { + fn serialise_v1(&self) -> Result { + Err(Error::UnsupportedVersion) + } + + fn serialise_v2(&self, reply_to: Recipient) -> Result { + use v2::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + _ => Err(Error::UnsupportedMessage), + } + } + + fn serialise_v3(&self, reply_to: Recipient) -> Result { + use v3::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } + + fn serialise_v4(&self, reply_to: Recipient) -> Result { + use v4::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } + + fn serialise_v5(&self) -> Result { + use v5::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { + pub_key: init_message.pub_key(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key()); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } + + fn serialise_v6(&self) -> Result { + use v6::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { + pub_key: init_message.pub_key(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key()); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } +} + impl ClientMessage { // check if message is wasteful e.g. contains a credential pub fn is_wasteful(&self) -> bool { @@ -40,205 +305,14 @@ impl ClientMessage { } } - pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec, u64), Error> { + pub fn bytes(&self, reply_to: Recipient) -> Result { match self.version() { - AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion), - AuthenticatorVersion::V2 => { - use v2::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ip: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)? - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - _ => Err(Error::UnsupportedMessage), - } - } - AuthenticatorVersion::V3 => { - use v3::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ip: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)? - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request( - TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - } - } - AuthenticatorVersion::V4 => { - use v4::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ips: IpPair { - ipv4: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)?, - ipv6: final_message - .gateway_client_ipv6() - .ok_or(Error::UnsupportedMessage)?, - } - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request( - TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - } - } - AuthenticatorVersion::V5 => { - use v5::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { - pub_key: init_message.pub_key(), - }); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ips: IpPair { - ipv4: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)?, - ipv6: final_message - .gateway_client_ipv6() - .ok_or(Error::UnsupportedMessage)?, - }, - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = - AuthenticatorRequest::new_query_request(query_message.pub_key()); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }); - Ok((req.to_bytes()?, id)) - } - } - } + AuthenticatorVersion::V1 => self.serialise_v1(), + AuthenticatorVersion::V2 => self.serialise_v2(reply_to), + AuthenticatorVersion::V3 => self.serialise_v3(reply_to), + AuthenticatorVersion::V4 => self.serialise_v4(reply_to), + AuthenticatorVersion::V5 => self.serialise_v5(), + AuthenticatorVersion::V6 => self.serialise_v6(), AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion), } } @@ -247,7 +321,7 @@ impl ClientMessage { use AuthenticatorVersion::*; match self.version() { V1 | V2 | V3 | V4 => false, - V5 => true, + V5 | V6 => true, UNKNOWN => true, } } diff --git a/common/authenticator-requests/src/error.rs b/common/authenticator-requests/src/error.rs index d940bd538dc..cbf0cde523f 100644 --- a/common/authenticator-requests/src/error.rs +++ b/common/authenticator-requests/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::fmt::Display; use thiserror::Error; #[derive(Debug, Error)] @@ -37,3 +38,13 @@ pub enum Error { #[error(transparent)] Bincode(#[from] bincode::Error), } + +impl Error { + pub fn conversion(msg: impl Into) -> Self { + Error::Conversion(msg.into()) + } + + pub fn conversion_display(msg: impl Display) -> Self { + Error::Conversion(msg.to_string()) + } +} diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index b1b0159da3e..226c78adeec 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod client_message; +pub mod models; pub mod request; pub mod response; pub mod traits; @@ -10,13 +11,14 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; +pub mod v6; mod error; mod util; mod version; pub use error::Error; -pub use v5 as latest; +pub use v6 as latest; pub use version::AuthenticatorVersion; pub const CURRENT_VERSION: u8 = latest::VERSION; diff --git a/common/authenticator-requests/src/models.rs b/common/authenticator-requests/src/models.rs new file mode 100644 index 00000000000..dc870df4d5a --- /dev/null +++ b/common/authenticator-requests/src/models.rs @@ -0,0 +1,52 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::{ + BandwidthCredential, CredentialSpendingData, TicketType, UnknownTicketType, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +pub enum CurrentUpgradeModeStatus { + Enabled, + Disabled, + // everything pre-v6 + Unknown, +} + +impl From for CurrentUpgradeModeStatus { + fn from(value: bool) -> Self { + if value { + CurrentUpgradeModeStatus::Enabled + } else { + CurrentUpgradeModeStatus::Disabled + } + } +} + +impl From for Option { + fn from(value: CurrentUpgradeModeStatus) -> Self { + match value { + CurrentUpgradeModeStatus::Enabled => Some(true), + CurrentUpgradeModeStatus::Disabled => Some(false), + CurrentUpgradeModeStatus::Unknown => None, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct BandwidthClaim { + pub credential: BandwidthCredential, + pub kind: TicketType, +} + +impl TryFrom for BandwidthClaim { + type Error = UnknownTicketType; + + fn try_from(credential: CredentialSpendingData) -> Result { + Ok(BandwidthClaim { + kind: TicketType::try_from_encoded(credential.payment.t_type)?, + credential: BandwidthCredential::from(credential), + }) + } +} diff --git a/common/authenticator-requests/src/request.rs b/common/authenticator-requests/src/request.rs index 3d98a8ed5e8..c1585b9bb69 100644 --- a/common/authenticator-requests/src/request.rs +++ b/common/authenticator-requests/src/request.rs @@ -4,8 +4,10 @@ use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; -use crate::traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage}; -use crate::{v1, v2, v3, v4, v5}; +use crate::traits::{ + FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage, +}; +use crate::{v1, v2, v3, v4, v5, v6}; #[derive(Debug)] pub enum AuthenticatorRequest { @@ -33,6 +35,11 @@ pub enum AuthenticatorRequest { reply_to: Option, request_id: u64, }, + CheckUpgradeMode { + msg: Box, + protocol: Protocol, + request_id: u64, + }, } impl From for AuthenticatorRequest { @@ -202,3 +209,45 @@ impl From for AuthenticatorRequest { } } } + +impl From for AuthenticatorRequest { + fn from(value: v6::request::AuthenticatorRequest) -> Self { + match value.data { + v6::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v6::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v6::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + v6::request::AuthenticatorRequestData::CheckUpgradeMode(upgrade_mode_check_msg) => { + Self::CheckUpgradeMode { + msg: Box::new(upgrade_mode_check_msg), + protocol: value.protocol, + request_id: value.request_id, + } + } + } + } +} diff --git a/common/authenticator-requests/src/response.rs b/common/authenticator-requests/src/response.rs index 8e196a7b722..84b6ae8526f 100644 --- a/common/authenticator-requests/src/response.rs +++ b/common/authenticator-requests/src/response.rs @@ -1,11 +1,12 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::models::CurrentUpgradeModeStatus; use crate::traits::{ Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse, - TopUpBandwidthResponse, + TopUpBandwidthResponse, UpgradeModeStatus, }; -use crate::{v2, v3, v4, v5}; +use crate::{v2, v3, v4, v5, v6}; #[derive(Debug)] pub enum AuthenticatorResponse { @@ -13,6 +14,29 @@ pub enum AuthenticatorResponse { Registered(Box), RemainingBandwidth(Box), TopUpBandwidth(Box), + UpgradeMode(Box), +} + +impl UpgradeModeStatus for AuthenticatorResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + match self { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + pending_registration_response.upgrade_mode_status() + } + AuthenticatorResponse::Registered(registered_response) => { + registered_response.upgrade_mode_status() + } + AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { + remaining_bandwidth_response.upgrade_mode_status() + } + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.upgrade_mode_status() + } + AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => { + upgrade_mode_response.upgrade_mode_status() + } + } + } } impl Id for AuthenticatorResponse { @@ -28,6 +52,7 @@ impl Id for AuthenticatorResponse { AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { top_up_bandwidth_response.id() } + AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => upgrade_mode_response.id(), } } } @@ -104,3 +129,25 @@ impl From for AuthenticatorResponse { } } } + +impl From for AuthenticatorResponse { + fn from(value: v6::response::AuthenticatorResponse) -> Self { + match value.data { + v6::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v6::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v6::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + v6::response::AuthenticatorResponseData::UpgradeMode(upgrade_mode_check_response) => { + Self::UpgradeMode(Box::new(upgrade_mode_check_response)) + } + } + } +} diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs index 36e999383d7..e16c0d96ba3 100644 --- a/common/authenticator-requests/src/traits.rs +++ b/common/authenticator-requests/src/traits.rs @@ -1,15 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt; -use std::net::{Ipv4Addr, Ipv6Addr}; - +use crate::latest::registration::IpPair; +use crate::models::{BandwidthClaim, CurrentUpgradeModeStatus}; +use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5, v6}; use nym_credentials_interface::CredentialSpendingData; -use nym_crypto::asymmetric::x25519::PrivateKey; +use nym_crypto::asymmetric::x25519; use nym_wireguard_types::PeerPublicKey; - -use crate::latest::registration::IpPair; -use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5}; +use std::fmt; +use std::net::{Ipv4Addr, Ipv6Addr}; +use tracing::error; pub trait Versionable { fn version(&self) -> AuthenticatorVersion; @@ -51,6 +51,12 @@ impl Versionable for v5::registration::InitMessage { } } +impl Versionable for v6::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + impl Versionable for v2::registration::FinalMessage { fn version(&self) -> AuthenticatorVersion { AuthenticatorVersion::V2 @@ -75,6 +81,12 @@ impl Versionable for v5::registration::FinalMessage { } } +impl Versionable for v6::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + impl Versionable for PeerPublicKey { fn version(&self) -> AuthenticatorVersion { AuthenticatorVersion::V3 @@ -98,6 +110,158 @@ impl Versionable for v5::topup::TopUpMessage { AuthenticatorVersion::V5 } } +impl Versionable for v6::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + +impl Versionable for v6::upgrade_mode_check::UpgradeModeCheckRequest { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + +pub trait UpgradeModeStatus: Id + fmt::Debug { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus; +} + +impl UpgradeModeStatus for v1::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v1::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v1::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} +impl UpgradeModeStatus for v3::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v6::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::UpgradeModeResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} pub trait InitMessage: Versionable + fmt::Debug { fn pub_key(&self) -> PeerPublicKey; @@ -133,14 +297,20 @@ impl InitMessage for v5::registration::InitMessage { } } +impl InitMessage for v6::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + pub trait FinalMessage: Versionable + fmt::Debug { fn gateway_client_pub_key(&self) -> PeerPublicKey; - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error>; fn private_ips(&self) -> IpPair; fn gateway_client_ipv4(&self) -> Option; fn gateway_client_ipv6(&self) -> Option; fn gateway_client_mac(&self) -> Vec; - fn credential(&self) -> Option; + fn credential(&self) -> Option; } impl FinalMessage for v1::GatewayClient { @@ -148,7 +318,7 @@ impl FinalMessage for v1::GatewayClient { self.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.verify(private_key, nonce) } @@ -171,7 +341,7 @@ impl FinalMessage for v1::GatewayClient { self.mac.to_vec() } - fn credential(&self) -> Option { + fn credential(&self) -> Option { None } } @@ -181,7 +351,7 @@ impl FinalMessage for v2::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -204,8 +374,12 @@ impl FinalMessage for v2::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } @@ -214,7 +388,7 @@ impl FinalMessage for v3::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -237,8 +411,12 @@ impl FinalMessage for v3::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } @@ -247,12 +425,13 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } fn private_ips(&self) -> IpPair { - self.gateway_client.private_ips.into() + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.gateway_client.private_ips).into() } fn gateway_client_ipv4(&self) -> Option { @@ -267,8 +446,12 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } @@ -277,7 +460,41 @@ impl FinalMessage for v5::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ips(&self) -> IpPair { + self.gateway_client.private_ips.into() + } + + fn gateway_client_ipv4(&self) -> Option { + Some(self.gateway_client.private_ips.ipv4) + } + + fn gateway_client_ipv6(&self) -> Option { + Some(self.gateway_client.private_ips.ipv6) + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) + } +} + +impl FinalMessage for v6::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -297,7 +514,7 @@ impl FinalMessage for v5::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { + fn credential(&self) -> Option { self.credential.clone() } } @@ -347,10 +564,42 @@ impl TopUpMessage for v5::topup::TopUpMessage { } } +impl TopUpMessage for v6::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +pub trait UpgradeModeMessage: Versionable + fmt::Debug { + // the idea is to expose different types of emergency credentials here, + // like upgrade mode JWT, emergency threshold credential issued by signers, etc. + fn upgrade_mode_global_attestation_jwt(&self) -> Option; +} + +impl UpgradeModeMessage for v6::upgrade_mode_check::UpgradeModeCheckRequest { + fn upgrade_mode_global_attestation_jwt(&self) -> Option { + use v6::upgrade_mode_check::UpgradeModeCheckRequest; + + match self { + UpgradeModeCheckRequest::UpgradeModeJwt { token } => Some(token.clone()), + } + } +} + pub trait Id { fn id(&self) -> u64; } +impl Id for v1::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::PendingRegistrationResponse { fn id(&self) -> u64 { self.request_id @@ -375,6 +624,18 @@ impl Id for v5::response::PendingRegistrationResponse { } } +impl Id for v6::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v1::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::RegisteredResponse { fn id(&self) -> u64 { self.request_id @@ -399,6 +660,18 @@ impl Id for v5::response::RegisteredResponse { } } +impl Id for v6::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v1::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::RemainingBandwidthResponse { fn id(&self) -> u64 { self.request_id @@ -423,6 +696,12 @@ impl Id for v5::response::RemainingBandwidthResponse { } } +impl Id for v6::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v3::response::TopUpBandwidthResponse { fn id(&self) -> u64 { self.request_id @@ -441,11 +720,28 @@ impl Id for v5::response::TopUpBandwidthResponse { } } -pub trait PendingRegistrationResponse: Id + fmt::Debug { +impl Id for v6::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v6::response::UpgradeModeResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +pub trait PendingRegistrationResponse: Id + UpgradeModeStatus + fmt::Debug { fn nonce(&self) -> u64; - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error>; + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error>; fn pub_key(&self) -> PeerPublicKey; fn private_ips(&self) -> IpPair; + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box; } impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { @@ -453,7 +749,7 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -464,6 +760,22 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ip.into() } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v2::registration::FinalMessage { + gateway_client: v2::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.private_ips().ipv4.into(), + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } } impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { @@ -471,7 +783,7 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -482,6 +794,22 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ip.into() } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v3::registration::FinalMessage { + gateway_client: v3::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.private_ips().ipv4.into(), + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } } impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { @@ -489,7 +817,7 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -498,7 +826,24 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { } fn private_ips(&self) -> IpPair { - self.reply.gateway_data.private_ips.into() + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.reply.gateway_data.private_ips).into() + } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v4::registration::FinalMessage { + gateway_client: v4::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) } } @@ -507,7 +852,41 @@ impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ips.into() + } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v5::registration::FinalMessage { + gateway_client: v5::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } +} + +impl PendingRegistrationResponse for v6::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -518,9 +897,25 @@ impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ips } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v6::registration::FinalMessage { + gateway_client: v6::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential, + }) + } } -pub trait RegisteredResponse: Id + fmt::Debug { +pub trait RegisteredResponse: Id + UpgradeModeStatus + fmt::Debug { fn private_ips(&self) -> IpPair; fn pub_key(&self) -> PeerPublicKey; fn wg_port(&self) -> u16; @@ -555,7 +950,8 @@ impl RegisteredResponse for v3::response::RegisteredResponse { } impl RegisteredResponse for v4::response::RegisteredResponse { fn private_ips(&self) -> IpPair { - self.reply.private_ips.into() + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.reply.private_ips).into() } fn pub_key(&self) -> PeerPublicKey { @@ -568,6 +964,20 @@ impl RegisteredResponse for v4::response::RegisteredResponse { } impl RegisteredResponse for v5::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ips.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +impl RegisteredResponse for v6::response::RegisteredResponse { fn private_ips(&self) -> IpPair { self.reply.private_ips } @@ -581,7 +991,7 @@ impl RegisteredResponse for v5::response::RegisteredResponse { } } -pub trait RemainingBandwidthResponse: Id + fmt::Debug { +pub trait RemainingBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug { fn available_bandwidth(&self) -> Option; } @@ -609,7 +1019,13 @@ impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse { } } -pub trait TopUpBandwidthResponse: Id + fmt::Debug { +impl RemainingBandwidthResponse for v6::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +pub trait TopUpBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug { fn available_bandwidth(&self) -> i64; } @@ -630,3 +1046,9 @@ impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse { self.reply.available_bandwidth } } + +impl TopUpBandwidthResponse for v6::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} diff --git a/common/authenticator-requests/src/v1/registration.rs b/common/authenticator-requests/src/v1/registration.rs index 68073259215..aa799109726 100644 --- a/common/authenticator-requests/src/v1/registration.rs +++ b/common/authenticator-requests/src/v1/registration.rs @@ -48,7 +48,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v1/response.rs b/common/authenticator-requests/src/v1/response.rs index 4e7ba61eb46..f4ff8f5da3b 100644 --- a/common/authenticator-requests/src/v1/response.rs +++ b/common/authenticator-requests/src/v1/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -34,7 +34,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -108,7 +108,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/common/authenticator-requests/src/v2/conversion.rs b/common/authenticator-requests/src/v2/conversion.rs index b8e16ac4bae..36dceaf3ff1 100644 --- a/common/authenticator-requests/src/v2/conversion.rs +++ b/common/authenticator-requests/src/v2/conversion.rs @@ -154,8 +154,8 @@ impl From for v1::registration::Registration } } -impl From for v1::registration::RegistredData { - fn from(value: v2::registration::RegistredData) -> Self { +impl From for v1::registration::RegisteredData { + fn from(value: v2::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, diff --git a/common/authenticator-requests/src/v2/registration.rs b/common/authenticator-requests/src/v2/registration.rs index a8d5f5e0896..34d3b7f4e0f 100644 --- a/common/authenticator-requests/src/v2/registration.rs +++ b/common/authenticator-requests/src/v2/registration.rs @@ -58,7 +58,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v2/response.rs b/common/authenticator-requests/src/v2/response.rs index 1b389de43f6..33da1b975d7 100644 --- a/common/authenticator-requests/src/v2/response.rs +++ b/common/authenticator-requests/src/v2/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -118,7 +118,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v3/conversion.rs b/common/authenticator-requests/src/v3/conversion.rs index 5a49c771cd9..d04bb6134f8 100644 --- a/common/authenticator-requests/src/v3/conversion.rs +++ b/common/authenticator-requests/src/v3/conversion.rs @@ -299,8 +299,8 @@ impl From for v3::registration::Registration } } -impl From for v2::registration::RegistredData { - fn from(value: v3::registration::RegistredData) -> Self { +impl From for v2::registration::RegisteredData { + fn from(value: v3::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, @@ -309,8 +309,8 @@ impl From for v2::registration::RegistredData { } } -impl From for v3::registration::RegistredData { - fn from(value: v2::registration::RegistredData) -> Self { +impl From for v3::registration::RegisteredData { + fn from(value: v2::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, @@ -674,7 +674,7 @@ mod tests { let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); let private_ip = IpAddr::from_str("10.10.10.10").unwrap(); let wg_port = 51822; - let registred_data = v2::registration::RegistredData { + let registred_data = v2::registration::RegisteredData { pub_key, private_ip, wg_port, @@ -701,7 +701,7 @@ mod tests { v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse { request_id, reply_to, - reply: v3::registration::RegistredData { + reply: v3::registration::RegisteredData { wg_port, pub_key, private_ip @@ -715,7 +715,7 @@ mod tests { let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); let private_ip = IpAddr::from_str("10.10.10.10").unwrap(); let wg_port = 51822; - let registred_data = v3::registration::RegistredData { + let registred_data = v3::registration::RegisteredData { pub_key, private_ip, wg_port, @@ -742,7 +742,7 @@ mod tests { v2::response::AuthenticatorResponseData::Registered(v2::response::RegisteredResponse { request_id, reply_to, - reply: v2::registration::RegistredData { + reply: v2::registration::RegisteredData { wg_port, pub_key, private_ip diff --git a/common/authenticator-requests/src/v3/registration.rs b/common/authenticator-requests/src/v3/registration.rs index 00cb1467723..66b02c5ed23 100644 --- a/common/authenticator-requests/src/v3/registration.rs +++ b/common/authenticator-requests/src/v3/registration.rs @@ -58,7 +58,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v3/response.rs b/common/authenticator-requests/src/v3/response.rs index ca44fb19f6c..4fd0a9729bd 100644 --- a/common/authenticator-requests/src/v3/response.rs +++ b/common/authenticator-requests/src/v3/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v4/conversion.rs b/common/authenticator-requests/src/v4/conversion.rs index 87312224465..3b2cf9a8f25 100644 --- a/common/authenticator-requests/src/v4/conversion.rs +++ b/common/authenticator-requests/src/v4/conversion.rs @@ -262,8 +262,8 @@ impl From for v3::response::TopUpBandwidth } } -impl From for v4::registration::RegistredData { - fn from(value: v3::registration::RegistredData) -> Self { +impl From for v4::registration::RegisteredData { + fn from(value: v3::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ips: value.private_ip.into(), @@ -272,8 +272,8 @@ impl From for v4::registration::RegistredData { } } -impl From for v3::registration::RegistredData { - fn from(value: v4::registration::RegistredData) -> Self { +impl From for v3::registration::RegisteredData { + fn from(value: v4::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ips.ipv4.into(), @@ -565,7 +565,7 @@ mod tests { let private_ips = v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::a0a").unwrap()); let wg_port = 51822; - let registred_data = v3::registration::RegistredData { + let registred_data = v3::registration::RegisteredData { pub_key, private_ip: ipv4.into(), wg_port, @@ -592,7 +592,7 @@ mod tests { v4::response::AuthenticatorResponseData::Registered(v4::response::RegisteredResponse { request_id, reply_to, - reply: v4::registration::RegistredData { + reply: v4::registration::RegisteredData { wg_port, pub_key, private_ips @@ -608,7 +608,7 @@ mod tests { let private_ips = v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::10").unwrap()); let wg_port = 51822; - let registred_data = v4::registration::RegistredData { + let registred_data = v4::registration::RegisteredData { pub_key, private_ips, wg_port, @@ -635,7 +635,7 @@ mod tests { v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse { request_id, reply_to, - reply: v3::registration::RegistredData { + reply: v3::registration::RegisteredData { wg_port, pub_key, private_ip: ipv4.into() diff --git a/common/authenticator-requests/src/v4/registration.rs b/common/authenticator-requests/src/v4/registration.rs index a383b79bebc..b1ee074dfd2 100644 --- a/common/authenticator-requests/src/v4/registration.rs +++ b/common/authenticator-requests/src/v4/registration.rs @@ -110,7 +110,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ips: IpPair, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v4/response.rs b/common/authenticator-requests/src/v4/response.rs index 9743e8db438..1bbf4557e97 100644 --- a/common/authenticator-requests/src/v4/response.rs +++ b/common/authenticator-requests/src/v4/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v5/conversion.rs b/common/authenticator-requests/src/v5/conversion.rs index 77ed294323d..f2287c03bda 100644 --- a/common/authenticator-requests/src/v5/conversion.rs +++ b/common/authenticator-requests/src/v5/conversion.rs @@ -186,8 +186,8 @@ impl From for v5::response::TopUpBandwidth } } -impl From for v5::registration::RegistredData { - fn from(value: v4::registration::RegistredData) -> Self { +impl From for v5::registration::RegisteredData { + fn from(value: v4::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ips: value.private_ips.into(), @@ -405,7 +405,7 @@ mod tests { let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); let private_ips = v4::registration::IpPair::new(ipv4, ipv6); let wg_port = 51822; - let registred_data = v4::registration::RegistredData { + let registred_data = v4::registration::RegisteredData { pub_key, private_ips, wg_port, @@ -431,7 +431,7 @@ mod tests { upgraded_msg.data, v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse { request_id, - reply: v5::registration::RegistredData { + reply: v5::registration::RegisteredData { wg_port, pub_key, private_ips: v5::registration::IpPair::new(ipv4, ipv6) diff --git a/common/authenticator-requests/src/v5/registration.rs b/common/authenticator-requests/src/v5/registration.rs index 151401da972..5154400f931 100644 --- a/common/authenticator-requests/src/v5/registration.rs +++ b/common/authenticator-requests/src/v5/registration.rs @@ -108,7 +108,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ips: IpPair, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v5/response.rs b/common/authenticator-requests/src/v5/response.rs index 044b803d0d4..b26fcf46271 100644 --- a/common/authenticator-requests/src/v5/response.rs +++ b/common/authenticator-requests/src/v5/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use serde::{Deserialize, Serialize}; @@ -32,7 +32,7 @@ impl AuthenticatorResponse { } } - pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self { + pub fn new_registered(registred_data: RegisteredData, request_id: u64) -> Self { Self { protocol: Protocol { service_provider_type: ServiceProviderType::Authenticator, @@ -116,7 +116,7 @@ pub struct PendingRegistrationResponse { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct RegisteredResponse { pub request_id: u64, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v6/conversion.rs b/common/authenticator-requests/src/v6/conversion.rs new file mode 100644 index 00000000000..8bcc204d7c4 --- /dev/null +++ b/common/authenticator-requests/src/v6/conversion.rs @@ -0,0 +1,441 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{v5, v6}; + +impl TryFrom for v6::request::AuthenticatorRequest { + type Error = crate::Error; + + fn try_from( + authenticator_request: v5::request::AuthenticatorRequest, + ) -> Result { + Ok(Self { + protocol: v6::PROTOCOL, + data: authenticator_request.data.try_into()?, + request_id: authenticator_request.request_id, + }) + } +} + +impl TryFrom for v6::request::AuthenticatorRequestData { + type Error = crate::Error; + + fn try_from( + authenticator_request_data: v5::request::AuthenticatorRequestData, + ) -> Result { + match authenticator_request_data { + v5::request::AuthenticatorRequestData::Initial(init_msg) => Ok( + v6::request::AuthenticatorRequestData::Initial(init_msg.into()), + ), + v5::request::AuthenticatorRequestData::Final(final_msg) => Ok( + v6::request::AuthenticatorRequestData::Final(Box::new((*final_msg).try_into()?)), + ), + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => Ok( + v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key), + ), + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => Ok( + v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into()), + ), + } + } +} + +impl From for v6::registration::InitMessage { + fn from(init_msg: v5::registration::InitMessage) -> Self { + Self { + pub_key: init_msg.pub_key, + } + } +} + +impl TryFrom for v6::registration::FinalMessage { + type Error = crate::Error; + + fn try_from(final_msg: v5::registration::FinalMessage) -> Result { + Ok(Self { + gateway_client: final_msg.gateway_client.into(), + credential: final_msg + .credential + .map(TryInto::try_into) + .transpose() + .map_err(Self::Error::conversion_display)?, + }) + } +} + +impl From for v6::registration::GatewayClient { + fn from(gateway_client: v5::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v5::registration::GatewayClient { + fn from(gateway_client: v6::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v6::registration::ClientMac { + fn from(client_mac: v5::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From for v5::registration::ClientMac { + fn from(client_mac: v6::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From> for Box { + fn from(top_up_message: Box) -> Self { + Box::new(v6::topup::TopUpMessage { + pub_key: top_up_message.pub_key, + credential: top_up_message.credential, + }) + } +} + +impl From for v6::response::AuthenticatorResponse { + fn from(value: v5::response::AuthenticatorResponse) -> Self { + Self { + protocol: v6::PROTOCOL, + data: value.data.into(), + } + } +} + +impl From for v6::response::AuthenticatorResponseData { + fn from(authenticator_response_data: v5::response::AuthenticatorResponseData) -> Self { + match authenticator_response_data { + v5::response::AuthenticatorResponseData::PendingRegistration(pending_response) => { + v6::response::AuthenticatorResponseData::PendingRegistration( + pending_response.into(), + ) + } + v5::response::AuthenticatorResponseData::Registered(registered_response) => { + v6::response::AuthenticatorResponseData::Registered(registered_response.into()) + } + v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => v6::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + ), + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => { + v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into()) + } + } + } +} + +impl From for v6::response::RegisteredResponse { + fn from(value: v5::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::response::PendingRegistrationResponse { + fn from(value: v5::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::registration::RegistrationData { + fn from(value: v5::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::registration::RegistrationData { + fn from(value: v6::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v6::response::RemainingBandwidthResponse { + fn from(value: v5::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.map(Into::into), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::response::TopUpBandwidthResponse { + fn from(value: v5::response::TopUpBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::registration::RegisteredData { + fn from(value: v5::registration::RegisteredData) -> Self { + Self { + pub_key: value.pub_key, + private_ips: value.private_ips.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v6::registration::RemainingBandwidthData { + fn from(value: v5::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} + +impl From for v6::registration::IpPair { + fn from(value: v5::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +impl From for v5::registration::IpPair { + fn from(value: v6::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, + }; + + use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType}; + use nym_crypto::asymmetric::x25519::PrivateKey; + use nym_wireguard_types::PeerPublicKey; + use x25519_dalek::PublicKey; + + use super::*; + use crate::models::BandwidthClaim; + use crate::{util::tests::CREDENTIAL_BYTES, v5}; + + #[test] + fn upgrade_initial_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + + let (msg, _) = v5::request::AuthenticatorRequest::new_initial_request( + v5::registration::InitMessage::new(pub_key), + ); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::Initial(v6::registration::InitMessage { + pub_key + }) + ); + } + + #[test] + fn upgrade_final_req() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v5::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let gateway_client = v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let final_message = v5::registration::FinalMessage { + gateway_client: gateway_client.clone(), + credential: Some(credential.clone()), + }; + + let (msg, _) = v5::request::AuthenticatorRequest::new_final_request(final_message); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::Final(Box::new( + v6::registration::FinalMessage { + gateway_client: v6::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v6::registration::IpPair::new(ipv4, ipv6), + nonce + ), + credential: Some(BandwidthClaim { + credential: BandwidthCredential::ZkNym(Box::new(credential)), + kind: TicketType::V1MixnetEntry, + }) + } + )) + ); + } + + #[test] + fn upgrade_query_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + + let (msg, _) = v5::request::AuthenticatorRequest::new_query_request(pub_key); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + ); + } + + #[test] + fn upgrade_pending_reg_resp() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v5::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let wg_port = 51822; + let gateway_data = v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let registration_data = v5::registration::RegistrationData { + nonce, + gateway_data, + wg_port, + }; + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_pending_registration_success( + registration_data, + request_id, + ); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::PendingRegistration( + v6::response::PendingRegistrationResponse { + request_id, + reply: v6::registration::RegistrationData { + nonce, + gateway_data: v6::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v6::registration::IpPair::new(ipv4, ipv6), + nonce + ), + wg_port + }, + upgrade_mode_enabled: false, + } + ) + ); + } + + #[test] + fn upgrade_registered_resp() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let private_ips = v5::registration::IpPair::new(ipv4, ipv6); + let wg_port = 51822; + let registered_data = v5::registration::RegisteredData { + pub_key, + private_ips, + wg_port, + }; + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_registered(registered_data, request_id); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::Registered(v6::response::RegisteredResponse { + request_id, + reply: v6::registration::RegisteredData { + wg_port, + pub_key, + private_ips: v6::registration::IpPair::new(ipv4, ipv6) + }, + upgrade_mode_enabled: false, + }) + ); + } + + #[test] + fn upgrade_remaining_bandwidth_resp() { + let available_bandwidth = 42; + let remaining_bandwidth_data = Some(v5::registration::RemainingBandwidthData { + available_bandwidth, + }); + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_remaining_bandwidth( + remaining_bandwidth_data, + request_id, + ); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::RemainingBandwidth( + v6::response::RemainingBandwidthResponse { + request_id, + reply: Some(v6::registration::RemainingBandwidthData { + available_bandwidth, + }), + upgrade_mode_enabled: false, + } + ) + ); + } +} diff --git a/common/authenticator-requests/src/v6/mod.rs b/common/authenticator-requests/src/v6/mod.rs new file mode 100644 index 00000000000..6fbc095ae94 --- /dev/null +++ b/common/authenticator-requests/src/v6/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; + +pub mod conversion; +pub mod registration; +pub mod request; +pub mod response; +pub mod topup; +pub mod upgrade_mode_check; + +pub const VERSION: u8 = 6; + +pub const PROTOCOL: Protocol = Protocol::new(VERSION, ServiceProviderType::Authenticator); diff --git a/common/authenticator-requests/src/v6/registration.rs b/common/authenticator-requests/src/v6/registration.rs new file mode 100644 index 00000000000..11fcf34116f --- /dev/null +++ b/common/authenticator-requests/src/v6/registration.rs @@ -0,0 +1,287 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use crate::models::BandwidthClaim; +use base64::{Engine, engine::general_purpose}; +use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::time::SystemTime; +use std::{fmt, ops::Deref, str::FromStr}; + +#[cfg(feature = "verify")] +use hmac::{Hmac, Mac}; +#[cfg(feature = "verify")] +use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey}; +#[cfg(feature = "verify")] +use sha2::Sha256; + +pub type PendingRegistrations = HashMap; +pub type PrivateIPs = HashMap; + +#[cfg(feature = "verify")] +pub type HmacSha256 = Hmac; + +pub type Nonce = u64; +pub type Taken = Option; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct IpPair { + pub ipv4: Ipv4Addr, + pub ipv6: Ipv6Addr, +} + +impl IpPair { + pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl From<(Ipv4Addr, Ipv6Addr)> for IpPair { + fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl fmt::Display for IpPair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.ipv4, self.ipv6) + } +} + +impl From for IpPair { + fn from(value: IpAddr) -> Self { + let (before_last_byte, last_byte) = match value { + IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), + IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), + }; + let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16; + let ipv4 = Ipv4Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0], + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1], + before_last_byte, + last_byte, + ); + let ipv6 = Ipv6Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6], + last_bytes, + ); + IpPair::new(ipv4, ipv6) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InitMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, +} + +impl InitMessage { + pub fn new(pub_key: PeerPublicKey) -> Self { + InitMessage { pub_key } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct FinalMessage { + /// Gateway client data + pub gateway_client: GatewayClient, + + /// Ecash credential + pub credential: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistrationData { + pub nonce: u64, + pub gateway_data: GatewayClient, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegisteredData { + pub pub_key: PeerPublicKey, + pub private_ips: IpPair, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RemainingBandwidthData { + pub available_bandwidth: i64, +} + +/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct GatewayClient { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Assigned private IPs (v4 and v6) + pub private_ips: IpPair, + + /// Sha256 hmac on the data (alongside the prior nonce) + pub mac: ClientMac, +} + +impl GatewayClient { + #[cfg(feature = "verify")] + pub fn new( + local_secret: &PrivateKey, + remote_public: x25519_dalek::PublicKey, + private_ips: IpPair, + nonce: u64, + ) -> Self { + let local_public = PublicKey::from(local_secret); + let remote_public = PublicKey::from(remote_public); + + let dh = local_secret.diffie_hellman(&remote_public); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(&dh[..]) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(local_public.as_bytes()); + mac.update(private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + GatewayClient { + pub_key: PeerPublicKey::new(local_public.into()), + private_ips, + mac: ClientMac(mac.finalize().into_bytes().to_vec()), + } + } + + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + #[cfg(feature = "verify")] + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + // use gateways key as a ref to an x25519_dalek key + let dh = gateway_key.inner().diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| Error::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +// TODO2: rely on our internal crypto/hmac +#[derive(Debug, Clone, PartialEq)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl From> for ClientMac { + fn from(v: Vec) -> Self { + ClientMac(v) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientMac { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = + general_purpose::STANDARD + .decode(s) + .map_err(|source| Error::MalformedClientMac { + mac: s.to_string(), + source, + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientMac { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::x25519; + use nym_test_utils::helpers::deterministic_rng; + + #[test] + fn create_ip_pair() { + let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into(); + let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into(); + + assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6)); + } + + #[test] + #[cfg(feature = "verify")] + fn client_request_roundtrip() { + let mut rng = deterministic_rng(); + + let gateway_key_pair = x25519::KeyPair::new(&mut rng); + let client_key_pair = x25519::KeyPair::new(&mut rng); + + let nonce = 1234567890; + + let client = GatewayClient::new( + client_key_pair.private_key(), + x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()), + IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()), + nonce, + ); + assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) + } +} diff --git a/common/authenticator-requests/src/v6/request.rs b/common/authenticator-requests/src/v6/request.rs new file mode 100644 index 00000000000..3bc8140b747 --- /dev/null +++ b/common/authenticator-requests/src/v6/request.rs @@ -0,0 +1,135 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + PROTOCOL, + registration::{FinalMessage, InitMessage}, + topup::TopUpMessage, + upgrade_mode_check::UpgradeModeCheckRequest, +}; +use nym_service_provider_requests_common::Protocol; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorRequest { + pub protocol: Protocol, + pub data: AuthenticatorRequestData, + pub request_id: u64, +} + +impl AuthenticatorRequest { + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::Initial(init_message), + request_id, + }, + request_id, + ) + } + + pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::Final(Box::new(final_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::QueryBandwidth(peer_public_key), + request_id, + }, + request_id, + ) + } + + pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_upgrade_mode_check_request(message: UpgradeModeCheckRequest) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::CheckUpgradeMode(message), + request_id, + }, + request_id, + ) + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorRequestData { + Initial(InitMessage), + Final(Box), + QueryBandwidth(PeerPublicKey), + TopUpBandwidth(Box), + CheckUpgradeMode(UpgradeModeCheckRequest), +} + +#[cfg(test)] +mod tests { + use super::super::VERSION; + use super::*; + use nym_service_provider_requests_common::ServiceProviderType; + use std::str::FromStr; + + #[test] + fn check_first_bytes_protocol() { + let version = VERSION; + let data = AuthenticatorRequest { + protocol: Protocol { + version, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: AuthenticatorRequestData::Initial(InitMessage::new( + PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(), + )), + request_id: 1, + }; + let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap(); + assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]); + } +} diff --git a/common/authenticator-requests/src/v6/response.rs b/common/authenticator-requests/src/v6/response.rs new file mode 100644 index 00000000000..c93c34e1c34 --- /dev/null +++ b/common/authenticator-requests/src/v6/response.rs @@ -0,0 +1,153 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; +use nym_service_provider_requests_common::Protocol; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::PROTOCOL; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorResponse { + pub protocol: Protocol, + pub data: AuthenticatorResponseData, +} + +impl AuthenticatorResponse { + pub fn new_pending_registration_success( + registration_data: RegistrationData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse { + reply: registration_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_registered( + registered_data: RegisteredData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::Registered(RegisteredResponse { + reply: registered_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_remaining_bandwidth( + remaining_bandwidth_data: Option, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_topup_bandwidth( + remaining_bandwidth_data: RemainingBandwidthData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_upgrade_mode_check(request_id: u64, upgrade_mode_enabled: bool) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::UpgradeMode(UpgradeModeResponse { + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn id(&self) -> Option { + match &self.data { + AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id), + AuthenticatorResponseData::Registered(response) => Some(response.request_id), + AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::UpgradeMode(response) => Some(response.request_id), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorResponseData { + PendingRegistration(PendingRegistrationResponse), + Registered(RegisteredResponse), + RemainingBandwidth(RemainingBandwidthResponse), + TopUpBandwidth(TopUpBandwidthResponse), + UpgradeMode(UpgradeModeResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingRegistrationResponse { + pub request_id: u64, + pub reply: RegistrationData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisteredResponse { + pub request_id: u64, + pub reply: RegisteredData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RemainingBandwidthResponse { + pub request_id: u64, + pub reply: Option, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopUpBandwidthResponse { + pub request_id: u64, + pub reply: RemainingBandwidthData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct UpgradeModeResponse { + pub request_id: u64, + pub upgrade_mode_enabled: bool, +} diff --git a/common/authenticator-requests/src/v6/topup.rs b/common/authenticator-requests/src/v6/topup.rs new file mode 100644 index 00000000000..b5d25a9dbf5 --- /dev/null +++ b/common/authenticator-requests/src/v6/topup.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TopUpMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Ecash credential + pub credential: CredentialSpendingData, +} diff --git a/common/authenticator-requests/src/v6/upgrade_mode_check.rs b/common/authenticator-requests/src/v6/upgrade_mode_check.rs new file mode 100644 index 00000000000..ae27af38006 --- /dev/null +++ b/common/authenticator-requests/src/v6/upgrade_mode_check.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum UpgradeModeCheckRequest { + /// Attempt to request upgrade mode recheck via the JWT issued as the result of + /// global attestation.json being published + UpgradeModeJwt { token: String }, +} diff --git a/common/authenticator-requests/src/version.rs b/common/authenticator-requests/src/version.rs index 4bb8b6d5918..d0f2bbf2178 100644 --- a/common/authenticator-requests/src/version.rs +++ b/common/authenticator-requests/src/version.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::{v1, v2, v3, v4, v5}; +use super::{v1, v2, v3, v4, v5, v6}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; #[derive(Copy, Clone, Debug, PartialEq, strum_macros::Display)] @@ -22,11 +22,15 @@ pub enum AuthenticatorVersion { /// introduced in dorina-patched release (1.6.1) V5, + /// introduced in niolo release (1.23.0) + V6, + + /// an unknown, future, variant that can be present if running outdated software UNKNOWN, } impl AuthenticatorVersion { - pub const LATEST: Self = Self::V5; + pub const LATEST: Self = Self::V6; pub const fn release_version(&self) -> semver::Version { match self { @@ -35,6 +39,7 @@ impl AuthenticatorVersion { AuthenticatorVersion::V3 => semver::Version::new(1, 1, 10), AuthenticatorVersion::V4 => semver::Version::new(1, 2, 0), AuthenticatorVersion::V5 => semver::Version::new(1, 6, 1), + AuthenticatorVersion::V6 => semver::Version::new(1, 23, 0), AuthenticatorVersion::UNKNOWN => semver::Version::new(0, 0, 0), } } @@ -54,6 +59,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V4 } else if value.version == v5::VERSION { AuthenticatorVersion::V5 + } else if value.version == v6::VERSION { + AuthenticatorVersion::V6 } else { AuthenticatorVersion::UNKNOWN } @@ -72,6 +79,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V4 } else if value == v5::VERSION { AuthenticatorVersion::V5 + } else if value == v6::VERSION { + AuthenticatorVersion::V6 } else { AuthenticatorVersion::UNKNOWN } @@ -126,11 +135,14 @@ impl From for AuthenticatorVersion { if semver < AuthenticatorVersion::V5.release_version() { return Self::V4; } - // if provided version is higher (or equal) to release version of V5, - // we return the latest (i.e. v5) + if semver < AuthenticatorVersion::V6.release_version() { + return Self::V5; + } + // if provided version is higher (or equal) to release version of V6, + // we return the latest (i.e. v6) debug_assert_eq!( - Self::V5, + Self::V6, Self::LATEST, "a new AuthenticatorVersion variant has been introduced without adjusting the `From` trait" ); @@ -191,5 +203,9 @@ mod tests { assert_eq!(AuthenticatorVersion::V5, "1.7.0".into()); assert_eq!(AuthenticatorVersion::V5, "1.16.11".into()); assert_eq!(AuthenticatorVersion::V5, "1.17.0".into()); + assert_eq!(AuthenticatorVersion::V5, "1.22.0".into()); + assert_eq!(AuthenticatorVersion::V6, "1.23.0".into()); + assert_eq!(AuthenticatorVersion::V6, "1.23.1".into()); + assert_eq!(AuthenticatorVersion::V6, "1.24.0".into()); } } diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index efc699473a1..969d94807e3 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -88,3 +88,6 @@ features = ["js"] [features] wasm = [] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 9fd43765bdd..304eff6c96a 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use si_scale::helpers::bibytes2; +use std::fmt::{Display, Formatter}; use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -26,6 +27,39 @@ pub struct ClientBandwidth { inner: Arc, } +// simple helper for logging purposes to accommodate 'unknown' case +pub(crate) enum UpgradeModeEnabledWrapper { + True, + False, + Unknown, +} + +impl From> for UpgradeModeEnabledWrapper { + fn from(value: Option) -> Self { + match value { + Some(true) => UpgradeModeEnabledWrapper::True, + Some(false) => UpgradeModeEnabledWrapper::False, + None => UpgradeModeEnabledWrapper::Unknown, + } + } +} + +impl From for UpgradeModeEnabledWrapper { + fn from(value: bool) -> Self { + Some(value).into() + } +} + +impl Display for UpgradeModeEnabledWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + UpgradeModeEnabledWrapper::True => write!(f, "true"), + UpgradeModeEnabledWrapper::False => write!(f, "false"), + UpgradeModeEnabledWrapper::Unknown => write!(f, "unknown"), + } + } +} + struct ClientBandwidthInner { /// the actual bandwidth amount (in bytes) available available: AtomicI64, @@ -71,26 +105,41 @@ impl ClientBandwidth { self.inner.available.load(Ordering::Acquire) } - pub(crate) fn maybe_log_bandwidth(&self, now: Option) { + pub(crate) fn maybe_log_bandwidth( + &self, + now: Option, + upgrade_mode: impl Into, + ) { let last = self.last_logged(); let now = now.unwrap_or_else(OffsetDateTime::now_utc); if last + Duration::from_secs(10) < now { - self.log_bandwidth(Some(now)) + self.log_bandwidth(Some(now), upgrade_mode) } } - pub(crate) fn log_bandwidth(&self, now: Option) { + pub(crate) fn log_bandwidth( + &self, + now: Option, + upgrade_mode: impl Into, + ) { let now = now.unwrap_or_else(OffsetDateTime::now_utc); + let upgrade_mode = upgrade_mode.into(); let remaining = self.remaining(); let remaining_bi2 = bibytes2(remaining as f64); if remaining < 0 { - tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}"); + tracing::warn!( + "OUT OF BANDWIDTH. remaining: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } else if remaining < 1_000_000 { - tracing::info!("remaining bandwidth: {remaining_bi2}"); + tracing::info!( + "remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } else { - tracing::debug!("remaining bandwidth: {remaining_bi2}"); + tracing::trace!( + "remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } self.inner @@ -98,26 +147,35 @@ impl ClientBandwidth { .store(now.unix_timestamp(), Ordering::Relaxed) } - pub(crate) fn update_and_maybe_log(&self, remaining: i64) { + pub(crate) fn update_and_maybe_log( + &self, + remaining: i64, + upgrade_mode: impl Into, + ) { let now = OffsetDateTime::now_utc(); self.inner.available.store(remaining, Ordering::Release); self.inner .last_updated_ts .store(now.unix_timestamp(), Ordering::Relaxed); - self.maybe_log_bandwidth(Some(now)) + self.maybe_log_bandwidth(Some(now), upgrade_mode) } - pub(crate) fn update_and_log(&self, remaining: i64) { + pub(crate) fn update_and_log( + &self, + remaining: i64, + upgrade_mode: impl Into, + ) { let now = OffsetDateTime::now_utc(); self.inner.available.store(remaining, Ordering::Release); self.inner .last_updated_ts .store(now.unix_timestamp(), Ordering::Relaxed); - self.log_bandwidth(Some(now)) + self.log_bandwidth(Some(now), upgrade_mode) } fn last_logged(&self) -> OffsetDateTime { // SAFETY: this value is always populated with valid timestamps + #[allow(clippy::unwrap_used)] OffsetDateTime::from_unix_timestamp(self.inner.last_logged_ts.load(Ordering::Relaxed)) .unwrap() } diff --git a/common/client-libs/gateway-client/src/client/config.rs b/common/client-libs/gateway-client/src/client/config.rs index fd7bfc142d2..af24a72b901 100644 --- a/common/client-libs/gateway-client/src/client/config.rs +++ b/common/client-libs/gateway-client/src/client/config.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::GatewayClientError; -use nym_network_defaults::TicketTypeRepr::V1MixnetEntry; +use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; use si_scale::helpers::bibytes2; use std::time::Duration; @@ -103,7 +103,7 @@ impl BandwidthTickets { // 20% of entry ticket value pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 = - (V1MixnetEntry.bandwidth_value() / 5) as i64; + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option = None; diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 52e5833eb29..e9d91beeb95 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -20,9 +20,9 @@ use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ - BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, - GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey, - SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, + BandwidthResponse, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersion, + GatewayProtocolVersionExt, GatewayRequestsError, SensitiveServerResponse, ServerResponse, + SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::clients::connection::ConnectionStatsEvent; @@ -101,8 +101,7 @@ pub struct GatewayClient { bandwidth_controller: Option>, stats_reporter: ClientStatsSender, - // currently unused (but populated) - negotiated_protocol: Option, + negotiated_protocol: Option, // Callback on the fd as soon as the connection has been established #[cfg(unix)] @@ -166,10 +165,12 @@ impl GatewayClient { } #[cfg(not(target_arch = "wasm32"))] + #[allow(clippy::unreachable)] async fn _close_connection(&mut self) -> Result<(), GatewayClientError> { match std::mem::replace(&mut self.connection, SocketState::NotConnected) { SocketState::Available(mut socket) => Ok((*socket).close(None).await?), SocketState::PartiallyDelegated(_) => { + // SAFETY: this is only called after the caller has already recovered the connection unreachable!("this branch should have never been reached!") } _ => Ok(()), // no need to do anything in those cases @@ -177,6 +178,7 @@ impl GatewayClient { } #[cfg(target_arch = "wasm32")] + #[allow(clippy::unreachable)] async fn _close_connection(&mut self) -> Result<(), GatewayClientError> { match std::mem::replace(&mut self.connection, SocketState::NotConnected) { SocketState::Available(socket) => { @@ -184,6 +186,7 @@ impl GatewayClient { Ok(()) } SocketState::PartiallyDelegated(_) => { + // SAFETY: this is only called after the caller has already recovered the connection unreachable!("this branch should have never been reached!") } _ => Ok(()), // no need to do anything in those cases @@ -458,43 +461,16 @@ impl GatewayClient { } } - fn check_gateway_protocol( - &self, - gateway_protocol: Option, - ) -> Result<(), GatewayClientError> { - debug!("gateway protocol: {gateway_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}"); - - // right now there are no failure cases here, but this might change in the future - match gateway_protocol { - None => { - warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in +1.2.0 we will have to return a hard error here - Ok(()) - } - Some(v) if v > CURRENT_PROTOCOL_VERSION => { - let err = GatewayClientError::IncompatibleProtocol { - gateway: Some(v), - current: CURRENT_PROTOCOL_VERSION, - }; - error!("{err}"); - Err(err) - } - - Some(_) => { - debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); - Ok(()) - } - } - } - async fn register( &mut self, - derive_aes256_gcm_siv_key: bool, + supported_gateway_protocol: Option, ) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } + let derive_aes256_gcm_siv_key = supported_gateway_protocol.supports_aes256_gcm_siv(); + debug_assert!(self.connection.is_available()); log::debug!( "registering with gateway. using legacy key derivation: {}", @@ -505,14 +481,13 @@ impl GatewayClient { // and putting it into the GatewayClient struct would be a hassle let mut rng = OsRng; - let shared_key = match &mut self.connection { + let handshake_result = match &mut self.connection { SocketState::Available(ws_stream) => client_handshake( &mut rng, ws_stream, self.local_identity.as_ref(), self.gateway_identity, - self.cfg.bandwidth.require_tickets, - derive_aes256_gcm_siv_key, + supported_gateway_protocol, #[cfg(not(target_arch = "wasm32"))] self.shutdown_token.clone(), ) @@ -521,26 +496,31 @@ impl GatewayClient { _ => return Err(GatewayClientError::ConnectionInInvalidState), }?; - let (authentication_status, gateway_protocol) = match self.read_control_response().await? { + let authentication_status = match self.read_control_response().await? { ServerResponse::Register { - protocol_version, status, - } => (status, protocol_version), + upgrade_mode, + .. + } => { + if upgrade_mode { + warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable") + } + status + } ServerResponse::Error { message } => { return Err(GatewayClientError::GatewayError(message)) } other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }; - self.check_gateway_protocol(gateway_protocol)?; self.authenticated = authentication_status; if self.authenticated { - self.shared_key = Some(Arc::new(shared_key)); + self.shared_key = Some(Arc::new(handshake_result.derived_key)); } // populate the negotiated protocol for future uses - self.negotiated_protocol = gateway_protocol; + self.negotiated_protocol = Some(handshake_result.negotiated_protocol); Ok(()) } @@ -623,13 +603,24 @@ impl GatewayClient { protocol_version, status, bandwidth_remaining, + upgrade_mode, } => { - self.check_gateway_protocol(protocol_version)?; + if protocol_version.is_future_version() { + // SAFETY: future version is always defined + #[allow(clippy::unwrap_used)] + let version = protocol_version.unwrap(); + error!("the gateway insists on using v{version} protocol which is not supported by this client"); + return Err(GatewayClientError::AuthenticationFailure); + } self.authenticated = status; - self.bandwidth.update_and_maybe_log(bandwidth_remaining); + self.bandwidth + .update_and_maybe_log(bandwidth_remaining, upgrade_mode); self.negotiated_protocol = protocol_version; log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}"); + if upgrade_mode { + warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable") + } Ok(()) } @@ -650,7 +641,7 @@ impl GatewayClient { .public_key() .derive_destination_address(); - let msg = ClientControlRequest::new_authenticate( + let msg = ClientControlRequest::new_legacy_authenticate( self_address, shared_key, self.cfg.bandwidth.require_tickets, @@ -659,25 +650,40 @@ impl GatewayClient { .await } - async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> { + async fn authenticate_v2( + &mut self, + requested_protocol_version: GatewayProtocolVersion, + ) -> Result<(), GatewayClientError> { debug!("using v2 authentication"); let Some(shared_key) = self.shared_key.as_ref() else { return Err(GatewayClientError::NoSharedKeyAvailable); }; - let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?; + let msg = ClientControlRequest::new_authenticate_v2( + shared_key, + &self.local_identity, + requested_protocol_version, + )?; self.send_authenticate_request_and_handle_response(msg) .await } - async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> { + async fn authenticate( + &mut self, + supported_gateway_protocol: Option, + ) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } debug!("authenticating with gateway"); - if use_v2 { - self.authenticate_v2().await + if supported_gateway_protocol.supports_authenticate_v2() { + // use the highest possible protocol version the gateway has announced support for + + // SAFETY: if announced protocol supports auth v2, it means it's properly set + #[allow(clippy::unwrap_used)] + self.authenticate_v2(supported_gateway_protocol.unwrap()) + .await } else { self.authenticate_v1().await } @@ -708,9 +714,12 @@ impl GatewayClient { } }; + debug!("supported gateway protocol: {gw_protocol:?}"); + let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv(); let supports_auth_v2 = gw_protocol.supports_authenticate_v2(); let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet(); + let supports_upgrade_mode = gw_protocol.supports_upgrade_mode(); if !supports_aes_gcm_siv { warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); @@ -721,6 +730,16 @@ impl GatewayClient { if !supports_key_rotation_info { warn!("this gateway is on an old version that doesn't support key rotation packets") } + if !supports_upgrade_mode { + warn!("this gateway is on an old version that doesn't support upgrade mode") + } + + let gw_protocol = if gw_protocol.is_future_version() { + warn!("we're running outdated software as gateway is announcing protocol {gw_protocol:?} whilst we're using {}. we're going to attempt to downgrade", GatewayProtocolVersion::CURRENT); + Some(GatewayProtocolVersion::CURRENT) + } else { + gw_protocol + }; if self.authenticated { debug!("Already authenticated"); @@ -735,10 +754,11 @@ impl GatewayClient { } if self.shared_key.is_some() { - self.authenticate(supports_auth_v2).await?; + self.authenticate(gw_protocol).await?; if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key + #[allow(clippy::unwrap_used)] let shared_key = self.shared_key.as_ref().unwrap(); let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv; @@ -751,9 +771,10 @@ impl GatewayClient { Err(GatewayClientError::AuthenticationFailure) } } else { - self.register(supports_aes_gcm_siv).await?; + self.register(gw_protocol).await?; // if registration didn't return an error, we MUST have an associated shared key + #[allow(clippy::unwrap_used)] let shared_key = self.shared_key.as_ref().unwrap(); // we're always registering with the highest supported protocol, @@ -783,51 +804,81 @@ impl GatewayClient { } } - async fn claim_ecash_bandwidth( + async fn wait_for_bandwidth_response( &mut self, - credential: CredentialSpendingData, - ) -> Result<(), GatewayClientError> { - let msg = ClientControlRequest::new_enc_ecash_credential( - credential, - self.shared_key.as_ref().unwrap(), - )?; - let bandwidth_remaining = match self + msg: ClientControlRequest, + ) -> Result { + let response = match self .send_websocket_message_with_non_send_response(msg) .await? { - ServerResponse::Bandwidth { available_total } => Ok(available_total), + ServerResponse::Bandwidth(response) => { + if response.upgrade_mode { + info!("the system is currently undergoing an upgrade. our bandwidth shouldn't have been metered") + } + Ok(response) + } ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), ServerResponse::TypedError { error } => { Err(GatewayClientError::TypedGatewayError(error)) } other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }?; + Ok(response) + } + + async fn claim_ecash_bandwidth( + &mut self, + credential: CredentialSpendingData, + ) -> Result<(), GatewayClientError> { + // SAFETY: claiming ecash bandwidth is called as part of `claim_bandwidth` which + // ensures the shared key is defined + #[allow(clippy::unwrap_used)] + let msg = ClientControlRequest::new_enc_ecash_credential( + credential, + self.shared_key.as_ref().unwrap(), + )?; + let response = self.wait_for_bandwidth_response(msg).await?; // TODO: create tracing span info!("managed to claim ecash bandwidth"); - self.bandwidth.update_and_log(bandwidth_remaining); + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); + + Ok(()) + } + + pub async fn send_upgrade_mode_jwt(&mut self, token: String) -> Result<(), GatewayClientError> { + let msg = ClientControlRequest::new_upgrade_mode_jwt(token); + let response = self.wait_for_bandwidth_response(msg).await?; + + // if gateway rejected our jwt, we would have returned an error + info!("gateway has accepted our jwt"); + if !response.upgrade_mode { + error!("but we're not in upgrade mode - something is wrong!"); + return Err(GatewayClientError::UnexpectedUpgradeModeState); + } + + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); Ok(()) } async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth; - let bandwidth_remaining = match self - .send_websocket_message_with_non_send_response(msg) - .await? - { - ServerResponse::Bandwidth { available_total } => Ok(available_total), - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), - }?; + let response = self.wait_for_bandwidth_response(msg).await?; info!("managed to claim testnet bandwidth"); - self.bandwidth.update_and_log(bandwidth_remaining); + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); Ok(()) } fn unchecked_bandwidth_controller(&self) -> &BandwidthController { + // this is an unchecked method + #[allow(clippy::unwrap_used)] self.bandwidth_controller.as_ref().unwrap() } @@ -919,6 +970,7 @@ impl GatewayClient { BinaryRequest::ForwardSphinx { packet } }; + #[allow(clippy::expect_used)] req.into_ws_message( self.shared_key .as_ref() @@ -1025,6 +1077,8 @@ impl GatewayClient { self.send_with_reconnection_on_failure(msg).await } + // SAFETY: this method is only called when the connection is in `PartiallyDelegated` state + #[allow(clippy::unreachable)] async fn recover_socket_connection(&mut self) -> Result<(), GatewayClientError> { if self.connection.is_available() { return Ok(()); @@ -1054,6 +1108,7 @@ impl GatewayClient { return Err(GatewayClientError::ConnectionInInvalidState); } + #[allow(clippy::expect_used)] let partially_delegated = match std::mem::replace(&mut self.connection, SocketState::Invalid) { SocketState::Available(conn) => { @@ -1069,7 +1124,13 @@ impl GatewayClient { self.shutdown_token.clone(), ) } - _ => unreachable!(), + other => { + error!( + "attempted to start mixnet listener whilst the connection is in {} state!", + other.name() + ); + return Err(GatewayClientError::ConnectionInInvalidState); + } }; self.connection = SocketState::PartiallyDelegated(partially_delegated); @@ -1082,8 +1143,7 @@ impl GatewayClient { } // if we're reconnecting, because we lost connection, we need to re-authenticate the connection - self.authenticate(self.negotiated_protocol.supports_authenticate_v2()) - .await?; + self.authenticate(self.negotiated_protocol).await?; // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index eaea37c586c..9f97b7c7f58 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -128,6 +128,9 @@ pub enum GatewayClientError { "this operation couldn't be completed as the program is in the process of shutting down" )] ShutdownInProgress, + + #[error("the system is an unexpected upgrade mode state")] + UnexpectedUpgradeModeState, } impl From for GatewayClientError { diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 7fb863947f3..93019564b97 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -35,6 +35,7 @@ impl PacketRouter { } } + #[allow(clippy::panic)] pub fn route_mixnet_messages( &self, received_messages: Vec>, @@ -54,6 +55,7 @@ impl PacketRouter { Ok(()) } + #[allow(clippy::panic)] pub fn route_acks(&self, received_acks: Vec>) -> Result<(), GatewayClientError> { if let Err(err) = self.ack_sender.unbounded_send(received_acks) { // check if the failure is due to the shutdown being in progress and thus the receiver channel diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 4f3009e3892..151ef4842ef 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -10,7 +10,9 @@ use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use nym_gateway_requests::shared_key::SharedGatewayKey; -use nym_gateway_requests::{SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError}; +use nym_gateway_requests::{ + SendResponse, SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError, +}; use nym_task::ShutdownToken; use si_scale::helpers::bibytes2; use std::os::raw::c_int as RawFd; @@ -154,11 +156,12 @@ impl PartiallyDelegatedRouter { fn handle_text_message(&self, text: String) -> Result<(), GatewayClientError> { // if we fail to deserialise the response, return a hard error. we can't handle garbage match ServerResponse::try_from(text).map_err(|_| GatewayClientError::MalformedResponse)? { - ServerResponse::Send { + ServerResponse::Send(SendResponse { remaining_bandwidth, - } => { + upgrade_mode, + }) => { self.client_bandwidth - .update_and_maybe_log(remaining_bandwidth); + .update_and_maybe_log(remaining_bandwidth, upgrade_mode); Ok(()) } ServerResponse::Error { message } => { @@ -174,7 +177,7 @@ impl PartiallyDelegatedRouter { let available_bi2 = bibytes2(available as f64); let required_bi2 = bibytes2(required as f64); warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message"); - self.client_bandwidth.update_and_log(available); + self.client_bandwidth.update_and_log(available, None); // UNIMPLEMENTED: we should stop sending messages until we recover bandwidth Ok(()) } @@ -327,6 +330,7 @@ impl PartiallyDelegatedHandle { Ok(self.sink_half.send_all(&mut send_stream).await?) } + #[allow(clippy::panic)] pub(crate) async fn merge(self) -> Result { let (mut stream_receiver, notify) = self.delegated_stream; @@ -355,8 +359,10 @@ impl PartiallyDelegatedHandle { // in receive_res .map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?; let stream = stream_results?; + // the error is thrown when trying to reunite sink and stream that did not originate // from the same split which is impossible to happen here + #[allow(clippy::unwrap_used)] Ok(self.sink_half.reunite(stream).unwrap()) } } @@ -387,4 +393,13 @@ impl SocketState { SocketState::Available(_) | SocketState::PartiallyDelegated(_) ) } + + pub(crate) fn name(&self) -> &'static str { + match self { + SocketState::Available(_) => "available", + SocketState::PartiallyDelegated(_) => "partially delegated", + SocketState::NotConnected => "not connected", + SocketState::Invalid => "invalid", + } + } } diff --git a/common/credential-proxy/src/error.rs b/common/credential-proxy/src/error.rs index d6e3e8d28fd..86219fc5bd2 100644 --- a/common/credential-proxy/src/error.rs +++ b/common/credential-proxy/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2025 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_crypto::asymmetric::ed25519; use nym_ecash_signer_check::SignerCheckError; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::{EpochId, error::NymAPIError}; @@ -174,8 +175,18 @@ pub enum CredentialProxyError { )] AttestationCheckUrlNotSet, - #[error("the provided attestation check url is malformed: {source}")] + #[error("the provided attester public key is malformed: {source}")] MalformedAttestationCheckUrl { source: url::ParseError }, + + #[error( + "the attester public key has not been provided through either the CLI nor the default .env config" + )] + AttesterPublicKeyNotSet, + + #[error("the provided attester public key is malformed: {source}")] + MalformedAttesterPublicKey { + source: ed25519::Ed25519RecoveryError, + }, } impl From for CredentialProxyError { diff --git a/common/credential-verification/Cargo.toml b/common/credential-verification/Cargo.toml index e85589eaaec..06c45efdc55 100644 --- a/common/credential-verification/Cargo.toml +++ b/common/credential-verification/Cargo.toml @@ -17,7 +17,6 @@ cosmwasm-std = { workspace = true } cw-utils = { workspace = true } dyn-clone = { workspace = true } futures = { workspace = true } -rand = { workspace = true } si-scale = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } @@ -27,8 +26,10 @@ tracing = { workspace = true } nym-api-requests = { path = "../../nym-api/nym-api-requests" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } +nym-crypto = { path = "../crypto", features = ["asymmetric"] } nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-gateway-requests = { path = "../gateway-requests" } nym-gateway-storage = { path = "../gateway-storage" } nym-task = { path = "../task" } nym-validator-client = { path = "../client-libs/validator-client" } +nym-upgrade-mode-check = { path = "../upgrade-mode-check" } diff --git a/common/credential-verification/src/bandwidth_storage_manager.rs b/common/credential-verification/src/bandwidth_storage_manager.rs index 7286229e688..9a78b9f95e2 100644 --- a/common/credential-verification/src/bandwidth_storage_manager.rs +++ b/common/credential-verification/src/bandwidth_storage_manager.rs @@ -6,7 +6,6 @@ use crate::ClientBandwidth; use crate::error::*; use nym_credentials::ecash::utils::ecash_today; use nym_credentials_interface::Bandwidth; -use nym_gateway_requests::ServerResponse; use nym_gateway_storage::traits::BandwidthGatewayStorage; use si_scale::helpers::bibytes2; use time::OffsetDateTime; @@ -66,7 +65,7 @@ impl BandwidthStorageManager { Ok(()) } - pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result { + pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result { debug!("handling testnet bandwidth request"); if self.only_coconut_credentials { @@ -76,8 +75,7 @@ impl BandwidthStorageManager { self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today()) .await?; let available_total = self.client_bandwidth.available().await; - - Ok(ServerResponse::Bandwidth { available_total }) + Ok(available_total) } #[instrument(skip_all)] @@ -96,7 +94,7 @@ impl BandwidthStorageManager { let available_bi2 = bibytes2(available_bandwidth as f64); let required_bi2 = bibytes2(required_bandwidth as f64); - debug!(available = available_bi2, required = required_bi2); + trace!(available = available_bi2, required = required_bi2); self.consume_bandwidth(required_bandwidth).await?; let remaining_bandwidth = self.client_bandwidth.available().await; diff --git a/common/credential-verification/src/error.rs b/common/credential-verification/src/error.rs index a00ce0e2689..23fb47d8c01 100644 --- a/common/credential-verification/src/error.rs +++ b/common/credential-verification/src/error.rs @@ -47,6 +47,25 @@ pub enum Error { UnknownTicketType(#[from] nym_credentials_interface::UnknownTicketType), } +impl Error { + pub fn is_out_of_bandwidth(&self) -> bool { + matches!(self, Error::OutOfBandwidth { .. }) + } +} + +pub trait OutOfBandwidthResultExt { + fn is_out_of_bandwidth(&self) -> bool; +} + +impl OutOfBandwidthResultExt for Result { + fn is_out_of_bandwidth(&self) -> bool { + match &self { + Ok(_) => false, + Err(err) => err.is_out_of_bandwidth(), + } + } +} + impl From for Error { fn from(err: EcashTicketError) -> Self { // don't expose storage issue details to the user diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index f306867d98e..3f2b77f8405 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -18,6 +18,7 @@ pub mod bandwidth_storage_manager; mod client_bandwidth; pub mod ecash; pub mod error; +pub mod upgrade_mode; pub struct CredentialVerifier { credential: CredentialSpendingRequest, diff --git a/common/credential-verification/src/upgrade_mode.rs b/common/credential-verification/src/upgrade_mode.rs new file mode 100644 index 00000000000..81385bece1e --- /dev/null +++ b/common/credential-verification/src/upgrade_mode.rs @@ -0,0 +1,284 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use nym_crypto::asymmetric::ed25519; +use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, validate_upgrade_mode_jwt, +}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::time::Duration; +use thiserror::Error; +use time::OffsetDateTime; +use tokio::sync::{Notify, RwLock}; +use tracing::{debug, error}; + +#[derive(Debug, Error)] +pub enum UpgradeModeEnableError { + #[error("too soon to perform another upgrade mode attestation check")] + TooManyRecheckRequests, + + #[error("provided upgrade mode JWT is invalid: {0}")] + InvalidUpgradeModeJWT(#[from] nym_upgrade_mode_check::UpgradeModeCheckError), + + #[error("the upgrade mode attestation does not appear to have been published")] + AttestationNotPublished, + + #[error("the provided upgrade mode attestation is different from the published one")] + MismatchedUpgradeModeAttestation, +} + +// the idea behind this is as follows: +// it's been relatively a long time since the watcher last performed its checks (since it's in 'regular' mode) +// and some client has just sent a JWT. we have to retrieve most recent information in case upgrade mode +// has just been enabled, and we haven't learned about it yet +#[derive(Clone)] +pub struct UpgradeModeCheckRequestSender(Option>); + +impl UpgradeModeCheckRequestSender { + pub fn new(sender: UnboundedSender) -> Self { + UpgradeModeCheckRequestSender(Some(sender)) + } + + pub fn new_empty() -> Self { + Self(None) + } + + pub(crate) fn send_request(&self, on_done: Arc) { + let Some(ref inner) = self.0 else { + // make sure the caller gets notified so it doesn't wait forever + on_done.notify_waiters(); + return; + }; + + if let Err(not_sent) = inner.unbounded_send(CheckRequest { on_done }) { + debug!("failed to send upgrade mode check request - {not_sent}"); + // make sure the caller gets notified so it doesn't wait forever + not_sent.into_inner().on_done.notify_waiters(); + } + } +} + +pub type UpgradeModeCheckRequestReceiver = UnboundedReceiver; + +pub struct CheckRequest { + on_done: Arc, +} + +impl CheckRequest { + pub fn finalize(self) { + self.on_done.notify_waiters(); + } +} + +#[derive(Clone, Copy)] +pub struct UpgradeModeCheckConfig { + /// The minimum duration since the last explicit check to allow creation of separate request. + pub min_staleness_recheck: Duration, +} + +/// Full upgrade mode information, that apart from boolean flag indicating the state +/// and the attestation information, includes channel connection to relevant +/// attestation watcher to request state rechecks +#[derive(Clone)] +pub struct UpgradeModeDetails { + pub(crate) config: UpgradeModeCheckConfig, + pub(crate) request_checker: UpgradeModeCheckRequestSender, + pub(crate) state: UpgradeModeState, +} + +impl UpgradeModeDetails { + pub fn new( + config: UpgradeModeCheckConfig, + request_checker: UpgradeModeCheckRequestSender, + state: UpgradeModeState, + ) -> Self { + UpgradeModeDetails { + config, + request_checker, + state, + } + } + + pub fn enabled(&self) -> bool { + self.state.upgrade_mode_enabled() + } + + fn since_last_query(&self) -> Duration { + self.state.since_last_query() + } + + pub fn can_request_recheck(&self) -> bool { + self.since_last_query() > self.config.min_staleness_recheck + } + + // explicitly request state update. this is only called when upgrade mode is NOT enabled, + // and client has sent a JWT instead of ticket + async fn request_recheck(&self) -> bool { + // send request + let on_done = Arc::new(Notify::new()); + self.request_checker.send_request(on_done.clone()); + + // wait for response - note, if we fail to send, notification will be sent regardless, + // so that we wouldn't get stuck in here + on_done.notified().await; + + // check the state again + self.enabled() + } + + pub async fn try_enable_via_received_jwt( + &self, + token: String, + ) -> Result<(), UpgradeModeEnableError> { + // see if it's viable to perform another expedited check + if !self.can_request_recheck() { + return Err(UpgradeModeEnableError::TooManyRecheckRequests); + } + + // first validate whether the received JWT is even valid + let attestation = validate_upgrade_mode_jwt(&token, Some(CREDENTIAL_PROXY_JWT_ISSUER))?; + + // send request to revalidate internal state + // this will, among other things, pull fresh attestation from the configured endpoint + // and also verify required signatures (and pubkeys) + self.request_recheck().await; + + // not strictly necessary, but check if provided attestation actually matches the one retrieved + // (if any) + let Some(retrieved_attestation) = self.state.attestation().await else { + return Err(UpgradeModeEnableError::AttestationNotPublished); + }; + if retrieved_attestation != attestation { + return Err(UpgradeModeEnableError::MismatchedUpgradeModeAttestation); + } + + // note: if attestation has been returned, it means we're definitely in upgrade mode + // (otherwise it wouldn't have existed in the state) + + Ok(()) + } +} + +/// Detailed upgrade mode information, that apart from boolean flag, +/// also includes, if applicable, the associated attestation +#[derive(Clone)] +pub struct UpgradeModeState { + inner: Arc, +} + +/// Just a shareable flag to indicate whether upgrade mode is enabled or disabled +#[derive(Clone, Default)] +pub struct UpgradeModeStatus(Arc); + +impl UpgradeModeStatus { + pub fn enabled(&self) -> bool { + self.0.load(Ordering::Acquire) + } + + pub fn enable(&self) { + self.0.store(true, Ordering::Relaxed); + } + + pub fn disable(&self) { + self.0.store(false, Ordering::Release); + } +} + +impl UpgradeModeState { + pub fn new(attester_public_key: ed25519::PublicKey) -> UpgradeModeState { + UpgradeModeState { + inner: Arc::new(UpgradeModeStateInner { + expected_attester_public_key: attester_public_key, + expected_attestation: RwLock::new(None), + last_queried_ts: AtomicI64::new(OffsetDateTime::UNIX_EPOCH.unix_timestamp()), + status: UpgradeModeStatus(Arc::new(AtomicBool::new(false))), + }), + } + } + + pub async fn attestation(&self) -> Option { + self.inner.expected_attestation.read().await.clone() + } + + pub async fn try_set_expected_attestation( + &self, + expected_attestation: Option, + ) { + // make sure to only enable upgrade mode flag AFTER we have written the expected value + // (or still hold the exclusive lock as in this instance) + let mut guard = self.inner.expected_attestation.write().await; + + // ensure that the attestation had been signed with the expected key + if let Some(attestation) = expected_attestation.as_ref() { + if attestation.content.attester_public_key != self.inner.expected_attester_public_key { + self.update_last_queried(OffsetDateTime::now_utc()); + return; + } + + self.enable_upgrade_mode() + } else { + self.disable_upgrade_mode() + } + + self.update_last_queried(OffsetDateTime::now_utc()); + *guard = expected_attestation; + } + + pub fn upgrade_mode_status(&self) -> UpgradeModeStatus { + self.inner.status.clone() + } + + pub fn upgrade_mode_enabled(&self) -> bool { + self.inner.status.enabled() + } + + pub fn enable_upgrade_mode(&self) { + self.inner.status.enable() + } + + pub fn disable_upgrade_mode(&self) { + self.inner.status.disable() + } + + pub fn last_queried(&self) -> OffsetDateTime { + // SAFETY: the stored value here is always a valid unix timestamp + #[allow(clippy::unwrap_used)] + OffsetDateTime::from_unix_timestamp(self.inner.last_queried_ts.load(Ordering::Acquire)) + .unwrap() + } + + pub fn update_last_queried(&self, queried_at: OffsetDateTime) { + self.inner + .last_queried_ts + .store(queried_at.unix_timestamp(), Ordering::Release); + } + + pub fn since_last_query(&self) -> Duration { + (OffsetDateTime::now_utc() - self.last_queried()) + .try_into() + .unwrap_or_else(|_| { + error!("somehow our last query for upgrade mode was in the future!"); + Duration::ZERO + }) + } +} + +struct UpgradeModeStateInner { + /// Expected public key of the entity issuing upgrade mode attestations. + expected_attester_public_key: ed25519::PublicKey, + + /// Contents of the published upgrade mode attestation, as queried by this node + expected_attestation: RwLock>, + + /// timestamp indicating last time this node has queried for the current upgrade mode attestation + /// it is used to determine if an additional expedited query should be made in case client sends a JWT + /// whilst this node is not aware of the upgrade mode + last_queried_ts: AtomicI64, + + /// flag indicating whether upgrade mode is currently enabled. this is to perform cheap checks + /// that avoid having to acquire the lock + // (and dealing with the async consequences of that) + status: UpgradeModeStatus, +} diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index d6d4ce842bb..57a88ea603f 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -23,3 +23,5 @@ rand = { workspace = true } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } nym-ecash-time = { path = "../ecash-time" } nym-network-defaults = { path = "../network-defaults" } +nym-upgrade-mode-check = { path = "../upgrade-mode-check" } + diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index 91e08ee7704..807016792b7 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -30,6 +30,35 @@ pub use nym_compact_ecash::{ }; pub use nym_ecash_time::{EcashTime, ecash_today}; pub use nym_network_defaults::TicketTypeRepr; +use nym_network_defaults::TicketTypeRepr::V1MixnetEntry; + +/// Default bandwidth amount under which [mixnet] clients will attempt to send additional zk-nyms +/// to increase their allowance. +// currently defined as 20% of entry ticket value +// clients are, of course, free to override this value +pub const DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD: i64 = + (V1MixnetEntry.bandwidth_value() / 5) as i64; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum BandwidthCredential { + ZkNym(Box), + UpgradeModeJWT { token: String }, +} + +impl BandwidthCredential { + pub fn into_zk_nym(self) -> Option> { + match self { + BandwidthCredential::ZkNym(credential) => Some(credential), + _ => None, + } + } +} + +impl From for BandwidthCredential { + fn from(credential: CredentialSpendingData) -> Self { + Self::ZkNym(Box::new(credential)) + } +} #[derive(Debug, Clone)] pub struct CredentialSigningData { diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index 83042ead140..65a5704fdc8 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -51,3 +51,6 @@ anyhow = { workspace = true } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests nym-test-utils = { path = "../test-utils" } tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true \ No newline at end of file diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index bdb9a30a0f1..a0f44b20fc5 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -19,7 +19,9 @@ pub use shared_key::{ SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, }; -pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION; +pub type GatewayProtocolVersion = u8; + +pub const CURRENT_PROTOCOL_VERSION: GatewayProtocolVersion = UPGRADE_MODE_VERSION; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. @@ -29,35 +31,73 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION; // 3 - change to AES-GCM-SIV and non-zero IVs // 4 - introduction of v2 authentication protocol to prevent reply attacks // 5 - add key rotation information to the serialised mix packet -pub const INITIAL_PROTOCOL_VERSION: u8 = 1; -pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; -pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; -pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4; -pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5; +// 6 - support for 'upgrade mode' +pub const INITIAL_PROTOCOL_VERSION: GatewayProtocolVersion = 1; +pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 2; +pub const AES_GCM_SIV_PROTOCOL_VERSION: GatewayProtocolVersion = 3; +pub const AUTHENTICATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 4; +pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: GatewayProtocolVersion = 5; +pub const UPGRADE_MODE_VERSION: GatewayProtocolVersion = 6; // TODO: could using `Mac` trait here for OutputSize backfire? // Should hmac itself be exposed, imported and used instead? pub type LegacyGatewayMacSize = ::OutputSize; pub trait GatewayProtocolVersionExt { + const CURRENT: GatewayProtocolVersion = CURRENT_PROTOCOL_VERSION; + fn supports_aes256_gcm_siv(&self) -> bool; fn supports_authenticate_v2(&self) -> bool; fn supports_key_rotation_packet(&self) -> bool; + fn supports_upgrade_mode(&self) -> bool; + fn is_future_version(&self) -> bool; +} + +impl GatewayProtocolVersionExt for Option { + fn supports_aes256_gcm_siv(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.supports_aes256_gcm_siv() + } + + fn supports_authenticate_v2(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.supports_authenticate_v2() + } + + fn supports_key_rotation_packet(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.supports_key_rotation_packet() + } + + fn supports_upgrade_mode(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.supports_upgrade_mode() + } + + fn is_future_version(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.is_future_version() + } } -impl GatewayProtocolVersionExt for Option { +impl GatewayProtocolVersionExt for GatewayProtocolVersion { fn supports_aes256_gcm_siv(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= AES_GCM_SIV_PROTOCOL_VERSION + *self >= AES_GCM_SIV_PROTOCOL_VERSION } fn supports_authenticate_v2(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION + *self >= AUTHENTICATE_V2_PROTOCOL_VERSION } fn supports_key_rotation_packet(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION + *self >= EMBEDDED_KEY_ROTATION_INFO_VERSION + } + + fn supports_upgrade_mode(&self) -> bool { + *self >= UPGRADE_MODE_VERSION + } + + fn is_future_version(&self) -> bool { + *self > CURRENT_PROTOCOL_VERSION } } diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index 549cddca39a..7f2a3ccb19e 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -3,10 +3,12 @@ use crate::registration::handshake::messages::{Finalization, GatewayMaterialExchange}; use crate::registration::handshake::state::State; -use crate::registration::handshake::SharedGatewayKey; +use crate::registration::handshake::HandshakeResult; use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crate::{GatewayProtocolVersionExt, INITIAL_PROTOCOL_VERSION}; use futures::{Sink, Stream}; use rand::{CryptoRng, RngCore}; +use tracing::info; use tungstenite::Message as WsMessage; impl State<'_, S, R> { @@ -25,10 +27,26 @@ impl State<'_, S, R> { // 2. wait for response with remote x25519 pubkey as well as encrypted signature // <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) || MAYBE_NONCE - let mid_res = self + let (mid_res, gateway_protocol) = self .receive_handshake_message::() .await?; + // NEGOTIATE PROTOCOL + if gateway_protocol.is_future_version() { + // SAFETY: future version means it's greater than CURRENT, which is always a `Some` + #[allow(clippy::unwrap_used)] + return Err(HandshakeError::UnsupportedProtocol { + version: gateway_protocol.unwrap(), + }); + } + let gateway_protocol = gateway_protocol.unwrap_or(INITIAL_PROTOCOL_VERSION); + + // that should never happen, but we're fine with that outcome + if Some(gateway_protocol) != self.proposed_protocol_version() { + info!("the gateway insists on protocol version different from the one we suggested. it wants {gateway_protocol} whilst we wanted {:?}, however, we can support it", self.proposed_protocol_version()); + self.set_protocol_version(gateway_protocol); + } + // 3. derive shared keys locally // hkdf::::(g^xy) self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref()); @@ -42,14 +60,14 @@ impl State<'_, S, R> { self.send_handshake_data(materials).await?; // 6. wait for remote confirmation of finalizing the handshake - let finalization = self.receive_handshake_message::().await?; + let (finalization, _) = self.receive_handshake_message::().await?; finalization.ensure_success()?; Ok(()) } pub(crate) async fn perform_client_handshake( mut self, - ) -> Result + ) -> Result where S: Stream + Sink + Unpin, R: CryptoRng + RngCore, diff --git a/common/gateway-requests/src/registration/handshake/error.rs b/common/gateway-requests/src/registration/handshake/error.rs index 8cc9cf1c0d6..67dc02d0095 100644 --- a/common/gateway-requests/src/registration/handshake/error.rs +++ b/common/gateway-requests/src/registration/handshake/error.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::shared_key::SharedKeyUsageError; +use crate::GatewayProtocolVersion; +use crate::GatewayProtocolVersionExt; use thiserror::Error; #[derive(Debug, Error)] @@ -34,4 +36,10 @@ pub enum HandshakeError { #[error("timed out waiting for a handshake message")] Timeout, + + #[error("Connection is in an invalid state - please send a bug report")] + ConnectionInInvalidState, + + #[error("the gateway requests protocol version that's not supported by this client. it wants to use v{version} whilst we only understand up to v{}", GatewayProtocolVersion::CURRENT)] + UnsupportedProtocol { version: GatewayProtocolVersion }, } diff --git a/common/gateway-requests/src/registration/handshake/gateway.rs b/common/gateway-requests/src/registration/handshake/gateway.rs index 5fec717c465..cea8fd67de9 100644 --- a/common/gateway-requests/src/registration/handshake/gateway.rs +++ b/common/gateway-requests/src/registration/handshake/gateway.rs @@ -5,9 +5,11 @@ use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; use crate::registration::handshake::state::State; -use crate::registration::handshake::SharedGatewayKey; +use crate::registration::handshake::HandshakeResult; use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crate::{GatewayProtocolVersion, GatewayProtocolVersionExt}; use futures::{Sink, Stream}; +use tracing::{debug, warn}; use tungstenite::Message as WsMessage; impl State<'_, S, R> { @@ -18,11 +20,39 @@ impl State<'_, S, R> { where S: Stream + Sink + Unpin, { + // NEGOTIATE PROTOCOL + // old clients were sending protocol version as defined by the following: + /* + fn request_protocol_version(&self) -> u8 { + if self.derive_aes256_gcm_siv_key { + AES_GCM_SIV_PROTOCOL_VERSION + } else if self.expects_credential_usage { + CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION + } else { + INITIAL_PROTOCOL_VERSION + } + } + */ + // meaning the highest possible value they could have sent was `4` (AUTHENTICATE_V2_PROTOCOL_VERSION) + // so if we received anything higher than that, it means they understand negotiation. + // currently not strictly needed as we just blindly accept what they proposed, + // but will be needed in the future. + if self.proposed_protocol_version().is_future_version() { + // this should never happen in a non-malicious client as it should use at most whatever version this gateway has announced + self.set_protocol_version(GatewayProtocolVersion::CURRENT) + } else { + // currently we accept all protocols, i.e. legacy keys, aes128, etc. so we downgrade to whatever + // the client has proposed. this will change in the future + debug!( + "using the protocol version proposed by the client: {:?}", + self.proposed_protocol_version() + ) + } + // 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and maybe a flag indicating non-legacy client // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY let init_message = Initialisation::try_from_bytes(&raw_init_message)?; self.update_remote_identity(init_message.identity); - self.set_aes256_gcm_siv_key_derivation(!init_message.is_legacy()); // 2. derive shared keys locally // hkdf::::(g^xy) @@ -39,7 +69,12 @@ impl State<'_, S, R> { self.send_handshake_data(material).await?; // 4. wait for the remote response with their own encrypted signature - let materials = self.receive_handshake_message::().await?; + let (materials, client_protocol) = + self.receive_handshake_message::().await?; + if client_protocol != self.proposed_protocol_version() { + warn!("the client hasn't accepted our proposed protocol version. we suggested {:?} while it returned {client_protocol:?}", self.proposed_protocol_version()); + // TBD what to do here + } // 5. verify the received signature using the locally derived keys self.verify_remote_key_material(&materials, &init_message.ephemeral_dh)?; @@ -54,7 +89,7 @@ impl State<'_, S, R> { pub(crate) async fn perform_gateway_handshake( mut self, raw_init_message: Vec, - ) -> Result + ) -> Result where S: Stream + Sink + Unpin, { diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index 10dda7edaf9..9958c17ba10 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -24,13 +24,6 @@ pub struct Initialisation { pub initiator_salt: Option>, } -impl Initialisation { - #[cfg(not(target_arch = "wasm32"))] - pub fn is_legacy(&self) -> bool { - self.initiator_salt.is_none() - } -} - #[derive(Debug)] pub struct MaterialExchange { pub signature_ciphertext: Vec, @@ -99,8 +92,9 @@ impl HandshakeMessage for Initialisation { let identity = ed25519::PublicKey::from_bytes(&bytes[..ed25519::PUBLIC_KEY_LENGTH]) .map_err(|_| HandshakeError::MalformedRequest)?; - // this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE + // SAFETY: this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE // which is impossible + #[allow(clippy::unwrap_used)] let ephemeral_dh = x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap(); @@ -194,6 +188,7 @@ impl HandshakeMessage for GatewayMaterialExchange { // this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE // which is impossible + #[allow(clippy::unwrap_used)] let ephemeral_dh = x25519::PublicKey::from_bytes(&bytes[..x25519::PUBLIC_KEY_SIZE]).unwrap(); let materials = MaterialExchange::try_from_bytes(&bytes[x25519::PUBLIC_KEY_SIZE..])?; diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index 2daed81b48e..8b1972f921c 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -3,7 +3,7 @@ use self::error::HandshakeError; use crate::registration::handshake::state::State; -use crate::SharedGatewayKey; +use crate::{GatewayProtocolVersion, SharedGatewayKey}; use futures::future::BoxFuture; use futures::{Sink, Stream}; use nym_crypto::asymmetric::ed25519; @@ -34,24 +34,29 @@ pub const KDF_SALT_LENGTH: usize = 16; // we do not need to worry about that. pub struct GatewayHandshake<'a> { - handshake_future: BoxFuture<'a, Result>, + handshake_future: BoxFuture<'a, Result>, } impl Future for GatewayHandshake<'_> { - type Output = Result; + type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.handshake_future).poll(cx) } } +#[derive(Debug, PartialEq)] +pub struct HandshakeResult { + pub negotiated_protocol: GatewayProtocolVersion, + pub derived_key: SharedGatewayKey, +} + pub fn client_handshake<'a, S, R>( rng: &'a mut R, ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, gateway_pubkey: ed25519::PublicKey, - expects_credential_usage: bool, - derive_aes256_gcm_siv_key: bool, + gateway_protocol: Option, #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where @@ -63,11 +68,10 @@ where ws_stream, identity, Some(gateway_pubkey), + gateway_protocol, #[cfg(not(target_arch = "wasm32"))] shutdown_token, - ) - .with_credential_usage(expects_credential_usage) - .with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key); + ); GatewayHandshake { handshake_future: Box::pin(state.perform_client_handshake()), @@ -80,13 +84,21 @@ pub fn gateway_handshake<'a, S, R>( ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, received_init_payload: Vec, + requested_client_protocol: Option, shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, R: CryptoRng + RngCore + Send, { - let state = State::new(rng, ws_stream, identity, None, shutdown_token); + let state = State::new( + rng, + ws_stream, + identity, + None, + requested_client_protocol, + shutdown_token, + ); GatewayHandshake { handshake_future: Box::pin(state.perform_gateway_handshake(received_init_payload)), } @@ -113,7 +125,8 @@ DONE(status) #[cfg(test)] mod tests { use super::*; - use crate::ClientControlRequest; + use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; + use anyhow::{bail, Context}; use futures::StreamExt; use nym_test_utils::helpers::u64_seeded_rng; use nym_test_utils::mocks::stream_sink::mock_streams; @@ -121,10 +134,53 @@ mod tests { use tokio::join; use tungstenite::Message; - #[tokio::test] - async fn basic_handshake() -> anyhow::Result<()> { - use anyhow::Context as _; + trait ClientControlRequestExt { + async fn get_handshake_init_data(&mut self) -> anyhow::Result> { + let ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version: _, + data, + } = self.get_control_request().await? + else { + bail!("unexpected ClientControlRequest") + }; + Ok(data) + } + async fn get_control_request(&mut self) -> anyhow::Result; + } + + impl ClientControlRequestExt for T + where + T: Stream + Unpin, + { + async fn get_control_request(&mut self) -> anyhow::Result { + let msg = self + .next() + .timeboxed() + .await + .context("timeout")? + .context("no message!")?? + .into_text()? + .parse::()?; + Ok(msg) + } + } + + struct Party { + rng: &'static mut R, + keys: &'static mut ed25519::KeyPair, + socket: &'static mut S, + } + fn setup() -> ( + Party< + impl CryptoRng + RngCore + Send, + impl Stream + Sink + Unpin, + >, + Party< + impl CryptoRng + RngCore + Send, + impl Stream + Sink + Unpin, + >, + ) { // solve the lifetime issue by just leaking the contents of the boxes // which is perfectly fine in test let client_rng = u64_seeded_rng(42).leak(); @@ -142,51 +198,139 @@ mod tests { let client_ws = client_ws.leak(); let gateway_ws = gateway_ws.leak(); + ( + Party { + rng: client_rng, + keys: client_keys, + socket: client_ws, + }, + Party { + rng: gateway_rng, + keys: gateway_keys, + socket: gateway_ws, + }, + ) + } + + #[tokio::test] + async fn basic_handshake() -> anyhow::Result<()> { + let (client, gateway) = setup(); + + let handshake_client = client_handshake( + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + Some(CURRENT_PROTOCOL_VERSION), + ShutdownToken::default(), + ); + + let client_fut = handshake_client.spawn_timeboxed(); + + // we need to receive the first message so that it could be propagated to the gateway side of the handshake + let init_msg = gateway.socket.get_handshake_init_data().await?; + + let handshake_gateway = gateway_handshake( + gateway.rng, + gateway.socket, + gateway.keys, + init_msg, + Some(CURRENT_PROTOCOL_VERSION), + ShutdownToken::default(), + ); + + let gateway_fut = handshake_gateway.spawn_timeboxed(); + let (client, gateway) = join!(client_fut, gateway_fut); + + let client_res = client???; + let gateway_res = gateway???; + + // ensure the created keys are the same + assert_eq!(client_res, gateway_res); + assert_eq!(client_res.negotiated_protocol, CURRENT_PROTOCOL_VERSION); + + Ok(()) + } + + #[tokio::test] + async fn protocol_downgrade() -> anyhow::Result<()> { + let (client, gateway) = setup(); + + let handshake_client = client_handshake( + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + Some(CURRENT_PROTOCOL_VERSION + 42), + ShutdownToken::default(), + ); + + let client_fut = handshake_client.spawn_timeboxed(); + // we need to receive the first message so that it could be propagated to the gateway side of the handshake + let init_msg = gateway.socket.get_handshake_init_data().await?; + + let handshake_gateway = gateway_handshake( + gateway.rng, + gateway.socket, + gateway.keys, + init_msg, + Some(CURRENT_PROTOCOL_VERSION + 42), + ShutdownToken::default(), + ); + + let gateway_fut = handshake_gateway.spawn_timeboxed(); + let (client, gateway) = join!(client_fut, gateway_fut); + + let client_res = client???; + let gateway_res = gateway???; + + // ensure the created keys are the same + assert_eq!(client_res, gateway_res); + + // and the protocol got downgraded for both parties + assert_eq!(client_res.negotiated_protocol, CURRENT_PROTOCOL_VERSION); + + Ok(()) + } + + #[tokio::test] + async fn protocol_upgrade() -> anyhow::Result<()> { + let (client, gateway) = setup(); + let handshake_client = client_handshake( - client_rng, - client_ws, - client_keys, - *gateway_keys.public_key(), - false, - true, + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + None, ShutdownToken::default(), ); let client_fut = handshake_client.spawn_timeboxed(); // we need to receive the first message so that it could be propagated to the gateway side of the handshake - let ClientControlRequest::RegisterHandshakeInitRequest { - protocol_version: _, - data, - } = (gateway_ws.next()) - .timeboxed() - .await - .context("timeout")? - .context("no message!")?? - .into_text()? - .parse::()? - else { - panic!("bad message") - }; - - let init_msg = data; + let init_msg = gateway.socket.get_handshake_init_data().await?; let handshake_gateway = gateway_handshake( - gateway_rng, - gateway_ws, - gateway_keys, + gateway.rng, + gateway.socket, + gateway.keys, init_msg, + None, ShutdownToken::default(), ); let gateway_fut = handshake_gateway.spawn_timeboxed(); let (client, gateway) = join!(client_fut, gateway_fut); - let client_key = client???; - let gateway_key = gateway???; + let client_res = client???; + let gateway_res = gateway???; // ensure the created keys are the same - assert_eq!(client_key, gateway_key); + assert_eq!(client_res, gateway_res); + + // and the protocol got upgraded to the first known version + assert_eq!(client_res.negotiated_protocol, INITIAL_PROTOCOL_VERSION); Ok(()) } diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 3d7b29e3f95..1e51fda37b1 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -5,11 +5,11 @@ use crate::registration::handshake::error::HandshakeError; use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; -use crate::registration::handshake::{SharedGatewayKey, WsItem, KDF_SALT_LENGTH}; +use crate::registration::handshake::{HandshakeResult, SharedGatewayKey, WsItem, KDF_SALT_LENGTH}; use crate::shared_key::SharedKeySize; use crate::{ - types, LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + types, GatewayProtocolVersion, GatewayProtocolVersionExt, LegacySharedKeySize, + LegacySharedKeys, SharedSymmetricKey, INITIAL_PROTOCOL_VERSION, }; use futures::{Sink, SinkExt, Stream, StreamExt}; use nym_crypto::asymmetric::{ed25519, x25519}; @@ -54,12 +54,11 @@ pub(crate) struct State<'a, S, R> { /// Ideally it would always be known before the handshake was initiated. remote_pubkey: Option, - // this field is really out of place here, however, we need to propagate this information somehow - // in order to establish correct protocol for backwards compatibility reasons - expects_credential_usage: bool, - - /// Specifies whether the end product should be an AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current) - derive_aes256_gcm_siv_key: bool, + /// Version of the protocol to use during the handshake that also implicitly specifies + /// additional features such as the type of derived shared keys, i.e. + /// AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current) + /// the above is decided by whether the specified protocol version supports the new variant or not. + protocol_version: Option, // channel to receive shutdown signal #[cfg(not(target_arch = "wasm32"))] @@ -72,6 +71,7 @@ impl<'a, S, R> State<'a, S, R> { ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, remote_pubkey: Option, + protocol_version: Option, #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> Self where @@ -84,40 +84,31 @@ impl<'a, S, R> State<'a, S, R> { ephemeral_keypair, identity, remote_pubkey, + protocol_version, derived_shared_keys: None, - // later on this should become the default - expects_credential_usage: false, - derive_aes256_gcm_siv_key: false, #[cfg(not(target_arch = "wasm32"))] shutdown_token, } } - pub(crate) fn with_credential_usage(mut self, expects_credential_usage: bool) -> Self { - self.expects_credential_usage = expects_credential_usage; - self - } - - pub(crate) fn with_aes256_gcm_siv_key(mut self, derive_aes256_gcm_siv_key: bool) -> Self { - self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; - self + #[cfg(not(target_arch = "wasm32"))] + pub(crate) fn local_ephemeral_key(&self) -> &x25519::PublicKey { + self.ephemeral_keypair.public_key() } - #[cfg(not(target_arch = "wasm32"))] - pub(crate) fn set_aes256_gcm_siv_key_derivation(&mut self, derive_aes256_gcm_siv_key: bool) { - self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; + pub(crate) fn proposed_protocol_version(&self) -> Option { + self.protocol_version } - #[cfg(not(target_arch = "wasm32"))] - pub(crate) fn local_ephemeral_key(&self) -> &x25519::PublicKey { - self.ephemeral_keypair.public_key() + pub(crate) fn set_protocol_version(&mut self, protocol_version: GatewayProtocolVersion) { + self.protocol_version = Some(protocol_version); } pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option> where R: CryptoRng + RngCore, { - if self.derive_aes256_gcm_siv_key { + if self.protocol_version.supports_aes256_gcm_siv() { let mut salt = vec![0u8; KDF_SALT_LENGTH]; self.rng.fill_bytes(&mut salt); Some(salt) @@ -154,13 +145,14 @@ impl<'a, S, R> State<'a, S, R> { .private_key() .diffie_hellman(remote_ephemeral_key); - let key_size = if self.derive_aes256_gcm_siv_key { + let key_size = if self.protocol_version.supports_aes256_gcm_siv() { SharedKeySize::to_usize() } else { LegacySharedKeySize::to_usize() }; - // there is no reason for this to fail as our okm is expected to be only 16 bytes + // SAFETY: there is no reason for this to fail as our okm is expected to be only 16 bytes + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( initiator_salt, &dh_result, @@ -169,11 +161,14 @@ impl<'a, S, R> State<'a, S, R> { ) .expect("somehow too long okm was provided"); - let shared_key = if self.derive_aes256_gcm_siv_key { + // SAFETY: the okm has been expanded to the length expected by the corresponding keys + let shared_key = if self.protocol_version.supports_aes256_gcm_siv() { + #[allow(clippy::expect_used)] let current_key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); SharedGatewayKey::Current(current_key) } else { + #[allow(clippy::expect_used)] let legacy_key = LegacySharedKeys::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); SharedGatewayKey::Legacy(legacy_key) @@ -196,7 +191,7 @@ impl<'a, S, R> State<'a, S, R> { .collect(); let signature = self.identity.private_key().sign(plaintext); - let nonce = if self.derive_aes256_gcm_siv_key { + let nonce = if self.protocol_version.supports_aes256_gcm_siv() { let mut rng = thread_rng(); Some(random_nonce::(&mut rng).to_vec()) } else { @@ -204,6 +199,7 @@ impl<'a, S, R> State<'a, S, R> { }; // SAFETY: this function is only called after the local key has already been derived + #[allow(clippy::expect_used)] let signature_ciphertext = self .derived_shared_keys .as_ref() @@ -222,13 +218,14 @@ impl<'a, S, R> State<'a, S, R> { remote_ephemeral_key: &x25519::PublicKey, ) -> Result<(), HandshakeError> { // SAFETY: this function is only called after the local key has already been derived + #[allow(clippy::expect_used)] let derived_shared_key = self .derived_shared_keys .as_ref() .expect("shared key was not derived!"); // if the [client] init message contained non-legacy flag, the associated nonce MUST be present - if self.derive_aes256_gcm_siv_key && remote_response.nonce.is_none() { + if self.protocol_version.supports_aes256_gcm_siv() && remote_response.nonce.is_none() { return Err(HandshakeError::MissingNonceForCurrentKey); } @@ -249,6 +246,7 @@ impl<'a, S, R> State<'a, S, R> { .chain(self.ephemeral_keypair.public_key().to_bytes()) .collect(); + #[allow(clippy::unwrap_used)] self.remote_pubkey .as_ref() .unwrap() @@ -261,7 +259,10 @@ impl<'a, S, R> State<'a, S, R> { self.remote_pubkey = Some(remote_pubkey) } - fn on_wg_msg(msg: Option) -> Result>, HandshakeError> { + #[allow(clippy::complexity)] + fn on_wg_msg( + msg: Option, + ) -> Result, Option)>, HandshakeError> { let Some(msg) = msg else { return Err(HandshakeError::ClosedStream); }; @@ -277,9 +278,10 @@ impl<'a, S, R> State<'a, S, R> { // hehe, that's a bit disgusting that the type system requires we explicitly ignore the // protocol_version field that we actually never attach at this point // yet another reason for the overdue refactor - types::RegistrationHandshake::HandshakePayload { data, .. } => { - Ok(Some(data)) - } + types::RegistrationHandshake::HandshakePayload { + protocol_version, + data, + } => Ok(Some((data, protocol_version))), types::RegistrationHandshake::HandshakeError { message } => { Err(HandshakeError::RemoteError(message)) } @@ -299,7 +301,9 @@ impl<'a, S, R> State<'a, S, R> { } #[cfg(not(target_arch = "wasm32"))] - async fn _receive_handshake_message_bytes(&mut self) -> Result, HandshakeError> + async fn _receive_handshake_message_bytes( + &mut self, + ) -> Result<(Vec, Option), HandshakeError> where S: Stream + Unpin, { @@ -318,7 +322,9 @@ impl<'a, S, R> State<'a, S, R> { } #[cfg(target_arch = "wasm32")] - async fn _receive_handshake_message_bytes(&mut self) -> Result, HandshakeError> + async fn _receive_handshake_message_bytes( + &mut self, + ) -> Result<(Vec, Option), HandshakeError> where S: Stream + Unpin, { @@ -331,20 +337,22 @@ impl<'a, S, R> State<'a, S, R> { } } - pub(crate) async fn receive_handshake_message(&mut self) -> Result + pub(crate) async fn receive_handshake_message( + &mut self, + ) -> Result<(M, Option), HandshakeError> where S: Stream + Unpin, M: HandshakeMessage, { // TODO: make timeout duration configurable - let bytes = timeout( + let (bytes, protocol) = timeout( Duration::from_secs(5), self._receive_handshake_message_bytes(), ) .await .map_err(|_| HandshakeError::Timeout)??; - M::try_from_bytes(&bytes) + M::try_from_bytes(&bytes).map(|msg| (msg, protocol)) } // upon receiving this, the receiver should terminate the handshake @@ -357,21 +365,11 @@ impl<'a, S, R> State<'a, S, R> { { let handshake_message = types::RegistrationHandshake::new_error(message); self.ws_stream - .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .send(WsMessage::Text(handshake_message.into())) .await .map_err(|_| HandshakeError::ClosedStream) } - fn request_protocol_version(&self) -> u8 { - if self.derive_aes256_gcm_siv_key { - AES_GCM_SIV_PROTOCOL_VERSION - } else if self.expects_credential_usage { - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION - } else { - INITIAL_PROTOCOL_VERSION - } - } - pub(crate) async fn send_handshake_data( &mut self, inner_message: M, @@ -384,18 +382,25 @@ impl<'a, S, R> State<'a, S, R> { let handshake_message = types::RegistrationHandshake::new_payload( inner_message.into_bytes(), - self.request_protocol_version(), + self.protocol_version, ); self.ws_stream - .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .send(WsMessage::Text(handshake_message.into())) .await .map_err(|_| HandshakeError::ClosedStream) } /// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed /// values. - pub(crate) fn finalize_handshake(self) -> SharedGatewayKey { - self.derived_shared_keys.unwrap() + pub(crate) fn finalize_handshake(self) -> HandshakeResult { + // SAFETY: handshake can't be finalised without deriving the shared keys + #[allow(clippy::unwrap_used)] + HandshakeResult { + negotiated_protocol: self + .proposed_protocol_version() + .unwrap_or(INITIAL_PROTOCOL_VERSION), + derived_key: self.derived_shared_keys.unwrap(), + } } // If any step along the way failed (that are non-network related), diff --git a/common/gateway-requests/src/shared_key/legacy.rs b/common/gateway-requests/src/shared_key/legacy.rs index 8fcf2866973..6f40fcd2ffa 100644 --- a/common/gateway-requests/src/shared_key/legacy.rs +++ b/common/gateway-requests/src/shared_key/legacy.rs @@ -43,6 +43,7 @@ impl LegacySharedKeys { rng.fill_bytes(&mut salt); let legacy_bytes = Zeroizing::new(self.to_bytes()); + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( Some(&salt), &legacy_bytes, @@ -51,6 +52,7 @@ impl LegacySharedKeys { ) .expect("somehow too long okm was provided"); + #[allow(clippy::expect_used)] let key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); (key, salt) @@ -62,6 +64,7 @@ impl LegacySharedKeys { expected_digest: &[u8], ) -> Option { let legacy_bytes = Zeroizing::new(self.to_bytes()); + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( Some(salt), &legacy_bytes, @@ -69,6 +72,8 @@ impl LegacySharedKeys { SharedKeySize::to_usize(), ) .expect("somehow too long okm was provided"); + + #[allow(clippy::expect_used)] let key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); if key.digest() != expected_digest { diff --git a/common/gateway-requests/src/shared_key/mod.rs b/common/gateway-requests/src/shared_key/mod.rs index b424fd49bf5..c0a72135a3f 100644 --- a/common/gateway-requests/src/shared_key/mod.rs +++ b/common/gateway-requests/src/shared_key/mod.rs @@ -47,6 +47,8 @@ impl SharedGatewayKey { } } + // it is responsibility of the caller to ensure the correct variant is present + #[allow(clippy::panic)] pub fn unwrap_legacy(&self) -> &LegacySharedKeys { match self { SharedGatewayKey::Current(_) => panic!("expected legacy key"), diff --git a/common/gateway-requests/src/types/error.rs b/common/gateway-requests/src/types/error.rs index edd8e41b22c..bdbfe6fbac9 100644 --- a/common/gateway-requests/src/types/error.rs +++ b/common/gateway-requests/src/types/error.rs @@ -13,7 +13,7 @@ use thiserror::Error; use time::OffsetDateTime; // specific errors (that should not be nested!!) for clients to match on -#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum SimpleGatewayRequestsError { #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] diff --git a/common/gateway-requests/src/types/registration_handshake_wrapper.rs b/common/gateway-requests/src/types/registration_handshake_wrapper.rs index 0dc6daa567e..be75af675be 100644 --- a/common/gateway-requests/src/types/registration_handshake_wrapper.rs +++ b/common/gateway-requests/src/types/registration_handshake_wrapper.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::GatewayProtocolVersion; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -9,7 +10,7 @@ use std::str::FromStr; pub enum RegistrationHandshake { HandshakePayload { #[serde(default)] - protocol_version: Option, + protocol_version: Option, data: Vec, }, HandshakeError { @@ -18,9 +19,9 @@ pub enum RegistrationHandshake { } impl RegistrationHandshake { - pub fn new_payload(data: Vec, protocol_version: u8) -> Self { + pub fn new_payload(data: Vec, protocol_version: Option) -> Self { RegistrationHandshake::HandshakePayload { - protocol_version: Some(protocol_version), + protocol_version, data, } } @@ -48,11 +49,11 @@ impl TryFrom for RegistrationHandshake { } } -impl TryInto for RegistrationHandshake { - type Error = serde_json::Error; - - fn try_into(self) -> Result { - serde_json::to_string(&self) +impl From for String { + fn from(value: RegistrationHandshake) -> Self { + // SAFETY: we have infallible serde implementation + #[allow(clippy::unwrap_used)] + serde_json::to_string(&value).unwrap() } } @@ -79,7 +80,7 @@ mod tests { assert_eq!(protocol_version, Some(42)); assert_eq!(data, handshake_data) } - _ => unreachable!("this branch shouldn't have been reached!"), + _ => panic!("this branch shouldn't have been reached!"), } let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload { @@ -97,7 +98,7 @@ mod tests { assert!(protocol_version.is_none()); assert_eq!(data, handshake_data) } - _ => unreachable!("this branch shouldn't have been reached!"), + _ => panic!("this branch shouldn't have been reached!"), } } } diff --git a/common/gateway-requests/src/types/text_request/authenticate.rs b/common/gateway-requests/src/types/text_request/authenticate.rs index 6c4c8849574..65dacb3f00b 100644 --- a/common/gateway-requests/src/types/text_request/authenticate.rs +++ b/common/gateway-requests/src/types/text_request/authenticate.rs @@ -1,7 +1,9 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey}; +use crate::{ + AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, +}; use nym_crypto::asymmetric::ed25519; use serde::{Deserialize, Serialize}; use std::iter; @@ -20,7 +22,7 @@ pub struct AuthenticateRequest { impl AuthenticateRequest { pub fn new( - protocol_version: u8, + protocol_version: GatewayProtocolVersion, shared_key: &SharedGatewayKey, identity_keys: &ed25519::KeyPair, ) -> Result { @@ -98,7 +100,7 @@ impl AuthenticateRequest { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct AuthenticateRequestContent { - pub protocol_version: u8, + pub protocol_version: GatewayProtocolVersion, // this is identical to the client's address pub client_identity: ed25519::PublicKey, diff --git a/common/gateway-requests/src/types/text_request/mod.rs b/common/gateway-requests/src/types/text_request/mod.rs index 342bc9adfd4..b15a27bf935 100644 --- a/common/gateway-requests/src/types/text_request/mod.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -4,9 +4,8 @@ use crate::models::CredentialSpendingRequest; use crate::text_request::authenticate::AuthenticateRequest; use crate::{ - GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, - INITIAL_PROTOCOL_VERSION, + GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, SymmetricKey, + AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, }; use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::ed25519; @@ -46,6 +45,7 @@ impl ClientRequest { // - the schema is self-describing which simplifies deserialisation // SAFETY: the trait has been derived correctly with no weird variants + #[allow(clippy::unwrap_used)] let plaintext = serde_json::to_vec(self).unwrap(); let nonce = key.random_nonce_or_iv(); let ciphertext = key.encrypt(&plaintext, Some(&nonce))?; @@ -72,7 +72,7 @@ pub enum ClientControlRequest { // have the shared key derived? Authenticate { #[serde(default)] - protocol_version: Option, + protocol_version: Option, address: String, enc_address: String, iv: String, @@ -83,7 +83,7 @@ pub enum ClientControlRequest { #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { #[serde(default)] - protocol_version: Option, + protocol_version: Option, data: Vec, }, BandwidthCredential { @@ -98,6 +98,10 @@ pub enum ClientControlRequest { enc_credential: Vec, iv: Vec, }, + UpgradeModeJWT { + // no need to encrypt it as it's public anyway + token: String, + }, ClaimFreeTestnetBandwidth, EncryptedRequest { ciphertext: Vec, @@ -108,12 +112,14 @@ pub enum ClientControlRequest { } impl ClientControlRequest { - pub fn new_authenticate( + pub fn new_legacy_authenticate( address: DestinationAddressBytes, shared_key: &SharedGatewayKey, uses_credentials: bool, ) -> Result { // if we're encrypting with non-legacy key, the remote must support AES256-GCM-SIV + // since we are using legacy authentication, the gateway definitely doesn't understand the protocol downgrade, + // so use the lowest possible version we can let protocol_version = if !shared_key.is_legacy() { Some(AES_GCM_SIV_PROTOCOL_VERSION) } else if uses_credentials { @@ -138,10 +144,8 @@ impl ClientControlRequest { pub fn new_authenticate_v2( shared_key: &SharedGatewayKey, identity_keys: &ed25519::KeyPair, + protocol_version: GatewayProtocolVersion, ) -> Result { - // if we're using v2 authentication, we must announce at least that protocol version - let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION; - Ok(ClientControlRequest::AuthenticateV2(Box::new( AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?, ))) @@ -159,6 +163,7 @@ impl ClientControlRequest { "BandwidthCredentialV2".to_string() } ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(), + ClientControlRequest::UpgradeModeJWT { .. } => "UpgradeModeJWT".to_string(), ClientControlRequest::ClaimFreeTestnetBandwidth => { "ClaimFreeTestnetBandwidth".to_string() } @@ -192,12 +197,16 @@ impl ClientControlRequest { CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) .map_err(|_| GatewayRequestsError::MalformedEncryption) } + + pub fn new_upgrade_mode_jwt(token: String) -> Self { + ClientControlRequest::UpgradeModeJWT { token } + } } impl From for Message { fn from(req: ClientControlRequest) -> Self { - // it should be safe to call `unwrap` here as the message is generated by the server - // so if it fails (and consequently panics) it's a bug that should be resolved + // SAFETY: all of the enum variants have valid (for json) serde impl + #[allow(clippy::unwrap_used)] let str_req = serde_json::to_string(&req).unwrap(); Message::Text(str_req) } diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index c3418649cfd..140aa26bbd7 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -1,7 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey}; +use crate::{ + GatewayProtocolVersion, GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey, +}; use serde::{Deserialize, Serialize}; use tungstenite::Message; @@ -26,6 +28,7 @@ impl SensitiveServerResponse { // - the schema is self-describing which simplifies deserialisation // SAFETY: the trait has been derived correctly with no weird variants + #[allow(clippy::unwrap_used)] let plaintext = serde_json::to_vec(self).unwrap(); let nonce = key.random_nonce_or_iv(); let ciphertext = key.encrypt(&plaintext, Some(&nonce))?; @@ -43,31 +46,57 @@ impl SensitiveServerResponse { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct BandwidthResponse { + pub available_total: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + pub upgrade_mode: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct SendResponse { + pub remaining_bandwidth: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + pub upgrade_mode: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(tag = "type", rename_all = "camelCase")] #[non_exhaustive] pub enum ServerResponse { Authenticate { #[serde(default)] - protocol_version: Option, + protocol_version: Option, status: bool, bandwidth_remaining: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + upgrade_mode: bool, }, Register { #[serde(default)] - protocol_version: Option, + protocol_version: Option, status: bool, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + upgrade_mode: bool, }, EncryptedResponse { ciphertext: Vec, nonce: Vec, }, - Bandwidth { - available_total: i64, - }, - Send { - remaining_bandwidth: i64, - }, + Bandwidth(BandwidthResponse), + Send(SendResponse), SupportedProtocol { version: u8, }, @@ -122,6 +151,7 @@ impl From for Message { fn from(res: ServerResponse) -> Self { // it should be safe to call `unwrap` here as the message is generated by the server // so if it fails (and consequently panics) it's a bug that should be resolved + #[allow(clippy::unwrap_used)] let str_res = serde_json::to_string(&res).unwrap(); Message::Text(str_res) } @@ -134,3 +164,79 @@ impl TryFrom for ServerResponse { serde_json::from_str(&msg) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn server_response_serde_compat() { + // make sure new serialisation is identical and compatible + #[derive(Serialize, Deserialize, Debug, PartialEq)] + #[serde(tag = "type", rename_all = "camelCase")] + #[non_exhaustive] + pub enum OldServerResponse { + Bandwidth { available_total: i64 }, + Send { remaining_bandwidth: i64 }, + } + + // OLD => NEW + let old_bandwidth = OldServerResponse::Bandwidth { + available_total: 100, + }; + let old_send = OldServerResponse::Send { + remaining_bandwidth: 100, + }; + + let old_bandwidth_str = serde_json::to_string(&old_bandwidth).unwrap(); + let old_send_str = serde_json::to_string(&old_send).unwrap(); + + let recovered_bandwidth = ServerResponse::try_from(old_bandwidth_str).unwrap(); + assert_eq!( + recovered_bandwidth, + ServerResponse::Bandwidth(BandwidthResponse { + available_total: 100, + upgrade_mode: false + }) + ); + + let recovered_send = ServerResponse::try_from(old_send_str).unwrap(); + assert_eq!( + recovered_send, + ServerResponse::Send(SendResponse { + remaining_bandwidth: 100, + upgrade_mode: false + }) + ); + + // NEW => OLD + let new_bandwidth = ServerResponse::Bandwidth(BandwidthResponse { + available_total: 100, + upgrade_mode: false, + }); + let new_send = ServerResponse::Send(SendResponse { + remaining_bandwidth: 100, + upgrade_mode: false, + }); + + let new_bandwidth_str = serde_json::to_string(&new_bandwidth).unwrap(); + let new_send_str = serde_json::to_string(&new_send).unwrap(); + + let recovered_bandwidth: OldServerResponse = + serde_json::from_str(&new_bandwidth_str).unwrap(); + assert_eq!( + recovered_bandwidth, + OldServerResponse::Bandwidth { + available_total: 100 + } + ); + + let recovered_send: OldServerResponse = serde_json::from_str(&new_send_str).unwrap(); + assert_eq!( + recovered_send, + OldServerResponse::Send { + remaining_bandwidth: 100 + } + ); + } +} diff --git a/common/service-provider-requests-common/src/lib.rs b/common/service-provider-requests-common/src/lib.rs index 9e392dff197..44e54dc2af0 100644 --- a/common/service-provider-requests-common/src/lib.rs +++ b/common/service-provider-requests-common/src/lib.rs @@ -82,6 +82,17 @@ pub struct Protocol { pub service_provider_type: ServiceProviderType, } +impl Protocol { + pub const fn new(version: u8, service_provider_type: ServiceProviderType) -> Self { + Self { + version, + service_provider_type, + } + } +} + +// NOTE: this only works under the assumption of using bincode for serialisation +// with the current field layout impl TryFrom<&[u8; 2]> for Protocol { type Error = ProtocolError; diff --git a/common/upgrade-mode-check/src/error.rs b/common/upgrade-mode-check/src/error.rs index 0edf4201e86..3b2f2b5b120 100644 --- a/common/upgrade-mode-check/src/error.rs +++ b/common/upgrade-mode-check/src/error.rs @@ -6,7 +6,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum UpgradeModeCheckError { - #[error("failed to decode jwt metadata")] + #[error("failed to decode jwt metadata: {source}")] TokenMetadataDecodeFailure { source: jwt_simple::Error }, #[error("the jwt metadata didn't contain explicit public key")] @@ -21,6 +21,6 @@ pub enum UpgradeModeCheckError { #[error("failed to verify the jwt: {source}")] JwtVerificationFailure { source: jwt_simple::Error }, - #[error("failed to retrieve attestation from {url}:{source}")] + #[error("failed to retrieve attestation from {url}: {source}")] AttestationRetrievalFailure { url: String, source: reqwest::Error }, } diff --git a/common/upgrade-mode-check/src/jwt.rs b/common/upgrade-mode-check/src/jwt.rs index 060546f0bb0..64c436b23e8 100644 --- a/common/upgrade-mode-check/src/jwt.rs +++ b/common/upgrade-mode-check/src/jwt.rs @@ -10,6 +10,8 @@ use nym_crypto::asymmetric::ed25519; use std::collections::HashSet; use std::time::Duration; +pub const CREDENTIAL_PROXY_JWT_ISSUER: &str = "nym-credential-proxy"; + // for now use static issuer such as "nym-credential-proxy" pub fn generate_jwt_for_upgrade_mode_attestation( attestation: UpgradeModeAttestation, @@ -109,11 +111,11 @@ mod tests { attestation.clone(), Duration::from_secs(60 * 60), &unauthorised_jwt_keys, - Some("nym-credential-proxy"), + Some(CREDENTIAL_PROXY_JWT_ISSUER), ); // we expect 'nym-credential-proxy' issuer - assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok()); + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some(CREDENTIAL_PROXY_JWT_ISSUER)).is_ok()); // we don't care about issuer assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok()); @@ -133,7 +135,9 @@ mod tests { None, ); // we expect 'nym-credential-proxy' issuer - assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err()); + assert!( + validate_upgrade_mode_jwt(&jwt_no_issuer, Some(CREDENTIAL_PROXY_JWT_ISSUER)).is_err() + ); // we don't care about issuer assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok()); diff --git a/common/upgrade-mode-check/src/lib.rs b/common/upgrade-mode-check/src/lib.rs index 36cbf43c91b..c2ab50284b7 100644 --- a/common/upgrade-mode-check/src/lib.rs +++ b/common/upgrade-mode-check/src/lib.rs @@ -9,7 +9,10 @@ pub use attestation::{ UpgradeModeAttestation, generate_new_attestation, generate_new_attestation_with_starting_time, }; pub use error::UpgradeModeCheckError; -pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt}; +pub use jwt::{ + CREDENTIAL_PROXY_JWT_ISSUER, generate_jwt_for_upgrade_mode_attestation, + validate_upgrade_mode_jwt, +}; #[cfg(not(target_arch = "wasm32"))] pub use attestation::attempt_retrieve_attestation; diff --git a/common/wireguard-private-metadata/client/src/lib.rs b/common/wireguard-private-metadata/client/src/lib.rs index 58d78fb6c65..3dd884518c7 100644 --- a/common/wireguard-private-metadata/client/src/lib.rs +++ b/common/wireguard-private-metadata/client/src/lib.rs @@ -46,6 +46,23 @@ pub trait WireguardMetadataApiClient: ApiClient { ) .await } + + #[instrument(level = "debug", skip(self, request_body))] + async fn request_upgrade_mode_check( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[ + routes::V1_API_VERSION, + routes::NETWORK, + routes::UPGRADE_MODE_CHECK, + ], + NO_PARAMS, + request_body, + ) + .await + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/common/wireguard-private-metadata/server/src/http/router.rs b/common/wireguard-private-metadata/server/src/http/router.rs index 099e1d57249..e76df7d3281 100644 --- a/common/wireguard-private-metadata/server/src/http/router.rs +++ b/common/wireguard-private-metadata/server/src/http/router.rs @@ -15,7 +15,7 @@ use utoipa_swagger_ui::SwaggerUi; use crate::http::openapi::ApiDoc; use crate::http::state::AppState; -use crate::network::bandwidth_routes; +use crate::network::{bandwidth_routes, network_routes}; /// Wrapper around `axum::Router` which ensures correct [order of layers][order]. /// Add new routes as if you were working directly with `axum`. @@ -35,7 +35,12 @@ impl RouterBuilder { 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())); + .nest( + "/v1", + Router::new() + .nest("/bandwidth", bandwidth_routes()) + .nest("/network", network_routes()), + ); Self { unfinished_router: default_routes, } diff --git a/common/wireguard-private-metadata/server/src/http/state.rs b/common/wireguard-private-metadata/server/src/http/state.rs index 06916bb35fa..3e913d70739 100644 --- a/common/wireguard-private-metadata/server/src/http/state.rs +++ b/common/wireguard-private-metadata/server/src/http/state.rs @@ -1,35 +1,137 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; +use nym_credentials_interface::BandwidthCredential; +use std::cmp::max; use std::net::IpAddr; -use nym_credentials_interface::CredentialSpendingData; - use crate::transceiver::PeerControllerTransceiver; use nym_wireguard_private_metadata_shared::error::MetadataError; +use nym_wireguard_private_metadata_shared::interface::{ResponseData, UpgradeModeCheckRequestType}; + +// we need to be above MINIMUM_REMAINING_BANDWIDTH (500MB) plus we also have to trick the client +// its depletion is low enough to not require sending new tickets +const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024; #[derive(Clone, axum::extract::FromRef)] pub struct AppState { transceiver: PeerControllerTransceiver, + #[from_ref(skip)] + upgrade_mode: UpgradeModeDetails, } impl AppState { - pub fn new(transceiver: PeerControllerTransceiver) -> Self { - Self { transceiver } + pub fn new(transceiver: PeerControllerTransceiver, upgrade_mode: UpgradeModeDetails) -> Self { + Self { + transceiver, + upgrade_mode, + } + } + + fn upgrade_mode_bandwidth(&self, true_bandwidth: i64) -> i64 { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket (hopefully) + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + max(DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD, true_bandwidth) } - pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { - self.transceiver.query_bandwidth(ip).await + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + let upgrade_mode = self.upgrade_mode.enabled(); + + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + let available_bandwidth = if upgrade_mode { + self.upgrade_mode_bandwidth(true_bandwidth) + } else { + true_bandwidth + }; + + Ok(ResponseData::AvailableBandwidth { + amount: available_bandwidth, + upgrade_mode, + }) } // 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 + claim: Box, + ) -> Result { + match *claim { + BandwidthCredential::ZkNym(zk_nym) => { + // if we got zk-nym, we just try to verify it + let available_bandwidth = self.transceiver.topup_bandwidth(ip, zk_nym).await?; + + // however, we still follow the same upgrade-mode logic, + // so that the client would not attempt to needlessly send more credentials + let upgrade_mode = self.upgrade_mode.enabled(); + let available_bandwidth = if upgrade_mode { + self.upgrade_mode_bandwidth(available_bandwidth) + } else { + available_bandwidth + }; + + Ok(ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }) + } + BandwidthCredential::UpgradeModeJWT { token } => { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode.enabled() { + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + return Ok(ResponseData::TopUpBandwidth { + available_bandwidth: self.upgrade_mode_bandwidth(true_bandwidth), + upgrade_mode: true, + }); + } + + // if the token is valid, try to check if we're behind + // and have to update our internal state + self.upgrade_mode + .try_enable_via_received_jwt(token) + .await + .map_err(|err| MetadataError::JWTVerification { + message: err.to_string(), + })?; + + // if we didn't return an error, it means token got accepted + // and we have transitioned into the upgrade mode + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + + Ok(ResponseData::TopUpBandwidth { + available_bandwidth: self.upgrade_mode_bandwidth(true_bandwidth), + upgrade_mode: true, + }) + } + } + } + + pub async fn upgrade_mode_check( + &self, + request: UpgradeModeCheckRequestType, + ) -> Result { + // if we're already in the upgrade mode - no need to do anything + if self.upgrade_mode.enabled() { + return Ok(ResponseData::UpgradeMode { upgrade_mode: true }); + } + + match request { + UpgradeModeCheckRequestType::UpgradeModeJwt { token } => { + self.upgrade_mode + .try_enable_via_received_jwt(token) + .await + .map_err(|err| MetadataError::JWTVerification { + message: err.to_string(), + })?; + } + } + + // if we didn't return an error, it means token got accepted + // and we have transitioned into the upgrade mode + Ok(ResponseData::UpgradeMode { upgrade_mode: true }) } } diff --git a/common/wireguard-private-metadata/server/src/network.rs b/common/wireguard-private-metadata/server/src/network.rs index 8e27eca5cd7..f1ad08a1087 100644 --- a/common/wireguard-private-metadata/server/src/network.rs +++ b/common/wireguard-private-metadata/server/src/network.rs @@ -9,8 +9,7 @@ use axum::{ }; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_wireguard_private_metadata_shared::{ - AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, - interface::{RequestData, ResponseData}, + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, interface::RequestData, latest, }; use tower_http::compression::CompressionLayer; @@ -25,6 +24,15 @@ pub(crate) fn bandwidth_routes() -> Router { .layer(CompressionLayer::new()) } +pub(crate) fn network_routes() -> Router { + Router::new() + .route( + "/upgrade-mode-check", + axum::routing::post(upgrade_mode_check), + ) + .layer(CompressionLayer::new()) +} + #[utoipa::path( tag = "bandwidth", get, @@ -59,20 +67,17 @@ async fn available_bandwidth( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let (RequestData::AvailableBandwidth(_), version) = + let (RequestData::AvailableBandwidth, version) = request.extract().map_err(AxumErrorResponse::bad_request)? else { return Err(AxumErrorResponse::bad_request("incorrect request type")); }; - let available_bandwidth = state + let available_bandwidth_response = 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)?; + let response = Response::construct(available_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; Ok(output.to_response(response)) } @@ -96,16 +101,49 @@ async fn topup_bandwidth( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let (RequestData::TopUpBandwidth(credential), version) = + let (RequestData::TopUpBandwidth { credential }, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let top_up_bandwidth_response = state + .topup_bandwidth(addr.ip(), credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(top_up_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} + +#[utoipa::path( + tag = "network", + post, + request_body = Request, + path = "/v1/network/upgrade-mode-check", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn upgrade_mode_check( + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::UpgradeModeCheck { typ }, 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) + let upgrade_mode_check_response = state + .upgrade_mode_check(typ) .await .map_err(AxumErrorResponse::bad_request)?; - let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + let response = Response::construct(upgrade_mode_check_response, 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 index cbe77126cf7..5614e7f8f76 100644 --- a/common/wireguard-private-metadata/server/src/transceiver.rs +++ b/common/wireguard-private-metadata/server/src/transceiver.rs @@ -37,12 +37,12 @@ impl PeerControllerTransceiver { }) } - pub(crate) async fn query_bandwidth(&self, ip: IpAddr) -> Result { + pub 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( + pub async fn topup_bandwidth( &self, ip: IpAddr, credential: Box, diff --git a/common/wireguard-private-metadata/shared/src/conversion_helpers.rs b/common/wireguard-private-metadata/shared/src/conversion_helpers.rs new file mode 100644 index 00000000000..117f9afb020 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/conversion_helpers.rs @@ -0,0 +1,218 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +/// A simple macro that given `TryFrom<&A> for B`, implements `TryFrom for B` +/// using the former implementation +#[macro_export] +macro_rules! impl_tryfrom_ref { + ($src:ty, $dst:ty, $err:ty) => { + impl TryFrom<$src> for $dst { + // can't use type Error = >::Error; + // due to lifetime interference within macros + type Error = $err; + + fn try_from(value: $src) -> Result { + >::try_from(&value) + } + } + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between particular versioned `VersionedRequest` and given request variant +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_request_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ($top_req_type:ty, $inner_req_type:ty, $query_type_pat:pat, $query_type_expr:expr) => { + $crate::impl_query_conversions!( + $crate::Request, + $top_req_type, + $inner_req_type, + $query_type_pat, + $query_type_expr + ); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between particular versioned `VersionedResponse` and given response variant +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_response_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ($top_resp_type:ty, $inner_resp_type:ty, $query_type_pat:pat, $query_type_expr:expr) => { + $crate::impl_query_conversions!( + $crate::Response, + $top_resp_type, + $inner_resp_type, + $query_type_pat, + $query_type_expr + ); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between [crate::models::Request] and corresponding versioned `VersionedRequest` +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_request_conversions { + ($req_type:ty, $version:expr) => { + $crate::impl_versioned_conversions!($crate::Request, $req_type, $version); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between [crate::models::Response] and corresponding versioned `VersionedResponse` +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_response_conversions { + ($req_type:ty, $version:expr) => { + $crate::impl_versioned_conversions!($crate::Response, $req_type, $version); + }; +} + +#[macro_export] +macro_rules! impl_versioned_conversions { + ( + // is it Request or Response + $main_type_ty:ty, + + // e.g. VersionedResponse + $top_type:ty, + + // request/response version type + $version:expr + ) => { + impl TryFrom<&$top_type> for $main_type_ty { + type Error = $crate::models::error::Error; + + fn try_from(value: &$top_type) -> Result { + use ::bincode::Options; + let data = $crate::make_bincode_serializer().serialize(value)?; + Ok(<$main_type_ty>::new($version, data)) + } + } + + // automatically generate `impl TryFrom<$top_type> for $main_type` + $crate::impl_tryfrom_ref!($top_type, $main_type_ty, $crate::models::error::Error); + + impl TryFrom<&$main_type_ty> for $top_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$main_type_ty) -> Result { + use ::bincode::Options; + if value.version != $version { + return Err($crate::models::error::Error::InvalidVersion { + source_version: value.version, + target_version: $version, + }); + } + Ok($crate::make_bincode_serializer().deserialize(&value.inner)?) + } + } + + // automatically generate `impl TryFrom<$main_type> for $top_type` + $crate::impl_tryfrom_ref!($main_type_ty, $top_type, $crate::models::error::Error); + }; +} + +#[macro_export] +macro_rules! impl_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ( + // is it Request or Response + $main_type:ty, + + // e.g. VersionedResponse + $top_type:ty, + + // e.g. InnerTopUpResponse + $inner_type:ty, + + // e.g. QueryType::TopUpBandwidth, + $query_type_pat:pat, + + // e.g. QueryType::TopUpBandwidth, + $query_type_expr:expr + ) => { + // conversion from the versioned type into the particular typ, + // e.g. TryFrom<&VersionedResponse> for InnerTopUpResponse + impl TryFrom<&$top_type> for $inner_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$top_type) -> Result { + use ::bincode::Options; + match value.query_type { + $query_type_pat => { + Ok($crate::make_bincode_serializer().deserialize(&value.inner)?) + } + other => Err($crate::models::error::Error::InvalidQueryType { + source_query_type: other.to_string(), + target_query_type: stringify!($query_type_pat).to_string(), + }), + } + } + } + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for InnerTopUpResponse + $crate::impl_tryfrom_ref!($top_type, $inner_type, $crate::models::error::Error); + + // conversion back from the particular type into the versioned type, i.e. + // e.g. TryFrom<&InnerTopUpResponse> for VersionedResponse + impl TryFrom<&$inner_type> for $top_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$inner_type) -> Result { + use ::bincode::Options; + Ok(Self { + query_type: $query_type_expr, + inner: $crate::make_bincode_serializer().serialize(value)?, + }) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for VersionedResponse + $crate::impl_tryfrom_ref!($inner_type, $top_type, $crate::models::error::Error); + + // conversion from the'main' type (Request/Response) into the particular type + // e.g. TryFrom<&Response> for InnerTopUpResponse + impl TryFrom<&$main_type> for $inner_type { + type Error = $crate::error::MetadataError; + + fn try_from(value: &$main_type) -> Result { + <$top_type>::try_from(value)?.try_into().map_err( + |err: $crate::models::error::Error| $crate::error::MetadataError::Models { + message: err.to_string(), + }, + ) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for InnerTopUpResponse + $crate::impl_tryfrom_ref!($main_type, $inner_type, $crate::error::MetadataError); + + // conversion from the particular type into the 'main' type (Request/Response) + // e.g. TryFrom<&InnerTopUpResponse> for Response + impl TryFrom<&$inner_type> for $main_type { + type Error = $crate::error::MetadataError; + + fn try_from(value: &$inner_type) -> Result { + <$top_type>::try_from(value)?.try_into().map_err( + |err: $crate::models::error::Error| $crate::error::MetadataError::Models { + message: err.to_string(), + }, + ) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for Response + $crate::impl_tryfrom_ref!($inner_type, $main_type, $crate::error::MetadataError); + }; +} diff --git a/common/wireguard-private-metadata/shared/src/error.rs b/common/wireguard-private-metadata/shared/src/error.rs index 3783462a4d7..f8db19c3046 100644 --- a/common/wireguard-private-metadata/shared/src/error.rs +++ b/common/wireguard-private-metadata/shared/src/error.rs @@ -17,6 +17,9 @@ pub enum MetadataError { #[error("Credential verification error: {message}")] CredentialVerification { message: String }, + + #[error("Upgrade Mode JWT verification error: {message}")] + JWTVerification { message: String }, } impl From for MetadataError { diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs index 54041fd8c79..c0d711ac423 100644 --- a/common/wireguard-private-metadata/shared/src/lib.rs +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod conversion_helpers; pub mod error; mod models; pub mod routes; @@ -9,7 +10,7 @@ pub mod routes; pub use models::v0; pub use models::{ AxumErrorResponse, AxumResult, Construct, ErrorResponse, Extract, Request, Response, Version, - error::Error as ModelError, interface, latest, v1, + error::Error as ModelError, interface, latest, v1, v2, }; fn make_bincode_serializer() -> impl bincode::Options { diff --git a/common/wireguard-private-metadata/shared/src/models/error.rs b/common/wireguard-private-metadata/shared/src/models/error.rs index 45dc88617d4..aef1ac5ae63 100644 --- a/common/wireguard-private-metadata/shared/src/models/error.rs +++ b/common/wireguard-private-metadata/shared/src/models/error.rs @@ -15,7 +15,7 @@ pub enum Error { }, #[error( - "trying to deserialize from query type {source_query_type} query type {target_query_type}" + "trying to deserialize from query type {source_query_type} into query type {target_query_type}" )] InvalidQueryType { source_query_type: String, diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs index 9d5a786c538..c97db77d2ae 100644 --- a/common/wireguard-private-metadata/shared/src/models/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -1,61 +1,90 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::CredentialSpendingData; +use nym_credentials_interface::BandwidthCredential; #[cfg(feature = "testing")] use crate::models::v0; -use crate::models::{Construct, Extract, Request, Response, Version, v1}; +use crate::models::{Construct, Extract, Request, Response, Version, latest, v1, v2}; + +pub use latest::check_upgrade_mode::request::UpgradeModeCheckRequestType; pub enum RequestData { - AvailableBandwidth(()), - TopUpBandwidth(Box), + AvailableBandwidth, + TopUpBandwidth { + credential: Box, + }, + UpgradeModeCheck { + typ: UpgradeModeCheckRequestType, + }, } -impl From for RequestData { - fn from(value: super::latest::interface::RequestData) -> Self { +impl From for RequestData { + fn from(value: latest::interface::RequestData) -> Self { match value { - super::latest::interface::RequestData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) + latest::interface::RequestData::AvailableBandwidth => Self::AvailableBandwidth, + latest::interface::RequestData::TopUpBandwidth { credential } => { + Self::TopUpBandwidth { credential } } - super::latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) + latest::interface::RequestData::UpgradeModeCheck { typ } => { + Self::UpgradeModeCheck { typ } } } } } -impl From for super::latest::interface::RequestData { +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) - } + RequestData::AvailableBandwidth => Self::AvailableBandwidth, + RequestData::TopUpBandwidth { credential } => Self::TopUpBandwidth { credential }, + RequestData::UpgradeModeCheck { typ } => Self::UpgradeModeCheck { typ }, } } } -impl From for ResponseData { - fn from(value: super::latest::interface::ResponseData) -> Self { +impl From for ResponseData { + fn from(value: 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) + latest::interface::ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Self::AvailableBandwidth { + amount, + upgrade_mode, + }, + latest::interface::ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Self::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }, + latest::interface::ResponseData::UpgradeMode { upgrade_mode } => { + Self::UpgradeMode { upgrade_mode } } } } } -impl From for super::latest::interface::ResponseData { +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) - } + ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Self::AvailableBandwidth { + amount, + upgrade_mode, + }, + ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Self::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }, + ResponseData::UpgradeMode { upgrade_mode } => Self::UpgradeMode { upgrade_mode }, } } } @@ -65,13 +94,26 @@ impl Construct for Request { 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)?; + // attempt to go through conversion chain for `info`: v2 => v1 => v0 + let v2_info = v2::interface::RequestData::from(info); + let v1_info = v1::interface::RequestData::try_from(v2_info)?; + let v0_info = v0::interface::RequestData::try_from(v1_info)?; + + let versioned_request = v0::VersionedRequest::construct(v0_info, version)?; Ok(versioned_request.try_into()?) } Version::V1 => { - let versioned_request = v1::VersionedRequest::construct(info.into(), version)?; + // attempt to go through conversion chain for `info`: v2 => v1 + let v2_info = v2::interface::RequestData::from(info); + let v1_info = v1::interface::RequestData::try_from(v2_info)?; + + let versioned_request = v1::VersionedRequest::construct(v1_info, version)?; + Ok(versioned_request.try_into()?) + } + Version::V2 => { + let v2_info = v2::interface::RequestData::from(info); + + let versioned_request = v2::VersionedRequest::construct(v2_info, version)?; Ok(versioned_request.try_into()?) } } @@ -84,24 +126,45 @@ impl Extract for Request { #[cfg(feature = "testing")] super::Version::V0 => { let versioned_request = v0::VersionedRequest::try_from(self.clone())?; - let (request, version) = versioned_request.extract()?; + let (extracted_v0_info, version) = versioned_request.extract()?; - let upgrade_request = super::latest::interface::RequestData::try_from(request)?; + let v1_info = v1::interface::RequestData::try_from(extracted_v0_info)?; + let v2_info = v2::interface::RequestData::try_from(v1_info)?; - Ok((upgrade_request.into(), version)) + let request_data = RequestData::from(v2_info); + Ok((request_data, version)) } super::Version::V1 => { - let versioned_request = v1::VersionedRequest::try_from(self.clone())?; - let (extracted, version) = versioned_request.extract()?; - Ok((extracted.into(), version)) + let versioned_request = v1::VersionedRequest::try_from(self)?; + let (extracted_v1_info, version) = versioned_request.extract()?; + let v2_info = v2::interface::RequestData::try_from(extracted_v1_info)?; + + let request_data = RequestData::from(v2_info); + Ok((request_data, version)) + } + super::Version::V2 => { + let versioned_request = v2::VersionedRequest::try_from(self)?; + let (extracted_v2_info, version) = versioned_request.extract()?; + + let request_data = RequestData::from(extracted_v2_info); + Ok((request_data, version)) } } } } pub enum ResponseData { - AvailableBandwidth(i64), - TopUpBandwidth(i64), + AvailableBandwidth { + amount: i64, + upgrade_mode: bool, + }, + TopUpBandwidth { + available_bandwidth: i64, + upgrade_mode: bool, + }, + UpgradeMode { + upgrade_mode: bool, + }, } impl Construct for Response { @@ -109,14 +172,26 @@ impl Construct for Response { 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)?; + // attempt to go through conversion chain for `info`: v2 => v1 => v0 + let v2_info = v2::interface::ResponseData::from(info); + let v1_info = v1::interface::ResponseData::try_from(v2_info)?; + let v0_info = v0::interface::ResponseData::try_from(v1_info)?; + + let versioned_response = v0::VersionedResponse::construct(v0_info, version)?; Ok(versioned_response.try_into()?) } Version::V1 => { - let versioned_response = v1::VersionedResponse::construct(info.into(), version)?; + // attempt to go through conversion chain for `info`: v2 => v1 + let v2_info = v2::interface::ResponseData::from(info); + let v1_info = v1::interface::ResponseData::try_from(v2_info)?; + + let versioned_response = v1::VersionedResponse::construct(v1_info, version)?; + Ok(versioned_response.try_into()?) + } + Version::V2 => { + let v2_info = v2::interface::ResponseData::from(info); + + let versioned_response = v2::VersionedResponse::construct(v2_info, version)?; Ok(versioned_response.try_into()?) } } @@ -129,16 +204,27 @@ impl Extract for Response { #[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)?; + let (extracted_v0_info, version) = versioned_response.extract()?; + let v1_info = v1::interface::ResponseData::try_from(extracted_v0_info)?; + let v2_info = v2::interface::ResponseData::try_from(v1_info)?; - Ok((upgrade_response.into(), version)) + let response_data = ResponseData::from(v2_info); + Ok((response_data, version)) } super::Version::V1 => { let versioned_response = v1::VersionedResponse::try_from(self.clone())?; - let (extracted, version) = versioned_response.extract()?; - Ok((extracted.into(), version)) + let (extracted_v1_info, version) = versioned_response.extract()?; + let v2_info = v2::interface::ResponseData::try_from(extracted_v1_info)?; + + let response_data = ResponseData::from(v2_info); + Ok((response_data, version)) + } + super::Version::V2 => { + let versioned_response = v2::VersionedResponse::try_from(self.clone())?; + let (extracted_v2_info, version) = versioned_response.extract()?; + + let response_data = ResponseData::from(extracted_v2_info); + Ok((response_data, version)) } } } diff --git a/common/wireguard-private-metadata/shared/src/models/mod.rs b/common/wireguard-private-metadata/shared/src/models/mod.rs index e408d7c5dfb..ae39c890ac4 100644 --- a/common/wireguard-private-metadata/shared/src/models/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/mod.rs @@ -14,7 +14,10 @@ pub mod interface; pub mod v0; // dummy version, only for filling boilerplate code for update/downgrade and testing pub mod v1; -pub use v1 as latest; +// adds upgrade mode information to bandwidth response +pub mod v2; + +pub use v2 as latest; use crate::models::error::Error; @@ -24,6 +27,7 @@ pub enum Version { /// only used for testing purposes, don't include it in your matching arms V0, V1, + V2, } impl From for Version { @@ -35,6 +39,7 @@ impl From for Version { match value { 0 => zero_version, 1 => Version::V1, + 2 => Version::V2, _ => latest::VERSION, // if unknown, it means we're behind, so we can use the latest we know about } } @@ -47,6 +52,7 @@ impl From for u64 { #[cfg(feature = "testing")] Version::V0 => 0, Version::V1 => 1, + Version::V2 => 2, } } } @@ -57,12 +63,24 @@ pub struct Request { pub(crate) inner: Vec, } -#[derive(Clone, Serialize, Deserialize, ToSchema)] +impl Request { + pub fn new(version: Version, inner: Vec) -> Self { + Request { version, inner } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct Response { pub version: Version, pub(crate) inner: Vec, } +impl Response { + pub fn new(version: Version, inner: Vec) -> Self { + Response { version, inner } + } +} + pub trait Extract { fn extract(&self) -> Result<(T, Version), Error>; } 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 index 78dfdec7b57..c9cef560897 100644 --- 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 @@ -1,66 +1,29 @@ // 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}; +use super::super::{QueryType, VersionedRequest}; +use crate::impl_default_bincode_request_query_conversions; #[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(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { 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 index 5243e312ee8..d822a72e60d 100644 --- 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 @@ -1,66 +1,30 @@ // 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 crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{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(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs index f52daba30ee..9c6fc462b32 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs @@ -9,6 +9,7 @@ use super::{ topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, }; use crate::models::{Construct, Extract, Version, error::Error}; +use crate::{Request, Response}; #[derive(Debug, Clone, PartialEq)] pub enum RequestData { @@ -35,11 +36,11 @@ impl Extract for VersionedRequest { fn extract(&self) -> Result<(RequestData, Version), Error> { match self.query_type { QueryType::AvailableBandwidth => { - let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + let _req = InnerAvailableBandwidthRequest::try_from(self)?; Ok((RequestData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { - let _req = InnerTopUpRequest::try_from(self.clone())?; + QueryType::TopUpBandwidth => { + let _req = InnerTopUpRequest::try_from(self)?; Ok((RequestData::TopUpBandwidth(()), VERSION)) } } @@ -61,13 +62,46 @@ impl Extract for VersionedResponse { fn extract(&self) -> Result<(ResponseData, Version), Error> { match self.query_type { QueryType::AvailableBandwidth => { - let _resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + let _resp = InnerAvailableBandwidthResponse::try_from(self)?; Ok((ResponseData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { - let _resp = InnerTopUpResponse::try_from(self.clone())?; + QueryType::TopUpBandwidth => { + let _resp = InnerTopUpResponse::try_from(self)?; Ok((ResponseData::TopUpBandwidth(()), VERSION)) } } } } + +#[cfg(feature = "testing")] +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.version { + Version::V0 => { + let versioned_request = VersionedRequest::try_from(self)?; + versioned_request.extract() + } + _ => Err(Error::UpdateNotPossible { + from: self.version, + to: VERSION, + }), + } + } +} + +#[cfg(feature = "testing")] +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + Version::V0 => { + let translate_response = info; + let versioned_response = VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + _ => Err(Error::DowngradeNotPossible { + from: version, + to: 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 index 997fbf6f572..fd90ac124a0 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs @@ -3,15 +3,13 @@ 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}, + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, }; pub(crate) mod available_bandwidth; @@ -31,7 +29,7 @@ pub use topup_bandwidth::{ #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub enum QueryType { AvailableBandwidth, - TopupBandwidth, + TopUpBandwidth, } impl Display for QueryType { @@ -45,62 +43,24 @@ 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)?) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); #[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)?) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); #[cfg(test)] mod tests { @@ -110,8 +70,10 @@ mod tests { }, topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, }; - use super::*; + use crate::make_bincode_serializer; + use crate::{Request, Response}; + use bincode::Options; #[test] fn serde_request_av_bw() { @@ -146,7 +108,7 @@ mod tests { #[test] fn serde_request_topup() { let req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpRequest {}) .unwrap(), @@ -161,7 +123,7 @@ mod tests { #[test] fn serde_response_topup() { let resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpResponse {}) .unwrap(), 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 index 9c333478d2e..3cb15b5ee8b 100644 --- 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 @@ -1,64 +1,30 @@ // 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 crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{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(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -68,7 +34,7 @@ mod tests { fn serde() { let req = InnerTopUpRequest {}; let ser = VersionedRequest::try_from(req.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpRequest::try_from(ser).unwrap(); assert_eq!(req, de); } @@ -76,7 +42,7 @@ mod tests { #[test] fn empty_content() { let future_req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + 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 index cd934b6e7e8..346792ce73b 100644 --- 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 @@ -1,64 +1,30 @@ // 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 crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{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(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -68,7 +34,7 @@ mod tests { fn serde() { let resp = InnerTopUpResponse {}; let ser = VersionedResponse::try_from(resp.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpResponse::try_from(ser).unwrap(); assert_eq!(resp, de); } @@ -76,7 +42,7 @@ mod tests { #[test] fn empty_content() { let future_resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + 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/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs index 78dfdec7b57..702c06377a6 100644 --- 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 @@ -1,66 +1,30 @@ // 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 crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{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(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { 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 index f5addd609f3..a65ae64aed7 100644 --- 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 @@ -1,68 +1,32 @@ // 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 crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{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(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs index 763222e04a0..7a02463956d 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs @@ -5,6 +5,8 @@ use nym_credentials_interface::CredentialSpendingData; #[cfg(feature = "testing")] use super::super::v0 as previous; +#[cfg(feature = "testing")] +use crate::{Request, Response, v0}; use super::{ QueryType, VERSION, VersionedRequest, VersionedResponse, @@ -46,7 +48,7 @@ impl Extract for VersionedRequest { let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; Ok((RequestData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { + QueryType::TopUpBandwidth => { let req = InnerTopUpRequest::try_from(self.clone())?; Ok(( RequestData::TopUpBandwidth(Box::new(req.credential)), @@ -84,7 +86,7 @@ impl Extract for VersionedResponse { VERSION, )) } - QueryType::TopupBandwidth => { + QueryType::TopUpBandwidth => { let resp = InnerTopUpResponse::try_from(self.clone())?; Ok(( ResponseData::TopUpBandwidth(resp.available_bandwidth), @@ -98,7 +100,7 @@ impl Extract for VersionedResponse { // 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; + type Error = crate::models::error::Error; fn try_from(value: previous::interface::RequestData) -> Result { match value { @@ -106,7 +108,7 @@ impl TryFrom for RequestData { Ok(Self::AvailableBandwidth(inner)) } previous::interface::RequestData::TopUpBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) @@ -118,7 +120,7 @@ impl TryFrom for RequestData { // 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; + type Error = crate::models::error::Error; fn try_from(value: RequestData) -> Result { match value { @@ -131,18 +133,18 @@ impl TryFrom for previous::interface::RequestData { // 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; + type Error = crate::models::error::Error; fn try_from(value: previous::interface::ResponseData) -> Result { match value { previous::interface::ResponseData::AvailableBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::error::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) } previous::interface::ResponseData::TopUpBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::error::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) @@ -154,7 +156,7 @@ impl TryFrom for ResponseData { // 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; + type Error = crate::models::error::Error; fn try_from(value: ResponseData) -> Result { match value { @@ -164,13 +166,64 @@ impl TryFrom for previous::interface::ResponseData { } } +#[cfg(feature = "testing")] +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.version { + Version::V0 => { + let versioned_request = v0::VersionedRequest::try_from(self)?; + let (extracted_v0_info, version) = versioned_request.extract()?; + + let v1_info = RequestData::try_from(extracted_v0_info)?; + Ok((v1_info, version)) + } + Version::V1 => { + let versioned_request = VersionedRequest::try_from(self)?; + versioned_request.extract() + } + // a v1 server does not have any code for downgrading v2 into v1 + _ => Err(Error::DowngradeNotPossible { + from: self.version, + to: VERSION, + }), + } + } +} + +#[cfg(feature = "testing")] +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + Version::V0 => { + let v1_info = info; + let v0_info = v0::interface::ResponseData::try_from(v1_info)?; + + let versioned_response = v0::VersionedResponse::construct(v0_info, version)?; + Ok(versioned_response.try_into()?) + } + Version::V1 => { + let translate_response = info; + let versioned_response = VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + // a v1 server does not have any code for downgrading v2 into v1 + _ => Err(Error::DowngradeNotPossible { + from: version, + to: VERSION, + }), + } + } +} + #[cfg(test)] mod test { - use crate::models::tests::CREDENTIAL_BYTES; - + #[cfg(feature = "testing")] use super::*; + #[cfg(feature = "testing")] + use crate::models::tests::CREDENTIAL_BYTES; #[test] + #[cfg(feature = "testing")] fn request_upgrade() { assert_eq!( RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) @@ -183,6 +236,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn response_upgrade() { assert!( ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(())) @@ -194,6 +248,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn request_downgrade() { assert_eq!( previous::interface::RequestData::try_from(RequestData::AvailableBandwidth(())) @@ -210,6 +265,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn response_downgrade() { assert_eq!( previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth(42)) diff --git a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs index 787a74f671b..050b97caa18 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs @@ -3,15 +3,13 @@ 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}, + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, }; pub use available_bandwidth::{ @@ -31,7 +29,7 @@ pub const VERSION: Version = Version::V1; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub enum QueryType { AvailableBandwidth, - TopupBandwidth, + TopUpBandwidth, } impl Display for QueryType { @@ -45,82 +43,44 @@ 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)?) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); #[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)?) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); #[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 crate::models::error::Error; + use crate::models::tests::CREDENTIAL_BYTES; + use crate::{Request, Response, make_bincode_serializer}; + use bincode::Options; + use nym_credentials_interface::CredentialSpendingData; use super::*; #[test] fn mismatched_request_version() { - let version = Version::V0; + let version = Version::V2; let future_bw = Request { version, inner: vec![], @@ -139,7 +99,7 @@ mod tests { #[test] fn mismatched_response_version() { - let version = Version::V0; + let version = Version::V2; let future_bw = Response { version, inner: vec![], @@ -191,7 +151,7 @@ mod tests { #[test] fn serde_request_topup() { let req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpRequest { credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), @@ -208,7 +168,7 @@ mod tests { #[test] fn serde_response_topup() { let resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpResponse { available_bandwidth: 42, 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 index 871cc127ef8..3a3de396892 100644 --- 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 @@ -1,13 +1,12 @@ // 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 crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpRequest { @@ -15,54 +14,21 @@ pub struct InnerTopUpRequest { 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(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -76,7 +42,7 @@ mod tests { credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), }; let ser = VersionedRequest::try_from(req.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpRequest::try_from(ser).unwrap(); assert_eq!(req, de); } @@ -84,7 +50,7 @@ mod tests { #[test] fn invalid_content() { let future_req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + 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 index 08e0ef111f4..dccc735b8f3 100644 --- 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 @@ -1,66 +1,32 @@ // 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 crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{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(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -72,7 +38,7 @@ mod tests { available_bandwidth: 42, }; let ser = VersionedResponse::try_from(resp.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpResponse::try_from(ser).unwrap(); assert_eq!(resp, de); } @@ -80,7 +46,7 @@ mod tests { #[test] fn invalid_content() { let future_resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpResponse::try_from(future_resp).is_err()); diff --git a/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/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/v2/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs new file mode 100644 index 00000000000..702c06377a6 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs @@ -0,0 +1,50 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); + +#[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/v2/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs new file mode 100644 index 00000000000..0da8eb0a2b4 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs @@ -0,0 +1,56 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse { + pub available_bandwidth: i64, + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse { + available_bandwidth: 42, + upgrade_mode: false, + }; + 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/v2/check_upgrade_mode/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/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/v2/check_upgrade_mode/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs new file mode 100644 index 00000000000..2baba00f8a6 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs @@ -0,0 +1,76 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum UpgradeModeCheckRequestType { + /// Attempt to request upgrade mode recheck via the JWT issued as the result of + /// global attestation.json being published + UpgradeModeJwt { token: String }, +} + +// each versioned variant should always be a subset of the latest one defined in the interface +// so a From trait should always be implementable (as opposed to having to do TryFrom) +// (but this is not applicable in this instance as this IS the latest (05.11.25) +// impl From for crate::models::interface::UpgradeModeCheckRequestType { +// fn from(typ: UpgradeModeCheckRequestType) -> Self { +// match typ { +// UpgradeModeCheckRequestType::UpgradeModeJwt { token } => { +// crate::models::interface::UpgradeModeCheckRequestType::UpgradeModeJwt { token } +// } +// } +// } +// } + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerUpgradeModeCheckRequest { + pub request_type: UpgradeModeCheckRequestType, +} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerUpgradeModeCheckRequest +// - TryFrom for InnerUpgradeModeCheckRequest +// - TryFrom<&InnerUpgradeModeCheckRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerUpgradeModeCheckRequest +// - TryFrom for InnerUpgradeModeCheckRequest +// - TryFrom<&InnerUpgradeModeCheckRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerUpgradeModeCheckRequest, + QueryType::UpgradeModeCheck, + QueryType::UpgradeModeCheck +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerUpgradeModeCheckRequest { + request_type: UpgradeModeCheckRequestType::UpgradeModeJwt { + token: "dummy.jwt.token".to_string(), + }, + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::UpgradeModeCheck, ser.query_type); + let de = InnerUpgradeModeCheckRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::UpgradeModeCheck, + inner: vec![], + }; + assert!(InnerUpgradeModeCheckRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs new file mode 100644 index 00000000000..3b83ae69ebe --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs @@ -0,0 +1,52 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerUpgradeModeCheckResponse { + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerUpgradeModeCheckResponse +// - TryFrom for InnerUpgradeModeCheckResponse +// - TryFrom<&InnerUpgradeModeCheckResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerUpgradeModeCheckResponse +// - TryFrom for InnerUpgradeModeCheckResponse +// - TryFrom<&InnerUpgradeModeCheckResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerUpgradeModeCheckResponse, + QueryType::UpgradeModeCheck, + QueryType::UpgradeModeCheck +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerUpgradeModeCheckResponse { upgrade_mode: true }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::UpgradeModeCheck, ser.query_type); + let de = InnerUpgradeModeCheckResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::UpgradeModeCheck, + inner: vec![], + }; + assert!(InnerUpgradeModeCheckResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/interface.rs b/common/wireguard-private-metadata/shared/src/models/v2/interface.rs new file mode 100644 index 00000000000..9a5e79ff61f --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/interface.rs @@ -0,0 +1,304 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::BandwidthCredential; + +use super::super::v1 as previous; + +use super::{ + QueryType, VERSION, VersionedRequest, VersionedResponse, + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + check_upgrade_mode::{ + request::{InnerUpgradeModeCheckRequest, UpgradeModeCheckRequestType}, + response::InnerUpgradeModeCheckResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, +}; +use crate::models::{Construct, Extract, Version, error::Error}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth, + TopUpBandwidth { + credential: Box, + }, + UpgradeModeCheck { + typ: UpgradeModeCheckRequestType, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth { + amount: i64, + upgrade_mode: bool, + }, + TopUpBandwidth { + available_bandwidth: i64, + upgrade_mode: bool, + }, + UpgradeMode { + upgrade_mode: bool, + }, +} + +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()?), + RequestData::UpgradeModeCheck { typ } => { + Ok(InnerUpgradeModeCheckRequest { request_type: typ }.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)?; + Ok((RequestData::AvailableBandwidth, VERSION)) + } + QueryType::TopUpBandwidth => { + let req = InnerTopUpRequest::try_from(self)?; + Ok(( + RequestData::TopUpBandwidth { + credential: Box::new(req.credential), + }, + VERSION, + )) + } + QueryType::UpgradeModeCheck => { + let req = InnerUpgradeModeCheckRequest::try_from(self)?; + Ok(( + RequestData::UpgradeModeCheck { + typ: req.request_type, + }, + VERSION, + )) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Ok(InnerAvailableBandwidthResponse { + available_bandwidth: amount, + upgrade_mode, + } + .try_into()?), + ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Ok(InnerTopUpResponse { + available_bandwidth, + upgrade_mode, + } + .try_into()?), + ResponseData::UpgradeMode { upgrade_mode } => { + Ok(InnerUpgradeModeCheckResponse { upgrade_mode }.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)?; + Ok(( + ResponseData::AvailableBandwidth { + amount: resp.available_bandwidth, + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + QueryType::TopUpBandwidth => { + let resp = InnerTopUpResponse::try_from(self)?; + Ok(( + ResponseData::TopUpBandwidth { + available_bandwidth: resp.available_bandwidth, + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + QueryType::UpgradeModeCheck => { + let resp = InnerUpgradeModeCheckResponse::try_from(self)?; + Ok(( + ResponseData::UpgradeMode { + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + } + } +} + +impl TryFrom for RequestData { + type Error = super::Error; + + fn try_from(value: previous::interface::RequestData) -> Result { + match value { + previous::interface::RequestData::AvailableBandwidth(_) => Ok(Self::AvailableBandwidth), + previous::interface::RequestData::TopUpBandwidth(zk_nym) => Ok(Self::TopUpBandwidth { + credential: Box::new((*zk_nym).into()), + }), + } + } +} + +impl TryFrom for previous::interface::RequestData { + type Error = super::Error; + + fn try_from(value: RequestData) -> Result { + match value { + RequestData::AvailableBandwidth => Ok(Self::AvailableBandwidth(())), + RequestData::TopUpBandwidth { credential } => match *credential { + BandwidthCredential::ZkNym(zk_nym) => Ok(Self::TopUpBandwidth(zk_nym)), + BandwidthCredential::UpgradeModeJWT { .. } => { + Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }) + } + }, + RequestData::UpgradeModeCheck { .. } => Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }), + } + } +} + +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, + }) + } + } + } +} + +impl TryFrom for previous::interface::ResponseData { + type Error = super::Error; + + fn try_from(value: ResponseData) -> Result { + match value { + ResponseData::AvailableBandwidth { amount, .. } => Ok(Self::AvailableBandwidth(amount)), + ResponseData::TopUpBandwidth { + available_bandwidth, + .. + } => Ok(Self::TopUpBandwidth(available_bandwidth)), + ResponseData::UpgradeMode { .. } => Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }), + } + } +} + +#[cfg(test)] +mod test { + use crate::models::tests::CREDENTIAL_BYTES; + use nym_credentials_interface::CredentialSpendingData; + + use super::*; + + #[test] + fn request_upgrade() { + assert_eq!( + RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) + .unwrap(), + RequestData::AvailableBandwidth + ); + assert_eq!( + RequestData::try_from(previous::interface::RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))) + .unwrap(), + RequestData::TopUpBandwidth { + credential: Box::new(BandwidthCredential::ZkNym(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))), + } + ); + } + + #[test] + fn response_upgrade() { + assert!( + ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(42)) + .is_err() + ); + assert!( + ResponseData::try_from(previous::interface::ResponseData::TopUpBandwidth(42)).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 { + credential: Box::new(BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + )) + }) + .unwrap(), + previous::interface::RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + )) + ); + } + + #[test] + fn response_downgrade() { + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth { + amount: 42, + upgrade_mode: true + }) + .unwrap(), + previous::interface::ResponseData::AvailableBandwidth(42) + ); + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::TopUpBandwidth { + available_bandwidth: 42, + upgrade_mode: true, + }) + .unwrap(), + previous::interface::ResponseData::TopUpBandwidth(42) + ); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/mod.rs new file mode 100644 index 00000000000..6eea7d0713e --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/mod.rs @@ -0,0 +1,197 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, +}; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use check_upgrade_mode::{ + request::{ + InnerUpgradeModeCheckRequest as UpgradeModeCheckRequest, UpgradeModeCheckRequestType, + }, + response::InnerUpgradeModeCheckResponse as UpgradeModeCheckResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +pub(crate) mod available_bandwidth; +pub(crate) mod check_upgrade_mode; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V2; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopUpBandwidth, + UpgradeModeCheck, +} + +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, +} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); + +#[cfg(test)] +mod tests { + + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + use crate::models::tests::CREDENTIAL_BYTES; + use crate::{Request, Response, make_bincode_serializer}; + use bincode::Options; + use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData}; + + use super::*; + + #[test] + fn mismatched_request_version() { + let version = Version::V1; + 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::V1; + 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, + upgrade_mode: true, + }) + .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: BandwidthCredential::from( + 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, + upgrade_mode: true, + }) + .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/v2/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/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/v2/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs new file mode 100644 index 00000000000..a029b3920e6 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs @@ -0,0 +1,61 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::BandwidthCredential; +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest { + /// Ecash credential + pub credential: BandwidthCredential, +} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); + +#[cfg(test)] +mod tests { + use crate::models::tests::CREDENTIAL_BYTES; + use nym_credentials_interface::CredentialSpendingData; + + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest { + credential: BandwidthCredential::from( + 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/v2/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs new file mode 100644 index 00000000000..0cbf3b00d89 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs @@ -0,0 +1,56 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse { + pub available_bandwidth: i64, + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse { + available_bandwidth: 42, + upgrade_mode: true, + }; + 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 index bda615fe1c8..2919908e5d0 100644 --- a/common/wireguard-private-metadata/shared/src/routes.rs +++ b/common/wireguard-private-metadata/shared/src/routes.rs @@ -4,7 +4,9 @@ pub const V1_API_VERSION: &str = "v1"; pub const BANDWIDTH: &str = "bandwidth"; +pub const NETWORK: &str = "network"; pub const VERSION: &str = "version"; pub const AVAILABLE: &str = "available"; pub const TOPUP: &str = "topup"; +pub const UPGRADE_MODE_CHECK: &str = "upgrade-mode-check"; diff --git a/common/wireguard-private-metadata/tests/Cargo.toml b/common/wireguard-private-metadata/tests/Cargo.toml index 6827c16f602..074d088f154 100644 --- a/common/wireguard-private-metadata/tests/Cargo.toml +++ b/common/wireguard-private-metadata/tests/Cargo.toml @@ -11,16 +11,21 @@ license.workspace = true [dependencies] async-trait = { workspace = true } axum = { workspace = true, features = ["tokio", "macros"] } +futures = { workspace = true } nym-credential-verification = { path = "../../credential-verification" } nym-credentials-interface = { path = "../../credentials-interface" } +nym-crypto = { path = "../../crypto", features = ["asymmetric"] } nym-http-api-client = { path = "../../http-api-client" } nym-http-api-common = { path = "../../http-api-common", features = [ "middleware", "utoipa", "output", ] } +nym-upgrade-mode-check = { path = "../../upgrade-mode-check" } nym-wireguard = { path = "../../wireguard" } +time = { workspace = true, features = ["macros"] } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tower = { workspace = true } tower-http = { workspace = true, features = [ "cors", "trace", @@ -37,5 +42,3 @@ nym-wireguard-private-metadata-shared = { path = "../shared", features = [ ] } 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 index 7c8cbec81db..6f3d5ca20f1 100644 --- a/common/wireguard-private-metadata/tests/src/lib.rs +++ b/common/wireguard-private-metadata/tests/src/lib.rs @@ -1,20 +1,86 @@ #[cfg(test)] mod v0; +#[cfg(test)] +mod v1; +#[cfg(test)] +mod v2; +// TODO: we might possibly want to move it to some common crate +// so that it could be re-used by other tests (if needed) #[cfg(test)] -mod tests { - use std::net::SocketAddr; +pub(crate) mod mock_connect_info; +#[cfg(test)] +mod tests { + use crate::v2::peer_controller::PeerControlRequestTypeV2; + use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_credential_verification::{ClientBandwidth, TicketVerifier}; - use nym_credentials_interface::CredentialSpendingData; - use nym_http_api_client::Client; - use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; - use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; - use nym_wireguard_private_metadata_server::{ - AppState, PeerControllerTransceiver, RouterBuilder, + use nym_credentials_interface::{ + AvailableBandwidth, BandwidthCredential, CredentialSpendingData, + }; + use nym_crypto::asymmetric::ed25519; + use nym_http_api_client::HttpClientError; + use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, + generate_jwt_for_upgrade_mode_attestation, generate_new_attestation_with_starting_time, }; - use nym_wireguard_private_metadata_shared::{latest, v0, v1}; - use tokio::{net::TcpListener, sync::mpsc}; + use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; + use nym_wireguard_private_metadata_shared::{v0, v1, v2}; + use std::net::IpAddr; + use std::time::Duration; + use time::OffsetDateTime; + use time::macros::datetime; + + fn unchecked_ip>(raw: S) -> IpAddr { + raw.into().parse().unwrap() + } + + const HIGH_BANDWIDTH: i64 = 20000000000000; + + const DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY: [u8; 32] = [ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, 2, + 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]; + + pub(crate) fn dummy_jwt_issuer_public_key() -> ed25519::PublicKey { + let private_key = + ed25519::PrivateKey::from_bytes(&DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY).unwrap(); + private_key.public_key() + } + + const DUMMY_ATTESTER_ED25519_PRIVATE_KEY: [u8; 32] = [ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]; + + pub(crate) fn dummy_attester_public_key() -> ed25519::PublicKey { + let private_key = + ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap(); + private_key.public_key() + } + + fn high_bandwidth() -> Result { + bandwidth_response(HIGH_BANDWIDTH) + } + + fn low_bandwidth() -> Result { + bandwidth_response(0) + } + + fn bandwidth_response(amount: i64) -> Result { + Ok::<_, nym_wireguard::Error>(ClientBandwidth::new(AvailableBandwidth { + bytes: amount, + expiration: OffsetDateTime::from_unix_timestamp(2000000000).unwrap(), + })) + } + + fn mock_verifier( + bandwidth: i64, + ) -> Result, nym_wireguard::Error> { + Ok::<_, nym_wireguard::Error>( + Box::new(MockVerifier::new(bandwidth)) as Box + ) + } pub(crate) const VERIFIER_AVAILABLE_BANDWIDTH: i64 = 42; pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ @@ -82,6 +148,52 @@ mod tests { 0, 0, 0, 0, 0, 1, ]; + pub(crate) fn mock_upgrade_mode_attestation() -> UpgradeModeAttestation { + let starting_time = datetime!(2025-10-20 12:00 UTC); + + // just some random, HARDCODED, key + let key = ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap(); + + generate_new_attestation_with_starting_time( + &key, + vec![dummy_jwt_issuer_public_key()], + starting_time, + ) + } + + pub(crate) fn mock_different_upgrade_mode_attestation() -> UpgradeModeAttestation { + let starting_time = datetime!(2025-10-30 12:00 UTC); + + // just some random, HARDCODED, key + let key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + + generate_new_attestation_with_starting_time( + &key, + vec![dummy_jwt_issuer_public_key()], + starting_time, + ) + } + + pub(crate) fn mock_upgrade_mode_jwt() -> String { + let jwt_key = + ed25519::PrivateKey::from_bytes(&DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY).unwrap(); + let keys = ed25519::KeyPair::from(jwt_key); + // sanity check in case hardcoded values were modified inconsistently + debug_assert_eq!(*keys.public_key(), dummy_jwt_issuer_public_key()); + + let attestation = mock_upgrade_mode_attestation(); + generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + Some(CREDENTIAL_PROXY_JWT_ISSUER), + ) + } + pub(crate) struct MockVerifier { ret: i64, } @@ -99,56 +211,12 @@ mod tests { } } - 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(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_latest_version() { + // let client = super::v2::network::test::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() { @@ -158,7 +226,7 @@ mod tests { let version = client.version().await.unwrap(); assert_eq!(version, v0::VERSION); - // v0 reqwests + // v0 requests let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); v0::AvailableBandwidthResponse::try_from(response).unwrap(); @@ -167,7 +235,7 @@ mod tests { let response = client.topup_bandwidth(&request).await.unwrap(); v0::TopUpResponse::try_from(response).unwrap(); - // v1 reqwests + // v1 requests let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); assert!(client.available_bandwidth(&request).await.is_err()); @@ -177,17 +245,30 @@ mod tests { .try_into() .unwrap(); assert!(client.topup_bandwidth(&request).await.is_err()); + + // v2 requests + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + 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; + let client = super::v1::network::test::spawn_server_and_create_client().await; // version check let version = client.version().await.unwrap(); assert_eq!(version, v1::VERSION); - // v0 reqwests + // v0 requests let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); v0::AvailableBandwidthResponse::try_from(response).unwrap(); @@ -195,7 +276,7 @@ mod tests { let request = v0::TopUpRequest {}.try_into().unwrap(); assert!(client.topup_bandwidth(&request).await.is_err()); - // v1 reqwests + // v1 requests let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) @@ -213,5 +294,272 @@ mod tests { .unwrap() .available_bandwidth; assert_eq!(available_bandwidth, VERIFIER_AVAILABLE_BANDWIDTH); + + // v2 requests + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + 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_v2() { + let server_test = super::v2::network::test::spawn_server_and_create_client().await; + let client = &server_test.api_client; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v2::VERSION); + + // =========== + // v0 requests + // =========== + let client_ip = unchecked_ip("0.0.0.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(10), + ) + .await; + + 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()); + server_test.reset_registered_responses().await; + + // =========== + // v1 requests + // =========== + let client_ip = unchecked_ip("1.1.1.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(100), + ) + .await; + + 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, 100); + server_test.reset_registered_responses().await; + + // =========== + // v2 requests + // =========== + let client_ip = unchecked_ip("2.2.2.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(200), + ) + .await; + + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available = v2::AvailableBandwidthResponse::try_from(response).unwrap(); + assert_eq!(available.available_bandwidth, 0); + assert!(!available.upgrade_mode); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, 200); + assert!(!top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // upgrade mode test + let upgrade_mode_client = unchecked_ip("2.2.2.2"); + server_test.set_client_ip(upgrade_mode_client); + let good_attestation_alt = mock_different_upgrade_mode_attestation(); + let good_jwt = mock_upgrade_mode_jwt(); + + // 1. send attestation when upgrade mode is not enabled + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response_err = client.topup_bandwidth(&request).await.unwrap_err(); + let HttpClientError::EndpointFailure { error, .. } = response_err else { + panic!("unexpected response") + }; + assert!(error.contains(&UpgradeModeEnableError::AttestationNotPublished.to_string())); + server_test.reset_registered_responses().await; + + // 2.1. send attestation when upgrade mode is enabled (low bandwidth) + let request_typ = PeerControlRequestTypeV2::GetClientBandwidthByIp { + ip: upgrade_mode_client, + }; + server_test + .register_peer_controller_response(request_typ, low_bandwidth()) + .await; + server_test.enable_upgrade_mode().await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + // as defined by `DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD` + assert_eq!(top_up.available_bandwidth, 1024 * 1024 * 1024); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 2.2. send attestation when upgrade mode is enabled (high bandwidth) + let request_typ = PeerControlRequestTypeV2::GetClientBandwidthByIp { + ip: upgrade_mode_client, + }; + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + server_test.enable_upgrade_mode().await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, HIGH_BANDWIDTH); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 3. send bad attestation when upgrade mode is enabled + // (we don't validate it, so client is let through) + // (the only case where invalid attestation would have been rejected is when server + // is not aware of the UM, and that was meant to trigger a refresh. however, a test for that + // is out of scope for these unit tests) + server_test + .change_upgrade_mode_attestation(good_attestation_alt) + .await; + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, HIGH_BANDWIDTH); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 4. send zk-nym when upgrade mode is enabled + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { + ip: upgrade_mode_client, + }, + mock_verifier(300), + ) + .await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + // as defined by `DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD` + assert_eq!(top_up.available_bandwidth, 1024 * 1024 * 1024); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // attempt to enable UM with a valid token + // no global attestation + server_test.disable_upgrade_mode().await; + let request = v2::UpgradeModeCheckRequest { + request_type: v2::UpgradeModeCheckRequestType::UpgradeModeJwt { + token: "".to_string(), + }, + } + .try_into() + .unwrap(); + let response = client.request_upgrade_mode_check(&request).await; + assert!(response.is_err()); + + server_test.publish_upgrade_mode_attestation().await; + // global attestation + let request = v2::UpgradeModeCheckRequest { + request_type: v2::UpgradeModeCheckRequestType::UpgradeModeJwt { + token: mock_upgrade_mode_jwt(), + }, + } + .try_into() + .unwrap(); + let response = client.request_upgrade_mode_check(&request).await.unwrap(); + let upgrade_mode = v2::UpgradeModeCheckResponse::try_from(response).unwrap(); + assert!(upgrade_mode.upgrade_mode); } } diff --git a/common/wireguard-private-metadata/tests/src/mock_connect_info.rs b/common/wireguard-private-metadata/tests/src/mock_connect_info.rs new file mode 100644 index 00000000000..95633d9e015 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/mock_connect_info.rs @@ -0,0 +1,121 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use axum::extract::FromRequestParts; +use axum::http::Request; +use axum::http::request::Parts; +use std::fmt::Display; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::task::{Context, Poll}; +use tower::Layer; +use tower::Service; + +#[derive(Clone)] +pub struct DummyConnectInfo { + // store it as atomic i32 to avoid having to use locks to read and set the value + address: Arc, +} + +impl Display for DummyConnectInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.address().fmt(f) + } +} + +impl DummyConnectInfo { + pub fn new() -> Self { + let dummy_ip = Ipv4Addr::new(1, 2, 3, 4); + DummyConnectInfo { + address: Arc::new(AtomicU32::new(dummy_ip.to_bits())), + } + } + + #[allow(clippy::panic)] + pub fn set(&self, address: IpAddr) { + let IpAddr::V4(v4_address) = address else { + // it would be relatively easy to support ipv6 with multiple atomics, + // but I didn't feel it was needed at the time + panic!("ipv6 not supported") + }; + + self.address.store(v4_address.to_bits(), Ordering::Relaxed); + } + + pub fn address(&self) -> SocketAddr { + let bits = self.address.load(Ordering::Relaxed); + let ipv4 = Ipv4Addr::from(bits); + + SocketAddr::new(IpAddr::V4(ipv4), 1791) + } + + pub fn ip(&self) -> IpAddr { + self.address().ip() + } +} + +#[async_trait] +impl FromRequestParts for DummyConnectInfo +where + S: Send + Sync, +{ + type Rejection = std::convert::Infallible; + + #[allow(clippy::panic)] + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + if let Some(info) = parts.extensions.get::() { + Ok(info.clone()) + } else { + // this is a test code so that's fine + panic!("DummyConnectInfo not set") + } + } +} + +#[derive(Clone)] +pub struct MockConnectInfoLayer { + info: DummyConnectInfo, +} + +impl MockConnectInfoLayer { + pub fn new(info: DummyConnectInfo) -> Self { + Self { info } + } +} + +impl Layer for MockConnectInfoLayer { + type Service = MockConnectInfoMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + MockConnectInfoMiddleware { + inner, + info: self.info.clone(), + } + } +} + +#[derive(Clone)] +pub struct MockConnectInfoMiddleware { + inner: S, + info: DummyConnectInfo, +} + +impl Service> for MockConnectInfoMiddleware +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + req.extensions_mut().insert(self.info.clone()); + self.inner.call(req) + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/app_state.rs b/common/wireguard-private-metadata/tests/src/v0/app_state.rs new file mode 100644 index 00000000000..45222bda15c --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/app_state.rs @@ -0,0 +1,7 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::v1::app_state::AppStateV1; + +// there have been no changes between v0 and v1 so there's no point in redefining it +pub type AppStateV0 = AppStateV1; diff --git a/common/wireguard-private-metadata/tests/src/v0/interface.rs b/common/wireguard-private-metadata/tests/src/v0/interface.rs deleted file mode 100644 index a09ff4307af..00000000000 --- a/common/wireguard-private-metadata/tests/src/v0/interface.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_wireguard_private_metadata_shared::{ - Construct, Extract, Request, Response, Version, v0 as latest, -}; - -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 index 7338cc5ae23..2af556497bf 100644 --- a/common/wireguard-private-metadata/tests/src/v0/mod.rs +++ b/common/wireguard-private-metadata/tests/src/v0/mod.rs @@ -1,2 +1,3 @@ -pub(crate) mod interface; +pub(crate) mod app_state; pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v0/network.rs b/common/wireguard-private-metadata/tests/src/v0/network.rs index b0b7361e843..e45c506b94b 100644 --- a/common/wireguard-private-metadata/tests/src/v0/network.rs +++ b/common/wireguard-private-metadata/tests/src/v0/network.rs @@ -5,25 +5,24 @@ pub(crate) mod test { use std::net::SocketAddr; - use crate::{ - tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}, - v0::interface::{RequestData, ResponseData}, - }; + use crate::tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}; use axum::{Json, Router, extract::Query}; use nym_credential_verification::ClientBandwidth; use nym_http_api_client::Client; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::v0::interface::{RequestData, ResponseData}; use nym_wireguard_private_metadata_shared::{ AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v0 as latest, }; + use tokio::sync::mpsc::Receiver; use tokio::{net::TcpListener, sync::mpsc}; use tower_http::compression::CompressionLayer; - use nym_wireguard_private_metadata_server::AppState; + use crate::v0::app_state::AppStateV0; - fn bandwidth_routes() -> Router { + fn bandwidth_routes() -> Router { Router::new() .route("/version", axum::routing::get(version)) .route("/available", axum::routing::post(available_bandwidth)) @@ -31,32 +30,11 @@ pub(crate) mod test { .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, @@ -74,17 +52,6 @@ pub(crate) mod test { 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, @@ -102,35 +69,41 @@ pub(crate) mod test { Ok(output.to_response(response)) } + fn spawn_mock_peer_controller(mut request_rx: Receiver) { + tokio::spawn(async move { + while let Some(request) = request_rx.recv().await { + match request { + 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"), + } + } + }); + } + 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 (request_tx, 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))); + .with_state(AppStateV0::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"), - } - }); + spawn_mock_peer_controller(request_rx); tokio::spawn(async move { axum::serve( diff --git a/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs new file mode 100644 index 00000000000..b1f80ad87c8 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs @@ -0,0 +1,9 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![allow(dead_code)] + +use crate::v2::peer_controller::{MockPeerControllerStateV2, MockPeerControllerV2}; + +pub type MockPeerControllerStateV0 = MockPeerControllerStateV2; +pub type MockPeerControllerV0 = MockPeerControllerV2; diff --git a/common/wireguard-private-metadata/tests/src/v1/app_state.rs b/common/wireguard-private-metadata/tests/src/v1/app_state.rs new file mode 100644 index 00000000000..f7731658194 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/app_state.rs @@ -0,0 +1,33 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_private_metadata_server::PeerControllerTransceiver; +use nym_wireguard_private_metadata_shared::error::MetadataError; +use std::net::IpAddr; + +#[derive(Clone, axum::extract::FromRef)] +pub struct AppStateV1 { + transceiver: PeerControllerTransceiver, +} + +impl AppStateV1 { + 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/tests/src/v1/mod.rs b/common/wireguard-private-metadata/tests/src/v1/mod.rs new file mode 100644 index 00000000000..3cf9729866d --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod app_state; +pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v1/network.rs b/common/wireguard-private-metadata/tests/src/v1/network.rs new file mode 100644 index 00000000000..07b23fccbcc --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/network.rs @@ -0,0 +1,134 @@ +// 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}; + use crate::v1::app_state::AppStateV1; + use axum::extract::{ConnectInfo, State}; + use axum::{Json, Router, extract::Query}; + use nym_credential_verification::ClientBandwidth; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::v1::interface::{RequestData, ResponseData}; + use nym_wireguard_private_metadata_shared::{ + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v1 as latest, + }; + use tokio::sync::mpsc::Receiver; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + 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()) + } + + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + 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)) + } + + 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)) + } + + fn spawn_mock_peer_controller(mut request_rx: Receiver) { + tokio::spawn(async move { + while let Some(request) = request_rx.recv().await { + match request { + 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"), + } + } + }); + } + + 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, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = Router::new() + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) + .with_state(AppStateV1::new(PeerControllerTransceiver::new(request_tx))); + + spawn_mock_peer_controller(request_rx); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url(addr.to_string(), None).unwrap() + } +} diff --git a/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs new file mode 100644 index 00000000000..7291d5c5b33 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs @@ -0,0 +1,9 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![allow(dead_code)] + +use crate::v2::peer_controller::{MockPeerControllerStateV2, MockPeerControllerV2}; + +pub type MockPeerControllerStateV1 = MockPeerControllerStateV2; +pub type MockPeerControllerV1 = MockPeerControllerV2; diff --git a/common/wireguard-private-metadata/tests/src/v2/app_state.rs b/common/wireguard-private-metadata/tests/src/v2/app_state.rs new file mode 100644 index 00000000000..3ebf886df1a --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/app_state.rs @@ -0,0 +1,8 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_wireguard_private_metadata_server::AppState; + +// given latest is v2, we just create a type alias +// for any future versions, this would have to be adjusted +pub type AppStateV2 = AppState; diff --git a/common/wireguard-private-metadata/tests/src/v2/mod.rs b/common/wireguard-private-metadata/tests/src/v2/mod.rs new file mode 100644 index 00000000000..3cf9729866d --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod app_state; +pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v2/network.rs b/common/wireguard-private-metadata/tests/src/v2/network.rs new file mode 100644 index 00000000000..fd2520e8e52 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/network.rs @@ -0,0 +1,329 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use crate::mock_connect_info::{DummyConnectInfo, MockConnectInfoLayer}; + use crate::tests::{dummy_attester_public_key, mock_upgrade_mode_attestation}; + use crate::v2::app_state::AppStateV2; + use crate::v2::peer_controller::{ + MockPeerControllerStateV2, MockPeerControllerV2, PeerControlRequestTypeV2, + }; + use axum::extract::State; + use axum::{Extension, Json, Router, extract::Query}; + use futures::StreamExt; + use nym_credential_verification::upgrade_mode::{ + CheckRequest, UpgradeModeCheckConfig, UpgradeModeCheckRequestReceiver, + UpgradeModeCheckRequestSender, UpgradeModeDetails, UpgradeModeState, + }; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_upgrade_mode_check::UpgradeModeAttestation; + use nym_wireguard::CONTROL_CHANNEL_SIZE; + use nym_wireguard_private_metadata_server::AppState; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::interface::RequestData; + use nym_wireguard_private_metadata_shared::{ + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v2 as latest, + }; + use std::any::Any; + use std::net::IpAddr; + use std::sync::Arc; + use std::time::Duration; + use tokio::sync::Mutex; + use tokio::task::JoinSet; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + pub struct MockUpgradeModeWatcher { + check_request_receiver: UpgradeModeCheckRequestReceiver, + upgrade_mode_state: UpgradeModeState, + + mock_published_attestation: Arc>>, + } + + impl MockUpgradeModeWatcher { + pub fn new( + check_request_receiver: UpgradeModeCheckRequestReceiver, + upgrade_mode_state: UpgradeModeState, + mock_published_attestation: Arc>>, + ) -> Self { + MockUpgradeModeWatcher { + check_request_receiver, + upgrade_mode_state, + mock_published_attestation, + } + } + + async fn handle_check_request(&mut self, polled_request: CheckRequest) { + let mut requests = vec![polled_request]; + while let Ok(Some(queued_up)) = self.check_request_receiver.try_next() { + requests.push(queued_up); + } + + let published = self.mock_published_attestation.lock().await; + self.upgrade_mode_state + .try_set_expected_attestation(published.clone()) + .await; + + for request in requests { + request.finalize() + } + } + + pub async fn run(&mut self) { + // for now don't do anything apart from notifying the caller + while let Some(polled_request) = self.check_request_receiver.next().await { + self.handle_check_request(polled_request).await + } + } + } + + pub struct ServerTest { + // among other things gives you access to the shared state, so you could toggle the flag + // and thus change server behaviour + upgrade_mode_state: UpgradeModeState, + + // shared state with the mock attestation watcher to make it think new attestation has been published + mock_published_attestation: Arc>>, + + connect_info: DummyConnectInfo, + + // handles to the following tasks: + // - the actual axum server + // - dummy attestation watcher + // - dummy peer controller + _server_tasks: JoinSet<()>, + + peer_controller_state: MockPeerControllerStateV2, + + pub(crate) api_client: Client, + } + + impl ServerTest { + pub(crate) async fn new() -> Self { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + + let (um_recheck_tx, um_recheck_rx) = futures::channel::mpsc::unbounded(); + let upgrade_mode_state = UpgradeModeState::new(dummy_attester_public_key()); + let upgrade_mode_details = UpgradeModeDetails::new( + UpgradeModeCheckConfig { + // essentially we never want to trigger this in our tests + min_staleness_recheck: Duration::from_nanos(1), + }, + UpgradeModeCheckRequestSender::new(um_recheck_tx), + upgrade_mode_state.clone(), + ); + + let dummy_connect_info = DummyConnectInfo::new(); + + let router = Router::new() + .nest( + "/v1", + Router::new() + .nest("/bandwidth", bandwidth_routes()) + .nest("/network", network_routes()), + ) + .with_state(AppStateV2::new( + PeerControllerTransceiver::new(request_tx), + upgrade_mode_details, + )); + + // register responses for expected requests + let peer_controller_state = MockPeerControllerStateV2::default(); + let mut server_tasks = JoinSet::new(); + + let mut peer_controller = + MockPeerControllerV2::new(peer_controller_state.clone(), request_rx); + + let mock_published_attestation = Arc::new(Mutex::new(None)); + let mut upgrade_mode_watcher = MockUpgradeModeWatcher::new( + um_recheck_rx, + upgrade_mode_state.clone(), + mock_published_attestation.clone(), + ); + + // spawn all the tasks + server_tasks.spawn(async move { + peer_controller.run().await; + }); + server_tasks.spawn(async move { + upgrade_mode_watcher.run().await; + }); + + let connect_info = dummy_connect_info.clone(); + server_tasks.spawn(async move { + axum::serve( + listener, + // router.into_make_service_with_connect_info::(), + router.layer(MockConnectInfoLayer::new(connect_info)), + ) + .await + .unwrap(); + }); + let api_client = Client::new_url(addr.to_string(), None).unwrap(); + + ServerTest { + upgrade_mode_state, + mock_published_attestation, + connect_info: dummy_connect_info, + _server_tasks: server_tasks, + peer_controller_state, + api_client, + } + } + + pub(crate) async fn enable_upgrade_mode(&self) { + self.change_upgrade_mode_attestation(mock_upgrade_mode_attestation()) + .await + } + + pub(crate) async fn change_upgrade_mode_attestation( + &self, + attestation: UpgradeModeAttestation, + ) { + self.upgrade_mode_state + .try_set_expected_attestation(Some(attestation)) + .await + } + + pub(crate) async fn publish_upgrade_mode_attestation(&self) { + *self.mock_published_attestation.lock().await = Some(mock_upgrade_mode_attestation()) + } + + #[allow(dead_code)] + pub(crate) async fn disable_upgrade_mode(&self) { + self.upgrade_mode_state + .try_set_expected_attestation(None) + .await; + } + + pub(crate) fn set_client_ip(&self, ip: IpAddr) { + self.connect_info.set(ip) + } + + #[allow(dead_code)] + pub(crate) fn client_ip(&self) -> IpAddr { + self.connect_info.ip() + } + + // note: it's caller's responsibility to make sure the response type is correct! + pub(crate) async fn register_peer_controller_response( + &self, + request: PeerControlRequestTypeV2, + response: impl Any + Send + Sync + 'static, + ) { + self.peer_controller_state + .register_response(request, response) + .await + } + + pub(crate) async fn reset_registered_responses(&self) { + self.peer_controller_state + .clear_registered_responses() + .await + } + } + + 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()) + } + + fn network_routes() -> Router { + Router::new() + .route( + "/upgrade-mode-check", + axum::routing::post(upgrade_mode_check), + ) + .layer(CompressionLayer::new()) + } + + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + async fn available_bandwidth( + // ❗ \/ DIFFERENT FROM ACTUAL SERVER \/ ❗ + // we use different ConnectInfo to be able to mock different ip addresses + Extension(addr): Extension, + // ❗ /\ DIFFERENT FROM ACTUAL SERVER /\ ❗ + 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_response = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(available_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + async fn topup_bandwidth( + // ❗ \/ DIFFERENT FROM ACTUAL SERVER \/ ❗ + // we use different ConnectInfo to be able to mock different ip addresses + Extension(addr): Extension, + // ❗ /\ DIFFERENT FROM ACTUAL SERVER /\ ❗ + 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 top_up_bandwidth_response = state + .topup_bandwidth(addr.ip(), credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(top_up_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + async fn upgrade_mode_check( + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::UpgradeModeCheck { typ }, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let upgrade_mode_check_response = state + .upgrade_mode_check(typ) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(upgrade_mode_check_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + pub(crate) async fn spawn_server_and_create_client() -> ServerTest { + ServerTest::new().await + } +} diff --git a/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs new file mode 100644 index 00000000000..435359efac8 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs @@ -0,0 +1,177 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// not declared as a 'global' since I can imagine it might change between versions + +use nym_wireguard::peer_controller::PeerControlRequest; +use std::any::Any; +use std::collections::{HashMap, VecDeque}; +use std::net::IpAddr; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio::sync::mpsc::Receiver; + +#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq, Copy)] +pub enum PeerControlRequestTypeV2 { + AddPeer, + RemovePeer, + QueryPeer, + GetClientBandwidthByKey, + GetClientBandwidthByIp { ip: IpAddr }, + GetVerifierByKey, + GetVerifierByIp { ip: IpAddr }, +} + +impl From<&PeerControlRequest> for PeerControlRequestTypeV2 { + fn from(req: &PeerControlRequest) -> Self { + match req { + PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer, + PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer, + PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer, + PeerControlRequest::GetClientBandwidthByKey { .. } => { + PeerControlRequestTypeV2::GetClientBandwidthByKey + } + PeerControlRequest::GetClientBandwidthByIp { ip, .. } => { + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: *ip } + } + PeerControlRequest::GetVerifierByKey { .. } => { + PeerControlRequestTypeV2::GetVerifierByKey + } + PeerControlRequest::GetVerifierByIp { ip, .. } => { + PeerControlRequestTypeV2::GetVerifierByIp { ip: *ip } + } + } + } +} + +// all responses are registered as a queue for particular type +// (this is because the actual type can't be cloned as the `Error` does not implement Clone) +type RegisteredResponses = + HashMap>>; + +#[derive(Clone, Default)] +pub struct MockPeerControllerStateV2 { + registered_responses: Arc>, +} + +impl MockPeerControllerStateV2 { + pub async fn register_response( + &self, + request: PeerControlRequestTypeV2, + response: impl Any + Send + Sync + 'static, + ) { + self.registered_responses + .write() + .await + .entry(request) + .or_default() + .push_back(Box::new(response)); + } + + pub async fn clear_registered_responses(&self) { + self.registered_responses.write().await.clear(); + } +} + +pub struct MockPeerControllerV2 { + state: MockPeerControllerStateV2, + request_rx: Receiver, +} + +impl MockPeerControllerV2 { + pub(crate) fn new( + state: MockPeerControllerStateV2, + request_rx: Receiver, + ) -> Self { + MockPeerControllerV2 { state, request_rx } + } + + async fn handle_request(&mut self, request: PeerControlRequest) { + let typ = PeerControlRequestTypeV2::from(&request); + + let mut guard = self.state.registered_responses.write().await; + let Some(registered_responses) = guard.get_mut(&typ) else { + panic!( + "received a request for {typ:?} but there are no registered responses - this is probably due to a bug in your test setup" + ); + }; + + let Some(response) = registered_responses.pop_front() else { + panic!( + "received a request for {typ:?} but there are no registered responses - this is probably due to a bug in your test setup" + ); + }; + + match request { + PeerControlRequest::AddPeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::RemovePeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::QueryPeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetClientBandwidthByKey { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetClientBandwidthByIp { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetVerifierByKey { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .ok(); + } + PeerControlRequest::GetVerifierByIp { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .ok(); + } + } + } + + pub(crate) async fn run(&mut self) { + while let Some(request) = self.request_rx.recv().await { + self.handle_request(request).await; + } + } +} diff --git a/common/wireguard-types/Cargo.toml b/common/wireguard-types/Cargo.toml index e1ffda761c9..9a8a783db01 100644 --- a/common/wireguard-types/Cargo.toml +++ b/common/wireguard-types/Cargo.toml @@ -12,14 +12,11 @@ license.workspace = true [dependencies] base64 = { workspace = true } -log = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } -nym-config = { path = "../config" } -nym-network-defaults = { path = "../network-defaults" } - x25519-dalek = { workspace = true, features = ["static_secrets"] } +nym-crypto = { path = "../crypto", features = ["asymmetric"] } [dev-dependencies] rand = { workspace = true } diff --git a/common/wireguard-types/src/public_key.rs b/common/wireguard-types/src/public_key.rs index 3b3bbd60d11..755c60d6289 100644 --- a/common/wireguard-types/src/public_key.rs +++ b/common/wireguard-types/src/public_key.rs @@ -9,11 +9,24 @@ use std::fmt; use std::ops::Deref; use std::str::FromStr; +use nym_crypto::asymmetric::x25519; use x25519_dalek::PublicKey; #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub struct PeerPublicKey(PublicKey); +impl From for PeerPublicKey { + fn from(pk: x25519::PublicKey) -> Self { + PeerPublicKey(pk.into()) + } +} + +impl From<&x25519::PublicKey> for PeerPublicKey { + fn from(pk: &x25519::PublicKey) -> Self { + (*pk).into() + } +} + impl PeerPublicKey { pub fn new(key: PublicKey) -> Self { PeerPublicKey(key) diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml index e98e5fc27df..f2a773d4ec3 100644 --- a/common/wireguard/Cargo.toml +++ b/common/wireguard/Cargo.toml @@ -11,27 +11,15 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait = { workspace = true } base64 = { workspace = true } -bincode = { workspace = true } -chrono = { workspace = true } -dashmap = { workspace = true } defguard_wireguard_rs = { workspace = true } -dyn-clone = { workspace = true } futures = { workspace = true } -# The latest version on crates.io at the time of writing this (6.0.0) has a -# version mismatch with x25519-dalek/curve25519-dalek that is resolved in the -# latest commit. So pick that for now. -x25519-dalek = { workspace = true } ip_network = { workspace = true } -log.workspace = true thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } tokio-stream = { workspace = true } -time = { workspace = true } tracing = { workspace = true } -nym-authenticator-requests = { path = "../authenticator-requests" } nym-credentials-interface = { path = "../credentials-interface" } nym-credential-verification = { path = "../credential-verification" } nym-crypto = { path = "../crypto", features = ["asymmetric"] } @@ -48,3 +36,6 @@ nym-gateway-storage = { path = "../gateway-storage", features = ["mock"] } [features] default = [] mock = ["nym-gateway-storage/mock"] + +[lints] +workspace = true diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 6b2c632d222..cf7ff7f32ff 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -7,13 +7,15 @@ // #![warn(clippy::unwrap_used)] use defguard_wireguard_rs::{WGApi, WireguardInterfaceApi, host::Peer, key::Key, net::IpAddrMask}; -#[cfg(target_os = "linux")] -use nym_credential_verification::ecash::EcashManager; use nym_crypto::asymmetric::x25519::KeyPair; use nym_wireguard_types::Config; use peer_controller::PeerControlRequest; use std::sync::Arc; use tokio::sync::mpsc::{self, Receiver, Sender}; +use tracing::error; + +#[cfg(target_os = "linux")] +use nym_credential_verification::ecash::EcashManager; #[cfg(target_os = "linux")] use nym_network_defaults::constants::WG_TUN_BASE_NAME; @@ -23,6 +25,8 @@ pub mod peer_controller; pub mod peer_handle; pub mod peer_storage_manager; +pub use error::Error; + pub const CONTROL_CHANNEL_SIZE: usize = 256; pub struct WgApiWrapper { @@ -114,7 +118,7 @@ impl Drop for WgApiWrapper { fn drop(&mut self) { if let Err(e) = defguard_wireguard_rs::WireguardInterfaceApi::remove_interface(&self.inner) { - log::error!("Could not remove the wireguard interface: {e:?}"); + error!("Could not remove the wireguard interface: {e:?}"); } } } @@ -163,6 +167,7 @@ pub async fn start_wireguard( ecash_manager: Arc, metrics: nym_node_metrics::NymNodeMetrics, peers: Vec, + upgrade_mode_status: nym_credential_verification::upgrade_mode::UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, wireguard_data: WireguardData, ) -> Result, Box> { @@ -250,6 +255,7 @@ pub async fn start_wireguard( peer_bandwidth_managers, wireguard_data.inner.peer_tx.clone(), wireguard_data.peer_rx, + upgrade_mode_status, shutdown_token, ); tokio::spawn(async move { controller.run().await }); diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index e9e4f78f2df..54b208c5afc 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -1,13 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::{ + error::{Error, Result}, + peer_handle::SharedBandwidthStorageManager, +}; +use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager}; use defguard_wireguard_rs::{ WireguardInterfaceApi, host::{Host, Peer}, key::Key, }; use futures::channel::oneshot; -use log::info; +use nym_credential_verification::upgrade_mode::UpgradeModeStatus; use nym_credential_verification::{ BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, TicketVerifier, bandwidth_storage_manager::BandwidthStorageManager, ecash::traits::EcashManager, @@ -24,12 +29,7 @@ use std::{ }; use tokio::sync::{RwLock, mpsc}; use tokio_stream::{StreamExt, wrappers::IntervalStream}; - -use crate::{ - error::{Error, Result}, - peer_handle::SharedBandwidthStorageManager, -}; -use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager}; +use tracing::{debug, error, info, trace}; pub enum PeerControlRequest { AddPeer { @@ -84,6 +84,10 @@ pub struct PeerController { host_information: Arc>, bw_storage_managers: HashMap, timeout_check_interval: IntervalStream, + + /// Flag indicating whether the system is undergoing an upgrade and thus peers shouldn't be getting + /// their bandwidth metered. + upgrade_mode: UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, } @@ -97,6 +101,7 @@ impl PeerController { bw_storage_managers: HashMap, request_tx: mpsc::Sender, request_rx: mpsc::Receiver, + upgrade_mode: UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, ) -> Self { let timeout_check_interval = @@ -110,12 +115,13 @@ impl PeerController { cached_peer_manager, bandwidth_storage_manager.clone(), request_tx.clone(), + upgrade_mode.clone(), &shutdown_token, ); let public_key = public_key.clone(); tokio::spawn(async move { handle.run().await; - log::debug!("Peer handle shut down for {public_key}"); + debug!("Peer handle shut down for {public_key}"); }); } let bw_storage_managers = bw_storage_managers @@ -131,6 +137,7 @@ impl PeerController { request_tx, request_rx, timeout_check_interval, + upgrade_mode, shutdown_token, metrics, } @@ -145,7 +152,7 @@ impl PeerController { self.bw_storage_managers.remove(key); let ret = self.wg_api.remove_peer(key); if ret.is_err() { - log::error!( + error!( "Wireguard peer could not be removed from wireguard kernel module. Process should be restarted so that the interface is reset." ); } @@ -192,6 +199,7 @@ impl PeerController { cached_peer_manager, bandwidth_storage_manager.clone(), self.request_tx.clone(), + self.upgrade_mode.clone(), &self.shutdown_token, ); self.bw_storage_managers @@ -203,7 +211,7 @@ impl PeerController { let public_key = peer.public_key.clone(); tokio::spawn(async move { handle.run().await; - log::debug!("Peer handle shut down for {public_key}"); + debug!("Peer handle shut down for {public_key}"); }); Ok(()) } @@ -357,6 +365,7 @@ impl PeerController { } } + #[allow(clippy::expect_used)] self.metrics.wireguard.update( // if the conversion fails it means we're running not running on a 64bit system // and that's a reason enough for this failure. @@ -377,7 +386,7 @@ impl PeerController { tokio::select! { _ = self.timeout_check_interval.next() => { let Ok(host) = self.wg_api.read_interface_data() else { - log::error!("Can't read wireguard kernel data"); + error!("Can't read wireguard kernel data"); continue; }; self.update_metrics(&host).await; @@ -385,7 +394,7 @@ impl PeerController { *self.host_information.write().await = host; } _ = self.shutdown_token.cancelled() => { - log::trace!("PeerController handler: Received shutdown"); + trace!("PeerController handler: Received shutdown"); break; } msg = self.request_rx.recv() => { @@ -412,7 +421,7 @@ impl PeerController { response_tx.send(self.handle_query_verifier_by_ip(ip, *credential).await).ok(); } None => { - log::trace!("PeerController [main loop]: stopping since channel closed"); + trace!("PeerController [main loop]: stopping since channel closed"); break; } } @@ -429,6 +438,9 @@ struct MockWgApi { } #[cfg(feature = "mock")] +// unwraps, etc. are fine in test code +#[allow(clippy::unwrap_used)] +#[allow(clippy::todo)] impl WireguardInterfaceApi for MockWgApi { fn create_interface( &self, @@ -534,6 +546,7 @@ pub fn start_controller( Default::default(), request_tx, request_rx, + UpgradeModeStatus::default(), shutdown_manager.child_shutdown_token(), ); tokio::spawn(async move { peer_controller.run().await }); @@ -542,12 +555,15 @@ pub fn start_controller( } #[cfg(feature = "mock")] +// unwraps are fine in test code +#[allow(clippy::unwrap_used)] pub async fn stop_controller(mut shutdown_manager: nym_task::ShutdownManager) { shutdown_manager.send_cancellation(); shutdown_manager.run_until_shutdown().await; } #[cfg(test)] +#[cfg(feature = "mock")] mod tests { use super::*; diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 57c92d50728..b5dd5d83fc0 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -6,12 +6,16 @@ use crate::peer_controller::PeerControlRequest; use crate::peer_storage_manager::{CachedPeerManager, PeerInformation}; use defguard_wireguard_rs::{host::Host, key::Key, net::IpAddrMask}; use futures::channel::oneshot; +use nym_credential_verification::OutOfBandwidthResultExt; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; +use nym_credential_verification::upgrade_mode::UpgradeModeStatus; use nym_task::ShutdownToken; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; +use std::fmt::Display; use std::sync::Arc; use tokio::sync::{RwLock, mpsc}; use tokio_stream::{StreamExt, wrappers::IntervalStream}; +use tracing::{debug, error, trace, warn}; #[derive(Clone)] pub(crate) struct SharedBandwidthStorageManager { @@ -43,9 +47,19 @@ pub struct PeerHandle { bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, timeout_check_interval: IntervalStream, + + /// Flag indicating whether the system is undergoing an upgrade and thus peers shouldn't be getting + /// their bandwidth metered. + upgrade_mode: UpgradeModeStatus, shutdown_token: ShutdownToken, } +impl Display for PeerHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "peer {}", self.public_key) + } +} + impl PeerHandle { pub(crate) fn new( public_key: Key, @@ -53,11 +67,11 @@ impl PeerHandle { cached_peer: CachedPeerManager, bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, + upgrade_mode: UpgradeModeStatus, shutdown_token: &ShutdownToken, ) -> Self { - let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new( - tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK), - ); + let timeout_check_interval = + IntervalStream::new(tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK)); let shutdown_token = shutdown_token.clone(); PeerHandle { public_key, @@ -66,10 +80,22 @@ impl PeerHandle { bandwidth_storage_manager, request_tx, timeout_check_interval, + upgrade_mode, shutdown_token, } } + /// Attempt to use the specified amount of bandwidth and update internal cache. + /// Returns the amount of bandwidth remaining + async fn try_use_bandwidth(&self, spent: i64) -> nym_credential_verification::Result { + self.bandwidth_storage_manager + .inner + .write() + .await + .try_use_bandwidth(spent) + .await + } + async fn remove_peer(&self) -> Result { let (response_tx, response_rx) = oneshot::channel(); self.request_tx @@ -87,73 +113,33 @@ impl PeerHandle { Ok(success) } - fn compute_spent_bandwidth( - kernel_peer: PeerInformation, - cached_peer: PeerInformation, - ) -> Option { - let kernel_total = kernel_peer - .rx_bytes - .checked_add(kernel_peer.tx_bytes) - .or_else(|| { - tracing::error!( - "Overflow on kernel adding bytes: {} + {}", - kernel_peer.rx_bytes, - kernel_peer.tx_bytes - ); - None - })?; - let cached_total = cached_peer - .rx_bytes - .checked_add(cached_peer.tx_bytes) - .or_else(|| { - tracing::error!( - "Overflow on cached adding bytes: {} + {}", - cached_peer.rx_bytes, - cached_peer.tx_bytes - ); - None - })?; - - kernel_total.checked_sub(cached_total).or_else(|| { - tracing::error!("Overflow on spent bandwidth subtraction: kernel - cached = {kernel_total} - {cached_total}"); - None - }) - } - async fn active_peer(&mut self, kernel_peer: PeerInformation) -> Result { let Some(cached_peer) = self.cached_peer.get_peer() else { - log::debug!( - "Peer {:?} not in storage anymore, shutting down handle", - self.public_key - ); + debug!("{self} not in storage anymore, shutting down handle"); return Ok(false); }; - let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, cached_peer) - .unwrap_or_default() - .try_into() - .inspect_err(|err| tracing::error!("Could not convert from u64 to i64: {err:?}")) - .unwrap_or_default(); - + let spent_bandwidth = kernel_peer.consumed_kernel_bandwidth(&cached_peer); self.cached_peer.update(kernel_peer); - if spent_bandwidth > 0 - && self - .bandwidth_storage_manager - .inner() - .write() - .await + if spent_bandwidth > 0 { + trace!("{self} has used {spent_bandwidth} of bandwidth"); + if self.upgrade_mode.enabled() { + debug!("we're in upgrade mode - {self} is not going to get its bandwidth deducted"); + return Ok(true); + } + + // 'regular' flow + if self .try_use_bandwidth(spent_bandwidth) .await - .is_err() - { - tracing::debug!( - "Peer {} is out of bandwidth, removing it", - self.public_key.to_string() - ); - let success = self.remove_peer().await?; - self.cached_peer.remove_peer(); - return Ok(!success); + .is_out_of_bandwidth() + { + debug!("{self} is out of bandwidth, removing it"); + let success = self.remove_peer().await?; + self.cached_peer.remove_peer(); + return Ok(!success); + } } Ok(true) @@ -169,10 +155,7 @@ impl PeerHandle { .ok_or(Error::MissingClientKernelEntry(self.public_key.to_string()))? .into(); if !self.active_peer(kernel_peer).await? { - log::debug!( - "Peer {:?} is not active anymore, shutting down handle", - self.public_key - ); + debug!("{self} is not active anymore, shutting down handle",); Ok(false) } else { Ok(true) @@ -184,12 +167,12 @@ impl PeerHandle { tokio::select! { biased; _ = self.shutdown_token.cancelled() => { - log::trace!("PeerHandle: Received shutdown"); + trace!("PeerHandle: Received shutdown"); 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"); + error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); } - log::trace!("PeerHandle: Finished shutdown"); + trace!("PeerHandle: Finished shutdown"); break; } _ = self.timeout_check_interval.next() => { @@ -199,11 +182,11 @@ impl PeerHandle { Err(err) => { match self.remove_peer().await { Ok(true) => { - tracing::debug!("Removed peer due to error {err}"); + debug!("Removed peer due to error {err}"); return; } _ => { - tracing::warn!("Could not remove peer yet, we'll try again later. If this message persists, the gateway might need to be restarted"); + warn!("Could not remove peer yet, we'll try again later. If this message persists, the gateway might need to be restarted"); continue; } } diff --git a/common/wireguard/src/peer_storage_manager.rs b/common/wireguard/src/peer_storage_manager.rs index 1675cf6b2f8..00c439350de 100644 --- a/common/wireguard/src/peer_storage_manager.rs +++ b/common/wireguard/src/peer_storage_manager.rs @@ -46,7 +46,7 @@ impl CachedPeerManager { pub(crate) fn update(&mut self, kernel_peer: PeerInformation) { if let Some(peer_information) = self.peer_information.as_mut() { - peer_information.update_trx_bytes(kernel_peer); + peer_information.update_tx_rx_bytes(kernel_peer); } } } @@ -67,8 +67,49 @@ impl From<&Peer> for PeerInformation { } impl PeerInformation { - pub(crate) fn update_trx_bytes(&mut self, peer: PeerInformation) { + pub(crate) fn update_tx_rx_bytes(&mut self, peer: PeerInformation) { self.tx_bytes = peer.tx_bytes; self.rx_bytes = peer.rx_bytes; } + + fn rx_tx_total(&self, typ: &'static str) -> Option { + self.rx_bytes.checked_add(self.tx_bytes).or_else(|| { + tracing::error!( + "overflow on {typ} adding bytes: {} + {}", + self.rx_bytes, + self.tx_bytes + ); + None + }) + } + + /// Attempt to determine the amount of consumed bandwidth based on the current peer information + /// and state from the last checkpoint. + pub(crate) fn consumed_bandwidth(kernel: &Self, previous_cached: &Self) -> Option { + let kernel_total = kernel.rx_tx_total("kernel")?; + let cached_total = previous_cached.rx_tx_total("cached")?; + kernel_total.checked_sub(cached_total).or_else(|| { + tracing::error!("Overflow on spent bandwidth subtraction: kernel - cached = {kernel_total} - {cached_total}"); + None + }) + } + + /// Attempt to determine the amount of consumed bandwidth based on the current peer information + /// and state from the last checkpoint. + /// On failures, it will attempt to default to most sensible alternative + /// + /// Note, it is responsibility of the caller to ensure that `self` corresponds to the kernel peer information + pub(crate) fn consumed_kernel_bandwidth(&self, previous_cached: &Self) -> i64 { + let Some(consumed) = Self::consumed_bandwidth(self, previous_cached) else { + // old behaviour of returning the `Default::default()` + return 0; + }; + + // old behaviour would have also returned 0 here, but I'd argue if u64 can't fit in i64, + // it means we're over i64::MAX, thus that's what we should return + consumed + .try_into() + .inspect_err(|err| tracing::error!("Could not convert from u64 to i64: {err:?}")) + .unwrap_or(i64::MAX) + } } diff --git a/envs/canary.env b/envs/canary.env index f494ad32707..fbf97b26434 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -22,3 +22,5 @@ NYXD=https://rpc.canary-validator.performance.nymte.ch NYM_API=https://canary-api.performance.nymte.ch/api/ NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket NYM_VPN_API=https://nym-vpn-api-git-deploy-canary-nyx-network-staging.vercel.app/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=U1NXToPYUTsh7pYPLcwXCXwcL6pGoLUou7fyAJrNz8b \ No newline at end of file diff --git a/envs/mainnet.env b/envs/mainnet.env index 444307f836c..01e5e9092bc 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -25,3 +25,5 @@ NYXD=https://rpc.nymtech.net NYM_API=https://validator.nymtech.net/api/ NYXD_WS=wss://rpc.nymtech.net/websocket NYM_VPN_API=https://nymvpn.com/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=3bgffBYcfFkTTXc2npNNn9MkddFZ3H2LrPjXDmnJzrqd \ No newline at end of file diff --git a/envs/sandbox.env b/envs/sandbox.env index e90257f015c..96a46881dd1 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -23,3 +23,5 @@ NYXD=https://rpc.sandbox.nymtech.net NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket NYM_API=https://sandbox-nym-api1.nymtech.net/api/ NYM_VPN_API=https://nym-vpn-api-git-deploy-sandbox-nyx-network-staging.vercel.app/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=EGwzKXPrqStv8cHF68VT2LbQuEBGDPzhCAixScvybfem \ No newline at end of file diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 1e4a5be4175..cf1b8f286b3 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -19,7 +19,6 @@ rust-version = "1.85" path = "src/lib.rs" [dependencies] -anyhow = { workspace = true } bincode = { workspace = true } async-trait = { workspace = true } bip39 = { workspace = true } @@ -30,7 +29,6 @@ futures = { workspace = true } ipnetwork = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } -sha2 = { workspace = true } thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = [ @@ -40,16 +38,14 @@ tokio = { workspace = true, features = [ "fs", "time", ] } -tokio-stream = { workspace = true, features = ["fs"] } +tokio-stream = { workspace = true } tokio-tungstenite = { workspace = true } -tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } url = { workspace = true, features = ["serde"] } zeroize = { workspace = true } # internal -nym-api-requests = { path = "../nym-api/nym-api-requests" } nym-credentials = { path = "../common/credentials" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-credential-verification = { path = "../common/credential-verification" } @@ -58,7 +54,6 @@ nym-gateway-storage = { path = "../common/gateway-storage" } nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" } nym-gateway-requests = { path = "../common/gateway-requests" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } -nym-mixnode-common = { path = "../common/mixnode-common" } nym-network-defaults = { path = "../common/network-defaults" } nym-network-requester = { path = "../service-providers/network-requester" } nym-sdk = { path = "../sdk/rust/nym-sdk" } @@ -66,10 +61,10 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-statistics-common = { path = "../common/statistics" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } nym-node-metrics = { path = "../nym-node/nym-node-metrics" } +nym-upgrade-mode-check = { path = "../common/upgrade-mode-check" } nym-wireguard = { path = "../common/wireguard" } nym-wireguard-private-metadata-server = { path = "../common/wireguard-private-metadata/server" } @@ -80,7 +75,6 @@ nym-client-core = { path = "../common/client-core", features = ["cli"] } nym-id = { path = "../common/nym-id" } nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" } - defguard_wireguard_rs = { workspace = true } [dev-dependencies] @@ -88,3 +82,6 @@ nym-gateway-storage = { path = "../common/gateway-storage", features = ["mock"] nym-wireguard = { path = "../common/wireguard", features = ["mock"] } mock_instant = "0.6.0" time = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 363ddbb88e5..8df528674b9 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -13,6 +13,8 @@ pub struct Config { pub ip_packet_router: IpPacketRouter, + pub upgrade_mode_watcher: UpgradeModeWatcher, + pub debug: Debug, } @@ -21,12 +23,14 @@ impl Config { gateway: impl Into, network_requester: impl Into, ip_packet_router: impl Into, + upgrade_mode_watcher: impl Into, debug: impl Into, ) -> Self { Config { gateway: gateway.into(), network_requester: network_requester.into(), ip_packet_router: ip_packet_router.into(), + upgrade_mode_watcher: upgrade_mode_watcher.into(), debug: debug.into(), } } @@ -57,6 +61,28 @@ pub struct Gateway { pub nyxd_urls: Vec, } +#[derive(Debug)] +pub struct UpgradeModeWatcher { + /// Specifies whether this gateway watches for upgrade mode changes + /// via the published attestation file. + pub enabled: bool, + + /// Endpoint to query to retrieve current upgrade mode attestation. + /// If not provided, it implicitly disables the watcher and upgrade-mode features + pub attestation_url: Url, + + pub debug: UpgradeModeWatcherDebug, +} + +#[derive(Debug)] +pub struct UpgradeModeWatcherDebug { + /// Default polling interval + pub regular_polling_interval: Duration, + + /// Expedited polling interval for once upgrade mode is detected + pub expedited_poll_interval: Duration, +} + #[derive(Debug, PartialEq)] pub struct NetworkRequester { /// Specifies whether network requester service is enabled in this process. @@ -104,6 +130,9 @@ pub struct Debug { /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. pub max_request_timestamp_skew: Duration, + + /// The minimum duration since the last explicit check for the upgrade mode to allow creation of new requests. + pub upgrade_mode_min_staleness_recheck: Duration, } #[derive(Debug, Clone)] diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index a215affdf9c..8f577ba13f1 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -147,7 +147,7 @@ impl ActiveClientsStore { handle: MixMessageSender, is_active_request_sender: IsActiveRequestSender, session_request_timestamp: OffsetDateTime, - ) { + ) -> bool { let entry = ActiveClient::Remote(RemoteClientData { session_request_timestamp, channels: ClientIncomingChannels { @@ -156,11 +156,16 @@ impl ActiveClientsStore { }, }); if self.inner.insert(client, entry).is_some() { - panic!("inserted a duplicate remote client") + // this should be impossible under normal circumstances, + // but in some rare edge cases of clients performing very careful timing attacks, + // this branch could be potentially triggered + return false; } + true } /// Inserts a handle to the embedded client + #[allow(clippy::panic)] pub fn insert_embedded(&self, local_client_handle: LocalEmbeddedClientHandle) { let key = local_client_handle.client_destination(); let entry = ActiveClient::Embedded(Box::new(local_client_handle)); diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index 7f4d66c828e..f3e9f711fad 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::ActiveClientsStore; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; use nym_credential_verification::{ecash::EcashManager, BandwidthFlushingBehaviourConfig}; use nym_crypto::asymmetric::ed25519; use nym_gateway_storage::GatewayStorage; @@ -11,7 +12,7 @@ use nym_node_metrics::NymNodeMetrics; use std::sync::Arc; use std::time::Duration; -#[derive(Clone)] +#[derive(Clone, Copy)] pub(crate) struct Config { pub(crate) enforce_zk_nym: bool, pub(crate) max_request_timestamp_skew: Duration, @@ -29,6 +30,7 @@ pub(crate) struct CommonHandlerState { pub(crate) metrics_sender: MetricEventsSender, pub(crate) outbound_mix_sender: MixForwardingSender, pub(crate) active_clients_store: ActiveClientsStore, + pub(crate) upgrade_mode: UpgradeModeDetails, } impl CommonHandlerState { diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 36b80293320..b40642930a8 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -14,14 +14,16 @@ use futures::{ future::{FusedFuture, OptionFuture}, FutureExt, StreamExt, }; +use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_credential_verification::CredentialVerifier; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ClientBandwidth, }; +use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; use nym_gateway_requests::{ types::{BinaryRequest, ServerResponse}, - ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse, - SimpleGatewayRequestsError, + BandwidthResponse, ClientControlRequest, ClientRequest, GatewayRequestsError, SendResponse, + SensitiveServerResponse, SimpleGatewayRequestsError, }; use nym_gateway_storage::error::GatewayStorageError; use nym_gateway_storage::traits::BandwidthGatewayStorage; @@ -31,6 +33,7 @@ use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::{gateways::GatewaySessionEvent, types::SessionType}; use nym_validator_client::coconut::EcashApiError; use rand::{random, CryptoRng, Rng}; +use std::cmp::max; use std::{process, time::Duration}; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; @@ -92,8 +95,11 @@ pub enum RequestHandlingError { #[error("failed to recover bandwidth value: {0}")] BandwidthRecoveryFailure(#[from] BandwidthError), - #[error("{0}")] + #[error(transparent)] CredentialVerification(#[from] nym_credential_verification::Error), + + #[error(transparent)] + UpgradeModeEnable(#[from] UpgradeModeEnableError), } impl RequestHandlingError { @@ -161,6 +167,10 @@ impl AuthenticatedHandler { &self.inner } + fn upgrade_mode_enabled(&self) -> bool { + self.inner.upgrade_mode_enabled() + } + /// Upgrades `FreshHandler` into the Authenticated variant implying the client is now authenticated /// and thus allowed to perform more actions with the gateway, such as redeeming bandwidth or /// sending sphinx packets. @@ -271,7 +281,50 @@ impl AuthenticatedHandler { .inspect_err(|verification_failure| debug!("{verification_failure}"))?; trace!("available total bandwidth: {available_total}"); - Ok(ServerResponse::Bandwidth { available_total }) + Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total, + upgrade_mode: self.upgrade_mode_enabled(), + })) + } + + async fn upgrade_mode_bandwidth(&self) -> i64 { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + let available_bandwidth = self.bandwidth_storage_manager.available_bandwidth().await; + max( + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD + 1, + available_bandwidth, + ) + } + + /// Tries to handle the received JWT token request by checking its correctness and + /// internally enables upgrade mode if it hasn't been set before. + /// Furthermore, clients bandwidth metering is getting disabled. + async fn handle_upgrade_mode_jwt( + &self, + token: String, + ) -> Result { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode_enabled() { + return Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total: self.upgrade_mode_bandwidth().await, + upgrade_mode: true, + })); + } + + self.inner + .shared_state + .upgrade_mode + .try_enable_via_received_jwt(token) + .await?; + + Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total: self.upgrade_mode_bandwidth().await, + upgrade_mode: true, + })) } /// Tries to handle request to forward sphinx packet into the network. The request can only succeed @@ -289,15 +342,22 @@ impl AuthenticatedHandler { ) -> Result { let required_bandwidth = mix_packet.packet().len() as i64; - let remaining_bandwidth = self - .bandwidth_storage_manager - .try_use_bandwidth(required_bandwidth) - .await?; + let upgrade_mode = self.upgrade_mode_enabled(); + + let remaining_bandwidth = if self.upgrade_mode_enabled() { + self.upgrade_mode_bandwidth().await + } else { + self.bandwidth_storage_manager + .try_use_bandwidth(required_bandwidth) + .await? + }; + self.forward_packet(mix_packet); - Ok(ServerResponse::Send { + Ok(ServerResponse::Send(SendResponse { remaining_bandwidth, - }) + upgrade_mode, + })) } /// Attempts to handle a binary data frame websocket message. @@ -432,6 +492,9 @@ impl AuthenticatedHandler { ClientControlRequest::EcashCredential { enc_credential, iv } => { self.handle_ecash_bandwidth(enc_credential, iv).await } + ClientControlRequest::UpgradeModeJWT { token } => { + self.handle_upgrade_mode_jwt(token).await + } ClientControlRequest::BandwidthCredential { .. } => { Err(RequestHandlingError::IllegalRequest { additional_context: "coconut credential are not longer supported".into(), @@ -446,7 +509,13 @@ impl AuthenticatedHandler { .bandwidth_storage_manager .handle_claim_testnet_bandwidth() .await - .map_err(|e| e.into()), + .map_err(|e| e.into()) + .map(|available_total| { + ServerResponse::Bandwidth(BandwidthResponse { + available_total, + upgrade_mode: self.upgrade_mode_enabled(), + }) + }), ClientControlRequest::SupportedProtocol { .. } => { Ok(self.inner.handle_supported_protocol_request()) } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 54d6e02f6e0..0eb18a602ad 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -20,11 +20,12 @@ use nym_gateway_requests::authenticate::AuthenticateRequest; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; +use nym_gateway_requests::registration::handshake::HandshakeResult; use nym_gateway_requests::{ registration::handshake::{error::HandshakeError, gateway_handshake}, types::{ClientControlRequest, ServerResponse}, - AuthenticationFailure, BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, - INITIAL_PROTOCOL_VERSION, + AuthenticationFailure, BinaryResponse, GatewayProtocolVersion, GatewayProtocolVersionExt, + SharedGatewayKey, CURRENT_PROTOCOL_VERSION, }; use nym_gateway_storage::error::GatewayStorageError; use nym_gateway_storage::traits::BandwidthGatewayStorage; @@ -88,9 +89,6 @@ pub(crate) enum InitialAuthenticationError { #[error("Experienced connection error: {0}")] ConnectionError(Box), - #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")] - IncompatibleProtocol { client: Option, current: u8 }, - #[error("failed to send authentication response: {source}")] ResponseSendFailure { #[source] @@ -130,7 +128,7 @@ pub(crate) struct FreshHandler { pub(crate) shutdown: ShutdownToken, // currently unused (but populated) - pub(crate) negotiated_protocol: Option, + pub(crate) negotiated_protocol: Option, } impl FreshHandler { @@ -138,6 +136,10 @@ impl FreshHandler { &self.shared_state } + pub(crate) fn upgrade_mode_enabled(&self) -> bool { + self.shared_state.upgrade_mode.enabled() + } + // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult pub(crate) fn new( @@ -189,7 +191,8 @@ impl FreshHandler { async fn perform_registration_handshake( &mut self, init_msg: Vec, - ) -> Result + requested_protocol: Option, + ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, @@ -202,15 +205,17 @@ impl FreshHandler { ws_stream, self.shared_state.local_identity.as_ref(), init_msg, + requested_protocol, self.shutdown.clone(), ) .await } - _ => unreachable!(), + _ => Err(HandshakeError::ConnectionInInvalidState), } } /// Attempts to read websocket message from the associated socket. + #[allow(clippy::panic)] pub(crate) async fn read_websocket_message(&mut self) -> Option> where S: AsyncRead + AsyncWrite + Unpin, @@ -226,6 +231,7 @@ impl FreshHandler { /// # Arguments /// /// * `msg`: WebSocket message to write back to the client. + #[allow(clippy::panic)] pub(crate) async fn send_websocket_message( &mut self, msg: impl Into, @@ -269,6 +275,7 @@ impl FreshHandler { /// /// * `shared_keys`: keys derived between the client and gateway. /// * `packets`: unwrapped packets that are to be pushed back to the client. + #[allow(clippy::panic)] pub(crate) async fn push_packets_to_client( &mut self, shared_keys: &SharedGatewayKey, @@ -411,59 +418,6 @@ impl FreshHandler { } } - fn negotiate_client_protocol( - &self, - client_protocol: Option, - ) -> Result { - debug!("client protocol: {client_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}"); - let Some(client_protocol_version) = client_protocol else { - warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in +1.2.0 we will have to return a hard error here - return Ok(INITIAL_PROTOCOL_VERSION); - }; - - // ##### - // On backwards compat: - // Currently it is the case that gateways will understand all previous protocol versions - // and will downgrade accordingly, but this will now always be the case. - // For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected - // ##### - - // a v2 gateway will understand v1 requests, but v1 client will not understand v2 responses - if client_protocol_version == 1 { - return Ok(1); - } - - // a v3 gateway will understand v2 requests (legacy keys) - if client_protocol_version == 2 { - return Ok(2); - } - - // a v4 gateway will understand v3 requests (aes256gcm-siv) - if client_protocol_version == 3 { - return Ok(3); - } - - // a v5 gateway will understand v4 requests (key-rotation) - if client_protocol_version == 4 { - return Ok(4); - } - - // we can't handle clients with higher protocol than ours - // (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement) - if client_protocol_version <= CURRENT_PROTOCOL_VERSION { - debug!("the client is using exactly the same (or older) protocol version as we are. We're good to continue!"); - Ok(CURRENT_PROTOCOL_VERSION) - } else { - let err = InitialAuthenticationError::IncompatibleProtocol { - client: client_protocol, - current: CURRENT_PROTOCOL_VERSION, - }; - error!("{err}"); - Err(err) - } - } - async fn handle_duplicate_client( &mut self, address: DestinationAddressBytes, @@ -551,6 +505,29 @@ impl FreshHandler { Ok(available_bandwidth) } + fn negotiate_proposed_protocol( + &self, + client_protocol_version: Option, + ) -> Option { + if client_protocol_version.is_future_version() { + // this should never happen in a non-malicious client as it should use at most whatever version this gateway has announced + warn!("client has announced protocol version greater than one known by this gateway (v{client_protocol_version:?} vs v{}). attempting to downgrade.", GatewayProtocolVersion::CURRENT); + // we just reply with our current version, and it's up to the client to accept it or terminate the connection + Some(GatewayProtocolVersion::CURRENT) + } else { + // ##### + // On backwards compat: + // Currently it is the case that gateways will understand all previous protocol versions + // and will downgrade accordingly, but this will not always be the case. + // For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected + // ##### + debug!( + "using the protocol version proposed by the client: v{client_protocol_version:?}" + ); + client_protocol_version + } + } + /// Tries to handle the received authentication request by checking correctness of the received data. /// /// # Arguments @@ -565,7 +542,7 @@ impl FreshHandler { )] async fn handle_legacy_authenticate( &mut self, - client_protocol_version: Option, + client_protocol_version: Option, address: String, enc_address: String, raw_nonce: String, @@ -575,9 +552,9 @@ impl FreshHandler { { debug!("handling client authentication (v1)"); - let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; + let negotiated_protocol = self.negotiate_proposed_protocol(client_protocol_version); // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); + self.negotiated_protocol = negotiated_protocol; let address = DestinationAddressBytes::try_from_base58_string(address) .map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?; @@ -592,7 +569,7 @@ impl FreshHandler { .await? else { // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour - return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); + return Ok(InitialAuthResult::new_legacy_failed(negotiated_protocol)); }; // in v1 we don't have explicit data so we have to use current timestamp @@ -634,9 +611,10 @@ impl FreshHandler { session_request_start, )), ServerResponse::Authenticate { - protocol_version: Some(negotiated_protocol), + protocol_version: negotiated_protocol, status: true, bandwidth_remaining, + upgrade_mode: self.upgrade_mode_enabled(), }, )) } @@ -651,9 +629,9 @@ impl FreshHandler { debug!("handling client authentication (v2)"); let negotiated_protocol = - self.negotiate_client_protocol(Some(request.content.protocol_version))?; + self.negotiate_proposed_protocol(Some(request.content.protocol_version)); // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); + self.negotiated_protocol = negotiated_protocol; let address = request.content.client_identity.derive_destination_address(); @@ -720,9 +698,10 @@ impl FreshHandler { session_request_start, )), ServerResponse::Authenticate { - protocol_version: Some(negotiated_protocol), + protocol_version: negotiated_protocol, status: true, bandwidth_remaining, + upgrade_mode: self.upgrade_mode_enabled(), }, )) } @@ -782,17 +761,13 @@ impl FreshHandler { /// * `init_data`: init payload of the registration handshake. async fn handle_register( &mut self, - client_protocol_version: Option, + client_protocol_version: Option, init_data: Vec, ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, { - let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; - // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); - let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); @@ -806,11 +781,20 @@ impl FreshHandler { return Err(InitialAuthenticationError::DuplicateConnection); } - let shared_keys = self.perform_registration_handshake(init_data).await?; + let handshake_result = self + .perform_registration_handshake(init_data, client_protocol_version) + .await?; + let shared_keys = handshake_result.derived_key; + + // populate the negotiated protocol for future uses + self.negotiated_protocol = Some(handshake_result.negotiated_protocol); + let client_id = self.register_client(remote_address, &shared_keys).await?; debug!(client_id = %client_id, "managed to finalize client registration"); + let upgrade_mode = self.upgrade_mode_enabled(); + let client_details = ClientDetails::new( client_id, remote_address, @@ -821,8 +805,9 @@ impl FreshHandler { Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { - protocol_version: Some(negotiated_protocol), + protocol_version: self.negotiated_protocol, status: true, + upgrade_mode, }, )) } @@ -946,12 +931,15 @@ impl FreshHandler { let (mix_sender, mix_receiver) = mpsc::unbounded(); // Channel for handlers to ask other handlers if they are still active. let (is_active_request_sender, is_active_request_receiver) = mpsc::unbounded(); - self.shared_state.active_clients_store.insert_remote( + if !self.shared_state.active_clients_store.insert_remote( registration_details.address, mix_sender, is_active_request_sender, registration_details.session_request_timestamp, - ); + ) { + error!("failed to insert remote client handle as it already existed!"); + return None; + } return AuthenticatedHandler::upgrade( self, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index 2fd97600a70..b8ecf12ec85 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -79,13 +79,16 @@ impl InitialAuthResult { } } - fn new_failed(protocol_version: Option) -> Self { + fn new_legacy_failed(protocol_version: Option) -> Self { InitialAuthResult { client_details: None, server_response: ServerResponse::Authenticate { protocol_version, status: false, bandwidth_remaining: 0, + // given this response is given only to legacy clients, + // we use the default value as clients wouldn't deserialise it anyway + upgrade_mode: false, }, } } diff --git a/gateway/src/node/internal_service_providers/authenticator/error.rs b/gateway/src/node/internal_service_providers/authenticator/error.rs index ac9b4fd2614..5bdde159194 100644 --- a/gateway/src/node/internal_service_providers/authenticator/error.rs +++ b/gateway/src/node/internal_service_providers/authenticator/error.rs @@ -3,7 +3,9 @@ use ipnetwork::IpNetworkError; use nym_client_core::error::ClientCoreError; +use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_id::NymIdError; +use nym_service_provider_requests_common::ProtocolError; #[derive(thiserror::Error, Debug)] pub enum AuthenticatorError { @@ -17,9 +19,6 @@ pub enum AuthenticatorError { #[error("{0}")] CredentialVerificationError(#[from] nym_credential_verification::Error), - #[error("invalid credential type")] - InvalidCredentialType, - #[error("the entity wrapping the network requester has disconnected")] DisconnectedParent, @@ -50,6 +49,12 @@ pub enum AuthenticatorError { #[error("internal error: {0}")] InternalError(String), + #[error(transparent)] + InvalidPacketHeader { + #[from] + source: ProtocolError, + }, + #[error("received packet has an invalid type: {0}")] InvalidPacketType(u8), @@ -100,4 +105,15 @@ pub enum AuthenticatorError { #[error("no credential received")] NoCredentialReceived, + + #[error(transparent)] + UpgradeModeEnable(#[from] UpgradeModeEnableError), +} + +impl AuthenticatorError { + pub fn response_serialisation(source: impl Into>) -> Self { + AuthenticatorError::FailedToSerializeResponsePacket { + source: source.into(), + } + } } 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 af476bfba78..c05d9d8cd6f 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs @@ -1,12 +1,6 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::{ - net::IpAddr, - sync::Arc, - time::{Duration, SystemTime}, -}; - use crate::node::internal_service_providers::authenticator::{ config::Config, error::AuthenticatorError, peer_manager::PeerManager, seen_credential_cache::SeenCredentialCache, @@ -14,47 +8,58 @@ use crate::node::internal_service_providers::authenticator::{ use defguard_wireguard_rs::net::IpAddrMask; use defguard_wireguard_rs::{host::Peer, key::Key}; use futures::StreamExt; -use nym_authenticator_requests::{ - latest::registration::RegistrationData, v4::registration::IpPair, -}; +use nym_authenticator_requests::models::BandwidthClaim; +use nym_authenticator_requests::traits::UpgradeModeMessage; +use nym_authenticator_requests::{latest, v4::registration::IpPair}; use nym_authenticator_requests::{ latest::registration::{GatewayClient, PendingRegistrations, PrivateIPs}, request::AuthenticatorRequest, traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage}, - v1, v2, v3, v4, v5, AuthenticatorVersion, CURRENT_VERSION, + v1, v2, v3, v4, v5, v6, AuthenticatorVersion, CURRENT_VERSION, }; use nym_credential_verification::ecash::traits::EcashManager; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, }; -use nym_credentials_interface::{CredentialSpendingData, TicketType}; +use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData}; use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::models::PersistedBandwidth; use nym_sdk::mixnet::{ AnonymousSenderTag, InputMessage, MixnetMessageSender, Recipient, TransmissionLane, }; -use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::ShutdownToken; use nym_wireguard::WireguardGatewayData; use nym_wireguard_types::PeerPublicKey; use rand::{prelude::IteratorRandom, thread_rng}; +use std::cmp::max; +use std::{ + net::IpAddr, + sync::Arc, + time::{Duration, SystemTime}, +}; use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; type AuthenticatorHandleResult = Result<(Vec, Option), AuthenticatorError>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute -pub(crate) struct RegistredAndFree { +// we need to be above MINIMUM_REMAINING_BANDWIDTH (500MB) plus we also have to trick the client +// its depletion is low enough to not require sending new tickets +const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024; + +pub(crate) struct RegisteredAndFree { registration_in_progres: PendingRegistrations, free_private_network_ips: PrivateIPs, } -impl RegistredAndFree { +impl RegisteredAndFree { pub(crate) fn new(free_private_network_ips: PrivateIPs) -> Self { - RegistredAndFree { + RegisteredAndFree { registration_in_progres: Default::default(), free_private_network_ips, } @@ -69,10 +74,12 @@ pub(crate) struct MixnetListener { pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient, // Registrations awaiting confirmation - pub(crate) registred_and_free: RwLock, + pub(crate) registered_and_free: RwLock, pub(crate) peer_manager: PeerManager, + pub(crate) upgrade_mode: UpgradeModeDetails, + pub(crate) ecash_verifier: Arc, pub(crate) timeout_check_interval: IntervalStream, @@ -86,6 +93,7 @@ impl MixnetListener { free_private_network_ips: PrivateIPs, wireguard_gateway_data: WireguardGatewayData, mixnet_client: nym_sdk::mixnet::MixnetClient, + upgrade_mode: UpgradeModeDetails, ecash_verifier: Arc, ) -> Self { let timeout_check_interval = @@ -93,27 +101,45 @@ impl MixnetListener { MixnetListener { config, mixnet_client, - registred_and_free: RwLock::new(RegistredAndFree::new(free_private_network_ips)), + registered_and_free: RwLock::new(RegisteredAndFree::new(free_private_network_ips)), peer_manager: PeerManager::new(wireguard_gateway_data), + upgrade_mode, ecash_verifier, timeout_check_interval, seen_credential_cache: SeenCredentialCache::new(), } } + fn upgrade_mode_enabled(&self) -> bool { + self.upgrade_mode.enabled() + } + fn keypair(&self) -> &Arc { self.peer_manager.wireguard_gateway_data.keypair() } + async fn upgrade_mode_bandwidth(&self, peer: PeerPublicKey) -> Result { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket (hopefully) + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + let available_bandwidth = self.peer_manager.query_bandwidth(peer).await?; + Ok(max( + DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD, + available_bandwidth, + )) + } + async fn remove_stale_registrations(&self) -> Result<(), AuthenticatorError> { - let mut registred_and_free = self.registred_and_free.write().await; - let registred_values: Vec<_> = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + let registered_values: Vec<_> = registered_and_free .registration_in_progres .values() .cloned() .collect(); - for reg in registred_values { - let ip = registred_and_free + for reg in registered_values { + let ip = registered_and_free .free_private_network_ips .get_mut(®.gateway_data.private_ips) .ok_or(AuthenticatorError::InternalDataCorruption(format!( @@ -122,7 +148,7 @@ impl MixnetListener { )))?; let Some(timestamp) = ip else { - registred_and_free + registered_and_free .registration_in_progres .remove(®.gateway_data.pub_key()); tracing::debug!( @@ -138,7 +164,7 @@ impl MixnetListener { })?; if duration > DEFAULT_REGISTRATION_TIMEOUT_CHECK { *ip = None; - registred_and_free + registered_and_free .registration_in_progres .remove(®.gateway_data.pub_key()); tracing::debug!( @@ -159,8 +185,8 @@ impl MixnetListener { ) -> AuthenticatorHandleResult { let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); - let mut registred_and_free = self.registred_and_free.write().await; - if let Some(registration_data) = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + if let Some(registration_data) = registered_and_free .registration_in_progres .get(&remote_public) { @@ -181,9 +207,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_pending_registration_success( @@ -201,9 +225,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_pending_registration_success( @@ -221,38 +243,49 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.clone().into(), + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + gateway_data: v5::registration::GatewayClient::from( + registration_data.gateway_data.clone(), + ) + .into(), wg_port: registration_data.wg_port, }, request_id, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_pending_registration_success( v5::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone().into(), + wg_port: registration_data.wg_port, + }, + request_id, + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_pending_registration_success( + v6::registration::RegistrationData { nonce: registration_data.nonce, gateway_data: registration_data.gateway_data.clone(), wg_port: registration_data.wg_port, }, request_id, + self.upgrade_mode_enabled(), ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; @@ -265,7 +298,7 @@ impl MixnetListener { .allowed_ips .iter() .find_map(|ip_mask| match ip_mask.ip { - std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + IpAddr::V4(ipv4_addr) => Some(ipv4_addr), _ => None, }) .ok_or(AuthenticatorError::InternalError( @@ -275,14 +308,14 @@ impl MixnetListener { .allowed_ips .iter() .find_map(|ip_mask| match ip_mask.ip { - std::net::IpAddr::V6(ipv6_addr) => Some(ipv6_addr), + IpAddr::V6(ipv6_addr) => Some(ipv6_addr), _ => None, }) .unwrap_or(IpPair::from(IpAddr::from(allowed_ipv4)).ipv6); let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( - v1::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v1::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -290,12 +323,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( - v2::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v2::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -303,12 +334,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( - v3::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v3::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -316,12 +345,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( - v4::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v4::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -329,27 +356,34 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( - v5::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v5::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.tunnel_announced_port, }, request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered( + v6::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), + private_ips: (allowed_ipv4, allowed_ipv6).into(), + wg_port: self.config.authenticator.tunnel_announced_port, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; return Ok((bytes, reply_to)); } - let private_ip_ref = registred_and_free + let private_ip_ref = registered_and_free .free_private_network_ips .iter_mut() .filter(|r| r.1.is_none()) @@ -364,12 +398,12 @@ impl MixnetListener { *private_ip_ref.0, nonce, ); - let registration_data = RegistrationData { + let registration_data = latest::registration::RegistrationData { nonce, gateway_data: gateway_data.clone(), wg_port: self.config.authenticator.tunnel_announced_port, }; - registred_and_free + registered_and_free .registration_in_progres .insert(remote_public, registration_data.clone()); let bytes = match AuthenticatorVersion::from(protocol) { @@ -389,9 +423,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_pending_registration_success( @@ -409,9 +441,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_pending_registration_success( @@ -429,38 +459,49 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.into(), + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + gateway_data: v5::registration::GatewayClient::from( + registration_data.gateway_data.clone(), + ) + .into(), wg_port: registration_data.wg_port, }, request_id, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_pending_registration_success( v5::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.into(), + wg_port: registration_data.wg_port, + }, + request_id, + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_pending_registration_success( + v6::registration::RegistrationData { nonce: registration_data.nonce, gateway_data: registration_data.gateway_data, wg_port: registration_data.wg_port, }, request_id, + self.upgrade_mode_enabled(), ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; @@ -468,6 +509,29 @@ impl MixnetListener { Ok((bytes, reply_to)) } + async fn handle_final_credential_claim( + &self, + claim: BandwidthClaim, + client_id: i64, + ) -> Result<(), AuthenticatorError> { + match claim.credential { + BandwidthCredential::ZkNym(zk_nym) => { + // if we got zk-nym, we just try to verify it + credential_verification(self.ecash_verifier.clone(), *zk_nym, client_id).await?; + Ok(()) + } + BandwidthCredential::UpgradeModeJWT { token } => { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode_enabled() { + return Ok(()); + } + + self.upgrade_mode.try_enable_via_received_jwt(token).await?; + Ok(()) + } + } + } + async fn on_final_request( &mut self, final_message: Box, @@ -475,8 +539,8 @@ impl MixnetListener { request_id: u64, reply_to: Option, ) -> AuthenticatorHandleResult { - let mut registred_and_free = self.registred_and_free.write().await; - let registration_data = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + let registration_data = registered_and_free .registration_in_progres .get(&final_message.gateway_client_pub_key()) .ok_or(AuthenticatorError::RegistrationNotInProgress)? @@ -497,28 +561,31 @@ impl MixnetListener { 128, )); + // ideally credential wouldn't have been required in upgrade mode, + // however, we need some basic information to insert valid wg peer let Some(credential) = final_message.credential() else { return Err(AuthenticatorError::NoCredentialReceived); }; + + let typ = credential.kind; + let client_id = self .ecash_verifier .storage() - .insert_wireguard_peer( - &peer, - TicketType::try_from_encoded(credential.payment.t_type) - .map_err(|_| AuthenticatorError::InvalidCredentialType)? - .into(), - ) + .insert_wireguard_peer(&peer, typ.into()) .await?; - if let Err(e) = - credential_verification(self.ecash_verifier.clone(), credential, client_id).await + + if let Err(err) = self + .handle_final_credential_claim(credential, client_id) + .await { self.ecash_verifier .storage() .remove_wireguard_peer(&peer.public_key.to_string()) .await?; - return Err(e); + return Err(err); } + let public_key = peer.public_key.to_string(); if let Err(e) = self.peer_manager.add_peer(peer).await { self.ecash_verifier @@ -528,13 +595,13 @@ impl MixnetListener { return Err(e); } - registred_and_free + registered_and_free .registration_in_progres .remove(&final_message.gateway_client_pub_key()); let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( - v1::registration::RegistredData { + v1::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -543,9 +610,9 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( - v2::registration::RegistredData { + v2::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -554,9 +621,9 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( - v3::registration::RegistredData { + v3::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -565,28 +632,43 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( - v4::registration::RegistredData { + v4::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, - private_ips: registration_data.gateway_data.private_ips.into(), + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + private_ips: v5::registration::IpPair::from( + registration_data.gateway_data.private_ips, + ) + .into(), wg_port: registration_data.wg_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( - v5::registration::RegistredData { + v5::registration::RegisteredData { + pub_key: registration_data.gateway_data.pub_key, + private_ips: registration_data.gateway_data.private_ips.into(), + wg_port: registration_data.wg_port, + }, + request_id, + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered( + v6::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ips: registration_data.gateway_data.private_ips, wg_port: registration_data.wg_port, }, request_id, + self.upgrade_mode_enabled(), ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; Ok((bytes, reply_to)) @@ -599,7 +681,12 @@ impl MixnetListener { request_id: u64, reply_to: Option, ) -> AuthenticatorHandleResult { - let available_bandwidth = self.peer_manager.query_bandwidth(msg.pub_key()).await?; + let available_bandwidth = if self.upgrade_mode_enabled() { + self.upgrade_mode_bandwidth(msg.pub_key()).await? + } else { + self.peer_manager.query_bandwidth(msg.pub_key()).await? + }; + let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => { v1::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -611,9 +698,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -624,9 +709,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -637,9 +720,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -650,9 +731,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -662,15 +741,25 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_remaining_bandwidth( + Some(v6::registration::RemainingBandwidthData { + available_bandwidth, + }), + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; Ok((bytes, reply_to)) } + // if we received a topup request, don't do anything with the upgrade mode async fn on_topup_bandwidth_request( &mut self, msg: Box, @@ -693,6 +782,15 @@ impl MixnetListener { }; let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_topup_bandwidth( + v6::registration::RemainingBandwidthData { + available_bandwidth, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth( v5::registration::RemainingBandwidthData { available_bandwidth, @@ -700,7 +798,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_topup_bandwidth( v4::registration::RemainingBandwidthData { available_bandwidth, @@ -709,7 +807,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_topup_bandwidth( v3::registration::RemainingBandwidthData { available_bandwidth, @@ -718,7 +816,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { return Err(AuthenticatorError::UnknownVersion) } @@ -727,6 +825,46 @@ impl MixnetListener { Ok((bytes, reply_to)) } + async fn on_upgrade_mode_check( + &mut self, + msg: Box, + protocol: Protocol, + request_id: u64, + ) -> AuthenticatorHandleResult { + // if upgrade mode is already enabled, we don't need to perform any additional checks + if !self.upgrade_mode_enabled() { + // currently upgrade mode JWT is the only type of emergency credentials supported + if let Some(upgrade_mode_jwt) = msg.upgrade_mode_global_attestation_jwt() { + self.upgrade_mode + .try_enable_via_received_jwt(upgrade_mode_jwt) + .await?; + } + } + + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::UNKNOWN + | AuthenticatorVersion::V1 + | AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 => { + // pre v6 this message hasn't existed + return Err(AuthenticatorError::UnknownVersion); + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_upgrade_mode_check( + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? + } + }; + + // no need to support reply_to, as this is never set in v6 and older versions do not include this message + Ok((bytes, None)) + } + fn received_retry(&self, msg: &(dyn TopUpMessage + Send + Sync + 'static)) -> bool { if let Some(peer_pub_key) = self .seen_credential_cache @@ -787,6 +925,11 @@ impl MixnetListener { self.on_topup_bandwidth_request(msg, protocol, request_id, reply_to) .await } + AuthenticatorRequest::CheckUpgradeMode { + msg, + protocol, + request_id, + } => self.on_upgrade_mode_check(msg, protocol, request_id).await, } } @@ -893,64 +1036,68 @@ async fn credential_verification( fn deserialize_request( reconstructed: &ReconstructedMessage, ) -> Result { - let request_version = *reconstructed + let header = reconstructed .message .first_chunk::<2>() .ok_or(AuthenticatorError::ShortPacket)?; - // Check version of the request and convert to the latest version if necessary - match request_version { - [1, _] => v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + let version = header[0]; + + // special case for v1 request where service provider information hasn't been exposed in the header + if version == v1::VERSION { + return v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) - .map(Into::into), - [2, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v2::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::::into) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + .map(Into::into); + } + + let protocol = Protocol::try_from(header)?; + + if !protocol.service_provider_type.is_authenticator() { + return Err(AuthenticatorError::InvalidPacketType( + protocol.service_provider_type as u8, + )); + } + + let version = AuthenticatorVersion::from(protocol.version); + + // Check version of the request and convert to the latest version if necessary + match version { + AuthenticatorVersion::V1 => { + // this branch should be unreachable as v1 has already been handled independently + Err(AuthenticatorError::UnknownVersion) } - [3, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v3::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V2 => { + v2::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::::into) + .map(Into::into) } - [4, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v4::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V3 => { + v3::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) } - [5, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V4 => { + v4::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) + } + AuthenticatorVersion::V5 => { + v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) + } + AuthenticatorVersion::V6 => { + v6::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) } - [version, _] => { - tracing::info!("Received packet with invalid version: v{version}"); - Err(AuthenticatorError::InvalidPacketVersion(version)) + AuthenticatorVersion::UNKNOWN => { + tracing::info!( + "Received packet with invalid version: v{}", + protocol.version + ); + Err(AuthenticatorError::InvalidPacketVersion(protocol.version)) } } } diff --git a/gateway/src/node/internal_service_providers/authenticator/mod.rs b/gateway/src/node/internal_service_providers/authenticator/mod.rs index 358bc5cb30c..f63a86fcc2d 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mod.rs @@ -11,6 +11,9 @@ use nym_task::ShutdownTracker; use nym_wireguard::WireguardGatewayData; use std::{net::IpAddr, path::Path, sync::Arc, time::SystemTime}; +pub use config::Config; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; + pub mod config; pub mod error; pub mod mixnet_client; @@ -18,8 +21,6 @@ pub mod mixnet_listener; mod peer_manager; mod seen_credential_cache; -pub use config::Config; - pub struct OnStartData { // to add more fields as required pub address: Recipient, @@ -33,7 +34,8 @@ impl OnStartData { pub struct Authenticator { #[allow(unused)] - config: crate::node::internal_service_providers::authenticator::Config, + config: Config, + upgrade_mode_state: UpgradeModeDetails, wait_for_gateway: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, @@ -46,7 +48,8 @@ pub struct Authenticator { impl Authenticator { pub fn new( - config: crate::node::internal_service_providers::authenticator::Config, + config: Config, + upgrade_mode_state: UpgradeModeDetails, wireguard_gateway_data: WireguardGatewayData, used_private_network_ips: Vec, ecash_verifier: Arc, @@ -54,6 +57,7 @@ impl Authenticator { ) -> Self { Self { config, + upgrade_mode_state, wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, @@ -152,6 +156,7 @@ impl Authenticator { free_private_network_ips, self.wireguard_gateway_data, mixnet_client, + self.upgrade_mode_state, self.ecash_verifier, ); 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 fce8cf71746..c057dfa57cc 100644 --- a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs +++ b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs @@ -19,7 +19,7 @@ impl PeerManager { wireguard_gateway_data, } } - pub async fn add_peer(&mut self, peer: Peer) -> Result<(), AuthenticatorError> { + pub async fn add_peer(&self, peer: Peer) -> Result<(), AuthenticatorError> { let (response_tx, response_rx) = oneshot::channel(); let msg = PeerControlRequest::AddPeer { peer, response_tx }; self.wireguard_gateway_data @@ -38,7 +38,7 @@ impl PeerManager { }) } - pub async fn _remove_peer(&mut self, pub_key: PeerPublicKey) -> Result<(), AuthenticatorError> { + pub async fn _remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), AuthenticatorError> { let key = Key::new(pub_key.to_bytes()); let (response_tx, response_rx) = oneshot::channel(); let msg = PeerControlRequest::RemovePeer { key, response_tx }; @@ -61,7 +61,7 @@ impl PeerManager { } pub async fn query_peer( - &mut self, + &self, public_key: PeerPublicKey, ) -> Result, AuthenticatorError> { let key = Key::new(public_key.to_bytes()); @@ -86,7 +86,7 @@ impl PeerManager { } pub async fn query_bandwidth( - &mut self, + &self, public_key: PeerPublicKey, ) -> Result { let client_bandwidth = self.query_client_bandwidth(public_key).await?; @@ -94,7 +94,7 @@ impl PeerManager { } pub async fn query_client_bandwidth( - &mut self, + &self, key: PeerPublicKey, ) -> Result { let key = Key::new(key.to_bytes()); @@ -121,7 +121,7 @@ impl PeerManager { } pub async fn query_verifier_by_key( - &mut self, + &self, key: PeerPublicKey, credential: CredentialSpendingData, ) -> Result, AuthenticatorError> { @@ -243,7 +243,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let peer_manager = PeerManager::new(wireguard_data); let (storage, task_manager) = start_controller( peer_manager.wireguard_gateway_data.peer_tx().clone(), request_rx, diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index e4542b0dd60..7f08a5e52c7 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -1,8 +1,10 @@ // Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::config::Config; use crate::error::GatewayError; use crate::node::client_handling::websocket; +use crate::node::internal_service_providers::authenticator::Authenticator; use crate::node::internal_service_providers::{ authenticator, ExitServiceProviders, ServiceProviderBeingBuilt, SpMessageRouterBuilder, }; @@ -11,6 +13,9 @@ use futures::channel::oneshot; use nym_credential_verification::ecash::{ credential_sender::CredentialHandlerConfig, EcashManager, }; +use nym_credential_verification::upgrade_mode::{ + UpgradeModeCheckConfig, UpgradeModeDetails, UpgradeModeState, +}; use nym_crypto::asymmetric::ed25519; use nym_ip_packet_router::IpPacketRouter; use nym_mixnet_client::forwarder::MixForwardingSender; @@ -30,13 +35,9 @@ use std::sync::Arc; use tracing::*; use zeroize::Zeroizing; -pub(crate) mod client_handling; -pub(crate) mod internal_service_providers; -mod stale_data_cleaner; - -use crate::config::Config; -use crate::node::internal_service_providers::authenticator::Authenticator; +pub use crate::node::upgrade_mode::watcher::UpgradeModeWatcher; pub use client_handling::active_clients::ActiveClientsStore; +pub use nym_credential_verification::upgrade_mode::UpgradeModeCheckRequestSender; pub use nym_gateway_stats_storage::PersistentStatsStorage; pub use nym_gateway_storage::{ error::GatewayStorageError, @@ -45,6 +46,11 @@ pub use nym_gateway_storage::{ }; pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; +pub(crate) mod client_handling; +pub(crate) mod internal_service_providers; +mod stale_data_cleaner; +pub(crate) mod upgrade_mode; + #[derive(Debug, Clone)] pub struct LocalNetworkRequesterOpts { pub config: nym_network_requester::Config, @@ -78,6 +84,8 @@ pub struct GatewayTasksBuilder { // TODO: combine with authenticator, since you have to start both wireguard_data: Option, + user_agent: UserAgent, + /// ed25519 keypair used to assert one's identity. identity_keypair: Arc, @@ -89,6 +97,8 @@ pub struct GatewayTasksBuilder { metrics: NymNodeMetrics, + upgrade_mode_state: UpgradeModeState, + mnemonic: Arc>, shutdown_tracker: ShutdownTracker, @@ -111,6 +121,8 @@ impl GatewayTasksBuilder { metrics_sender: MetricEventsSender, metrics: NymNodeMetrics, mnemonic: Arc>, + user_agent: UserAgent, + upgrade_mode_attester_public_key: ed25519::PublicKey, shutdown_tracker: ShutdownTracker, ) -> GatewayTasksBuilder { GatewayTasksBuilder { @@ -119,11 +131,13 @@ impl GatewayTasksBuilder { ip_packet_router_opts: None, authenticator_opts: None, wireguard_data: None, + user_agent, identity_keypair: identity, storage, mix_packet_sender, metrics_sender, metrics, + upgrade_mode_state: UpgradeModeState::new(upgrade_mode_attester_public_key), mnemonic, shutdown_tracker, ecash_manager: None, @@ -247,6 +261,7 @@ impl GatewayTasksBuilder { pub async fn build_websocket_listener( &mut self, active_clients_store: ActiveClientsStore, + upgrade_mode_common_state: UpgradeModeDetails, ) -> Result { let shared_state = websocket::CommonHandlerState { cfg: websocket::Config { @@ -261,6 +276,7 @@ impl GatewayTasksBuilder { metrics_sender: self.metrics_sender.clone(), outbound_mix_sender: self.mix_packet_sender.clone(), active_clients_store: active_clients_store.clone(), + upgrade_mode: upgrade_mode_common_state, }; Ok(websocket::Listener::new( @@ -407,6 +423,7 @@ impl GatewayTasksBuilder { pub async fn build_wireguard_authenticator( &mut self, + upgrade_mode_common: UpgradeModeDetails, topology_provider: Box, ) -> Result, GatewayError> { let ecash_manager = self.ecash_manager().await?; @@ -431,6 +448,7 @@ impl GatewayTasksBuilder { let mut authenticator_server = Authenticator::new( opts.config.clone(), + upgrade_mode_common, wireguard_data.inner.clone(), used_private_network_ips, ecash_manager, @@ -462,9 +480,47 @@ impl GatewayTasksBuilder { ) } + pub fn build_upgrade_mode_common_state( + &self, + request_checker: UpgradeModeCheckRequestSender, + ) -> UpgradeModeDetails { + UpgradeModeDetails::new( + UpgradeModeCheckConfig { + min_staleness_recheck: self.config.debug.upgrade_mode_min_staleness_recheck, + }, + request_checker, + self.upgrade_mode_state.clone(), + ) + } + + pub fn try_build_upgrade_mode_watcher(&self) -> Option { + if !self.config.upgrade_mode_watcher.enabled { + warn!("upgrade mode watcher is disabled"); + return None; + } + + Some(UpgradeModeWatcher::new( + self.config + .upgrade_mode_watcher + .debug + .regular_polling_interval, + self.config + .upgrade_mode_watcher + .debug + .expedited_poll_interval, + self.config.debug.upgrade_mode_min_staleness_recheck, + self.config.upgrade_mode_watcher.attestation_url.clone(), + self.upgrade_mode_state.clone(), + self.user_agent.clone(), + self.shutdown_tracker.clone_shutdown_token(), + )) + } + #[cfg(not(target_os = "linux"))] + #[allow(clippy::unimplemented)] pub async fn try_start_wireguard( &mut self, + _upgrade_mode_details: UpgradeModeDetails, ) -> Result, Box> { let _ = self.metrics.clone(); let _ = self.shutdown_tracker.clone(); @@ -474,6 +530,7 @@ impl GatewayTasksBuilder { #[cfg(target_os = "linux")] pub async fn try_start_wireguard( &mut self, + upgrade_mode_details: UpgradeModeDetails, ) -> Result< nym_wireguard_private_metadata_server::ShutdownHandles, Box, @@ -497,6 +554,7 @@ impl GatewayTasksBuilder { nym_wireguard_private_metadata_server::PeerControllerTransceiver::new( wireguard_data.inner.peer_tx().clone(), ), + upgrade_mode_details, )); let bind_address = std::net::SocketAddr::new( @@ -508,6 +566,7 @@ impl GatewayTasksBuilder { ecash_manager, self.metrics.clone(), all_peers, + self.upgrade_mode_state.upgrade_mode_status(), self.shutdown_tracker.clone_shutdown_token(), wireguard_data, ) diff --git a/gateway/src/node/upgrade_mode/mod.rs b/gateway/src/node/upgrade_mode/mod.rs new file mode 100644 index 00000000000..c8df59133bb --- /dev/null +++ b/gateway/src/node/upgrade_mode/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod watcher; diff --git a/gateway/src/node/upgrade_mode/watcher.rs b/gateway/src/node/upgrade_mode/watcher.rs new file mode 100644 index 00000000000..c798ddc55b7 --- /dev/null +++ b/gateway/src/node/upgrade_mode/watcher.rs @@ -0,0 +1,146 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::UserAgent; +use futures::channel::mpsc::unbounded; +use futures::StreamExt; +use nym_credential_verification::upgrade_mode::{ + CheckRequest, UpgradeModeCheckRequestReceiver, UpgradeModeCheckRequestSender, UpgradeModeState, +}; +use nym_task::ShutdownToken; +use nym_upgrade_mode_check::attempt_retrieve_attestation; +use std::time::Duration; +use tokio::task::JoinHandle; +use tokio::time::Instant; +use tracing::{debug, error, info, trace}; +use url::Url; + +pub struct UpgradeModeWatcher { + // default polling interval + regular_polling_interval: Duration, + + // expedited polling interval once upgrade mode is detected + expedited_poll_interval: Duration, + + min_staleness_recheck: Duration, + + attestation_url: Url, + + check_request_sender: UpgradeModeCheckRequestSender, + + check_request_receiver: UpgradeModeCheckRequestReceiver, + + upgrade_mode_state: UpgradeModeState, + + user_agent: UserAgent, + + shutdown_token: ShutdownToken, +} + +impl UpgradeModeWatcher { + pub(crate) fn new( + regular_polling_interval: Duration, + expedited_poll_interval: Duration, + min_staleness_recheck: Duration, + attestation_url: Url, + upgrade_mode_state: UpgradeModeState, + user_agent: UserAgent, + shutdown_token: ShutdownToken, + ) -> Self { + let (tx, rx) = unbounded(); + UpgradeModeWatcher { + regular_polling_interval, + expedited_poll_interval, + min_staleness_recheck, + attestation_url, + check_request_sender: UpgradeModeCheckRequestSender::new(tx), + check_request_receiver: rx, + upgrade_mode_state, + user_agent, + shutdown_token, + } + } + + pub fn request_sender(&self) -> UpgradeModeCheckRequestSender { + self.check_request_sender.clone() + } + + async fn try_update_state(&self) { + match attempt_retrieve_attestation( + self.attestation_url.as_str(), + Some(self.user_agent.clone()), + ) + .await + { + Err(err) => { + info!("upgrade mode attestation is not available at this time"); + debug!("retrieval error: {err}") + } + Ok(attestation) => { + self.upgrade_mode_state + .try_set_expected_attestation(attestation) + .await + } + } + } + + fn timer_reset_deadline(&self) -> Instant { + if self.upgrade_mode_state.upgrade_mode_enabled() { + Instant::now() + self.expedited_poll_interval + } else { + Instant::now() + self.regular_polling_interval + } + } + + async fn handle_check_request(&mut self, polled_request: CheckRequest) { + let mut requests = vec![polled_request]; + while let Ok(Some(queued_up)) = self.check_request_receiver.try_next() { + requests.push(queued_up); + } + + if self.upgrade_mode_state.since_last_query() > self.min_staleness_recheck { + self.try_update_state().await; + } + + for request in requests { + request.finalize() + } + } + + async fn run(&mut self) { + info!("starting the update mode watcher"); + + let check_wait = tokio::time::sleep(self.regular_polling_interval); + tokio::pin!(check_wait); + + loop { + tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("UpdateModeWatcher: received shutdown"); + break; + } + polled_request = self.check_request_receiver.next() => { + let Some(request) = polled_request else { + // this should NEVER happen as `UpgradeModeWatcher` itself holds one sender instance + // but just in case, don't blow up + error!("UpgradeModeCheckRequestReceiver is finished even though we still hold one of the senders!"); + break; + }; + self.handle_check_request(request).await + } + + _ = &mut check_wait => { + self.try_update_state().await; + check_wait.as_mut().reset(self.timer_reset_deadline()); + } + } + } + + debug!("UpdateModeWatcher: Exiting"); + } + + pub fn start(mut self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) + } +} diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs index 43598acdd85..142fbe69815 100644 --- a/nym-authenticator-client/src/error.rs +++ b/nym-authenticator-client/src/error.rs @@ -42,12 +42,15 @@ pub enum AuthenticationClientError { #[error("unknown authenticator version number")] UnsupportedAuthenticatorVersion, + + #[error("encountered an internal error")] + InternalError, } #[derive(thiserror::Error, Debug)] pub enum RegistrationError { #[error(transparent)] - NoCredentialSent(AuthenticationClientError), // This intentionnally doesn't use `from` to avoid random ? operator to land here when they shouldn't + NoCredentialSent(AuthenticationClientError), // This intentionally doesn't use `from` to avoid random ? operator to land here when they shouldn't #[error("an error occured after a credential was sent : {source}")] CredentialSent { diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs index ab811ef79d8..10b651b184f 100644 --- a/nym-authenticator-client/src/lib.rs +++ b/nym-authenticator-client/src/lib.rs @@ -12,18 +12,21 @@ use tracing::{debug, error, trace}; use crate::error::Result; use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender}; +use crate::types::{AvailableBandwidthClientResponse, TopUpClientResponse}; +use nym_authenticator_requests::traits::UpgradeModeStatus; use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, - traits::Id, v2, v3, v4, v5, + traits::Id, v2, v3, v4, v5, v6, }; use nym_credentials_interface::{CredentialSpendingData, TicketType}; -use nym_sdk::mixnet::{IncludedSurbs, Recipient}; +use nym_sdk::mixnet::{IncludedSurbs, Recipient, ReconstructedMessage}; use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_wireguard_types::PeerPublicKey; mod error; mod helpers; mod mixnet_listener; +pub mod types; pub use crate::error::{AuthenticationClientError, RegistrationError}; pub use crate::mixnet_listener::{AuthClientMixnetListener, AuthClientMixnetListenerHandle}; @@ -61,6 +64,10 @@ impl AuthenticatorClient { } } + fn peer_public_key(&self) -> PeerPublicKey { + PeerPublicKey::from(self.keypair.public_key().inner()) + } + pub async fn send_and_wait_for_response( &mut self, message: &ClientMessage, @@ -72,7 +79,9 @@ impl AuthenticatorClient { } async fn send_request(&self, message: &ClientMessage) -> Result { - let (data, request_id) = message.bytes(self.our_nym_address)?; + let serialised = message.bytes(self.our_nym_address)?; + let data = serialised.bytes; + let request_id = serialised.request_id; // We use 20 surbs for the connect request because typically the // authenticator mixnet client on the nym-node is configured to have a min @@ -96,6 +105,81 @@ impl AuthenticatorClient { Ok(request_id) } + fn handle_response( + &self, + msg: Arc, + request_id: u64, + ) -> Option> { + let Some(header) = msg.message.first_chunk::<2>() else { + debug!( + "received too short message that couldn't have been from the authenticator while waiting for connect response" + ); + return None; + }; + + let Ok(protocol) = Protocol::try_from(header) else { + debug!( + "received a message not meant to any service provider while waiting for connect response" + ); + return None; + }; + + if !protocol.service_provider_type.is_authenticator() { + debug!("Received non-authenticator message while waiting for connect response"); + return None; + } + // Confirm that the version is correct + let version = AuthenticatorVersion::from(protocol.version); + + // Then we deserialize the message + debug!( + "AuthClient: got message while waiting for connect response with version {version:?}" + ); + let ret: Result = match version { + AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { + return Some(Err( + AuthenticationClientError::UnsupportedAuthenticatorVersion, + )); + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V4 => { + v4::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + }; + let Ok(response) = ret else { + // This is ok, it's likely just one of our self-pings + debug!("Failed to deserialize reconstructed message"); + return None; + }; + + if response.id() == request_id { + debug!("Got response with matching id"); + return Some(Ok(response)); + } + None + } + async fn listen_for_response(&mut self, request_id: u64) -> Result { let timeout = tokio::time::sleep(Duration::from_secs(10)); tokio::pin!(timeout); @@ -111,42 +195,9 @@ impl AuthenticatorClient { return Err(AuthenticationClientError::NoMixnetMessagesReceived); } Ok(msg) => { - let Some(header) = msg.message.first_chunk::<2>() else { - debug!("received too short message that couldn't have been from the authenticator while waiting for connect response"); - continue; - }; - - let Ok(protocol) = Protocol::try_from(header) else { - debug!("received a message not meant to any service provider while waiting for connect response"); - continue; - }; - - if !protocol.service_provider_type.is_authenticator() { - debug!("Received non-authenticator message while waiting for connect response"); - continue; - } - // Confirm that the version is correct - let version = AuthenticatorVersion::from(protocol.version); - - // Then we deserialize the message - debug!("AuthClient: got message while waiting for connect response with version {version:?}"); - let ret: Result = match version { - AuthenticatorVersion::V1 => Err(AuthenticationClientError::UnsupportedVersion), - AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::UNKNOWN => Err(AuthenticationClientError::UnknownVersion), - }; - let Ok(response) = ret else { - // This is ok, it's likely just one of our self-pings - debug!("Failed to deserialize reconstructed message"); - continue; - }; - - if response.id() == request_id { - debug!("Got response with matching id"); - return Ok(response); + match self.handle_response(msg, request_id) { + None => continue, + Some(res) => return res, } } } @@ -160,6 +211,8 @@ impl AuthenticatorClient { ticketbook_type: TicketType, ) -> std::result::Result { debug!("Registering with the wg gateway..."); + let pub_key = self.peer_public_key(); + let init_message = match self.auth_version { AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { return Err(RegistrationError::NoCredentialSent( @@ -167,24 +220,19 @@ impl AuthenticatorClient { )); } AuthenticatorVersion::V2 => { - ClientMessage::Initial(Box::new(v2::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v2::registration::InitMessage { pub_key })) } AuthenticatorVersion::V3 => { - ClientMessage::Initial(Box::new(v3::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v3::registration::InitMessage { pub_key })) } AuthenticatorVersion::V4 => { - ClientMessage::Initial(Box::new(v4::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v4::registration::InitMessage { pub_key })) } AuthenticatorVersion::V5 => { - ClientMessage::Initial(Box::new(v5::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v5::registration::InitMessage { pub_key })) + } + AuthenticatorVersion::V6 => { + ClientMessage::Initial(Box::new(v6::registration::InitMessage { pub_key })) } }; trace!("sending init msg to {}: {:?}", &self.ip_addr, &init_message); @@ -224,65 +272,22 @@ impl AuthenticatorClient { })? .data, ); + let credential = credential + .map(TryInto::try_into) + .transpose() + .inspect_err(|err| error!("failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}")) + .map_err(|_| RegistrationError::CredentialSent { + source: AuthenticationClientError::InternalError, + })?; - let finalized_message = match self.auth_version { - AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { - return Err(RegistrationError::CredentialSent { - source: AuthenticationClientError::UnsupportedAuthenticatorVersion, - }); - } - AuthenticatorVersion::V2 => { - ClientMessage::Final(Box::new(v2::registration::FinalMessage { - gateway_client: v2::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V3 => { - ClientMessage::Final(Box::new(v3::registration::FinalMessage { - gateway_client: v3::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V4 => { - ClientMessage::Final(Box::new(v4::registration::FinalMessage { - gateway_client: v4::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V5 => { - ClientMessage::Final(Box::new(v5::registration::FinalMessage { - gateway_client: v5::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips(), - pending_registration_response.nonce(), - ), - credential, - })) - } - }; - trace!( - "sending final msg to {}: {:?}", - &self.ip_addr, &finalized_message - ); + let finalized_message = pending_registration_response + .finalise_registration(self.keypair.private_key(), credential); + let client_message = ClientMessage::Final(finalized_message); + + trace!("sending final msg to {}: {client_message:?}", &self.ip_addr); let response = self - .send_and_wait_for_response(&finalized_message) + .send_and_wait_for_response(&client_message) .await .map_err(|source| RegistrationError::CredentialSent { source })?; let AuthenticatorResponse::Registered(registered_response) = response else { @@ -316,32 +321,24 @@ impl AuthenticatorClient { } // This is up to the caller to know nothing is ever spent there - pub async fn query_bandwidth(&mut self) -> Result> { + pub async fn query_bandwidth(&mut self) -> Result { + let pub_key = self.peer_public_key(); + let version = self.auth_version; + let query_message = match self.auth_version { - AuthenticatorVersion::V1 => { + AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); } - AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V2, - })), - AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V3, - })), - AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V4, - })), - AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V5, - })), - AuthenticatorVersion::UNKNOWN => { - return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); + AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 + | AuthenticatorVersion::V6 => { + ClientMessage::Query(Box::new(QueryMessageImpl { pub_key, version })) } }; let response = self.send_and_wait_for_response(&query_message).await?; + let current_upgrade_mode_status = response.upgrade_mode_status(); let available_bandwidth = match response { AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { @@ -350,7 +347,10 @@ impl AuthenticatorClient { { available_bandwidth } else { - return Ok(None); + return Ok(AvailableBandwidthClientResponse { + available_bandwidth_bytes: None, + current_upgrade_mode_status, + }); } } _ => return Err(AuthenticationClientError::InvalidGatewayAuthResponse), @@ -371,24 +371,35 @@ impl AuthenticatorClient { "Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon" ); } - Ok(Some(available_bandwidth)) + Ok(AvailableBandwidthClientResponse { + available_bandwidth_bytes: Some(available_bandwidth), + current_upgrade_mode_status, + }) } // Since the caller provides the credential, it knows it is spent - pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result { + pub async fn top_up( + &mut self, + credential: CredentialSpendingData, + ) -> Result { + let pub_key = self.peer_public_key(); let top_up_message = match self.auth_version { AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, credential, })), // NOTE: looks like a bug here using v3. But we're leaving it as is since it's working // and V4 is deprecated in favour of V5 AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, credential, })), AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, + credential, + })), + AuthenticatorVersion::V6 => ClientMessage::TopUp(Box::new(v6::topup::TopUpMessage { + pub_key, credential, })), AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { @@ -396,14 +407,18 @@ impl AuthenticatorClient { } }; let response = self.send_and_wait_for_response(&top_up_message).await?; + let current_upgrade_mode_status = response.upgrade_mode_status(); - let remaining_bandwidth = match response { + let remaining_bandwidth_bytes = match response { AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { top_up_bandwidth_response.available_bandwidth() } _ => return Err(AuthenticationClientError::InvalidGatewayAuthResponse), }; - Ok(remaining_bandwidth) + Ok(TopUpClientResponse { + remaining_bandwidth_bytes, + current_upgrade_mode_status, + }) } } diff --git a/nym-authenticator-client/src/types.rs b/nym-authenticator-client/src/types.rs new file mode 100644 index 00000000000..6ed3188fd02 --- /dev/null +++ b/nym-authenticator-client/src/types.rs @@ -0,0 +1,16 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use nym_authenticator_requests::models::CurrentUpgradeModeStatus; + +#[derive(Debug, Clone, Copy)] +pub struct TopUpClientResponse { + pub remaining_bandwidth_bytes: i64, + pub current_upgrade_mode_status: CurrentUpgradeModeStatus, +} + +#[derive(Debug, Clone, Copy)] +pub struct AvailableBandwidthClientResponse { + pub available_bandwidth_bytes: Option, + pub current_upgrade_mode_status: CurrentUpgradeModeStatus, +} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs index 6ff65c9fda6..54ba8126b5a 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs @@ -5,8 +5,6 @@ pub mod api; pub mod client; mod helpers; -pub const CREDENTIAL_PROXY_JWT_ISSUER: &str = "nym-credential-proxy"; - macro_rules! absolute_route { ( $name:ident, $parent:expr, $suffix:expr ) => { pub fn $name() -> String { diff --git a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs index e0ff97ac050..b544f80792c 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs @@ -21,6 +21,8 @@ pub struct AttestationWatcher { attestation_url: Url, + expected_attester_public_key: ed25519::PublicKey, + jwt_signing_keys: ed25519::KeyPair, jwt_validity: Duration, @@ -32,6 +34,7 @@ impl AttestationWatcher { pub(crate) fn new( regular_polling_interval: Duration, expedited_poll_interval: Duration, + expected_attester_public_key: ed25519::PublicKey, attestation_url: Url, jwt_signing_keys: ed25519::KeyPair, jwt_validity: Duration, @@ -40,6 +43,7 @@ impl AttestationWatcher { regular_polling_interval, expedited_poll_interval, attestation_url, + expected_attester_public_key, jwt_signing_keys, jwt_validity, upgrade_mode_state: UpgradeModeState { @@ -65,7 +69,12 @@ impl AttestationWatcher { } Ok(attestation) => { self.upgrade_mode_state - .update(attestation, &self.jwt_signing_keys, self.jwt_validity) + .update( + attestation, + self.expected_attester_public_key, + &self.jwt_signing_keys, + self.jwt_validity, + ) .await } } @@ -74,7 +83,7 @@ impl AttestationWatcher { pub async fn run_forever(self, cancellation_token: CancellationToken) { info!("starting the attestation watcher task"); - let check_wait = tokio::time::sleep(self.regular_polling_interval); + let check_wait = tokio::time::sleep(Duration::new(0, 0)); tokio::pin!(check_wait); loop { diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index d5c27a7730c..5ab32daf320 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -159,6 +159,10 @@ pub struct UpgradeModeConfig { #[clap(long, env = "NYM_CREDENTIAL_PROXY_ATTESTATION_CHECK_URL")] pub(crate) attestation_check_url: Option, + /// Base58-encoded expected upgrade mode attestation ed25519 public key. + #[clap(long, env = "NYM_CREDENTIAL_PROXY_ATTESTER_PUBKEY")] + pub(crate) attester_pubkey: Option, + /// Default polling interval of the upgrade mode endpoint. #[clap( long, diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index cd061d89626..7044112d791 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -79,6 +79,29 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { } }; + let attester_pubkey = match cli.upgrade_mode.attester_pubkey { + Some(pubkey) => pubkey, + None => { + // argument hasn't been provided and env is not configured + if std::env::var(CONFIGURED).is_err() { + return Err(CredentialProxyError::AttesterPublicKeyNotSet); + } + // argument hasn't been provided and the relevant env value hasn't been set + // (technically this shouldn't be possible) + let Ok(env_key) = std::env::var(var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY) + else { + return Err(CredentialProxyError::AttesterPublicKeyNotSet); + }; + + match env_key.parse() { + Ok(key) => key, + Err(err) => { + return Err(CredentialProxyError::MalformedAttesterPublicKey { source: err }); + } + } + } + }; + let ticketbook_manager = TicketbookManager::new( build_sha_short(), cli.quorum_check_interval, @@ -94,6 +117,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { cli.upgrade_mode.attestation_check_regular_polling_interval, cli.upgrade_mode .attestation_check_expedited_polling_interval, + attester_pubkey, upgrade_mode_attestation_check_url, jwt_signing_keys, cli.upgrade_mode.upgrade_mode_jwt_validity, diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs index 80c94a055f5..eed655d695a 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs @@ -1,14 +1,15 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_credential_proxy_requests::CREDENTIAL_PROXY_JWT_ISSUER; -use nym_credential_proxy_requests::api::v1::ticketbook::models::UpgradeModeAttestation; use nym_crypto::asymmetric::ed25519; -use nym_upgrade_mode_check::generate_jwt_for_upgrade_mode_attestation; +use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, generate_jwt_for_upgrade_mode_attestation, +}; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; use tokio::sync::RwLock; +use tracing::error; #[derive(Debug, Clone)] pub(crate) struct UpgradeModeState { @@ -23,6 +24,7 @@ impl UpgradeModeState { pub(crate) async fn update( &self, retrieved_attestation: Option, + expected_attester_public_key: ed25519::PublicKey, jwt_signing_keys: &ed25519::KeyPair, jwt_validity: Duration, ) { @@ -32,6 +34,14 @@ impl UpgradeModeState { return; }; + if attestation.content.attester_public_key != expected_attester_public_key { + error!( + "the retrieved attestation has been signed with an unexpected key! expected pubkey: {} actual: {}", + expected_attester_public_key, attestation.content.attester_public_key + ); + return; + } + match guard.as_mut() { None => { // no existing state - it's the first time we're going into upgrade mode, diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index cac1225a8c0..76b8c5c567a 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -16,7 +16,7 @@ use futures::StreamExt; use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient}; use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, v2, v3, - v4, v5, + v4, v5, v6, }; use nym_client_core::config::ForgetMe; use nym_config::defaults::{ @@ -467,6 +467,7 @@ async fn wg_probe( auth_version: AuthenticatorVersion, awg_args: String, netstack_args: NetstackArgs, + // TODO: update type credential: CredentialSpendingData, ) -> anyhow::Result { info!("attempting to use authenticator version {auth_version:?}"); @@ -493,6 +494,9 @@ async fn wg_probe( AuthenticatorVersion::V5 => ClientMessage::Initial(Box::new( v5::registration::InitMessage::new(authenticator_pub_key), )), + AuthenticatorVersion::V6 => ClientMessage::Initial(Box::new( + v6::registration::InitMessage::new(authenticator_pub_key), + )), AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => bail!("unknown version number"), }; @@ -512,57 +516,17 @@ async fn wg_probe( debug!("Verifying data"); pending_registration_response.verify(&private_key)?; - let finalized_message = match auth_version { - AuthenticatorVersion::V2 => { - ClientMessage::Final(Box::new(v2::registration::FinalMessage { - gateway_client: v2::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V3 => { - ClientMessage::Final(Box::new(v3::registration::FinalMessage { - gateway_client: v3::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V4 => { - ClientMessage::Final(Box::new(v4::registration::FinalMessage { - gateway_client: v4::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V5 => { - ClientMessage::Final(Box::new(v5::registration::FinalMessage { - gateway_client: v5::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { - bail!("Unknown version number") - } - }; + let credential = credential + .try_into() + .inspect_err(|err| error!("invalid zk-nym data: {err}")) + .ok(); + + let finalized_message = + pending_registration_response.finalise_registration(&private_key, credential); + let client_message = ClientMessage::Final(finalized_message); + let response = auth_client - .send_and_wait_for_response(&finalized_message) + .send_and_wait_for_response(&client_message) .await?; let AuthenticatorResponse::Registered(registered_response) = response else { bail!("Unexpected response"); diff --git a/nym-node/src/cli/commands/run/args.rs b/nym-node/src/cli/commands/run/args.rs index 36117d0ba61..8c214f5e5e3 100644 --- a/nym-node/src/cli/commands/run/args.rs +++ b/nym-node/src/cli/commands/run/args.rs @@ -159,7 +159,7 @@ impl Args { name: "id".to_string(), })?; - let config = ConfigBuilder::new(id, config_path.clone(), data_dir.clone()) + ConfigBuilder::new(id, config_path.clone(), data_dir.clone()) // the old default behaviour of running in mixnode mode if nothing is explicitly set .with_modes( self.custom_modes() @@ -172,11 +172,9 @@ impl Args { .with_storage_paths(NymNodePaths::new(&data_dir)) .with_verloc(self.verloc.build_config_section()) .with_metrics(self.metrics.build_config_section()) - .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)) + .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)?) .with_service_providers(self.exit_gateway.build_config_section(&data_dir)) - .build(); - - Ok(config) + .build() } pub(crate) fn override_config(self, mut config: Config) -> Config { diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 52116964852..e7eb81623fa 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -5,6 +5,7 @@ use super::DEFAULT_NYMNODE_ID; use crate::config; use crate::config::default_config_filepath; use crate::env::vars::*; +use crate::error::NymNodeError; use celes::Country; use clap::Args; use clap::builder::ArgPredicate; @@ -426,6 +427,14 @@ pub(crate) struct EntryGatewayArgs { env = NYMNODE_MNEMONIC_ARG )] pub(crate) mnemonic: Option, + + /// Endpoint to query to retrieve current upgrade mode attestation. + #[clap( + long, + env = NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG + )] + #[zeroize(skip)] + pub(crate) upgrade_mode_attestation_url: Option, } impl EntryGatewayArgs { @@ -433,12 +442,12 @@ impl EntryGatewayArgs { pub(crate) fn build_config_section>( self, data_dir: P, - ) -> config::GatewayTasksConfig { - self.override_config_section(config::GatewayTasksConfig::new_default(data_dir)) + ) -> Result { + Ok(self.override_config_section(config::GatewayTasksConfig::new(data_dir)?)) } pub(crate) fn override_config_section( - self, + mut self, mut section: config::GatewayTasksConfig, ) -> config::GatewayTasksConfig { if let Some(bind_address) = self.entry_bind_address { @@ -453,6 +462,9 @@ impl EntryGatewayArgs { if let Some(enforce_zk_nyms) = self.enforce_zk_nyms { section.enforce_zk_nyms = enforce_zk_nyms } + if let Some(upgrade_mode_attestation_url) = self.upgrade_mode_attestation_url.take() { + section.upgrade_mode.attestation_url = upgrade_mode_attestation_url + } section } diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 0fcf54d9dd5..16517b77bcb 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -1,14 +1,22 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::config::helpers::log_error_and_return; use crate::config::persistence::GatewayTasksPaths; -use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, TICKETBOOK_VALIDITY_DAYS}; +use crate::error::NymNodeError; +use nym_config::defaults::{ + DEFAULT_CLIENT_LISTENING_PORT, TICKETBOOK_VALIDITY_DAYS, mainnet, var_names, +}; use nym_config::helpers::in6addr_any_init; use nym_config::serde_helpers::de_maybe_port; +use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; use serde::{Deserialize, Serialize}; +use std::env; use std::net::SocketAddr; use std::path::Path; use std::time::Duration; +use tracing::info; +use url::Url; pub const DEFAULT_WS_PORT: u16 = DEFAULT_CLIENT_LISTENING_PORT; @@ -36,6 +44,8 @@ pub struct GatewayTasksConfig { #[serde(deserialize_with = "de_maybe_port")] pub announce_wss_port: Option, + pub upgrade_mode: UpgradeModeWatcher, + #[serde(default)] pub debug: Debug, } @@ -63,6 +73,10 @@ pub struct Debug { pub client_bandwidth: ClientBandwidthDebug, pub zk_nym_tickets: ZkNymTicketHandlerDebug, + + /// The minimum duration since the last explicit check for the upgrade mode to allow creation of new requests. + #[serde(with = "humantime_serde")] + pub upgrade_mode_min_staleness_recheck: Duration, } impl Debug { @@ -70,6 +84,7 @@ impl Debug { 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; + pub const DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK: Duration = Duration::from_secs(30); } impl Default for Debug { @@ -82,6 +97,7 @@ impl Default for Debug { stale_messages: Default::default(), client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), + upgrade_mode_min_staleness_recheck: Self::DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK, } } } @@ -201,14 +217,149 @@ impl Default for StaleMessageDebug { } impl GatewayTasksConfig { - pub fn new_default>(data_dir: P) -> Self { - GatewayTasksConfig { + pub fn new>(data_dir: P) -> Result { + Ok(GatewayTasksConfig { storage_paths: GatewayTasksPaths::new(data_dir), enforce_zk_nyms: false, ws_bind_address: SocketAddr::new(in6addr_any_init(), DEFAULT_WS_PORT), announce_ws_port: None, announce_wss_port: None, + upgrade_mode: UpgradeModeWatcher::new()?, debug: Default::default(), + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpgradeModeWatcher { + /// Specifies whether this gateway watches for upgrade mode changes + /// via the published attestation file. + pub enabled: bool, + + /// Endpoint to query to retrieve current upgrade mode attestation. + pub attestation_url: Url, + + /// Expected public key of the attester providing the upgrade mode attestation + /// on the specified endpoint + #[serde(with = "bs58_ed25519_pubkey")] + pub attester_public_key: ed25519::PublicKey, + + pub debug: UpgradeModeWatcherDebug, +} + +impl From for nym_gateway::config::UpgradeModeWatcher { + fn from(config: UpgradeModeWatcher) -> Self { + nym_gateway::config::UpgradeModeWatcher { + enabled: config.enabled, + attestation_url: config.attestation_url, + debug: nym_gateway::config::UpgradeModeWatcherDebug { + regular_polling_interval: config.debug.regular_polling_interval, + expedited_poll_interval: config.debug.expedited_poll_interval, + }, + } + } +} + +impl UpgradeModeWatcher { + pub fn new_mainnet() -> UpgradeModeWatcher { + info!("using mainnet configuration for the upgrade mode:"); + info!("\t- url: {}", mainnet::UPGRADE_MODE_ATTESTATION_URL); + info!( + "\t- attester public key: {}", + mainnet::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + ); + + // SAFETY: + // our hardcoded values should always be valid + #[allow(clippy::expect_used)] + let attestation_url = mainnet::UPGRADE_MODE_ATTESTATION_URL + .parse() + .expect("invalid default upgrade mode attestation URL"); + + #[allow(clippy::expect_used)] + let attester_public_key = mainnet::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + .parse() + .expect("invalid default upgrade mode attester public key"); + + UpgradeModeWatcher { + enabled: true, + attestation_url, + attester_public_key, + debug: UpgradeModeWatcherDebug::default(), + } + } + + pub fn new() -> Result { + // if env is configured, extract relevant values from there, otherwise fallback to mainnet + if env::var(var_names::CONFIGURED).is_err() { + return Ok(Self::new_mainnet()); + } + + // if env is configured, the relevant values should be set + let Ok(env_attestation_url) = env::var(var_names::UPGRADE_MODE_ATTESTATION_URL) else { + return log_error_and_return(format!( + "'{}' is not set whilst the env is set to be configured", + var_names::UPGRADE_MODE_ATTESTATION_URL + )); + }; + + let Ok(env_attester_pubkey) = + env::var(var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY) + else { + return log_error_and_return(format!( + "'{}' is not set whilst the env is set to be configured", + var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + )); + }; + + let attestation_url = match env_attestation_url.parse() { + Ok(url) => url, + Err(err) => { + return log_error_and_return(format!( + "provided attestation url {env_attestation_url} is invalid: {err}!" + )); + } + }; + + let attester_public_key = match env_attester_pubkey.parse() { + Ok(public_key) => public_key, + Err(err) => { + return log_error_and_return(format!( + "provided attester public key {env_attester_pubkey} is invalid: {err}!" + )); + } + }; + + Ok(UpgradeModeWatcher { + enabled: true, + attestation_url, + attester_public_key, + debug: UpgradeModeWatcherDebug::default(), + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct UpgradeModeWatcherDebug { + /// Default polling interval + #[serde(with = "humantime_serde")] + pub regular_polling_interval: Duration, + + /// Expedited polling interval for once upgrade mode is detected + #[serde(with = "humantime_serde")] + pub expedited_poll_interval: Duration, +} + +impl UpgradeModeWatcherDebug { + const DEFAULT_REGULAR_POLLING_INTERVAL: Duration = Duration::from_secs(15 * 60); + const DEFAULT_EXPEDITED_POLL_INTERVAL: Duration = Duration::from_secs(2 * 60); +} + +impl Default for UpgradeModeWatcherDebug { + fn default() -> Self { + UpgradeModeWatcherDebug { + regular_polling_interval: Self::DEFAULT_REGULAR_POLLING_INTERVAL, + expedited_poll_interval: Self::DEFAULT_EXPEDITED_POLL_INTERVAL, } } } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index cdbffb0f257..9605302aa20 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -3,11 +3,13 @@ use super::LocalWireguardOpts; use crate::config::Config; +use crate::error::NymNodeError; use clap::crate_version; use nym_gateway::node::{ LocalAuthenticatorOpts, LocalIpPacketRouterOpts, LocalNetworkRequesterOpts, }; use nym_gateway::nym_authenticator; +use tracing::error; // a temporary solution until further refactoring is made fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { @@ -24,6 +26,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { nym_gateway::config::IpPacketRouter { enabled: config.service_providers.network_requester.debug.enabled, }, + config.gateway_tasks.upgrade_mode.clone(), nym_gateway::config::Debug { client_bandwidth_max_flushing_rate: config .gateway_tasks @@ -62,6 +65,10 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { .maximum_time_between_redemption, }, max_request_timestamp_skew: config.gateway_tasks.debug.max_request_timestamp_skew, + upgrade_mode_min_staleness_recheck: config + .gateway_tasks + .debug + .upgrade_mode_min_staleness_recheck, }, ) } @@ -218,3 +225,9 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { wg_opts, } } + +pub(crate) fn log_error_and_return(msg: impl Into) -> Result { + let msg = msg.into(); + error!("{msg}"); + Err(NymNodeError::config_validation_failure(msg)) +} diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 3a20022a15a..edec2a80af2 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -262,8 +262,13 @@ impl ConfigBuilder { self } - pub fn build(self) -> Config { - Config { + pub fn build(self) -> Result { + let gateway_tasks = match self.gateway_tasks { + Some(gateway_tasks) => gateway_tasks, + None => GatewayTasksConfig::new(&self.data_dir)?, + }; + + Ok(Config { id: self.id, modes: self.modes, host: self.host.unwrap_or_default(), @@ -279,16 +284,14 @@ impl ConfigBuilder { .storage_paths .unwrap_or_else(|| NymNodePaths::new(&self.data_dir)), metrics: self.metrics.unwrap_or_default(), - gateway_tasks: self - .gateway_tasks - .unwrap_or_else(|| GatewayTasksConfig::new_default(&self.data_dir)), + gateway_tasks, service_providers: self .service_providers .unwrap_or_else(|| ServiceProvidersConfig::new_default(&self.data_dir)), logging: self.logging.unwrap_or_default(), save_path: Some(self.config_path), debug: Default::default(), - } + }) } } diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs index 3a395685f54..e45cca8dd21 100644 --- a/nym-node/src/config/old_configs/old_config_v10.rs +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -3,7 +3,7 @@ use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; use crate::config::gateway_tasks::{ - ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, + ClientBandwidthDebug, StaleMessageDebug, UpgradeModeWatcher, ZkNymTicketHandlerDebug, }; use crate::config::persistence::{ AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, @@ -33,7 +33,7 @@ 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 tracing::{debug, error, instrument}; use url::Url; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] @@ -1346,6 +1346,13 @@ pub async fn try_upgrade_config_v10>( 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, + upgrade_mode: UpgradeModeWatcher::new() + .inspect_err(|_| { + error!( + "failed to set custom upgrade mode configuration - falling back to mainnet" + ) + }) + .unwrap_or(UpgradeModeWatcher::new_mainnet()), 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, @@ -1394,6 +1401,7 @@ pub async fn try_upgrade_config_v10>( .zk_nym_tickets .maximum_time_between_redemption, }, + ..Default::default() }, }, service_providers: ServiceProvidersConfig { diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index bf8ed72710e..8177119780d 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -201,6 +201,18 @@ announce_ws_port = {{#if gateway_tasks.announce_ws_port }} {{ gateway_tasks.anno # (default: 0 - disabled) announce_wss_port = {{#if gateway_tasks.announce_wss_port }} {{ gateway_tasks.announce_wss_port }} {{else}} 0 {{/if}} +[gateway_tasks.upgrade_mode] +# Specifies whether this gateway watches for upgrade mode changes +# via the published attestation file. +enabled = {{ gateway_tasks.upgrade_mode.enabled }} + +# Endpoint to query to retrieve current upgrade mode attestation. +# If not provided, it implicitly disables the watcher and upgrade-mode features +attestation_url = '{{ gateway_tasks.upgrade_mode.attestation_url }}' + +# Expected public key of the attester providing the upgrade mode attestation +# on the specified endpoint +attester_public_key = '{{ gateway_tasks.upgrade_mode.attester_public_key }}' [gateway_tasks.storage_paths] # Path to sqlite database containing all persistent data: messages for offline clients, diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 13aedc7c0a2..8f9a6d65b85 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -61,6 +61,8 @@ pub mod vars { pub const NYMNODE_ENTRY_ANNOUNCE_WSS_PORT_ARG: &str = "NYMNODE_ENTRY_ANNOUNCE_WSS_PORT"; pub const NYMNODE_ENFORCE_ZK_NYMS_ARG: &str = "NYMNODE_ENFORCE_ZK_NYMS"; pub const NYMNODE_MNEMONIC_ARG: &str = "NYMNODE_MNEMONIC"; + pub const NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG: &str = + "NYMNODE_UPGRADE_MODE_ATTESTATION_URL"; // exit gateway: pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY"; diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 44d35b5d410..c0f94eb6da8 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -40,7 +40,7 @@ use crate::node::shared_network::{ }; use nym_bin_common::bin_info; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder}; +use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender}; use nym_mixnet_client::client::ActiveConnections; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_requester::{ @@ -624,9 +624,27 @@ impl NymNode { metrics_sender, self.metrics.clone(), self.entry_gateway.mnemonic.clone(), + Self::user_agent(), + self.config.gateway_tasks.upgrade_mode.attester_public_key, self.shutdown_tracker().clone(), ); + // start task for watching the changes in upgrade mode attestation + let upgrade_check_request_sender = if let Some(upgrade_mode_watcher) = + gateway_tasks_builder.try_build_upgrade_mode_watcher() + { + let req_sender = upgrade_mode_watcher.request_sender(); + upgrade_mode_watcher.start(); + req_sender + } else { + UpgradeModeCheckRequestSender::new_empty() + }; + + // create the common state for subtasks relying on the upgrade mode information + // (i.e. everything that'd require ticket/bandwidth processing) + let upgrade_mode_common_state = + gateway_tasks_builder.build_upgrade_mode_common_state(upgrade_check_request_sender); + // if we're running in entry mode, start the websocket if self.modes().entry { info!( @@ -634,7 +652,10 @@ impl NymNode { self.config.gateway_tasks.ws_bind_address ); let mut websocket = gateway_tasks_builder - .build_websocket_listener(active_clients_store.clone()) + .build_websocket_listener( + active_clients_store.clone(), + upgrade_mode_common_state.clone(), + ) .await?; self.shutdown_tracker() .try_spawn_named(async move { websocket.run().await }, "EntryWebsocket"); @@ -682,7 +703,7 @@ impl NymNode { gateway_tasks_builder.set_wireguard_data(wg_data.into()); let authenticator = gateway_tasks_builder - .build_wireguard_authenticator(topology_provider) + .build_wireguard_authenticator(upgrade_mode_common_state.clone(), topology_provider) .await?; let started_authenticator = authenticator.start_service_provider().await?; active_clients_store.insert_embedded(started_authenticator.handle); @@ -693,7 +714,7 @@ impl NymNode { ); gateway_tasks_builder - .try_start_wireguard() + .try_start_wireguard(upgrade_mode_common_state) .await .map_err(NymNodeError::GatewayTasksStartupFailure)?; } else { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 26df1b52e66..120e1cc969a 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -385,6 +385,18 @@ dependencies = [ "rayon", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.7.2" @@ -650,6 +662,12 @@ dependencies = [ "serde", ] +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + [[package]] name = "bip32" version = "0.5.3" @@ -721,6 +739,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -1069,6 +1098,17 @@ dependencies = [ "error-code", ] +[[package]] +name = "coarsetime" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -1127,6 +1167,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -1421,6 +1467,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "ct-codecs" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" + [[package]] name = "ctor" version = "0.2.9" @@ -1624,6 +1676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1869,6 +1922,16 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.15", +] + [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -1930,6 +1993,8 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -2886,6 +2951,15 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2895,6 +2969,30 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac-sha1-compact" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" + +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hostname" version = "0.4.0" @@ -3560,6 +3658,32 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jwt-simple" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.12", + "zeroize", +] + [[package]] name = "k256" version = "0.13.4" @@ -3603,6 +3727,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libappindicator" @@ -3644,6 +3771,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.3" @@ -3936,6 +4069,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3951,6 +4101,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3958,6 +4119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4111,6 +4273,7 @@ dependencies = [ "nym-compact-ecash", "nym-ecash-time", "nym-network-defaults", + "nym-upgrade-mode-check", "rand 0.8.5", "serde", "strum", @@ -4127,6 +4290,7 @@ dependencies = [ "base64 0.22.1", "bs58", "ed25519-dalek", + "jwt-simple", "nym-pemstore", "nym-sphinx-types", "rand 0.8.5", @@ -4429,6 +4593,21 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-upgrade-mode-check" +version = "0.1.0" +dependencies = [ + "jwt-simple", + "nym-crypto", + "nym-http-api-client", + "reqwest 0.12.15", + "serde", + "serde_json", + "thiserror 2.0.12", + "time", + "tracing", +] + [[package]] name = "nym-validator-client" version = "0.1.0" @@ -4529,9 +4708,7 @@ name = "nym-wireguard-types" version = "0.1.0" dependencies = [ "base64 0.22.1", - "log", - "nym-config", - "nym-network-defaults", + "nym-crypto", "serde", "thiserror 2.0.12", "x25519-dalek", @@ -4900,6 +5077,18 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "pairing" version = "0.23.0" @@ -5024,6 +5213,15 @@ dependencies = [ "regex", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -5262,6 +5460,17 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -5944,6 +6153,27 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -6619,6 +6849,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" @@ -6714,6 +6950,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" +[[package]] +name = "superboring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" +dependencies = [ + "getrandom 0.2.15", + "hmac-sha256", + "hmac-sha512", + "rand 0.8.5", + "rsa", +] + [[package]] name = "swift-rs" version = "1.0.7" @@ -8079,6 +8328,15 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "wasm-bindgen" version = "0.2.100"