diff --git a/Cargo.lock b/Cargo.lock index 43d67d10f4..61b9c928e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5010,6 +5010,25 @@ dependencies = [ "tokio", ] +[[package]] +name = "nym-authenticator-client" +version = "0.1.0" +dependencies = [ + "bincode", + "futures", + "nym-authenticator-requests", + "nym-credentials-interface", + "nym-crypto", + "nym-sdk", + "nym-service-provider-requests-common", + "nym-wireguard-types", + "semver 1.0.26", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "nym-authenticator-requests" version = "0.1.0" @@ -5607,6 +5626,7 @@ dependencies = [ "aead", "aes", "aes-gcm-siv", + "base64 0.22.1", "blake3", "bs58", "cipher", @@ -6005,6 +6025,20 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "nym-ip-packet-client" +version = "0.1.0" +dependencies = [ + "bytes", + "futures", + "nym-ip-packet-requests", + "nym-sdk", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "nym-ip-packet-requests" version = "0.1.0" @@ -6409,7 +6443,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "3.3.1" +version = "3.3.2" dependencies = [ "ammonia", "anyhow", @@ -7375,6 +7409,26 @@ 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" @@ -12002,17 +12056,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - [[package]] name = "windows-result" version = "0.3.4" diff --git a/Cargo.toml b/Cargo.toml index 3ad9a3e0a3..d92cda4976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,10 +111,12 @@ members = [ "gateway", "nym-api", "nym-api/nym-api-requests", + "nym-authenticator-client", "nym-browser-extension/storage", "nym-credential-proxy/nym-credential-proxy", "nym-credential-proxy/nym-credential-proxy-requests", "nym-credential-proxy/vpn-api-lib-wasm", + "nym-ip-packet-client", "nym-network-monitor", "nym-node", "nym-node-status-api/nym-node-status-agent", @@ -125,6 +127,7 @@ members = [ "nym-outfox", "nym-signers-monitor", "nym-statistics-api", "nym-validator-rewarder", + "nym-wg-gateway-client", "nyx-chain-watcher", "sdk/ffi/cpp", "sdk/ffi/go", diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 4e5af9129a..96549a448d 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -11,6 +11,7 @@ repository = { workspace = true } aes-gcm-siv = { workspace = true, optional = true } aes = { workspace = true, optional = true } aead = { workspace = true, optional = true } +base64.workspace = true bs58 = { workspace = true } blake3 = { workspace = true, features = ["traits-preview"], optional = true } ctr = { workspace = true, optional = true } diff --git a/common/crypto/src/asymmetric/x25519/mod.rs b/common/crypto/src/asymmetric/x25519/mod.rs index 9b73adb910..3f41319a97 100644 --- a/common/crypto/src/asymmetric/x25519/mod.rs +++ b/common/crypto/src/asymmetric/x25519/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use base64::Engine; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; @@ -158,6 +159,15 @@ impl PublicKey { .map_err(|source| KeyRecoveryError::MalformedPublicKeyString { source })?; Self::from_bytes(&bytes) } + + pub fn from_base64(s: &str) -> Option { + let bytes = base64::engine::general_purpose::STANDARD.decode(s).ok()?; + Self::from_bytes(&bytes).ok() + } + + pub fn to_base64(&self) -> String { + base64::engine::general_purpose::STANDARD.encode(self.as_bytes()) + } } impl FromStr for PublicKey { diff --git a/nym-authenticator-client/Cargo.toml b/nym-authenticator-client/Cargo.toml new file mode 100644 index 0000000000..35a72ae019 --- /dev/null +++ b/nym-authenticator-client/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nym-authenticator-client" +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] +bincode.workspace = true +futures.workspace = true +semver.workspace = true +thiserror.workspace = true +tokio-util.workspace = true +tokio.workspace = true +tracing.workspace = true + +nym-authenticator-requests = { path = "../common/authenticator-requests" } +nym-credentials-interface = { path = "../common/credentials-interface" } +nym-crypto = { path = "../common/crypto" } +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" } +nym-wireguard-types = { path = "../common/wireguard-types" } diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs new file mode 100644 index 0000000000..7bb0cd4dc4 --- /dev/null +++ b/nym-authenticator-client/src/error.rs @@ -0,0 +1,42 @@ +#[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), + + #[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(transparent)] + Bincode(#[from] bincode::Error), + + #[error("gateway doesn't support this type of message")] + UnsupportedMessage, + + #[error(transparent)] + AuthenticatorRequests(#[from] nym_authenticator_requests::Error), +} + +// Result type based on our error type +pub type Result = std::result::Result; diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs new file mode 100644 index 0000000000..03112e1651 --- /dev/null +++ b/nym-authenticator-client/src/lib.rs @@ -0,0 +1,1232 @@ +use std::{ + fmt, + net::{Ipv4Addr, Ipv6Addr}, + time::Duration, +}; + +use nym_authenticator_requests::{ + v2, v3, v4, + v5::{self, registration::IpPair}, +}; + +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_wireguard_types::PeerPublicKey; +use tracing::{debug, error}; + +mod error; +mod mixnet_listener; + +pub use crate::{ + error::{Error, Result}, + mixnet_listener::{ + AuthClientMixnetListener, AuthClientMixnetListenerHandle, MixnetMessageBroadcastReceiver, + }, +}; + +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, + auth_version: AuthenticatorVersion, +} + +impl AuthenticatorClient { + pub fn new( + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ) -> Self { + Self { + auth_mix_client, + auth_recipient, + auth_version, + } + } + + 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 { + Self { + mixnet_listener, + mixnet_sender, + our_nym_address, + } + } + + pub async fn send( + &mut self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> Result { + self.send_inner(message, authenticator_address).await + } + + async fn send_inner( + &mut self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> Result { + let request_id = self + .send_connect_request(message, authenticator_address) + .await?; + + debug!("Waiting for reply..."); + self.listen_for_connect_response(request_id).await + } + + async fn send_connect_request( + &self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> 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 = create_input_message(authenticator_address, data, surbs); + + self.mixnet_sender + .send(input_message) + .await + .map_err(|e| Error::SendMixnetMessage(Box::new(e)))?; + + Ok(request_id) + } + + async fn listen_for_connect_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) => { + if !check_if_authenticator_message(&msg) { + debug!("Received non-authenticator message while waiting for connect response"); + continue; + } + // Confirm that the version is correct + let version = check_auth_message_version(&msg)?; + + // Then we deserialize the message + debug!("AuthClient: got message while waiting for connect response with version {version:?}"); + let ret: Result = match version { + 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); + } + } + } + } + } + } +} + +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, + ), + } +} diff --git a/nym-authenticator-client/src/mixnet_listener.rs b/nym-authenticator-client/src/mixnet_listener.rs new file mode 100644 index 0000000000..113148b8ce --- /dev/null +++ b/nym-authenticator-client/src/mixnet_listener.rs @@ -0,0 +1,113 @@ +// 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 tokio_util::sync::CancellationToken; + +use crate::AuthenticatorMixnetClient; + +pub type SharedMixnetClient = Arc>>; +pub type MixnetMessageBroadcastSender = broadcast::Sender>; +pub type MixnetMessageBroadcastReceiver = broadcast::Receiver>; + +// 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. +// +// 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, + + // Broadcast channel for the messages that we re-broadcast to the AuthClients + message_broadcast: MixnetMessageBroadcastSender, + + // Listen to cancel from the outside world + shutdown_token: CancellationToken, +} + +impl AuthClientMixnetListener { + pub fn new(mixnet_client: SharedMixnetClient, shutdown_token: CancellationToken) -> Self { + let (message_broadcast, _) = broadcast::channel(100); + Self { + mixnet_client, + message_broadcast, + shutdown_token, + } + } + + pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver { + self.message_broadcast.subscribe() + } + + 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}"); + } + } + tracing::error!("Mixnet client stream ended unexpectedly"); + }) + .await; + self.mixnet_client.lock().await.replace(mixnet_client); + } + + pub fn start(self) -> AuthClientMixnetListenerHandle { + let mixnet_client = self.mixnet_client.clone(); + let message_broadcast = self.message_broadcast.clone(); + let handle = tokio::spawn(self.run()); + + AuthClientMixnetListenerHandle { + mixnet_client, + message_broadcast, + handle, + } + } +} + +pub struct AuthClientMixnetListenerHandle { + mixnet_client: SharedMixnetClient, + message_broadcast: MixnetMessageBroadcastSender, + 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 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}"); + } + } +} diff --git a/nym-ip-packet-client/Cargo.toml b/nym-ip-packet-client/Cargo.toml new file mode 100644 index 0000000000..7a66b1358b --- /dev/null +++ b/nym-ip-packet-client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "nym-ip-packet-client" +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] +bytes.workspace = true +futures.workspace = true +thiserror.workspace = true +tokio-util.workspace = true +tokio.workspace = true +tracing.workspace = true + +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-ip-packet-requests = { path = "../common/ip-packet-requests" } diff --git a/nym-ip-packet-client/src/connect.rs b/nym-ip-packet-client/src/connect.rs new file mode 100644 index 0000000000..f499e2373f --- /dev/null +++ b/nym-ip-packet-client/src/connect.rs @@ -0,0 +1,199 @@ +// 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 nym_ip_packet_requests::IpPair; +use nym_sdk::mixnet::{ + InputMessage, MixnetClient, MixnetClientSender, MixnetMessageSender, Recipient, + TransmissionLane, +}; +use tokio::time::sleep; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error}; + +use crate::{ + current::{ + request::IpPacketRequest, + response::{ + ConnectResponse, ConnectResponseReply, ControlResponse, IpPacketResponse, + IpPacketResponseData, + }, + }, + error::{Error, Result}, + helpers::check_ipr_message_version, +}; + +pub type SharedMixnetClient = Arc>>; + +const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ConnectionState { + Disconnected, + Connecting, + Connected, + #[allow(unused)] + Disconnecting, +} + +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, + 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(); + Self { + mixnet_client, + mixnet_sender, + connected: ConnectionState::Disconnected, + cancel_token, + } + } + + pub async fn connect(&mut self, ip_packet_router_address: Recipient) -> Result { + if self.connected != ConnectionState::Disconnected { + return Err(Error::AlreadyConnected); + } + + tracing::info!("Connecting to exit gateway"); + self.connected = ConnectionState::Connecting; + match self.connect_inner(ip_packet_router_address).await { + Ok(ips) => { + debug!("Successfully connected to the ip-packet-router"); + self.connected = ConnectionState::Connected; + Ok(ips) + } + Err(err) => { + error!("Failed to connect to the ip-packet-router: {:?}", err); + self.connected = ConnectionState::Disconnected; + Err(err) + } + } + } + + async fn connect_inner(&mut self, ip_packet_router_address: Recipient) -> Result { + let request_id = self.send_connect_request(ip_packet_router_address).await?; + + debug!("Waiting for reply..."); + self.listen_for_connect_response(request_id).await + } + + async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result { + let (request, request_id) = IpPacketRequest::new_connect_request(None); + + // 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 + .send(create_input_message( + ip_packet_router_address, + request, + surbs, + )) + .await + .map_err(|err| Error::SdkError(Box::new(err)))?; + + Ok(request_id) + } + + async fn handle_connect_response(&self, response: ConnectResponse) -> Result { + debug!("Handling dynamic connect response"); + match response.reply { + ConnectResponseReply::Success(r) => Ok(r.ips), + ConnectResponseReply::Failure(reason) => Err(Error::ConnectRequestDenied { reason }), + } + } + + async fn handle_ip_packet_router_response(&self, response: IpPacketResponse) -> Result { + let control_response = match response.data { + IpPacketResponseData::Control(control_response) => control_response, + _ => { + error!("Received non-control response while waiting for connect response"); + return Err(Error::UnexpectedConnectResponse); + } + }; + + match *control_response { + ControlResponse::Connect(resp) => self.handle_connect_response(resp).await, + response => { + error!("Unexpected response: {response:?}"); + Err(Error::UnexpectedConnectResponse) + } + } + } + + async fn listen_for_connect_response(&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); + + loop { + tokio::select! { + _ = self.cancel_token.cancelled() => { + error!("Cancelled 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 { + None => { + return Err(Error::NoMixnetMessagesReceived); + } + Some(msgs) => { + for msg in msgs { + // Confirm that the version is correct + if let Err(err) = check_ipr_message_version(&msg) { + tracing::info!("Mixnet message version mismatch: {err}"); + continue; + } + + // Then we deserialize the message + tracing::debug!("IprClient: got message while waiting for connect response"); + let Ok(response) = IpPacketResponse::from_reconstructed_message(&msg) else { + // This is ok, it's likely just one of our self-pings + tracing::debug!("Failed to deserialize mixnet message"); + continue; + }; + + if response.id() == Some(request_id) { + tracing::debug!("Got response with matching id"); + return self.handle_ip_packet_router_response(response).await; + } + } + } + } + } + } + } +} + +fn create_input_message( + recipient: Recipient, + request: IpPacketRequest, + surbs: u32, +) -> InputMessage { + InputMessage::new_anonymous( + recipient, + request.to_bytes().unwrap(), + surbs, + TransmissionLane::General, + None, + ) +} diff --git a/nym-ip-packet-client/src/error.rs b/nym-ip-packet-client/src/error.rs new file mode 100644 index 0000000000..0979602dc2 --- /dev/null +++ b/nym-ip-packet-client/src/error.rs @@ -0,0 +1,52 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::current::response::ConnectFailureReason; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("nym sdk")] + SdkError(#[source] Box), + + #[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("got reply for connect request, but it appears intended for the wrong address?")] + GotReplyIntendedForWrongAddress, + + #[error("unexpected connect response")] + UnexpectedConnectResponse, + + #[error("mixnet client stopped returning responses")] + NoMixnetMessagesReceived, + + #[error("timeout waiting for connect response from exit gateway (ipr)")] + TimeoutWaitingForConnectResponse, + + #[error("connection cancelled")] + Cancelled, + + #[error("connect request denied: {reason}")] + ConnectRequestDenied { reason: ConnectFailureReason }, + + #[error("failed to get version from message")] + NoVersionInMessage, + + #[error("already connected to the mixnet")] + AlreadyConnected, + + #[error("failed to create connect request")] + FailedToCreateConnectRequest { + source: nym_ip_packet_requests::sign::SignatureError, + }, +} + +// Result type based on our error type +pub type Result = std::result::Result; diff --git a/nym-ip-packet-client/src/helpers.rs b/nym-ip-packet-client/src/helpers.rs new file mode 100644 index 0000000000..2f1cf718cb --- /dev/null +++ b/nym-ip-packet-client/src/helpers.rs @@ -0,0 +1,30 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::cmp::Ordering; + +use nym_sdk::mixnet::ReconstructedMessage; + +use crate::{current::VERSION as CURRENT_VERSION, error::Result, Error}; + +pub(crate) fn check_ipr_message_version(message: &ReconstructedMessage) -> Result<()> { + // Assuming it's a IPR message, it will have a version as its first byte + if let Some(version) = message.message.first() { + match version.cmp(&CURRENT_VERSION) { + Ordering::Greater => Err(Error::ReceivedResponseWithNewVersion { + expected: CURRENT_VERSION, + received: *version, + }), + Ordering::Less => Err(Error::ReceivedResponseWithOldVersion { + expected: CURRENT_VERSION, + received: *version, + }), + Ordering::Equal => { + // We're good + Ok(()) + } + } + } else { + Err(Error::NoVersionInMessage) + } +} diff --git a/nym-ip-packet-client/src/lib.rs b/nym-ip-packet-client/src/lib.rs new file mode 100644 index 0000000000..8aa5ffdf42 --- /dev/null +++ b/nym-ip-packet-client/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod connect; +mod error; +mod helpers; +mod listener; + +pub use connect::IprClientConnect; +pub use error::Error; +pub use listener::{IprListener, MixnetMessageOutcome}; + +// Re-export the currently used version +pub use nym_ip_packet_requests::v8 as current; diff --git a/nym-ip-packet-client/src/listener.rs b/nym-ip-packet-client/src/listener.rs new file mode 100644 index 0000000000..7380300175 --- /dev/null +++ b/nym-ip-packet-client/src/listener.rs @@ -0,0 +1,121 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use bytes::Bytes; +use futures::StreamExt; +use nym_ip_packet_requests::{codec::MultiIpPacketCodec, v8::response::ControlResponse}; +use nym_sdk::mixnet::ReconstructedMessage; +use tokio_util::codec::FramedRead; +use tracing::{debug, error, info, warn}; + +use crate::{ + current::{ + request::{ControlRequest, IpPacketRequest, IpPacketRequestData}, + response::{InfoLevel, IpPacketResponse, IpPacketResponseData}, + }, + helpers::check_ipr_message_version, +}; + +pub enum MixnetMessageOutcome { + IpPackets(Vec), + MixnetSelfPing, + Disconnect, +} + +pub struct IprListener {} + +#[derive(Debug, thiserror::Error)] +pub enum IprListenerError { + #[error(transparent)] + IprClientError(#[from] crate::Error), +} + +impl IprListener { + pub fn new() -> Self { + Self {} + } + + fn is_mix_ping(&self, request: &IpPacketRequest) -> bool { + match request.data { + IpPacketRequestData::Control(ref control) => { + matches!(**control, ControlRequest::Ping(_)) + } + _ => { + debug!("Received unexpected request: {request:?}"); + false + } + } + } + + pub async fn handle_reconstructed_message( + &mut self, + message: ReconstructedMessage, + ) -> Result, IprListenerError> { + check_ipr_message_version(&message)?; + + match IpPacketResponse::from_reconstructed_message(&message) { + Ok(response) => { + match response.data { + IpPacketResponseData::Data(data_response) => { + // Un-bundle the mixnet message and send the individual IP packets + // to the tun device + let framed_reader = FramedRead::new( + data_response.ip_packet.as_ref(), + MultiIpPacketCodec::new(), + ); + let responses: Vec = framed_reader + .filter_map(|res| async { res.ok().map(|packet| packet.into_bytes()) }) + .collect() + .await; + return Ok(Some(MixnetMessageOutcome::IpPackets(responses))); + } + IpPacketResponseData::Control(control_response) => match *control_response { + ControlResponse::Connect(_) => { + info!("Received connect response when already connected - ignoring"); + } + ControlResponse::Disconnect(_) => { + info!("Received disconnect response"); + return Ok(Some(MixnetMessageOutcome::Disconnect)); + } + ControlResponse::UnrequestedDisconnect(_) => { + info!("Received unrequested disconnect response, ignoring for now"); + } + ControlResponse::Pong(_) => { + info!("Received pong response, ignoring for now"); + } + ControlResponse::Health(_) => { + info!("Received health response, ignoring for now"); + } + ControlResponse::Info(info) => { + let msg = + format!("Received info response from the mixnet: {}", info.reply); + match info.level { + InfoLevel::Info => info!("{msg}"), + InfoLevel::Warn => warn!("{msg}"), + InfoLevel::Error => error!("{msg}"), + } + } + }, + } + } + Err(err) => { + // The exception to when we are not expecting a response, is when we + // are sending a ping to ourselves. + if let Ok(request) = IpPacketRequest::from_reconstructed_message(&message) { + if self.is_mix_ping(&request) { + return Ok(Some(MixnetMessageOutcome::MixnetSelfPing)); + } + } else { + warn!("Failed to deserialize reconstructed message: {err}"); + } + } + } + Ok(None) + } +} + +impl Default for IprListener { + fn default() -> Self { + Self::new() + } +} diff --git a/nym-wg-gateway-client/Cargo.toml b/nym-wg-gateway-client/Cargo.toml new file mode 100644 index 0000000000..7870fd96b7 --- /dev/null +++ b/nym-wg-gateway-client/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nym-wg-gateway-client" +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] +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 +tracing.workspace = true +url.workspace = true diff --git a/nym-wg-gateway-client/src/deprecated.rs b/nym-wg-gateway-client/src/deprecated.rs new file mode 100644 index 0000000000..65a9d47950 --- /dev/null +++ b/nym-wg-gateway-client/src/deprecated.rs @@ -0,0 +1,154 @@ +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 new file mode 100644 index 0000000000..9223c04a12 --- /dev/null +++ b/nym-wg-gateway-client/src/error.rs @@ -0,0 +1,48 @@ +// 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 new file mode 100644 index 0000000000..3d425a6e2f --- /dev/null +++ b/nym-wg-gateway-client/src/lib.rs @@ -0,0 +1,302 @@ +// 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 + } + } +}