diff --git a/Cargo.lock b/Cargo.lock index 12527ecdd7..bf1fe092e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4923,11 +4923,16 @@ dependencies = [ "bincode", "futures", "nym-authenticator-requests", + "nym-bandwidth-controller", "nym-credentials-interface", "nym-crypto", + "nym-pemstore", + "nym-registration-common", "nym-sdk", "nym-service-provider-requests-common", + "nym-validator-client", "nym-wireguard-types", + "rand 0.8.5", "semver 1.0.26", "thiserror 2.0.12", "tokio", @@ -4949,8 +4954,10 @@ dependencies = [ "nym-sphinx", "nym-wireguard-types", "rand 0.8.5", + "semver 1.0.26", "serde", "sha2 0.10.9", + "strum_macros", "thiserror 2.0.12", "x25519-dalek", ] @@ -4959,6 +4966,7 @@ dependencies = [ name = "nym-bandwidth-controller" version = "0.1.0" dependencies = [ + "async-trait", "bip39", "log", "nym-credential-storage", @@ -6007,6 +6015,7 @@ dependencies = [ name = "nym-ip-packet-client" version = "0.1.0" dependencies = [ + "bincode", "bytes", "futures", "nym-ip-packet-requests", @@ -6659,6 +6668,36 @@ dependencies = [ "time", ] +[[package]] +name = "nym-registration-client" +version = "0.1.0" +dependencies = [ + "nym-authenticator-client", + "nym-bandwidth-controller", + "nym-credential-storage", + "nym-credentials-interface", + "nym-ip-packet-client", + "nym-registration-common", + "nym-sdk", + "nym-validator-client", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "nym-registration-common" +version = "0.1.0" +dependencies = [ + "nym-authenticator-requests", + "nym-crypto", + "nym-ip-packet-requests", + "nym-sphinx", + "tokio-util", +] + [[package]] name = "nym-sdk" version = "0.1.0" @@ -7404,26 +7443,6 @@ dependencies = [ "ts-rs", ] -[[package]] -name = "nym-wg-gateway-client" -version = "0.1.0" -dependencies = [ - "nym-authenticator-client", - "nym-authenticator-requests", - "nym-bandwidth-controller", - "nym-credentials-interface", - "nym-crypto", - "nym-node-requests", - "nym-pemstore", - "nym-sdk", - "nym-statistics-common", - "nym-validator-client", - "rand 0.8.5", - "thiserror 2.0.12", - "tracing", - "url", -] - [[package]] name = "nym-wireguard" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f53d9d979d..11aad891fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ members = [ "common/nymsphinx/types", "common/nyxd-scraper", "common/pemstore", + "common/registration", "common/serde-helpers", "common/service-provider-requests-common", "common/socks5-client-core", @@ -92,7 +93,8 @@ members = [ "common/socks5/requests", "common/statistics", "common/store-cipher", - "common/task", "common/test-utils", + "common/task", + "common/test-utils", "common/ticketbooks-merkle", "common/topology", "common/tun", @@ -125,10 +127,11 @@ members = [ "nym-node-status-api/nym-node-status-client", "nym-node/nym-node-metrics", "nym-node/nym-node-requests", - "nym-outfox", "nym-signers-monitor", + "nym-outfox", + "nym-registration-client", + "nym-signers-monitor", "nym-statistics-api", "nym-validator-rewarder", - "nym-wg-gateway-client", "nyx-chain-watcher", "sdk/ffi/cpp", "sdk/ffi/go", @@ -145,7 +148,6 @@ members = [ # "tools/internal/sdk-version-bump", "tools/internal/ssl-inject", "tools/internal/testnet-manager", - "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check", "tools/nym-cli", diff --git a/common/authenticator-requests/Cargo.toml b/common/authenticator-requests/Cargo.toml index cb7236e700..60ff6826ba 100644 --- a/common/authenticator-requests/Cargo.toml +++ b/common/authenticator-requests/Cargo.toml @@ -13,6 +13,8 @@ base64 = { workspace = true } bincode = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } +semver = { workspace = true } +strum_macros = { workspace = true } thiserror = { workspace = true } nym-credentials-interface = { path = "../credentials-interface" } diff --git a/common/authenticator-requests/src/client_message.rs b/common/authenticator-requests/src/client_message.rs new file mode 100644 index 0000000000..d0a19ab6a9 --- /dev/null +++ b/common/authenticator-requests/src/client_message.rs @@ -0,0 +1,272 @@ +// Copyright 2025 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_sphinx::addressing::Recipient; +use nym_wireguard_types::PeerPublicKey; + +use crate::{ + latest::registration::IpPair, + traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable}, + v2, v3, v4, v5, AuthenticatorVersion, Error, +}; + +// This is very redundant with AuthenticatorRequest and I reckon they could be smooshed. +// It is a bit out of scope for me at the moment though +#[derive(Debug)] +pub enum ClientMessage { + Initial(Box), + Final(Box), + Query(Box), + TopUp(Box), +} + +impl ClientMessage { + // check if message is wasteful e.g. contains a credential + pub fn is_wasteful(&self) -> bool { + match self { + Self::Final(msg) => msg.credential().is_some(), + Self::TopUp(_) => true, + Self::Initial(_) | Self::Query(_) => false, + } + } + + fn version(&self) -> AuthenticatorVersion { + match self { + ClientMessage::Initial(msg) => msg.version(), + ClientMessage::Final(msg) => msg.version(), + ClientMessage::Query(msg) => msg.version(), + ClientMessage::TopUp(msg) => msg.version(), + } + } + + pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec, u64), Error> { + 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::UNKNOWN => Err(Error::UnknownVersion), + } + } + + pub fn use_surbs(&self) -> bool { + use AuthenticatorVersion::*; + match self.version() { + V1 | V2 | V3 | V4 => false, + V5 => true, + UNKNOWN => true, + } + } +} + +// Same comment as above struct +#[derive(Debug)] +pub struct QueryMessageImpl { + pub pub_key: PeerPublicKey, + pub version: AuthenticatorVersion, +} + +impl Versionable for QueryMessageImpl { + fn version(&self) -> AuthenticatorVersion { + self.version + } +} + +impl QueryBandwidthMessage for QueryMessageImpl { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} diff --git a/common/authenticator-requests/src/error.rs b/common/authenticator-requests/src/error.rs index 1c43da3cad..d940bd538d 100644 --- a/common/authenticator-requests/src/error.rs +++ b/common/authenticator-requests/src/error.rs @@ -23,6 +23,17 @@ pub enum Error { #[error("conversion: {0}")] Conversion(String), - #[error("failed to serialize response packet: {source}")] - FailedToSerializeResponsePacket { source: Box }, + // TODO add version number for debugging + #[error("unknown version number")] + UnknownVersion, + + // TODO add version number for debugging + #[error("unsupported request version")] + UnsupportedVersion, + + #[error("gateway doesn't support this type of message")] + UnsupportedMessage, + + #[error(transparent)] + Bincode(#[from] bincode::Error), } diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index 351a9b443c..b1b0159da3 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -1,6 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod client_message; +pub mod request; +pub mod response; pub mod traits; pub mod v1; pub mod v2; @@ -10,11 +13,13 @@ pub mod v5; mod error; mod util; +mod version; pub use error::Error; pub use v5 as latest; +pub use version::AuthenticatorVersion; -pub const CURRENT_VERSION: u8 = 5; +pub const CURRENT_VERSION: u8 = latest::VERSION; fn make_bincode_serializer() -> impl bincode::Options { use bincode::Options; diff --git a/common/authenticator-requests/src/request.rs b/common/authenticator-requests/src/request.rs new file mode 100644 index 0000000000..3d98a8ed5e --- /dev/null +++ b/common/authenticator-requests/src/request.rs @@ -0,0 +1,204 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +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}; + +#[derive(Debug)] +pub enum AuthenticatorRequest { + Initial { + msg: Box, + protocol: Protocol, + reply_to: Option, + request_id: u64, + }, + Final { + msg: Box, + protocol: Protocol, + reply_to: Option, + request_id: u64, + }, + QueryBandwidth { + msg: Box, + protocol: Protocol, + reply_to: Option, + request_id: u64, + }, + TopUpBandwidth { + msg: Box, + protocol: Protocol, + reply_to: Option, + request_id: u64, + }, +} + +impl From for AuthenticatorRequest { + fn from(value: v1::request::AuthenticatorRequest) -> Self { + match value.data { + v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: Some(value.reply_to), + request_id: value.request_id, + }, + v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final { + msg: Box::new(gateway_client), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: Some(value.reply_to), + request_id: value.request_id, + }, + v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: Some(value.reply_to), + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v2::request::AuthenticatorRequest) -> Self { + match value.data { + v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + }, + v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + }, + v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v3::request::AuthenticatorRequest) -> Self { + match value.data { + v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + }, + v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + }, + v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + } + } + v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v4::request::AuthenticatorRequest) -> Self { + match value.data { + v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + }, + v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + }, + v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + } + } + v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: Some(value.reply_to), + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v5::request::AuthenticatorRequest) -> Self { + match value.data { + v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v5::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, + } + } + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + } + } +} diff --git a/common/authenticator-requests/src/response.rs b/common/authenticator-requests/src/response.rs new file mode 100644 index 0000000000..8e196a7b72 --- /dev/null +++ b/common/authenticator-requests/src/response.rs @@ -0,0 +1,106 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::traits::{ + Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse, + TopUpBandwidthResponse, +}; +use crate::{v2, v3, v4, v5}; + +#[derive(Debug)] +pub enum AuthenticatorResponse { + PendingRegistration(Box), + Registered(Box), + RemainingBandwidth(Box), + TopUpBandwidth(Box), +} + +impl Id for AuthenticatorResponse { + fn id(&self) -> u64 { + match self { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + pending_registration_response.id() + } + AuthenticatorResponse::Registered(registered_response) => registered_response.id(), + AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { + remaining_bandwidth_response.id() + } + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.id() + } + } + } +} + +impl From for AuthenticatorResponse { + fn from(value: v2::response::AuthenticatorResponse) -> Self { + match value.data { + v2::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v2::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v2::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + } + } +} + +impl From for AuthenticatorResponse { + fn from(value: v3::response::AuthenticatorResponse) -> Self { + match value.data { + v3::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v3::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v3::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v3::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} + +impl From for AuthenticatorResponse { + fn from(value: v4::response::AuthenticatorResponse) -> Self { + match value.data { + v4::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v4::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v4::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} + +impl From for AuthenticatorResponse { + fn from(value: v5::response::AuthenticatorResponse) -> Self { + match value.data { + v5::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v5::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs index baaaf8b422..48e1a4cb23 100644 --- a/common/authenticator-requests/src/traits.rs +++ b/common/authenticator-requests/src/traits.rs @@ -1,49 +1,105 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::fmt; +use std::net::{Ipv4Addr, Ipv6Addr}; + use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::x25519::PrivateKey; -use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; -use nym_sphinx::addressing::clients::Recipient; use nym_wireguard_types::PeerPublicKey; -use crate::{ - v1, v2, v3, v4, - v5::{self, registration::IpPair}, - Error, -}; +use crate::latest::registration::IpPair; +use crate::{v1, v2, v3, v4, v5, AuthenticatorVersion, Error}; -#[derive(Copy, Clone, Debug)] -pub enum AuthenticatorVersion { - V1, - V2, - V3, - V4, - V5, - UNKNOWN, +pub trait Versionable { + fn version(&self) -> AuthenticatorVersion; } -impl From for AuthenticatorVersion { - fn from(value: Protocol) -> Self { - if value.service_provider_type != ServiceProviderType::Authenticator { - AuthenticatorVersion::UNKNOWN - } else if value.version == v1::VERSION { - AuthenticatorVersion::V1 - } else if value.version == v2::VERSION { - AuthenticatorVersion::V2 - } else if value.version == v3::VERSION { - AuthenticatorVersion::V3 - } else if value.version == v4::VERSION { - AuthenticatorVersion::V4 - } else if value.version == v5::VERSION { - AuthenticatorVersion::V5 - } else { - AuthenticatorVersion::UNKNOWN - } +impl Versionable for v1::GatewayClient { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V1 } } -pub trait InitMessage { +impl Versionable for v1::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V1 + } +} + +impl Versionable for v2::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V2 + } +} + +impl Versionable for v3::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +impl Versionable for v2::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V2 + } +} + +impl Versionable for v3::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +impl Versionable for PeerPublicKey { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v3::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +pub trait InitMessage: Versionable + fmt::Debug { fn pub_key(&self) -> PeerPublicKey; } @@ -77,15 +133,18 @@ impl InitMessage for v5::registration::InitMessage { } } -pub trait FinalMessage { - fn pub_key(&self) -> PeerPublicKey; +pub trait FinalMessage: Versionable + fmt::Debug { + fn gateway_client_pub_key(&self) -> PeerPublicKey; fn verify(&self, private_key: &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; } impl FinalMessage for v1::GatewayClient { - fn pub_key(&self) -> PeerPublicKey { + fn gateway_client_pub_key(&self) -> PeerPublicKey { self.pub_key } @@ -97,13 +156,28 @@ impl FinalMessage for v1::GatewayClient { self.private_ip.into() } + fn gateway_client_ipv4(&self) -> Option { + match self.private_ip { + std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + std::net::IpAddr::V6(_) => None, + } + } + + fn gateway_client_ipv6(&self) -> Option { + None + } + + fn gateway_client_mac(&self) -> Vec { + self.mac.to_vec() + } + fn credential(&self) -> Option { None } } impl FinalMessage for v2::registration::FinalMessage { - fn pub_key(&self) -> PeerPublicKey { + fn gateway_client_pub_key(&self) -> PeerPublicKey { self.gateway_client.pub_key } @@ -115,13 +189,28 @@ impl FinalMessage for v2::registration::FinalMessage { self.gateway_client.private_ip.into() } + fn gateway_client_ipv4(&self) -> Option { + match self.gateway_client.private_ip { + std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + std::net::IpAddr::V6(_) => None, + } + } + + fn gateway_client_ipv6(&self) -> Option { + None + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + fn credential(&self) -> Option { self.credential.clone() } } impl FinalMessage for v3::registration::FinalMessage { - fn pub_key(&self) -> PeerPublicKey { + fn gateway_client_pub_key(&self) -> PeerPublicKey { self.gateway_client.pub_key } @@ -133,13 +222,28 @@ impl FinalMessage for v3::registration::FinalMessage { self.gateway_client.private_ip.into() } + fn gateway_client_ipv4(&self) -> Option { + match self.gateway_client.private_ip { + std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + std::net::IpAddr::V6(_) => None, + } + } + + fn gateway_client_ipv6(&self) -> Option { + None + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + fn credential(&self) -> Option { self.credential.clone() } } impl FinalMessage for v4::registration::FinalMessage { - fn pub_key(&self) -> PeerPublicKey { + fn gateway_client_pub_key(&self) -> PeerPublicKey { self.gateway_client.pub_key } @@ -151,13 +255,25 @@ impl FinalMessage for v4::registration::FinalMessage { 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() } } impl FinalMessage for v5::registration::FinalMessage { - fn pub_key(&self) -> PeerPublicKey { + fn gateway_client_pub_key(&self) -> PeerPublicKey { self.gateway_client.pub_key } @@ -169,12 +285,24 @@ impl FinalMessage for v5::registration::FinalMessage { self.gateway_client.private_ips } + 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() } } -pub trait QueryBandwidthMessage { +pub trait QueryBandwidthMessage: Versionable + fmt::Debug { fn pub_key(&self) -> PeerPublicKey; } @@ -184,7 +312,7 @@ impl QueryBandwidthMessage for PeerPublicKey { } } -pub trait TopUpMessage { +pub trait TopUpMessage: Versionable + fmt::Debug { fn pub_key(&self) -> PeerPublicKey; fn credential(&self) -> CredentialSpendingData; } @@ -219,197 +347,286 @@ impl TopUpMessage for v5::topup::TopUpMessage { } } -pub enum AuthenticatorRequest { - Initial { - msg: Box, - protocol: Protocol, - reply_to: Option, - request_id: u64, - }, - Final { - msg: Box, - protocol: Protocol, - reply_to: Option, - request_id: u64, - }, - QueryBandwidth { - msg: Box, - protocol: Protocol, - reply_to: Option, - request_id: u64, - }, - TopUpBandwidth { - msg: Box, - protocol: Protocol, - reply_to: Option, - request_id: u64, - }, +pub trait Id { + fn id(&self) -> u64; } -impl From for AuthenticatorRequest { - fn from(value: v1::request::AuthenticatorRequest) -> Self { - match value.data { - v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { - msg: Box::new(init_message), - protocol: Protocol { - version: value.version, - service_provider_type: ServiceProviderType::Authenticator, - }, - reply_to: Some(value.reply_to), - request_id: value.request_id, - }, - v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final { - msg: Box::new(gateway_client), - protocol: Protocol { - version: value.version, - service_provider_type: ServiceProviderType::Authenticator, - }, - reply_to: Some(value.reply_to), - request_id: value.request_id, - }, - v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { - Self::QueryBandwidth { - msg: Box::new(peer_public_key), - protocol: Protocol { - version: value.version, - service_provider_type: ServiceProviderType::Authenticator, - }, - reply_to: Some(value.reply_to), - request_id: value.request_id, - } - } - } +impl Id for v2::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id } } -impl From for AuthenticatorRequest { - fn from(value: v2::request::AuthenticatorRequest) -> Self { - match value.data { - v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { - msg: Box::new(init_message), - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - }, - v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final { - msg: final_message, - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - }, - v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { - Self::QueryBandwidth { - msg: Box::new(peer_public_key), - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - } - } - } +impl Id for v3::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id } } -impl From for AuthenticatorRequest { - fn from(value: v3::request::AuthenticatorRequest) -> Self { - match value.data { - v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { - msg: Box::new(init_message), - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - }, - v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final { - msg: final_message, - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - }, - v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { - Self::QueryBandwidth { - msg: Box::new(peer_public_key), - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - } - } - v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { - Self::TopUpBandwidth { - msg: top_up_message, - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - } - } - } +impl Id for v4::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id } } -impl From for AuthenticatorRequest { - fn from(value: v4::request::AuthenticatorRequest) -> Self { - match value.data { - v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { - msg: Box::new(init_message), - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - }, - v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final { - msg: final_message, - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - }, - v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { - Self::QueryBandwidth { - msg: Box::new(peer_public_key), - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - } - } - v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { - Self::TopUpBandwidth { - msg: top_up_message, - protocol: value.protocol, - reply_to: Some(value.reply_to), - request_id: value.request_id, - } - } - } +impl Id for v5::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id } } -impl From for AuthenticatorRequest { - fn from(value: v5::request::AuthenticatorRequest) -> Self { - match value.data { - v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { - msg: Box::new(init_message), - protocol: value.protocol, - reply_to: None, - request_id: value.request_id, - }, - v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final { - msg: final_message, - protocol: value.protocol, - reply_to: None, - request_id: value.request_id, - }, - v5::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, - } - } - v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { - Self::TopUpBandwidth { - msg: top_up_message, - protocol: value.protocol, - reply_to: None, - request_id: value.request_id, - } - } - } +impl Id for v2::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v2::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +pub trait PendingRegistrationResponse: Id + fmt::Debug { + fn nonce(&self) -> u64; + fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error>; + fn pub_key(&self) -> PeerPublicKey; + fn private_ips(&self) -> IpPair; +} + +impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify(&self, gateway_key: &PrivateKey) -> std::result::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_ip.into() + } +} + +impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify(&self, gateway_key: &PrivateKey) -> std::result::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_ip.into() + } +} + +impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify(&self, gateway_key: &PrivateKey) -> std::result::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() + } +} + +impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify(&self, gateway_key: &PrivateKey) -> std::result::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 + } +} + +pub trait RegisteredResponse: Id + fmt::Debug { + fn private_ips(&self) -> IpPair; + fn pub_key(&self) -> PeerPublicKey; + fn wg_port(&self) -> u16; +} + +impl RegisteredResponse for v2::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ip.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +impl RegisteredResponse for v3::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ip.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} +impl RegisteredResponse for v4::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 v5::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ips + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +pub trait RemainingBandwidthResponse: Id + fmt::Debug { + fn available_bandwidth(&self) -> Option; +} + +impl RemainingBandwidthResponse for v2::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v3::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v4::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +pub trait TopUpBandwidthResponse: Id + fmt::Debug { + fn available_bandwidth(&self) -> i64; +} + +impl TopUpBandwidthResponse for v3::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} + +impl TopUpBandwidthResponse for v4::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} + +impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth } } diff --git a/common/authenticator-requests/src/version.rs b/common/authenticator-requests/src/version.rs new file mode 100644 index 0000000000..ac71de9ae0 --- /dev/null +++ b/common/authenticator-requests/src/version.rs @@ -0,0 +1,191 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{v1, v2, v3, v4, v5}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; + +#[derive(Copy, Clone, Debug, PartialEq, strum_macros::Display)] +#[strum(serialize_all = "snake_case")] +pub enum AuthenticatorVersion { + /// introduced in wispa release (1.1.5) + V1, + + /// introduced in aero release (1.1.9) + V2, + + /// introduced in magura release (1.1.10) + V3, + + /// introduced in crunch release (1.2.0) + V4, + + /// introduced in dorina-patched release (1.6.1) + V5, + + UNKNOWN, +} + +impl AuthenticatorVersion { + pub const LATEST: Self = Self::V5; + + pub const fn release_version(&self) -> semver::Version { + match self { + AuthenticatorVersion::V1 => semver::Version::new(1, 1, 5), + AuthenticatorVersion::V2 => semver::Version::new(1, 1, 9), + 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::UNKNOWN => semver::Version::new(0, 0, 0), + } + } +} + +impl From for AuthenticatorVersion { + fn from(value: Protocol) -> Self { + if value.service_provider_type != ServiceProviderType::Authenticator { + AuthenticatorVersion::UNKNOWN + } else if value.version == v1::VERSION { + AuthenticatorVersion::V1 + } else if value.version == v2::VERSION { + AuthenticatorVersion::V2 + } else if value.version == v3::VERSION { + AuthenticatorVersion::V3 + } else if value.version == v4::VERSION { + AuthenticatorVersion::V4 + } else if value.version == v5::VERSION { + AuthenticatorVersion::V5 + } else { + AuthenticatorVersion::UNKNOWN + } + } +} + +impl From for AuthenticatorVersion { + fn from(value: u8) -> Self { + if value == v1::VERSION { + AuthenticatorVersion::V1 + } else if value == v2::VERSION { + AuthenticatorVersion::V2 + } else if value == v3::VERSION { + AuthenticatorVersion::V3 + } else if value == v4::VERSION { + AuthenticatorVersion::V4 + } else if value == v5::VERSION { + AuthenticatorVersion::V5 + } else { + AuthenticatorVersion::UNKNOWN + } + } +} + +impl From<&str> for AuthenticatorVersion { + fn from(value: &str) -> Self { + let Ok(semver) = semver::Version::parse(value) else { + return Self::UNKNOWN; + }; + + semver.into() + } +} + +impl From> for AuthenticatorVersion { + fn from(value: Option<&String>) -> Self { + match value { + None => Self::UNKNOWN, + Some(value) => value.as_str().into(), + } + } +} + +impl From for AuthenticatorVersion { + fn from(value: String) -> Self { + Self::from(value.as_str()) + } +} + +impl From> for AuthenticatorVersion { + fn from(value: Option) -> Self { + value.as_ref().into() + } +} + +impl From for AuthenticatorVersion { + fn from(semver: semver::Version) -> Self { + if semver < AuthenticatorVersion::V1.release_version() { + return Self::UNKNOWN; + } + if semver < AuthenticatorVersion::V2.release_version() { + return Self::V1; + } + if semver < AuthenticatorVersion::V3.release_version() { + return Self::V2; + } + if semver < AuthenticatorVersion::V4.release_version() { + return Self::V3; + } + 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) + + debug_assert_eq!(Self::V5, Self::LATEST, "a new AuthenticatorVersion variant has been introduced without adjusting the `From` trait"); + Self::LATEST + } +} + +#[cfg(test)] +mod tests { + use super::super::latest; + use super::*; + + #[test] + fn strum_display() { + // sanity check on formatting and casing + assert_eq!("v1", AuthenticatorVersion::V1.to_string()); + assert_eq!("v2", AuthenticatorVersion::V2.to_string()); + assert_eq!("unknown", AuthenticatorVersion::UNKNOWN.to_string()); + } + + #[test] + fn u8_conversion() { + assert_eq!(AuthenticatorVersion::V1, AuthenticatorVersion::from(1u8)); + assert_eq!(AuthenticatorVersion::V2, AuthenticatorVersion::from(2u8)); + + assert_eq!( + AuthenticatorVersion::UNKNOWN, + AuthenticatorVersion::from(latest::VERSION + 1) + ); + assert_eq!( + AuthenticatorVersion::UNKNOWN, + AuthenticatorVersion::from(0u8) + ); + assert_eq!( + AuthenticatorVersion::UNKNOWN, + AuthenticatorVersion::from(255u8) + ); + } + + #[test] + fn semver_checks() { + assert_eq!(AuthenticatorVersion::UNKNOWN, "1.1.4".into()); + assert_eq!(AuthenticatorVersion::UNKNOWN, "0.1.0".into()); + assert_eq!(AuthenticatorVersion::UNKNOWN, "1.0.4".into()); + assert_eq!(AuthenticatorVersion::V1, "1.1.5".into()); + assert_eq!(AuthenticatorVersion::V1, "1.1.6".into()); + assert_eq!(AuthenticatorVersion::V1, "1.1.8".into()); + assert_eq!(AuthenticatorVersion::V2, "1.1.9".into()); + assert_eq!(AuthenticatorVersion::V3, "1.1.10".into()); + assert_eq!(AuthenticatorVersion::V3, "1.1.11".into()); + assert_eq!(AuthenticatorVersion::V3, "1.1.60".into()); + assert_eq!(AuthenticatorVersion::V4, "1.2.0".into()); + assert_eq!(AuthenticatorVersion::V4, "1.2.1".into()); + assert_eq!(AuthenticatorVersion::V4, "1.5.1".into()); + assert_eq!(AuthenticatorVersion::V4, "1.6.0".into()); + assert_eq!(AuthenticatorVersion::V5, "1.6.1".into()); + assert_eq!(AuthenticatorVersion::V5, "1.6.11".into()); + assert_eq!(AuthenticatorVersion::V5, "1.7.0".into()); + assert_eq!(AuthenticatorVersion::V5, "1.16.11".into()); + assert_eq!(AuthenticatorVersion::V5, "1.17.0".into()); + } +} diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index 1f5a4e9586..075c1702ae 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = { workspace = true } bip39 = { workspace = true } log = { workspace = true } rand = { workspace = true } diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index 4d4d4d6952..05be75cbdc 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -23,10 +23,12 @@ use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; pub use event::BandwidthStatusMessage; +pub use traits::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND}; pub mod acquire; pub mod error; mod event; +mod traits; mod utils; #[derive(Debug)] diff --git a/common/bandwidth-controller/src/traits.rs b/common/bandwidth-controller/src/traits.rs new file mode 100644 index 0000000000..8ff508f3f3 --- /dev/null +++ b/common/bandwidth-controller/src/traits.rs @@ -0,0 +1,42 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use nym_credential_storage::storage::Storage; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::nyxd::contract_traits::DkgQueryClient; + +use crate::{error::BandwidthControllerError, BandwidthController, PreparedCredential}; + +pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait BandwidthTicketProvider: Send + Sync { + async fn get_ecash_ticket( + &self, + ticket_type: TicketType, + gateway_id: ed25519::PublicKey, + tickets_to_spend: u32, + ) -> Result; +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl BandwidthTicketProvider for BandwidthController +where + C: DkgQueryClient + Sync + Send, + St: nym_credential_storage::storage::Storage, + ::StorageError: Send + Sync + 'static, +{ + async fn get_ecash_ticket( + &self, + ticket_type: TicketType, + gateway_id: ed25519::PublicKey, + tickets_to_spend: u32, + ) -> Result { + self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend) + .await + } +} diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 2a3e30bf64..021d57815a 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -1050,7 +1050,7 @@ where gateway_connection: GatewayConnection { gateway_ws_fd }, }, stats_reporter, - shutdown_handle: Some(shutdown_tracker), // The primary tracker for this client + shutdown_handle: shutdown_tracker, // The primary tracker for this client client_request_sender, forget_me: self.config.debug.forget_me, remember_me: self.config.debug.remember_me, @@ -1066,7 +1066,7 @@ pub struct BaseClient { pub client_state: ClientState, pub stats_reporter: ClientStatsSender, pub client_request_sender: ClientRequestSender, - pub shutdown_handle: Option, + pub shutdown_handle: ShutdownTracker, pub forget_me: ForgetMe, pub remember_me: RememberMe, } diff --git a/common/registration/Cargo.toml b/common/registration/Cargo.toml new file mode 100644 index 0000000000..22749ccdc9 --- /dev/null +++ b/common/registration/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nym-registration-common" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +tokio-util.workspace = true + +nym-authenticator-requests = { path = "../authenticator-requests" } +nym-crypto = { path = "../crypto" } +nym-ip-packet-requests = { path = "../ip-packet-requests" } +nym-sphinx = { path = "../nymsphinx" } diff --git a/common/registration/src/lib.rs b/common/registration/src/lib.rs new file mode 100644 index 0000000000..4b4253d3c2 --- /dev/null +++ b/common/registration/src/lib.rs @@ -0,0 +1,40 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + +use nym_authenticator_requests::AuthenticatorVersion; +use nym_crypto::asymmetric::x25519::PublicKey; +use nym_ip_packet_requests::IpPair; +use nym_sphinx::addressing::{NodeIdentity, Recipient}; + +pub const DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_private_entry_wireguard.pem"; +pub const DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_public_entry_wireguard.pem"; +pub const DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_private_exit_wireguard.pem"; +pub const DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_public_exit_wireguard.pem"; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct NymNode { + pub identity: NodeIdentity, + pub ip_address: IpAddr, + pub ipr_address: Option, + pub authenticator_address: Option, + pub version: AuthenticatorVersion, +} + +#[derive(Clone, Debug)] +pub struct GatewayData { + pub public_key: PublicKey, + pub endpoint: SocketAddr, + pub private_ipv4: Ipv4Addr, + pub private_ipv6: Ipv6Addr, +} + +#[derive(Clone, Copy, Debug)] +pub struct AssignedAddresses { + pub entry_mixnet_gateway_ip: IpAddr, + pub exit_mixnet_gateway_ip: IpAddr, + pub mixnet_client_address: Recipient, + pub exit_mix_address: Recipient, + pub interface_addresses: IpPair, +} diff --git a/common/service-provider-requests-common/src/lib.rs b/common/service-provider-requests-common/src/lib.rs index 2840a9321c..9e392dff19 100644 --- a/common/service-provider-requests-common/src/lib.rs +++ b/common/service-provider-requests-common/src/lib.rs @@ -42,6 +42,40 @@ impl TryFrom for ServiceProviderType { } } +pub trait ServiceProviderTypeExt { + fn is_network_requester(&self) -> bool; + fn is_ip_packet_router(&self) -> bool; + fn is_authenticator(&self) -> bool; +} + +impl ServiceProviderTypeExt for ServiceProviderType { + fn is_network_requester(&self) -> bool { + matches!(self, Self::NetworkRequester) + } + + fn is_ip_packet_router(&self) -> bool { + matches!(self, Self::IpPacketRouter) + } + + fn is_authenticator(&self) -> bool { + matches!(self, Self::Authenticator) + } +} + +impl ServiceProviderTypeExt for u8 { + fn is_network_requester(&self) -> bool { + ServiceProviderType::NetworkRequester as u8 == *self + } + + fn is_ip_packet_router(&self) -> bool { + ServiceProviderType::IpPacketRouter as u8 == *self + } + + fn is_authenticator(&self) -> bool { + ServiceProviderType::Authenticator as u8 == *self + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Protocol { pub version: u8, 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 9a6c9d3a50..af476bfba7 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs @@ -19,11 +19,9 @@ use nym_authenticator_requests::{ }; use nym_authenticator_requests::{ latest::registration::{GatewayClient, PendingRegistrations, PrivateIPs}, - traits::{ - AuthenticatorRequest, AuthenticatorVersion, FinalMessage, InitMessage, - QueryBandwidthMessage, TopUpMessage, - }, - v1, v2, v3, v4, v5, CURRENT_VERSION, + request::AuthenticatorRequest, + traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage}, + v1, v2, v3, v4, v5, AuthenticatorVersion, CURRENT_VERSION, }; use nym_credential_verification::ecash::traits::EcashManager; use nym_credential_verification::{ @@ -480,7 +478,7 @@ impl MixnetListener { let mut registred_and_free = self.registred_and_free.write().await; let registration_data = registred_and_free .registration_in_progres - .get(&final_message.pub_key()) + .get(&final_message.gateway_client_pub_key()) .ok_or(AuthenticatorError::RegistrationNotInProgress)? .clone(); @@ -491,7 +489,7 @@ impl MixnetListener { return Err(AuthenticatorError::MacVerificationFailure); } - let mut peer = Peer::new(Key::new(final_message.pub_key().to_bytes())); + let mut peer = Peer::new(Key::new(final_message.gateway_client_pub_key().to_bytes())); peer.allowed_ips .push(IpAddrMask::new(final_message.private_ips().ipv4.into(), 32)); peer.allowed_ips.push(IpAddrMask::new( @@ -532,7 +530,7 @@ impl MixnetListener { registred_and_free .registration_in_progres - .remove(&final_message.pub_key()); + .remove(&final_message.gateway_client_pub_key()); let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( diff --git a/nym-authenticator-client/Cargo.toml b/nym-authenticator-client/Cargo.toml index 35a72ae019..90174ff8d1 100644 --- a/nym-authenticator-client/Cargo.toml +++ b/nym-authenticator-client/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] bincode.workspace = true futures.workspace = true +rand.workspace = true semver.workspace = true thiserror.workspace = true tokio-util.workspace = true @@ -21,8 +22,12 @@ tokio.workspace = true tracing.workspace = true nym-authenticator-requests = { path = "../common/authenticator-requests" } +nym-bandwidth-controller = { path = "../common/bandwidth-controller" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-crypto = { path = "../common/crypto" } +nym-pemstore = { path = "../common/pemstore" } +nym-registration-common = { path = "../common/registration" } nym-sdk = { path = "../sdk/rust/nym-sdk" } nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" } +nym-validator-client = { path = "../common/client-libs/validator-client" } nym-wireguard-types = { path = "../common/wireguard-types" } diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs index 7bb0cd4dc4..39d991f886 100644 --- a/nym-authenticator-client/src/error.rs +++ b/nym-authenticator-client/src/error.rs @@ -1,41 +1,50 @@ +use nym_credentials_interface::TicketType; +use nym_sdk::mixnet::InputMessage; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("mixnet client stopped returning responses")] NoMixnetMessagesReceived, - #[error("failed to get version from message")] - NoVersionInMessage, - - #[error( - "received response with version v{received}, the client is too new and can only understand v{expected}" - )] - ReceivedResponseWithOldVersion { expected: u8, received: u8 }, - - #[error( - "received response with version v{received}, the client is too old and can only understand v{expected}" - )] - ReceivedResponseWithNewVersion { expected: u8, received: u8 }, - #[error("failed to send mixnet message")] - SendMixnetMessage(#[source] Box), + SendMixnetMessage(#[source] Box>), #[error("timeout waiting for connect response from exit gateway (authenticator)")] TimeoutWaitingForConnectResponse, - #[error("unable to get mixnet handle when sending authenticator message")] - UnableToGetMixnetHandle, - #[error("unknown version number")] UnknownVersion, + #[error("unsupported request version")] + UnsupportedVersion, + #[error(transparent)] Bincode(#[from] bincode::Error), - #[error("gateway doesn't support this type of message")] - UnsupportedMessage, - #[error(transparent)] AuthenticatorRequests(#[from] nym_authenticator_requests::Error), + + #[error("verification failure")] + VerificationFailed(#[source] nym_authenticator_requests::Error), + + #[error("failed to parse entry gateway socket addr")] + FailedToParseEntryGatewaySocketAddr(#[source] std::net::AddrParseError), + + #[error("received invalid response from gateway authenticator")] + InvalidGatewayAuthResponse, + + #[error("failed to get {ticketbook_type} ticket")] + GetTicket { + ticketbook_type: TicketType, + #[source] + source: nym_bandwidth_controller::error::BandwidthControllerError, + }, + + #[error("unknown authenticator version number")] + UnsupportedAuthenticatorVersion, + + #[error("failed to wait on AuthenticatorClientListener")] + FailedToJoinOnTask(#[from] tokio::task::JoinError), } // Result type based on our error type diff --git a/nym-authenticator-client/src/helpers.rs b/nym-authenticator-client/src/helpers.rs new file mode 100644 index 0000000000..e88d2f58a4 --- /dev/null +++ b/nym-authenticator-client/src/helpers.rs @@ -0,0 +1,49 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::x25519::KeyPair; +use nym_pemstore::KeyPairPath; +use nym_sdk::mixnet::{IncludedSurbs, Recipient, TransmissionLane}; +use rand::{CryptoRng, RngCore}; + +pub(crate) fn create_input_message( + recipient: Recipient, + data: Vec, + surbs: IncludedSurbs, +) -> nym_sdk::mixnet::InputMessage { + match surbs { + IncludedSurbs::Amount(surbs) => nym_sdk::mixnet::InputMessage::new_anonymous( + recipient, + data, + surbs, + TransmissionLane::General, + None, + ), + IncludedSurbs::ExposeSelfAddress => nym_sdk::mixnet::InputMessage::new_regular( + recipient, + data, + TransmissionLane::General, + None, + ), + } +} + +pub(crate) fn load_or_generate_keypair( + rng: &mut R, + paths: KeyPairPath, +) -> KeyPair { + match nym_pemstore::load_keypair(&paths) { + Ok(keypair) => keypair, + Err(_) => { + let keypair = KeyPair::new(rng); + if let Err(e) = nym_pemstore::store_keypair(&keypair, &paths) { + tracing::error!( + "could not store generated keypair at {:?} - {:?}; will use ephemeral keys", + paths, + e + ); + } + keypair + } + } +} diff --git a/nym-authenticator-client/src/legacy.rs b/nym-authenticator-client/src/legacy.rs new file mode 100644 index 0000000000..fa213582e8 --- /dev/null +++ b/nym-authenticator-client/src/legacy.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::time::Duration; +use tracing::{debug, error}; + +use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender}; +use crate::{helpers, ClientMessage, Error, Result}; +use nym_authenticator_requests::{ + client_message::QueryMessageImpl, response::AuthenticatorResponse, traits::Id, v2, v3, v4, v5, + AuthenticatorVersion, +}; +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::x25519::{KeyPair, PublicKey}; +use nym_sdk::mixnet::{IncludedSurbs, Recipient}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; +use nym_wireguard_types::PeerPublicKey; + +impl crate::AuthenticatorClient { + pub fn into_legacy_and_keypair(self) -> (LegacyAuthenticatorClient, KeyPair) { + ( + LegacyAuthenticatorClient { + public_key: *self.keypair.public_key(), + mixnet_listener: self.mixnet_listener, + mixnet_sender: self.mixnet_sender, + our_nym_address: self.our_nym_address, + auth_recipient: self.auth_recipient, + auth_version: self.auth_version, + }, + self.keypair, + ) + } +} + +// This is the legacy Authenticator that has to be used to handle bandwidth top up for legacy gateaways +pub struct LegacyAuthenticatorClient { + public_key: PublicKey, + mixnet_listener: MixnetMessageBroadcastReceiver, + mixnet_sender: MixnetMessageInputSender, + our_nym_address: Recipient, + pub auth_recipient: Recipient, + auth_version: AuthenticatorVersion, +} + +impl LegacyAuthenticatorClient { + pub async fn send_and_wait_for_response( + &mut self, + message: &ClientMessage, + ) -> Result { + let request_id = self.send_request(message).await?; + + debug!("Waiting for reply..."); + self.listen_for_response(request_id).await + } + + async fn send_request(&self, message: &ClientMessage) -> Result { + let (data, request_id) = message.bytes(self.our_nym_address)?; + + // We use 20 surbs for the connect request because typically the + // authenticator mixnet client on the nym-node is configured to have a min + // threshold of 10 surbs that it reserves for itself to request additional + // surbs. + let surbs = if message.use_surbs() { + match &message { + ClientMessage::Initial(_) => IncludedSurbs::new(20), + _ => IncludedSurbs::new(1), + } + } else { + IncludedSurbs::ExposeSelfAddress + }; + let input_message = helpers::create_input_message(self.auth_recipient, data, surbs); + + self.mixnet_sender + .send(input_message) + .await + .map_err(|e| Error::SendMixnetMessage(Box::new(e)))?; + + Ok(request_id) + } + + async fn listen_for_response(&mut self, request_id: u64) -> Result { + let timeout = tokio::time::sleep(Duration::from_secs(10)); + tokio::pin!(timeout); + + loop { + tokio::select! { + _ = &mut timeout => { + error!("Timed out waiting for reply to connect request"); + return Err(Error::TimeoutWaitingForConnectResponse); + } + msg = self.mixnet_listener.recv() => match msg { + Err(_) => { + return Err(Error::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(Error::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(Error::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); + } + } + } + } + } + } + + pub async fn query_bandwidth(&mut self) -> Result> { + let query_message = match self.auth_version { + AuthenticatorVersion::V1 => return Err(Error::UnsupportedAuthenticatorVersion), + AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V2, + })), + AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V3, + })), + AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V4, + })), + AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V5, + })), + AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion), + }; + let response = self.send_and_wait_for_response(&query_message).await?; + + let available_bandwidth = match response { + AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { + if let Some(available_bandwidth) = + remaining_bandwidth_response.available_bandwidth() + { + available_bandwidth + } else { + return Ok(None); + } + } + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + let remaining_pretty = if available_bandwidth > 1024 * 1024 { + format!("{:.2} MB", available_bandwidth as f64 / 1024.0 / 1024.0) + } else { + format!("{} KB", available_bandwidth / 1024) + }; + tracing::debug!( + "Remaining wireguard bandwidth with gateway {} for today: {}", + self.auth_recipient.gateway(), + remaining_pretty + ); + if available_bandwidth < 1024 * 1024 { + tracing::warn!( + "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)) + } + + pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result { + let top_up_message = match self.auth_version { + AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + 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.public_key.to_bytes().into()), + credential, + })), + AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + credential, + })), + AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { + return Err(Error::UnsupportedAuthenticatorVersion); + } + }; + let response = self.send_and_wait_for_response(&top_up_message).await?; + + let remaining_bandwidth = match response { + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.available_bandwidth() + } + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + Ok(remaining_bandwidth) + } +} diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs index 03112e1651..ced281d1d2 100644 --- a/nym-authenticator-client/src/lib.rs +++ b/nym-authenticator-client/src/lib.rs @@ -1,1122 +1,141 @@ -use std::{ - fmt, - net::{Ipv4Addr, Ipv6Addr}, - time::Duration, -}; +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 +use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND}; +use nym_registration_common::{ + GatewayData, DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME, + DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME, DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME, + DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME, +}; +use rand::rngs::OsRng; +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; +use tracing::{debug, error, trace}; + +use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender}; use nym_authenticator_requests::{ - v2, v3, v4, - v5::{self, registration::IpPair}, + client_message::ClientMessage, response::AuthenticatorResponse, traits::Id, v2, v3, v4, v5, + AuthenticatorVersion, }; - -use nym_credentials_interface::CredentialSpendingData; -use nym_crypto::asymmetric::x25519::PrivateKey; -use nym_sdk::mixnet::{ - IncludedSurbs, MixnetClientSender, MixnetMessageSender, Recipient, ReconstructedMessage, - TransmissionLane, -}; -use nym_service_provider_requests_common::ServiceProviderType; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::x25519::KeyPair; +use nym_pemstore::KeyPairPath; +use nym_sdk::mixnet::{IncludedSurbs, Recipient}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_wireguard_types::PeerPublicKey; -use tracing::{debug, error}; mod error; +mod helpers; +mod legacy; mod mixnet_listener; -pub use crate::{ - error::{Error, Result}, - mixnet_listener::{ - AuthClientMixnetListener, AuthClientMixnetListenerHandle, MixnetMessageBroadcastReceiver, - }, -}; +pub use crate::error::{Error, Result}; +pub use crate::legacy::LegacyAuthenticatorClient; +pub use crate::mixnet_listener::{AuthClientMixnetListener, AuthClientMixnetListenerHandle}; -pub trait Versionable { - fn version(&self) -> AuthenticatorVersion; -} - -impl Versionable for v2::registration::InitMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V2 - } -} - -impl Versionable for v3::registration::InitMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V3 - } -} - -impl Versionable for v4::registration::InitMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V4 - } -} - -impl Versionable for v5::registration::InitMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V5 - } -} - -impl Versionable for v2::registration::FinalMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V2 - } -} - -impl Versionable for v3::registration::FinalMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V3 - } -} - -impl Versionable for v4::registration::FinalMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V4 - } -} - -impl Versionable for v5::registration::FinalMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V5 - } -} - -impl Versionable for PeerPublicKey { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V3 - } -} - -impl Versionable for v3::topup::TopUpMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V3 - } -} - -impl Versionable for v4::topup::TopUpMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V4 - } -} - -impl Versionable for v5::topup::TopUpMessage { - fn version(&self) -> AuthenticatorVersion { - AuthenticatorVersion::V5 - } -} - -pub trait InitMessage: Versionable + fmt::Debug { - fn pub_key(&self) -> PeerPublicKey; -} - -impl InitMessage for v2::registration::InitMessage { - fn pub_key(&self) -> PeerPublicKey { - self.pub_key - } -} - -impl InitMessage for v3::registration::InitMessage { - fn pub_key(&self) -> PeerPublicKey { - self.pub_key - } -} - -impl InitMessage for v4::registration::InitMessage { - fn pub_key(&self) -> PeerPublicKey { - self.pub_key - } -} - -impl InitMessage for v5::registration::InitMessage { - fn pub_key(&self) -> PeerPublicKey { - self.pub_key - } -} - -pub trait FinalMessage: Versionable + fmt::Debug { - fn gateway_client_pub_key(&self) -> PeerPublicKey; - fn gateway_client_ipv4(&self) -> Option; - fn gateway_client_ipv6(&self) -> Option; - fn gateway_client_mac(&self) -> Vec; - fn credential(&self) -> Option; -} - -impl FinalMessage for v2::registration::FinalMessage { - fn gateway_client_pub_key(&self) -> PeerPublicKey { - self.gateway_client.pub_key - } - - fn gateway_client_ipv4(&self) -> Option { - match self.gateway_client.private_ip { - std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), - std::net::IpAddr::V6(_) => None, - } - } - - fn gateway_client_ipv6(&self) -> Option { - None - } - - fn gateway_client_mac(&self) -> Vec { - self.gateway_client.mac.to_vec() - } - - fn credential(&self) -> Option { - self.credential.clone() - } -} - -impl FinalMessage for v3::registration::FinalMessage { - fn gateway_client_pub_key(&self) -> PeerPublicKey { - self.gateway_client.pub_key - } - - fn gateway_client_ipv4(&self) -> Option { - match self.gateway_client.private_ip { - std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), - std::net::IpAddr::V6(_) => None, - } - } - - fn gateway_client_ipv6(&self) -> Option { - None - } - - fn gateway_client_mac(&self) -> Vec { - self.gateway_client.mac.to_vec() - } - - fn credential(&self) -> Option { - self.credential.clone() - } -} - -impl FinalMessage for v4::registration::FinalMessage { - fn gateway_client_pub_key(&self) -> PeerPublicKey { - self.gateway_client.pub_key - } - - 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() - } -} - -impl FinalMessage for v5::registration::FinalMessage { - fn gateway_client_pub_key(&self) -> PeerPublicKey { - self.gateway_client.pub_key - } - - 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() - } -} - -// Temporary solution for lacking a query message wrapper in monorepo -#[derive(Debug)] -pub struct QueryMessageImpl { - pub pub_key: PeerPublicKey, - pub version: AuthenticatorVersion, -} - -impl Versionable for QueryMessageImpl { - fn version(&self) -> AuthenticatorVersion { - self.version - } -} - -pub trait QueryMessage: Versionable + fmt::Debug { - fn pub_key(&self) -> PeerPublicKey; -} - -impl QueryMessage for QueryMessageImpl { - fn pub_key(&self) -> PeerPublicKey { - self.pub_key - } -} - -pub trait TopUpMessage: Versionable + fmt::Debug { - fn pub_key(&self) -> PeerPublicKey; - fn credential(&self) -> CredentialSpendingData; -} - -impl TopUpMessage for v3::topup::TopUpMessage { - fn pub_key(&self) -> PeerPublicKey { - self.pub_key - } - - fn credential(&self) -> CredentialSpendingData { - self.credential.clone() - } -} - -impl TopUpMessage for v4::topup::TopUpMessage { - fn pub_key(&self) -> PeerPublicKey { - self.pub_key - } - - fn credential(&self) -> CredentialSpendingData { - self.credential.clone() - } -} - -impl TopUpMessage for v5::topup::TopUpMessage { - fn pub_key(&self) -> PeerPublicKey { - self.pub_key - } - - fn credential(&self) -> CredentialSpendingData { - self.credential.clone() - } -} - -#[derive(Debug)] -pub enum ClientMessage { - Initial(Box), - Final(Box), - Query(Box), - TopUp(Box), -} - -impl ClientMessage { - // check if message is wasteful e.g. contains a credential - pub fn is_wasteful(&self) -> bool { - match self { - Self::Final(msg) => msg.credential().is_some(), - Self::TopUp(_) => true, - Self::Initial(_) | Self::Query(_) => false, - } - } - - fn version(&self) -> AuthenticatorVersion { - match self { - ClientMessage::Initial(msg) => msg.version(), - ClientMessage::Final(msg) => msg.version(), - ClientMessage::Query(msg) => msg.version(), - ClientMessage::TopUp(msg) => msg.version(), - } - } - - pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec, u64)> { - match self.version() { - 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::UNKNOWN => Err(Error::UnknownVersion), - } - } - - pub fn use_surbs(&self) -> bool { - match self.version() { - AuthenticatorVersion::V2 | AuthenticatorVersion::V3 | AuthenticatorVersion::V4 => false, - AuthenticatorVersion::V5 => true, - AuthenticatorVersion::UNKNOWN => true, - } - } -} - -pub trait Id { - fn id(&self) -> u64; -} - -impl Id for v2::response::PendingRegistrationResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v3::response::PendingRegistrationResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v4::response::PendingRegistrationResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v5::response::PendingRegistrationResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v2::response::RegisteredResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v3::response::RegisteredResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v4::response::RegisteredResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v5::response::RegisteredResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v2::response::RemainingBandwidthResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v3::response::RemainingBandwidthResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v4::response::RemainingBandwidthResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v5::response::RemainingBandwidthResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v3::response::TopUpBandwidthResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v4::response::TopUpBandwidthResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -impl Id for v5::response::TopUpBandwidthResponse { - fn id(&self) -> u64 { - self.request_id - } -} - -pub trait PendingRegistrationResponse: Id + fmt::Debug { - fn nonce(&self) -> u64; - fn verify( - &self, - gateway_key: &PrivateKey, - ) -> std::result::Result<(), nym_authenticator_requests::Error>; - fn pub_key(&self) -> PeerPublicKey; - fn private_ips(&self) -> IpPair; -} - -impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { - fn nonce(&self) -> u64 { - self.reply.nonce - } - - fn verify( - &self, - gateway_key: &PrivateKey, - ) -> std::result::Result<(), nym_authenticator_requests::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_ip.into() - } -} - -impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { - fn nonce(&self) -> u64 { - self.reply.nonce - } - - fn verify( - &self, - gateway_key: &PrivateKey, - ) -> std::result::Result<(), nym_authenticator_requests::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_ip.into() - } -} - -impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { - fn nonce(&self) -> u64 { - self.reply.nonce - } - - fn verify( - &self, - gateway_key: &PrivateKey, - ) -> std::result::Result<(), nym_authenticator_requests::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() - } -} - -impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { - fn nonce(&self) -> u64 { - self.reply.nonce - } - - fn verify( - &self, - gateway_key: &PrivateKey, - ) -> std::result::Result<(), nym_authenticator_requests::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 - } -} - -pub trait RegisteredResponse: Id + fmt::Debug { - fn private_ips(&self) -> IpPair; - fn pub_key(&self) -> PeerPublicKey; - fn wg_port(&self) -> u16; -} - -impl RegisteredResponse for v2::response::RegisteredResponse { - fn private_ips(&self) -> IpPair { - self.reply.private_ip.into() - } - - fn pub_key(&self) -> PeerPublicKey { - self.reply.pub_key - } - - fn wg_port(&self) -> u16 { - self.reply.wg_port - } -} - -impl RegisteredResponse for v3::response::RegisteredResponse { - fn private_ips(&self) -> IpPair { - self.reply.private_ip.into() - } - - fn pub_key(&self) -> PeerPublicKey { - self.reply.pub_key - } - - fn wg_port(&self) -> u16 { - self.reply.wg_port - } -} -impl RegisteredResponse for v4::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 v5::response::RegisteredResponse { - fn private_ips(&self) -> IpPair { - self.reply.private_ips - } - - fn pub_key(&self) -> PeerPublicKey { - self.reply.pub_key - } - - fn wg_port(&self) -> u16 { - self.reply.wg_port - } -} - -pub trait RemainingBandwidthResponse: Id + fmt::Debug { - fn available_bandwidth(&self) -> Option; -} - -impl RemainingBandwidthResponse for v2::response::RemainingBandwidthResponse { - fn available_bandwidth(&self) -> Option { - self.reply.as_ref().map(|r| r.available_bandwidth) - } -} - -impl RemainingBandwidthResponse for v3::response::RemainingBandwidthResponse { - fn available_bandwidth(&self) -> Option { - self.reply.as_ref().map(|r| r.available_bandwidth) - } -} - -impl RemainingBandwidthResponse for v4::response::RemainingBandwidthResponse { - fn available_bandwidth(&self) -> Option { - self.reply.as_ref().map(|r| r.available_bandwidth) - } -} - -impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse { - fn available_bandwidth(&self) -> Option { - self.reply.as_ref().map(|r| r.available_bandwidth) - } -} - -pub trait TopUpBandwidthResponse: Id + fmt::Debug { - fn available_bandwidth(&self) -> i64; -} - -impl TopUpBandwidthResponse for v3::response::TopUpBandwidthResponse { - fn available_bandwidth(&self) -> i64 { - self.reply.available_bandwidth - } -} - -impl TopUpBandwidthResponse for v4::response::TopUpBandwidthResponse { - fn available_bandwidth(&self) -> i64 { - self.reply.available_bandwidth - } -} - -impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse { - fn available_bandwidth(&self) -> i64 { - self.reply.available_bandwidth - } -} - -#[derive(Debug)] -pub enum AuthenticatorResponse { - PendingRegistration(Box), - Registered(Box), - RemainingBandwidth(Box), - TopUpBandwidth(Box), -} - -impl Id for AuthenticatorResponse { - fn id(&self) -> u64 { - match self { - AuthenticatorResponse::PendingRegistration(pending_registration_response) => { - pending_registration_response.id() - } - AuthenticatorResponse::Registered(registered_response) => registered_response.id(), - AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { - remaining_bandwidth_response.id() - } - AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { - top_up_bandwidth_response.id() - } - } - } -} - -impl From for AuthenticatorResponse { - fn from(value: v2::response::AuthenticatorResponse) -> Self { - match value.data { - v2::response::AuthenticatorResponseData::PendingRegistration( - pending_registration_response, - ) => Self::PendingRegistration(Box::new(pending_registration_response)), - v2::response::AuthenticatorResponseData::Registered(registered_response) => { - Self::Registered(Box::new(registered_response)) - } - v2::response::AuthenticatorResponseData::RemainingBandwidth( - remaining_bandwidth_response, - ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), - } - } -} - -impl From for AuthenticatorResponse { - fn from(value: v3::response::AuthenticatorResponse) -> Self { - match value.data { - v3::response::AuthenticatorResponseData::PendingRegistration( - pending_registration_response, - ) => Self::PendingRegistration(Box::new(pending_registration_response)), - v3::response::AuthenticatorResponseData::Registered(registered_response) => { - Self::Registered(Box::new(registered_response)) - } - v3::response::AuthenticatorResponseData::RemainingBandwidth( - remaining_bandwidth_response, - ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), - v3::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { - Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) - } - } - } -} - -impl From for AuthenticatorResponse { - fn from(value: v4::response::AuthenticatorResponse) -> Self { - match value.data { - v4::response::AuthenticatorResponseData::PendingRegistration( - pending_registration_response, - ) => Self::PendingRegistration(Box::new(pending_registration_response)), - v4::response::AuthenticatorResponseData::Registered(registered_response) => { - Self::Registered(Box::new(registered_response)) - } - v4::response::AuthenticatorResponseData::RemainingBandwidth( - remaining_bandwidth_response, - ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), - v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { - Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) - } - } - } -} - -impl From for AuthenticatorResponse { - fn from(value: v5::response::AuthenticatorResponse) -> Self { - match value.data { - v5::response::AuthenticatorResponseData::PendingRegistration( - pending_registration_response, - ) => Self::PendingRegistration(Box::new(pending_registration_response)), - v5::response::AuthenticatorResponseData::Registered(registered_response) => { - Self::Registered(Box::new(registered_response)) - } - v5::response::AuthenticatorResponseData::RemainingBandwidth( - remaining_bandwidth_response, - ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), - v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { - Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) - } - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum AuthenticatorVersion { - V2, - V3, - V4, - V5, - UNKNOWN, -} - -impl AuthenticatorVersion { - pub const LATEST: AuthenticatorVersion = AuthenticatorVersion::V5; -} - -impl fmt::Display for AuthenticatorVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::V2 => write!(f, "v2"), - Self::V3 => write!(f, "v3"), - Self::V4 => write!(f, "v4"), - Self::V5 => write!(f, "v5"), - Self::UNKNOWN => write!(f, "unknown"), - } - } -} - -impl From for AuthenticatorVersion { - fn from(value: u8) -> Self { - if value == 2 { - Self::V2 - } else if value == 3 { - Self::V3 - } else if value == 4 { - Self::V4 - } else if value == 5 { - Self::V5 - } else { - Self::UNKNOWN - } - } -} - -impl From<&str> for AuthenticatorVersion { - fn from(value: &str) -> Self { - let Ok(semver) = semver::Version::parse(value) else { - return Self::UNKNOWN; - }; - - semver.into() - } -} - -impl From> for AuthenticatorVersion { - fn from(value: Option<&String>) -> Self { - match value { - None => Self::UNKNOWN, - Some(value) => value.as_str().into(), - } - } -} - -impl From for AuthenticatorVersion { - fn from(value: String) -> Self { - Self::from(value.as_str()) - } -} - -impl From> for AuthenticatorVersion { - fn from(value: Option) -> Self { - value.as_ref().into() - } -} - -impl From for AuthenticatorVersion { - fn from(semver: semver::Version) -> Self { - if semver.major < 1 { - return Self::UNKNOWN; - } - if semver.minor < 1 { - return Self::UNKNOWN; - } - if semver.minor == 1 && semver.patch == 9 { - return Self::V2; - } - if semver.minor == 1 && semver.patch >= 10 { - return Self::V3; - } - if semver.minor < 6 { - return Self::V4; - } - if semver.minor == 6 && semver.patch == 0 { - return Self::V4; - } - if semver.minor == 6 && semver.patch >= 1 { - return Self::V5; - } - Self::LATEST - } -} - -#[derive(Clone)] pub struct AuthenticatorClient { - auth_mix_client: AuthenticatorMixnetClient, - auth_recipient: Recipient, + mixnet_listener: MixnetMessageBroadcastReceiver, + mixnet_sender: MixnetMessageInputSender, + our_nym_address: Recipient, + pub auth_recipient: Recipient, auth_version: AuthenticatorVersion, + + keypair: KeyPair, + ip_addr: IpAddr, } impl AuthenticatorClient { - pub fn new( - auth_mix_client: AuthenticatorMixnetClient, + #[allow(clippy::too_many_arguments)] + fn new_type( + data_path: &Option, + mixnet_listener: MixnetMessageBroadcastReceiver, + mixnet_sender: MixnetMessageInputSender, + our_nym_address: Recipient, auth_recipient: Recipient, auth_version: AuthenticatorVersion, + private_file_name: &str, + public_file_name: &str, + ip_addr: IpAddr, ) -> Self { - Self { - auth_mix_client, - auth_recipient, - auth_version, - } - } + let mut rng = OsRng; - pub fn auth_recipient(&self) -> Recipient { - self.auth_recipient - } - - pub fn auth_version(&self) -> AuthenticatorVersion { - self.auth_version - } - - pub async fn send(&mut self, message: &ClientMessage) -> Result { - self.auth_mix_client - .send(message, self.auth_recipient) - .await - } -} - -pub struct AuthenticatorMixnetClient { - mixnet_listener: MixnetMessageBroadcastReceiver, - mixnet_sender: MixnetClientSender, - our_nym_address: Recipient, -} - -impl Clone for AuthenticatorMixnetClient { - fn clone(&self) -> Self { - Self { - mixnet_listener: self.mixnet_listener.resubscribe(), - mixnet_sender: self.mixnet_sender.clone(), - our_nym_address: self.our_nym_address, - } - } -} - -impl AuthenticatorMixnetClient { - pub async fn new( - mixnet_sender: MixnetClientSender, - mixnet_listener: MixnetMessageBroadcastReceiver, - our_nym_address: Recipient, - ) -> Self { + let keypair = if let Some(data_path) = data_path { + let paths = KeyPairPath::new( + data_path.join(private_file_name), + data_path.join(public_file_name), + ); + helpers::load_or_generate_keypair(&mut rng, paths) + } else { + KeyPair::new(&mut rng) + }; Self { mixnet_listener, mixnet_sender, our_nym_address, + auth_recipient, + auth_version, + keypair, + ip_addr, } } - pub async fn send( - &mut self, - message: &ClientMessage, - authenticator_address: Recipient, - ) -> Result { - self.send_inner(message, authenticator_address).await + pub fn new_entry( + data_path: &Option, + mixnet_listener: MixnetMessageBroadcastReceiver, + mixnet_sender: MixnetMessageInputSender, + our_nym_address: Recipient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ip_addr: IpAddr, + ) -> Self { + Self::new_type( + data_path, + mixnet_listener, + mixnet_sender, + our_nym_address, + auth_recipient, + auth_version, + DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME, + DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME, + ip_addr, + ) } - async fn send_inner( + pub fn new_exit( + data_path: &Option, + mixnet_listener: MixnetMessageBroadcastReceiver, + mixnet_sender: MixnetMessageInputSender, + our_nym_address: Recipient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ip_addr: IpAddr, + ) -> Self { + Self::new_type( + data_path, + mixnet_listener, + mixnet_sender, + our_nym_address, + auth_recipient, + auth_version, + DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME, + DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME, + ip_addr, + ) + } + + pub async fn send_and_wait_for_response( &mut self, message: &ClientMessage, - authenticator_address: Recipient, ) -> Result { - let request_id = self - .send_connect_request(message, authenticator_address) - .await?; + let request_id = self.send_request(message).await?; debug!("Waiting for reply..."); - self.listen_for_connect_response(request_id).await + self.listen_for_response(request_id).await } - async fn send_connect_request( - &self, - message: &ClientMessage, - authenticator_address: Recipient, - ) -> Result { + async fn send_request(&self, message: &ClientMessage) -> Result { let (data, request_id) = message.bytes(self.our_nym_address)?; // We use 20 surbs for the connect request because typically the @@ -1131,7 +150,7 @@ impl AuthenticatorMixnetClient { } else { IncludedSurbs::ExposeSelfAddress }; - let input_message = create_input_message(authenticator_address, data, surbs); + let input_message = helpers::create_input_message(self.auth_recipient, data, surbs); self.mixnet_sender .send(input_message) @@ -1141,10 +160,7 @@ impl AuthenticatorMixnetClient { Ok(request_id) } - async fn listen_for_connect_response( - &mut self, - request_id: u64, - ) -> Result { + async fn listen_for_response(&mut self, request_id: u64) -> Result { let timeout = tokio::time::sleep(Duration::from_secs(10)); tokio::pin!(timeout); @@ -1159,16 +175,27 @@ impl AuthenticatorMixnetClient { return Err(Error::NoMixnetMessagesReceived); } Ok(msg) => { - if !check_if_authenticator_message(&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 = check_auth_message_version(&msg)?; + 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(Error::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), @@ -1190,43 +217,152 @@ impl AuthenticatorMixnetClient { } } } -} -fn check_if_authenticator_message(message: &ReconstructedMessage) -> bool { - if let Some(msg_type) = message.message.get(1) { - ServiceProviderType::Authenticator as u8 == *msg_type - } else { - false - } -} - -fn check_auth_message_version(message: &ReconstructedMessage) -> Result { - // Assuing it's an Authenticator message, it will have a version as its first byte - if let Some(&version) = message.message.first() { - Ok(version.into()) - } else { - Err(Error::NoVersionInMessage) - } -} - -fn create_input_message( - recipient: Recipient, - data: Vec, - surbs: IncludedSurbs, -) -> nym_sdk::mixnet::InputMessage { - match surbs { - IncludedSurbs::Amount(surbs) => nym_sdk::mixnet::InputMessage::new_anonymous( - recipient, - data, - surbs, - TransmissionLane::General, - None, - ), - IncludedSurbs::ExposeSelfAddress => nym_sdk::mixnet::InputMessage::new_regular( - recipient, - data, - TransmissionLane::General, - None, - ), + pub async fn register_wireguard( + &mut self, + controller: &dyn BandwidthTicketProvider, + ticketbook_type: TicketType, + ) -> Result { + debug!("Registering with the wg gateway..."); + let init_message = match self.auth_version { + AuthenticatorVersion::V1 => return Err(Error::UnsupportedAuthenticatorVersion), + AuthenticatorVersion::V2 => { + ClientMessage::Initial(Box::new(v2::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V3 => { + ClientMessage::Initial(Box::new(v3::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V4 => { + ClientMessage::Initial(Box::new(v4::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V5 => { + ClientMessage::Initial(Box::new(v5::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion), + }; + trace!("sending init msg to {}: {:?}", &self.ip_addr, &init_message); + let response = self.send_and_wait_for_response(&init_message).await?; + let registered_data = match response { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + // Unwrap since we have already checked that we have the keypair. + debug!("Verifying data"); + if let Err(e) = pending_registration_response.verify(self.keypair.private_key()) { + return Err(Error::VerificationFailed(e)); + } + + trace!( + "received \"pending-registration\" msg from {}: {:?}", + &self.ip_addr, + &pending_registration_response + ); + + let credential = Some( + controller + .get_ecash_ticket( + ticketbook_type, + self.auth_recipient.gateway(), + DEFAULT_TICKETS_TO_SPEND, + ) + .await + .map_err(|source| Error::GetTicket { + ticketbook_type, + source, + })? + .data, + ); + + let finalized_message = match self.auth_version { + AuthenticatorVersion::V1 => return Err(Error::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, + })) + } + AuthenticatorVersion::UNKNOWN => { + return Err(Error::UnsupportedAuthenticatorVersion); + } + }; + trace!( + "sending final msg to {}: {:?}", + &self.ip_addr, + &finalized_message + ); + + let response = self.send_and_wait_for_response(&finalized_message).await?; + let AuthenticatorResponse::Registered(registered_response) = response else { + return Err(Error::InvalidGatewayAuthResponse); + }; + registered_response + } + AuthenticatorResponse::Registered(registered_response) => registered_response, + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + trace!( + "received \"registered\" msg from {}: {:?}", + &self.ip_addr, + ®istered_data + ); + + let gateway_data = GatewayData { + public_key: registered_data.pub_key().inner().into(), + endpoint: SocketAddr::from_str(&format!( + "{}:{}", + self.ip_addr, + registered_data.wg_port() + )) + .map_err(Error::FailedToParseEntryGatewaySocketAddr)?, + private_ipv4: registered_data.private_ips().ipv4, + private_ipv6: registered_data.private_ips().ipv6, + }; + + Ok(gateway_data) } } diff --git a/nym-authenticator-client/src/mixnet_listener.rs b/nym-authenticator-client/src/mixnet_listener.rs index 113148b8ce..9cb6828e83 100644 --- a/nym-authenticator-client/src/mixnet_listener.rs +++ b/nym-authenticator-client/src/mixnet_listener.rs @@ -1,113 +1,160 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// To remove with the Registration Client PR -#![allow(clippy::unwrap_used)] - use std::sync::Arc; use futures::StreamExt; -use nym_sdk::mixnet::{MixnetClient, ReconstructedMessage}; -use tokio::{sync::broadcast, task::JoinHandle}; +use nym_sdk::mixnet::{InputMessage, MixnetClient, MixnetMessageSender, ReconstructedMessage}; +use tokio::{ + sync::{broadcast, mpsc}, + task::JoinHandle, +}; use tokio_util::sync::CancellationToken; -use crate::AuthenticatorMixnetClient; - -pub type SharedMixnetClient = Arc>>; pub type MixnetMessageBroadcastSender = broadcast::Sender>; pub type MixnetMessageBroadcastReceiver = broadcast::Receiver>; +pub type MixnetMessageInputSender = mpsc::Sender; +pub type MixnetMessageInputReceiver = mpsc::Receiver; // This could be another type, to abstract the mixnet message creation to here // The AuthClientsMixnetListener listens to mixnet messages and rebroadcasts them to the // AuthClients, or whoever else is interested. -// While it is running, it has a lock on the shared mixnet client. This is the reason it's -// designed to be able to start and stop, so that the lock can be released when it's not needed. +// It also manages the message input for the mixnet so it can keep the sole ownership of the MixnetClient // // NOTE: this is potentially bit wasteful. Ideally we should have proper channels where the // recipient only gets messages they're interested in. pub struct AuthClientMixnetListener { - // The shared mixnet client that we're listening to - mixnet_client: SharedMixnetClient, + // The mixnet client that we're listening to + mixnet_client: MixnetClient, // Broadcast channel for the messages that we re-broadcast to the AuthClients message_broadcast: MixnetMessageBroadcastSender, + // Channel for message to send to the mixnet + input_message_tx: MixnetMessageInputSender, // we keep on to make sure it's open + input_message_rx: MixnetMessageInputReceiver, + // Listen to cancel from the outside world shutdown_token: CancellationToken, } impl AuthClientMixnetListener { - pub fn new(mixnet_client: SharedMixnetClient, shutdown_token: CancellationToken) -> Self { + pub fn new(mixnet_client: MixnetClient, shutdown_token: CancellationToken) -> Self { let (message_broadcast, _) = broadcast::channel(100); + let (input_message_tx, input_message_rx) = mpsc::channel(100); Self { mixnet_client, message_broadcast, + input_message_tx, + input_message_rx, shutdown_token, } } - pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver { - self.message_broadcast.subscribe() - } + async fn run(mut self) -> Self { + let mixnet_cancel_token = self.mixnet_client.cancellation_token(); + self.shutdown_token.run_until_cancelled(async { + loop { + tokio::select! { + biased; + _ = mixnet_cancel_token.cancelled() => { + tracing::debug!("AuthClientMixnetListener: mixnet client was shutdown"); + break; + } - async fn run(self) { - let mut mixnet_client = self.mixnet_client.lock().await.take().unwrap(); - self.shutdown_token - .run_until_cancelled(async { - while let Some(event) = mixnet_client.next().await { - if let Err(err) = self.message_broadcast.send(Arc::new(event)) { - tracing::error!("Failed to broadcast mixnet message: {err}"); + // Sending loop + input_msg = self.input_message_rx.recv() => { + match input_msg { + None => { + tracing::error!("All senders were dropped. It shouldn't happen as we're holding one"); + break; + }, + Some(mix_msg) => { + if let Err(err) = self.mixnet_client.send(mix_msg).await { + tracing::error!("Failed to send mixnet message: {err}"); + } + }, + } + } + // Receiving loop + msg = self.mixnet_client.next() => { + match msg { + None => { + tracing::error!("Mixnet client stream ended unexpectedly"); + break; + }, + Some(event) => { + if let Err(err) = self.message_broadcast.send(Arc::new(event)) { + tracing::error!("Failed to broadcast mixnet message: {err}"); + } + }, + + } } } - tracing::error!("Mixnet client stream ended unexpectedly"); - }) - .await; - self.mixnet_client.lock().await.replace(mixnet_client); + } + tracing::debug!("AuthClientMixnetListener is shutting down"); + }).await; + + self + } + + // Disconnects the mixnet client and effectively drop itself, since it doesn't work without one, and reconnecting isn't supported + pub async fn disconnect_mixnet_client(self) { + if !self.mixnet_client.cancellation_token().is_cancelled() { + self.mixnet_client.disconnect().await; + } } pub fn start(self) -> AuthClientMixnetListenerHandle { - let mixnet_client = self.mixnet_client.clone(); let message_broadcast = self.message_broadcast.clone(); + let message_sender = self.input_message_tx.clone(); + // Allows stopping only this, e.g. if we don't need it in the new bandwidth controller + let cancellation_token = self.shutdown_token.clone(); + let mixnet_cancellation_token = self.mixnet_client.cancellation_token(); let handle = tokio::spawn(self.run()); AuthClientMixnetListenerHandle { - mixnet_client, message_broadcast, + message_sender, + cancellation_token, + mixnet_cancellation_token, handle, } } } pub struct AuthClientMixnetListenerHandle { - mixnet_client: SharedMixnetClient, message_broadcast: MixnetMessageBroadcastSender, - handle: JoinHandle<()>, + message_sender: MixnetMessageInputSender, + cancellation_token: CancellationToken, + mixnet_cancellation_token: CancellationToken, + handle: JoinHandle, } impl AuthClientMixnetListenerHandle { - /// Returns new `AuthClient` or `None` if `MixnetClient` is already moved from shared reference. - pub async fn new_auth_client(&self) -> Option { - let mixnet_client_guard = self.mixnet_client.lock().await; - let mixnet_client_ref = mixnet_client_guard.as_ref()?; - let mixnet_sender = mixnet_client_ref.split_sender(); - let nym_address = *mixnet_client_ref.nym_address(); - - Some( - AuthenticatorMixnetClient::new( - mixnet_sender, - self.message_broadcast.subscribe(), - nym_address, - ) - .await, - ) + pub fn mixnet_sender(&self) -> MixnetMessageInputSender { + self.message_sender.clone() } pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver { self.message_broadcast.subscribe() } - pub async fn wait(self) { - if let Err(err) = self.handle.await { - tracing::error!("Error waiting for auth clients mixnet listener to stop: {err}"); + pub fn mixnet_cancel_token(&self) -> CancellationToken { + self.mixnet_cancellation_token.clone() + } + + pub async fn stop(self) { + // If shutdown was externally called, that call is a no-op + // If we're only stopping this, it is very much needed + self.cancellation_token.cancel(); + match self.handle.await { + Ok(auth_client_mixnet_listener) => { + auth_client_mixnet_listener.disconnect_mixnet_client().await; + } + Err(e) => { + tracing::error!("Error waiting for auth clients mixnet listener to stop: {e}"); + } } } } diff --git a/nym-ip-packet-client/Cargo.toml b/nym-ip-packet-client/Cargo.toml index 7a66b1358b..86b0e21830 100644 --- a/nym-ip-packet-client/Cargo.toml +++ b/nym-ip-packet-client/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true workspace = true [dependencies] +bincode.workspace = true bytes.workspace = true futures.workspace = true thiserror.workspace = true diff --git a/nym-ip-packet-client/src/connect.rs b/nym-ip-packet-client/src/connect.rs index f499e2373f..5a37cf6d82 100644 --- a/nym-ip-packet-client/src/connect.rs +++ b/nym-ip-packet-client/src/connect.rs @@ -1,15 +1,11 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// To remove with the Registration Client PR -#![allow(clippy::unwrap_used)] - -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use nym_ip_packet_requests::IpPair; use nym_sdk::mixnet::{ - InputMessage, MixnetClient, MixnetClientSender, MixnetMessageSender, Recipient, - TransmissionLane, + InputMessage, MixnetClient, MixnetMessageSender, Recipient, TransmissionLane, }; use tokio::time::sleep; use tokio_util::sync::CancellationToken; @@ -27,8 +23,6 @@ use crate::{ helpers::check_ipr_message_version, }; -pub type SharedMixnetClient = Arc>>; - const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); #[derive(Clone, Debug, PartialEq, Eq)] @@ -43,24 +37,24 @@ enum ConnectionState { pub struct IprClientConnect { // During connection we need the mixnet client, but once connected we expect to setup a channel // from the main mixnet listener at the top-level. - // As such, we drop the shared mixnet client once we're connected. - mixnet_client: SharedMixnetClient, - mixnet_sender: MixnetClientSender, + mixnet_client: MixnetClient, connected: ConnectionState, cancel_token: CancellationToken, } impl IprClientConnect { - pub async fn new(mixnet_client: SharedMixnetClient, cancel_token: CancellationToken) -> Self { - let mixnet_sender = mixnet_client.lock().await.as_ref().unwrap().split_sender(); + pub async fn new(mixnet_client: MixnetClient, cancel_token: CancellationToken) -> Self { Self { mixnet_client, - mixnet_sender, connected: ConnectionState::Disconnected, cancel_token, } } + pub fn into_mixnet_client(self) -> MixnetClient { + self.mixnet_client + } + pub async fn connect(&mut self, ip_packet_router_address: Recipient) -> Result { if self.connected != ConnectionState::Disconnected { return Err(Error::AlreadyConnected); @@ -95,12 +89,12 @@ impl IprClientConnect { // We use 20 surbs for the connect request because typically the IPR is configured to have // a min threshold of 10 surbs that it reserves for itself to request additional surbs. let surbs = 20; - self.mixnet_sender + self.mixnet_client .send(create_input_message( ip_packet_router_address, request, surbs, - )) + )?) .await .map_err(|err| Error::SdkError(Box::new(err)))?; @@ -133,26 +127,31 @@ impl IprClientConnect { } } - async fn listen_for_connect_response(&self, request_id: u64) -> Result { + async fn listen_for_connect_response(&mut self, request_id: u64) -> Result { // Connecting is basically synchronous from the perspective of the mixnet client, so it's safe // to just grab ahold of the mutex and keep it until we get the response. - let mut mixnet_client_handle = self.mixnet_client.lock().await; - let mixnet_client = mixnet_client_handle.as_mut().unwrap(); let timeout = sleep(IPR_CONNECT_TIMEOUT); tokio::pin!(timeout); + let mixnet_cancel_token = self.mixnet_client.cancellation_token(); + loop { tokio::select! { _ = self.cancel_token.cancelled() => { error!("Cancelled while waiting for reply to connect request"); return Err(Error::Cancelled); }, + + _ = mixnet_cancel_token.cancelled() => { + error!("Mixnet client stopped while waiting for reply to connect request"); + return Err(Error::Cancelled); + }, _ = &mut timeout => { error!("Timed out waiting for reply to connect request"); return Err(Error::TimeoutWaitingForConnectResponse); }, - msgs = mixnet_client.wait_for_messages() => match msgs { + msgs = self.mixnet_client.wait_for_messages() => match msgs { None => { return Err(Error::NoMixnetMessagesReceived); } @@ -188,12 +187,12 @@ fn create_input_message( recipient: Recipient, request: IpPacketRequest, surbs: u32, -) -> InputMessage { - InputMessage::new_anonymous( +) -> Result { + Ok(InputMessage::new_anonymous( recipient, - request.to_bytes().unwrap(), + request.to_bytes()?, surbs, TransmissionLane::General, None, - ) + )) } diff --git a/nym-ip-packet-client/src/error.rs b/nym-ip-packet-client/src/error.rs index 0979602dc2..b88c332f67 100644 --- a/nym-ip-packet-client/src/error.rs +++ b/nym-ip-packet-client/src/error.rs @@ -18,9 +18,6 @@ pub enum Error { )] ReceivedResponseWithNewVersion { expected: u8, received: u8 }, - #[error("got reply for connect request, but it appears intended for the wrong address?")] - GotReplyIntendedForWrongAddress, - #[error("unexpected connect response")] UnexpectedConnectResponse, @@ -42,10 +39,8 @@ pub enum Error { #[error("already connected to the mixnet")] AlreadyConnected, - #[error("failed to create connect request")] - FailedToCreateConnectRequest { - source: nym_ip_packet_requests::sign::SignatureError, - }, + #[error(transparent)] + Bincode(#[from] bincode::Error), } // Result type based on our error type diff --git a/nym-wg-gateway-client/Cargo.toml b/nym-registration-client/Cargo.toml similarity index 65% rename from nym-wg-gateway-client/Cargo.toml rename to nym-registration-client/Cargo.toml index 7870fd96b7..faa96b86cc 100644 --- a/nym-wg-gateway-client/Cargo.toml +++ b/nym-registration-client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "nym-wg-gateway-client" +name = "nym-registration-client" version = "0.1.0" authors.workspace = true repository.workspace = true @@ -12,17 +12,17 @@ license.workspace = true workspace = true [dependencies] -nym-authenticator-client = { path = "../nym-authenticator-client" } -nym-authenticator-requests = { path = "../common/authenticator-requests" } -nym-bandwidth-controller = { path = "../common/bandwidth-controller" } -nym-credentials-interface = { path = "../common/credentials-interface" } -nym-crypto = { path = "../common/crypto" } -nym-node-requests = { path = "../nym-node/nym-node-requests" } -nym-pemstore = { path = "../common/pemstore" } -nym-sdk = { path = "../sdk/rust/nym-sdk" } -nym-statistics-common = { path = "../common/statistics" } -nym-validator-client = { path = "../common/client-libs/validator-client" } -rand.workspace = true thiserror.workspace = true +tokio.workspace = true +tokio-util.workspace = true tracing.workspace = true url.workspace = true + +nym-authenticator-client = { path = "../nym-authenticator-client" } +nym-bandwidth-controller = { path = "../common/bandwidth-controller" } +nym-credential-storage = { path = "../common/credential-storage" } +nym-credentials-interface = { path = "../common/credentials-interface" } +nym-ip-packet-client = { path = "../nym-ip-packet-client" } +nym-registration-common = { path = "../common/registration" } +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-validator-client = { path = "../common/client-libs/validator-client" } diff --git a/nym-registration-client/src/builder/config.rs b/nym-registration-client/src/builder/config.rs new file mode 100644 index 0000000000..317d6a7bbc --- /dev/null +++ b/nym-registration-client/src/builder/config.rs @@ -0,0 +1,206 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_credential_storage::persistent_storage::PersistentStorage; +use nym_registration_common::NymNode; +use nym_sdk::{ + mixnet::{ + CredentialStorage, GatewaysDetailsStore, KeyStore, MixnetClient, MixnetClientBuilder, + MixnetClientStorage, OnDiskPersistent, ReplyStorageBackend, StoragePaths, + }, + DebugConfig, NymNetworkDetails, RememberMe, TopologyProvider, UserAgent, +}; + +#[cfg(unix)] +use std::{os::fd::RawFd, sync::Arc}; +use std::{path::PathBuf, time::Duration}; +use tokio_util::sync::CancellationToken; + +use crate::error::RegistrationClientError; + +const VPN_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(15); + +pub struct BuilderConfig { + pub entry_node: NymNode, + pub exit_node: NymNode, + pub data_path: Option, + pub mixnet_client_config: MixnetClientConfig, + pub two_hops: bool, + pub user_agent: UserAgent, + pub custom_topology_provider: Box, + pub network_env: NymNetworkDetails, + pub cancel_token: CancellationToken, + #[cfg(unix)] + pub connection_fd_callback: Arc, +} + +#[derive(Clone, Default, Debug, Eq, PartialEq)] +pub struct MixnetClientConfig { + /// Disable Poission process rate limiting of outbound traffic. + pub disable_poisson_rate: bool, + + /// Disable constant rate background loop cover traffic + pub disable_background_cover_traffic: bool, + + /// The minimum performance of mixnodes to use. + pub min_mixnode_performance: Option, + + /// The minimum performance of gateways to use. + pub min_gateway_performance: Option, +} + +impl BuilderConfig { + pub fn mixnet_client_debug_config(&self) -> DebugConfig { + if self.two_hops { + two_hop_debug_config(&self.mixnet_client_config) + } else { + mixnet_debug_config(&self.mixnet_client_config) + } + } + + pub async fn setup_storage( + &self, + ) -> Result, RegistrationClientError> { + if let Some(path) = &self.data_path { + tracing::debug!("Using custom key storage path: {}", path.display()); + + let storage_paths = StoragePaths::new_from_dir(path) + .map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?; + + let mixnet_client_storage = storage_paths + .initialise_persistent_storage(&self.mixnet_client_debug_config()) + .await + .map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?; + let credential_storage = storage_paths + .persistent_credential_storage() + .await + .map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?; + + Ok(Some((mixnet_client_storage, credential_storage))) + } else { + Ok(None) + } + } + + pub async fn build_and_connect_mixnet_client( + self, + builder: MixnetClientBuilder, + ) -> Result + where + S: MixnetClientStorage + Clone + 'static, + S::ReplyStore: Send + Sync, + S::GatewaysDetailsStore: Sync, + ::StorageError: Sync + Send, + ::StorageError: Send + Sync, + ::StorageError: Send + Sync, + ::StorageError: Send + Sync, + { + let debug_config = self.mixnet_client_debug_config(); + let remember_me = if self.two_hops { + RememberMe::new_vpn() + } else { + RememberMe::new_mixnet() + }; + + let builder = builder + .with_user_agent(self.user_agent) + .request_gateway(self.entry_node.identity.to_string()) + .network_details(self.network_env) + .debug_config(debug_config) + .credentials_mode(true) + .with_remember_me(remember_me) + .custom_topology_provider(self.custom_topology_provider); + + #[cfg(unix)] + let builder = builder.with_connection_fd_callback(self.connection_fd_callback); + + builder + .build() + .map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))? + .connect_to_mixnet() + .await + .map_err(|err| RegistrationClientError::ConnectToMixnet(Box::new(err))) + } +} + +fn two_hop_debug_config(mixnet_client_config: &MixnetClientConfig) -> DebugConfig { + let mut debug_config = DebugConfig::default(); + + debug_config.traffic.average_packet_delay = VPN_AVERAGE_PACKET_DELAY; + + // We disable mix hops for the mixnet connection. + debug_config.traffic.disable_mix_hops = true; + // Always disable poisson process for outbound traffic in wireguard. + debug_config + .traffic + .disable_main_poisson_packet_distribution = true; + // Always disable background cover traffic in wireguard. + debug_config.cover_traffic.disable_loop_cover_traffic_stream = true; + + if let Some(min_mixnode_performance) = mixnet_client_config.min_mixnode_performance { + debug_config.topology.minimum_mixnode_performance = min_mixnode_performance; + } + + if let Some(min_gateway_performance) = mixnet_client_config.min_gateway_performance { + debug_config.topology.minimum_gateway_performance = min_gateway_performance; + } + + log_mixnet_client_config(&debug_config); + debug_config +} + +fn mixnet_debug_config(mixnet_client_config: &MixnetClientConfig) -> DebugConfig { + let mut debug_config = DebugConfig::default(); + debug_config.traffic.average_packet_delay = VPN_AVERAGE_PACKET_DELAY; + + debug_config + .traffic + .disable_main_poisson_packet_distribution = mixnet_client_config.disable_poisson_rate; + + debug_config.cover_traffic.disable_loop_cover_traffic_stream = + mixnet_client_config.disable_background_cover_traffic; + + if let Some(min_mixnode_performance) = mixnet_client_config.min_mixnode_performance { + debug_config.topology.minimum_mixnode_performance = min_mixnode_performance; + } + + if let Some(min_gateway_performance) = mixnet_client_config.min_gateway_performance { + debug_config.topology.minimum_gateway_performance = min_gateway_performance; + } + log_mixnet_client_config(&debug_config); + debug_config +} + +fn log_mixnet_client_config(debug_config: &DebugConfig) { + tracing::info!( + "mixnet client poisson rate limiting: {}", + true_to_disabled( + debug_config + .traffic + .disable_main_poisson_packet_distribution + ) + ); + + tracing::info!( + "mixnet client background loop cover traffic stream: {}", + true_to_disabled(debug_config.cover_traffic.disable_loop_cover_traffic_stream) + ); + + tracing::info!( + "mixnet client minimum mixnode performance: {}", + debug_config.topology.minimum_mixnode_performance, + ); + + tracing::info!( + "mixnet client minimum gateway performance: {}", + debug_config.topology.minimum_gateway_performance, + ); +} + +fn true_to_disabled(val: bool) -> &'static str { + if val { + "disabled" + } else { + "enabled" + } +} diff --git a/nym-registration-client/src/builder/mod.rs b/nym-registration-client/src/builder/mod.rs new file mode 100644 index 0000000000..c798ee1ffb --- /dev/null +++ b/nym-registration-client/src/builder/mod.rs @@ -0,0 +1,96 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_bandwidth_controller::{BandwidthController, BandwidthTicketProvider}; +use nym_credential_storage::ephemeral_storage::EphemeralCredentialStorage; +use nym_sdk::{ + mixnet::{MixnetClient, MixnetClientBuilder}, + NymNetworkDetails, +}; +use nym_validator_client::{ + nyxd::{Config as NyxdClientConfig, NyxdClient}, + QueryHttpRpcNyxdClient, +}; +use std::time::Duration; + +use crate::{config::RegistrationClientConfig, error::RegistrationClientError, RegistrationClient}; +use config::BuilderConfig; + +pub(crate) mod config; + +pub(crate) const MIXNET_CLIENT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30); + +pub struct RegistrationClientBuilder { + pub config: BuilderConfig, +} + +impl RegistrationClientBuilder { + pub fn new(config: BuilderConfig) -> Self { + Self { config } + } + + pub async fn build(self) -> Result { + let storage = self.config.setup_storage().await?; + let config = RegistrationClientConfig { + entry: self.config.entry_node, + exit: self.config.exit_node, + two_hops: self.config.two_hops, + data_path: self.config.data_path.clone(), + }; + let cancel_token = self.config.cancel_token.clone(); + + let nyxd_client = get_nyxd_client(&self.config.network_env)?; + + let (mixnet_client, bandwidth_controller): ( + MixnetClient, + Box, + ) = if let Some((mixnet_client_storage, credential_storage)) = storage { + let builder = MixnetClientBuilder::new_with_storage(mixnet_client_storage); + let mixnet_client = tokio::time::timeout( + MIXNET_CLIENT_STARTUP_TIMEOUT, + self.config.build_and_connect_mixnet_client(builder), + ) + .await??; + let bandwidth_controller = + Box::new(BandwidthController::new(credential_storage, nyxd_client)); + (mixnet_client, bandwidth_controller) + } else { + let builder = MixnetClientBuilder::new_ephemeral(); + let mixnet_client = tokio::time::timeout( + MIXNET_CLIENT_STARTUP_TIMEOUT, + self.config.build_and_connect_mixnet_client(builder), + ) + .await??; + let bandwidth_controller = Box::new(BandwidthController::new( + EphemeralCredentialStorage::default(), + nyxd_client, + )); + (mixnet_client, bandwidth_controller) + }; + let mixnet_client_address = *mixnet_client.nym_address(); + + Ok(RegistrationClient { + mixnet_client, + config, + cancel_token, + mixnet_client_address, + bandwidth_controller, + }) + } +} + +// temporary while we use the legacy bandwidth-controller +fn get_nyxd_client( + network: &NymNetworkDetails, +) -> Result { + let config = NyxdClientConfig::try_from_nym_network_details(network) + .map_err(RegistrationClientError::FailedToCreateNyxdClientConfig)?; + let nyxd_url = network + .endpoints + .first() + .map(|ep| ep.nyxd_url()) + .ok_or(RegistrationClientError::InvalidNyxdUrl)?; + + NyxdClient::connect(config, nyxd_url.as_str()) + .map_err(RegistrationClientError::FailedToConnectUsingNyxdClient) +} diff --git a/nym-registration-client/src/config.rs b/nym-registration-client/src/config.rs new file mode 100644 index 0000000000..4d76fc68ee --- /dev/null +++ b/nym-registration-client/src/config.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_registration_common::NymNode; +use std::path::PathBuf; + +pub struct RegistrationClientConfig { + pub(crate) entry: NymNode, + pub(crate) exit: NymNode, + pub(crate) two_hops: bool, + pub(crate) data_path: Option, +} diff --git a/nym-registration-client/src/error.rs b/nym-registration-client/src/error.rs new file mode 100644 index 0000000000..ed3b09c220 --- /dev/null +++ b/nym-registration-client/src/error.rs @@ -0,0 +1,53 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#[derive(thiserror::Error, Debug)] +pub enum RegistrationClientError { + #[error("failed to build mixnet client")] + BuildMixnetClient(#[source] Box), + + #[error("failed to connect to mixnet")] + ConnectToMixnet(#[source] Box), + + #[error("failed to connect to ip packet router")] + ConnectToIpPacketRouter(#[source] nym_ip_packet_client::Error), + + #[error("the selected node does not have an IP packet router : {node_id}")] + NoIpPacketRouterAddress { node_id: String }, + + #[error( + "wireguard authentication is not possible due to one of the gateways not running the authenticator process: {node_id} " + )] + AuthenticationNotPossible { node_id: String }, + + #[error("Failed to create nyxd client config")] + FailedToCreateNyxdClientConfig(nym_validator_client::nyxd::error::NyxdError), + + #[error("failed to parse nyxd_url")] + InvalidNyxdUrl, + + #[error("Failed to connect using nyxd client")] + FailedToConnectUsingNyxdClient(nym_validator_client::nyxd::error::NyxdError), + + #[error("connection cancelled")] + Cancelled, + + #[error("timeout connecting the mixnet client")] + Timeout(#[from] tokio::time::error::Elapsed), + + #[error("failed to register wireguard with the gateway for {gateway_id}")] + EntryGatewayRegisterWireguard { + gateway_id: String, + authenticator_address: Box, + #[source] + source: Box, + }, + + #[error("failed to register wireguard with the gateway for {gateway_id}")] + ExitGatewayRegisterWireguard { + gateway_id: String, + authenticator_address: Box, + #[source] + source: Box, + }, +} diff --git a/nym-registration-client/src/lib.rs b/nym-registration-client/src/lib.rs new file mode 100644 index 0000000000..e71dd1979c --- /dev/null +++ b/nym-registration-client/src/lib.rs @@ -0,0 +1,157 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use tokio_util::sync::CancellationToken; + +use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient}; +use nym_bandwidth_controller::BandwidthTicketProvider; +use nym_credentials_interface::TicketType; +use nym_ip_packet_client::IprClientConnect; +use nym_registration_common::AssignedAddresses; +use nym_sdk::mixnet::{MixnetClient, Recipient}; + +use crate::config::RegistrationClientConfig; + +mod builder; +mod config; +mod error; +mod types; + +pub use builder::config::{BuilderConfig as RegistrationClientBuilderConfig, MixnetClientConfig}; +pub use builder::RegistrationClientBuilder; +pub use error::RegistrationClientError; +pub use types::{MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult}; + +pub struct RegistrationClient { + mixnet_client: MixnetClient, + config: RegistrationClientConfig, + mixnet_client_address: Recipient, + bandwidth_controller: Box, + cancel_token: CancellationToken, +} + +impl RegistrationClient { + async fn register_mix_exit(self) -> Result { + let entry_mixnet_gateway_ip = self.config.entry.ip_address; + + let exit_mixnet_gateway_ip = self.config.exit.ip_address; + + let ipr_address = self.config.exit.ipr_address.ok_or( + RegistrationClientError::NoIpPacketRouterAddress { + node_id: self.config.exit.identity.to_base58_string(), + }, + )?; + let mut ipr_client = + IprClientConnect::new(self.mixnet_client, self.cancel_token.clone()).await; + let interface_addresses = ipr_client + .connect(ipr_address) + .await + .map_err(RegistrationClientError::ConnectToIpPacketRouter)?; + + Ok(RegistrationResult::Mixnet(Box::new( + MixnetRegistrationResult { + mixnet_client: ipr_client.into_mixnet_client(), + assigned_addresses: AssignedAddresses { + interface_addresses, + exit_mix_address: ipr_address, + mixnet_client_address: self.mixnet_client_address, + entry_mixnet_gateway_ip, + exit_mixnet_gateway_ip, + }, + }, + ))) + } + + async fn register_wg(self) -> Result { + let entry_auth_address = self.config.entry.authenticator_address.ok_or( + RegistrationClientError::AuthenticationNotPossible { + node_id: self.config.entry.identity.to_base58_string(), + }, + )?; + + let exit_auth_address = self.config.exit.authenticator_address.ok_or( + RegistrationClientError::AuthenticationNotPossible { + node_id: self.config.exit.identity.to_base58_string(), + }, + )?; + + let entry_version = self.config.entry.version; + tracing::debug!("Entry gateway version: {entry_version}"); + let exit_version = self.config.exit.version; + tracing::debug!("Exit gateway version: {exit_version}"); + + // Start the auth client mixnet listener, which will listen for incoming messages from the + // mixnet and rebroadcast them to the auth clients. + let mixnet_listener = + AuthClientMixnetListener::new(self.mixnet_client, self.cancel_token.clone()).start(); + + let mut entry_auth_client = AuthenticatorClient::new_entry( + &self.config.data_path, + mixnet_listener.subscribe(), + mixnet_listener.mixnet_sender(), + self.mixnet_client_address, + entry_auth_address, + entry_version, + self.config.entry.ip_address, + ); + + let mut exit_auth_client = AuthenticatorClient::new_exit( + &self.config.data_path, + mixnet_listener.subscribe(), + mixnet_listener.mixnet_sender(), + self.mixnet_client_address, + exit_auth_address, + exit_version, + self.config.exit.ip_address, + ); + + let entry_fut = entry_auth_client + .register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardEntry); + let exit_fut = exit_auth_client + .register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardExit); + + let (entry, exit) = Box::pin(async { tokio::join!(entry_fut, exit_fut) }).await; + + let entry = + entry.map_err( + |source| RegistrationClientError::EntryGatewayRegisterWireguard { + gateway_id: self.config.entry.identity.to_base58_string(), + authenticator_address: Box::new(entry_auth_address), + source: Box::new(source), + }, + )?; + let exit = + exit.map_err( + |source| RegistrationClientError::ExitGatewayRegisterWireguard { + gateway_id: self.config.exit.identity.to_base58_string(), + authenticator_address: Box::new(exit_auth_address), + source: Box::new(source), + }, + )?; + + Ok(RegistrationResult::Wireguard(Box::new( + WireguardRegistrationResult { + entry_gateway_client: entry_auth_client, + exit_gateway_client: exit_auth_client, + entry_gateway_data: entry, + exit_gateway_data: exit, + authenticator_listener_handle: mixnet_listener, + bw_controller: self.bandwidth_controller, + }, + ))) + } + + pub async fn register(self) -> Result { + self.cancel_token + .clone() + .run_until_cancelled(async { + if self.config.two_hops { + self.register_wg().await + } else { + self.register_mix_exit().await + } + }) + .await + .ok_or(RegistrationClientError::Cancelled)? + } +} diff --git a/nym-registration-client/src/types.rs b/nym-registration-client/src/types.rs new file mode 100644 index 0000000000..d7f7ad80be --- /dev/null +++ b/nym-registration-client/src/types.rs @@ -0,0 +1,26 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_authenticator_client::{AuthClientMixnetListenerHandle, AuthenticatorClient}; +use nym_bandwidth_controller::BandwidthTicketProvider; +use nym_registration_common::{AssignedAddresses, GatewayData}; +use nym_sdk::mixnet::MixnetClient; + +pub enum RegistrationResult { + Mixnet(Box), + Wireguard(Box), +} + +pub struct MixnetRegistrationResult { + pub assigned_addresses: AssignedAddresses, + pub mixnet_client: MixnetClient, +} + +pub struct WireguardRegistrationResult { + pub entry_gateway_client: AuthenticatorClient, + pub exit_gateway_client: AuthenticatorClient, + pub entry_gateway_data: GatewayData, + pub exit_gateway_data: GatewayData, + pub authenticator_listener_handle: AuthClientMixnetListenerHandle, + pub bw_controller: Box, +} diff --git a/nym-wg-gateway-client/src/deprecated.rs b/nym-wg-gateway-client/src/deprecated.rs deleted file mode 100644 index 65a9d47950..0000000000 --- a/nym-wg-gateway-client/src/deprecated.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::time::Duration; - -use nym_authenticator_client::{ - AuthenticatorClient, AuthenticatorResponse, AuthenticatorVersion, ClientMessage, - QueryMessageImpl, -}; -use nym_authenticator_requests::{v3, v4, v5}; -use nym_credentials_interface::CredentialSpendingData; -use nym_crypto::asymmetric::encryption; -use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::PeerPublicKey; -use nym_sdk::mixnet::Recipient; - -use crate::error::{Error, Result}; - -const RETRY_PERIOD: Duration = Duration::from_secs(30); - -impl crate::WgGatewayClient { - pub fn light_client(&self) -> WgGatewayLightClient { - WgGatewayLightClient { - public_key: *self.keypair.public_key(), - auth_client: self.auth_client.clone(), - } - } -} - -#[derive(Clone)] -pub struct WgGatewayLightClient { - public_key: encryption::PublicKey, - auth_client: AuthenticatorClient, -} -impl WgGatewayLightClient { - pub fn auth_recipient(&self) -> Recipient { - self.auth_client.auth_recipient() - } - - pub fn auth_client(&self) -> &AuthenticatorClient { - &self.auth_client - } - - pub fn set_auth_client(&mut self, auth_client: AuthenticatorClient) { - self.auth_client = auth_client; - } - pub async fn query_bandwidth(&mut self) -> Result> { - let query_message = match self.auth_client.auth_version() { - AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), - version: AuthenticatorVersion::V2, - })), - AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), - version: AuthenticatorVersion::V3, - })), - AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), - version: AuthenticatorVersion::V4, - })), - AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), - version: AuthenticatorVersion::V5, - })), - AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion), - }; - let response = self.auth_client.send(&query_message).await?; - - let available_bandwidth = match response { - nym_authenticator_client::AuthenticatorResponse::RemainingBandwidth( - remaining_bandwidth_response, - ) => { - if let Some(available_bandwidth) = - remaining_bandwidth_response.available_bandwidth() - { - available_bandwidth - } else { - return Ok(None); - } - } - _ => return Err(Error::InvalidGatewayAuthResponse), - }; - - let remaining_pretty = if available_bandwidth > 1024 * 1024 { - format!("{:.2} MB", available_bandwidth as f64 / 1024.0 / 1024.0) - } else { - format!("{} KB", available_bandwidth / 1024) - }; - tracing::debug!( - "Remaining wireguard bandwidth with gateway {} for today: {}", - self.auth_client.auth_recipient().gateway(), - remaining_pretty - ); - if available_bandwidth < 1024 * 1024 { - tracing::warn!( - "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)) - } - async fn send(&mut self, msg: ClientMessage) -> Result { - let now = std::time::Instant::now(); - while now.elapsed() < RETRY_PERIOD { - match self.auth_client.send(&msg).await { - Ok(response) => return Ok(response), - Err(nym_authenticator_client::Error::TimeoutWaitingForConnectResponse) => continue, - Err(source) => { - if msg.is_wasteful() { - return Err(Error::NoRetry { source }); - } else { - return Err(Error::AuthenticatorClientError(source)); - } - } - } - } - if msg.is_wasteful() { - Err(Error::NoRetry { - source: nym_authenticator_client::Error::TimeoutWaitingForConnectResponse, - }) - } else { - Err(Error::AuthenticatorClientError( - nym_authenticator_client::Error::TimeoutWaitingForConnectResponse, - )) - } - } - - pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result { - let top_up_message = match self.auth_client.auth_version() { - AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), - 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.public_key.to_bytes().into()), - credential, - })), - AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), - credential, - })), - AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { - return Err(Error::UnsupportedAuthenticatorVersion); - } - }; - let response = self.send(top_up_message).await?; - - let remaining_bandwidth = match response { - AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { - top_up_bandwidth_response.available_bandwidth() - } - _ => return Err(Error::InvalidGatewayAuthResponse), - }; - - Ok(remaining_bandwidth) - } -} diff --git a/nym-wg-gateway-client/src/error.rs b/nym-wg-gateway-client/src/error.rs deleted file mode 100644 index 9223c04a12..0000000000 --- a/nym-wg-gateway-client/src/error.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use nym_credentials_interface::TicketType; -use nym_sdk::mixnet::NodeIdentity; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("received invalid response from gateway authenticator")] - InvalidGatewayAuthResponse, - - #[error("unknown authenticator version number")] - UnsupportedAuthenticatorVersion, - - #[error(transparent)] - AuthenticatorClientError(#[from] nym_authenticator_client::Error), - - #[error("error that should stop auto retrying")] - NoRetry { - #[source] - source: nym_authenticator_client::Error, - }, - - #[error("verification failure")] - VerificationFailed(#[source] nym_authenticator_requests::Error), - - #[error("failed to parse entry gateway socket addr")] - FailedToParseEntryGatewaySocketAddr(#[source] std::net::AddrParseError), - - #[error("failed to get {ticketbook_type} ticket")] - GetTicket { - ticketbook_type: TicketType, - #[source] - source: nym_bandwidth_controller::error::BandwidthControllerError, - }, -} - -#[derive(Debug, thiserror::Error)] -pub enum ErrorMessage { - #[error("out of bandwidth for gateway: {gateway_id}")] - OutOfBandwidth { gateway_id: Box }, - - #[error("gateway {gateway_id} is erroring out")] - ErrorsFromGateway { gateway_id: Box }, -} - -// Result type based on our error type -pub type Result = std::result::Result; diff --git a/nym-wg-gateway-client/src/lib.rs b/nym-wg-gateway-client/src/lib.rs deleted file mode 100644 index 3d425a6e2f..0000000000 --- a/nym-wg-gateway-client/src/lib.rs +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -pub mod deprecated; -mod error; - -use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - path::PathBuf, - str::FromStr, -}; - -pub use error::{Error, ErrorMessage}; -use nym_authenticator_client::{ - AuthenticatorClient, AuthenticatorMixnetClient, AuthenticatorResponse, AuthenticatorVersion, - ClientMessage, -}; -use nym_authenticator_requests::{v2, v3, v4, v5}; -use nym_bandwidth_controller::PreparedCredential; -use nym_credentials_interface::TicketType; -use nym_crypto::asymmetric::{encryption, x25519::KeyPair, x25519::PublicKey}; -use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::PeerPublicKey; -use nym_pemstore::KeyPairPath; -use nym_sdk::mixnet::{CredentialStorage, NodeIdentity, Recipient}; -use nym_validator_client::QueryHttpRpcNyxdClient; -use rand::{rngs::OsRng, CryptoRng, RngCore}; -use tracing::{debug, error, trace}; - -use crate::error::Result; - -pub const DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_private_entry_wireguard.pem"; -pub const DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_public_entry_wireguard.pem"; -pub const DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_private_exit_wireguard.pem"; -pub const DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_public_exit_wireguard.pem"; - -pub const TICKETS_TO_SPEND: u32 = 1; - -#[derive(Clone, Debug)] -pub struct GatewayData { - pub public_key: PublicKey, - pub endpoint: SocketAddr, - pub private_ipv4: Ipv4Addr, - pub private_ipv6: Ipv6Addr, -} - -pub struct WgGatewayClient { - keypair: encryption::KeyPair, - auth_client: AuthenticatorClient, -} - -impl WgGatewayClient { - fn new_type( - data_path: &Option, - auth_mix_client: AuthenticatorMixnetClient, - auth_recipient: Recipient, - auth_version: AuthenticatorVersion, - private_file_name: &str, - public_file_name: &str, - ) -> Self { - let mut rng = OsRng; - let auth_client = AuthenticatorClient::new(auth_mix_client, auth_recipient, auth_version); - if let Some(data_path) = data_path { - let paths = KeyPairPath::new( - data_path.join(private_file_name), - data_path.join(public_file_name), - ); - let keypair = load_or_generate_keypair(&mut rng, paths); - WgGatewayClient { - keypair, - auth_client, - } - } else { - WgGatewayClient { - keypair: KeyPair::new(&mut rng), - auth_client, - } - } - } - - pub fn new_entry( - data_path: &Option, - auth_mix_client: AuthenticatorMixnetClient, - auth_recipient: Recipient, - auth_version: AuthenticatorVersion, - ) -> Self { - Self::new_type( - data_path, - auth_mix_client, - auth_recipient, - auth_version, - DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME, - DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME, - ) - } - - pub fn new_exit( - data_path: &Option, - auth_mix_client: AuthenticatorMixnetClient, - auth_recipient: Recipient, - auth_version: AuthenticatorVersion, - ) -> Self { - Self::new_type( - data_path, - auth_mix_client, - auth_recipient, - auth_version, - DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME, - DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME, - ) - } - - pub fn keypair(&self) -> &encryption::KeyPair { - &self.keypair - } - - pub fn auth_recipient(&self) -> Recipient { - self.auth_client.auth_recipient() - } - - pub fn auth_version(&self) -> AuthenticatorVersion { - self.auth_client.auth_version() - } - - pub async fn request_bandwidth( - gateway_id: NodeIdentity, - controller: &nym_bandwidth_controller::BandwidthController, - ticketbook_type: TicketType, - ) -> Result - where - ::StorageError: Send + Sync + 'static, - { - let credential = controller - .prepare_ecash_ticket(ticketbook_type, gateway_id.to_bytes(), TICKETS_TO_SPEND) - .await - .map_err(|source| Error::GetTicket { - ticketbook_type, - source, - })?; - Ok(credential) - } - - pub async fn register_wireguard( - &mut self, - gateway_host: IpAddr, - controller: &nym_bandwidth_controller::BandwidthController, - ticketbook_type: TicketType, - ) -> Result - where - ::StorageError: Send + Sync + 'static, - { - debug!("Registering with the wg gateway..."); - let init_message = match self.auth_version() { - AuthenticatorVersion::V2 => { - ClientMessage::Initial(Box::new(v2::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) - } - AuthenticatorVersion::V3 => { - ClientMessage::Initial(Box::new(v3::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) - } - AuthenticatorVersion::V4 => { - ClientMessage::Initial(Box::new(v4::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) - } - AuthenticatorVersion::V5 => { - ClientMessage::Initial(Box::new(v5::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) - } - AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion), - }; - trace!("sending init msg to {}: {:?}", &gateway_host, &init_message); - let response = self.auth_client.send(&init_message).await?; - let registered_data = match response { - AuthenticatorResponse::PendingRegistration(pending_registration_response) => { - // Unwrap since we have already checked that we have the keypair. - debug!("Verifying data"); - if let Err(e) = pending_registration_response.verify(self.keypair.private_key()) { - return Err(Error::VerificationFailed(e)); - } - - trace!( - "received \"pending-registration\" msg from {}: {:?}", - &gateway_host, - &pending_registration_response - ); - - let credential = Some( - Self::request_bandwidth( - self.auth_recipient().gateway(), - controller, - ticketbook_type, - ) - .await? - .data, - ); - - let finalized_message = match self.auth_version() { - 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, - })) - } - AuthenticatorVersion::UNKNOWN => { - return Err(Error::UnsupportedAuthenticatorVersion); - } - }; - trace!( - "sending final msg to {}: {:?}", - &gateway_host, - &finalized_message - ); - - let response = self.auth_client.send(&finalized_message).await?; - let AuthenticatorResponse::Registered(registered_response) = response else { - return Err(Error::InvalidGatewayAuthResponse); - }; - registered_response - } - AuthenticatorResponse::Registered(registered_response) => registered_response, - _ => return Err(Error::InvalidGatewayAuthResponse), - }; - - trace!( - "received \"registered\" msg from {}: {:?}", - &gateway_host, - ®istered_data - ); - - let gateway_data = GatewayData { - public_key: registered_data.pub_key().inner().into(), - endpoint: SocketAddr::from_str(&format!( - "{}:{}", - gateway_host, - registered_data.wg_port() - )) - .map_err(Error::FailedToParseEntryGatewaySocketAddr)?, - private_ipv4: registered_data.private_ips().ipv4, - private_ipv6: registered_data.private_ips().ipv6, - }; - - Ok(gateway_data) - } -} - -fn load_or_generate_keypair(rng: &mut R, paths: KeyPairPath) -> KeyPair { - match nym_pemstore::load_keypair(&paths) { - Ok(keypair) => keypair, - Err(_) => { - let keypair = KeyPair::new(rng); - if let Err(e) = nym_pemstore::store_keypair(&keypair, &paths) { - error!( - "could not store generated keypair at {:?} - {:?}; will use ephemeral keys", - paths, e - ); - } - keypair - } - } -} diff --git a/sdk/rust/nym-sdk/src/lib.rs b/sdk/rust/nym-sdk/src/lib.rs index ddc6088829..8b5bb45568 100644 --- a/sdk/rust/nym-sdk/src/lib.rs +++ b/sdk/rust/nym-sdk/src/lib.rs @@ -20,7 +20,7 @@ pub use nym_client_core::{ NymApiTopologyProvider, NymApiTopologyProviderConfig, TopologyProvider, }, }, - config::DebugConfig, + config::{DebugConfig, RememberMe}, }; pub use nym_network_defaults::{ ChainDetails, DenomDetails, DenomDetailsOwned, NymContracts, NymNetworkDetails, diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index dfe5269d09..bcad96ba9d 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -37,7 +37,6 @@ use std::path::Path; use std::path::PathBuf; #[cfg(unix)] use std::sync::Arc; -use tokio_util::sync::CancellationToken; use url::Url; use zeroize::Zeroizing; @@ -119,11 +118,6 @@ where ::StorageError: Send + Sync, ::StorageError: Send + Sync, { - pub fn with_shutdown_token(self, token: CancellationToken) -> Self { - let shutdown_tracker = ShutdownTracker::new_from_external_shutdown_token(token.into()); - self.custom_shutdown(shutdown_tracker) - } - /// Creates a client builder with the provided client storage implementation. #[must_use] pub fn new_with_storage(storage: S) -> MixnetClientBuilder { @@ -755,13 +749,6 @@ where let packet_type = self.config.debug_config.traffic.packet_type; let (mut started_client, nym_address) = self.connect_to_mixnet_common().await?; - // TODO: more graceful handling here, surely both variants should work... I think? - let Some(tracker) = started_client.shutdown_handle else { - return Err(Error::new_unsupported( - "connecting with socks5 is currently unsupported with custom shutdown", - )); - }; - let client_input = started_client.client_input.register_producer(); let client_output = started_client.client_output.register_consumer(); let client_state = started_client.client_state; @@ -773,14 +760,14 @@ where client_output, client_state.clone(), nym_address, - tracker.child_tracker(), + started_client.shutdown_handle.child_tracker(), packet_type, ); Ok(Socks5MixnetClient { nym_address, client_state, - task_handle: tracker, + task_handle: started_client.shutdown_handle, socks5_config, }) } diff --git a/sdk/rust/nym-sdk/src/mixnet/native_client.rs b/sdk/rust/nym-sdk/src/mixnet/native_client.rs index 4cd7f65890..c9495680b2 100644 --- a/sdk/rust/nym-sdk/src/mixnet/native_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/native_client.rs @@ -24,6 +24,7 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use tokio::sync::RwLockReadGuard; +use tokio_util::sync::CancellationToken; /// Client connected to the Nym mixnet. pub struct MixnetClient { @@ -52,7 +53,7 @@ pub struct MixnetClient { pub(crate) stats_events_reporter: ClientStatsSender, /// The task manager that controls all the spawned tasks that the clients uses to do it's job. - pub(crate) shutdown_handle: Option, + pub(crate) shutdown_handle: ShutdownTracker, pub(crate) packet_type: Option, // internal state used for the `Stream` implementation @@ -72,7 +73,7 @@ impl MixnetClient { client_state: ClientState, reconstructed_receiver: ReconstructedMessagesReceiver, stats_events_reporter: ClientStatsSender, - task_handle: Option, + task_handle: ShutdownTracker, packet_type: Option, client_request_sender: ClientRequestSender, forget_me: ForgetMe, @@ -122,6 +123,11 @@ impl MixnetClient { &self.nym_address } + /// Get a child token of the root, to monitor unexpected shutdown, without causing one + pub fn cancellation_token(&self) -> CancellationToken { + self.shutdown_handle.child_shutdown_token().inner().clone() + } + pub fn client_request_sender(&self) -> ClientRequestSender { self.client_request_sender.clone() } @@ -238,9 +244,7 @@ impl MixnetClient { tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } - if let Some(tracker) = self.shutdown_handle { - tracker.shutdown().await; - } + self.shutdown_handle.shutdown().await; } pub async fn send_forget_me(&self) -> Result<()> { diff --git a/wasm/client/src/client.rs b/wasm/client/src/client.rs index 0a9b0cc053..e423672a1d 100644 --- a/wasm/client/src/client.rs +++ b/wasm/client/src/client.rs @@ -249,10 +249,7 @@ impl NymClientBuilder { client_input: Arc::new(client_input), client_state: Arc::new(started_client.client_state), _full_topology: None, - // this cannot fail as we haven't passed an external task manager - _task_manager: started_client - .shutdown_handle - .expect("shutdown manager missing"), + _task_manager: started_client.shutdown_handle, packet_type, }) } diff --git a/wasm/mix-fetch/src/client.rs b/wasm/mix-fetch/src/client.rs index b68a6d0db1..736c82c413 100644 --- a/wasm/mix-fetch/src/client.rs +++ b/wasm/mix-fetch/src/client.rs @@ -187,12 +187,7 @@ impl MixFetchClientBuilder { self_address, client_input, requests: active_requests, - // this cannot fail as we haven't passed an external task manager - _shutdown_manager: Mutex::new( - started_client - .shutdown_handle - .expect("shutdown manager missing"), - ), + _shutdown_manager: Mutex::new(started_client.shutdown_handle), }) } }