diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bad8f2cf..6c4e17091f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2025.4-dorina-patched] (2025-03-06) + +- use legacy crypto for constructing SURB headers ([#5579]) +- bugfix: make sure to correctly decode response content when putting it into error message ([#5571]) +- Tweak surb management to be more conservative ([#5570]) +- Deserialize v5 authenticator requests ([#5568]) +- chore: additional logs when attempting to load ecash keys ([#5567]) +- add full response body to error message upon decoding failure ([#5566]) +- hotfix: ensure we bail on merkle leaves insertion upon missing data ([#5565]) +- feature: v2 authentication request (#5537) ([#5563]) +- Create authenticator v5 request/response types ([#5561]) + +[#5579]: https://github.com/nymtech/nym/pull/5579 +[#5571]: https://github.com/nymtech/nym/pull/5571 +[#5570]: https://github.com/nymtech/nym/pull/5570 +[#5568]: https://github.com/nymtech/nym/pull/5568 +[#5567]: https://github.com/nymtech/nym/pull/5567 +[#5566]: https://github.com/nymtech/nym/pull/5566 +[#5565]: https://github.com/nymtech/nym/pull/5565 +[#5563]: https://github.com/nymtech/nym/pull/5563 +[#5561]: https://github.com/nymtech/nym/pull/5561 + ## [2025.4-dorina] (2025-03-04) - fixed sphinx version metrics registration ([#5546]) diff --git a/Cargo.lock b/Cargo.lock index d6ef0a5a80..568f9e55b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2433,7 +2433,7 @@ dependencies = [ [[package]] name = "explorer-api" -version = "1.1.47" +version = "1.1.48" dependencies = [ "chrono", "clap", @@ -4786,7 +4786,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.51" +version = "1.1.53" dependencies = [ "anyhow", "async-trait", @@ -5035,7 +5035,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.49" +version = "1.1.50" dependencies = [ "anyhow", "base64 0.22.1", @@ -5118,7 +5118,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.49" +version = "1.1.50" dependencies = [ "bs58", "clap", @@ -5880,8 +5880,11 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bytes", + "encoding_rs", "hickory-resolver", "http 1.2.0", + "mime", "nym-bin-common", "once_cell", "reqwest 0.12.4", @@ -6159,7 +6162,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.50" +version = "1.1.51" dependencies = [ "addr", "anyhow", @@ -6210,7 +6213,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.6.0" +version = "1.6.2" dependencies = [ "anyhow", "arc-swap", @@ -6495,6 +6498,7 @@ name = "nym-pemstore" version = "0.3.0" dependencies = [ "pem", + "tracing", ] [[package]] @@ -6594,7 +6598,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.49" +version = "1.1.50" dependencies = [ "bs58", "clap", @@ -7199,7 +7203,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.14" +version = "0.1.15" dependencies = [ "anyhow", "bytes", @@ -9448,9 +9452,9 @@ dependencies = [ [[package]] name = "sphinx-packet" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535f2c430778bf59c22249fcc1ed6d384129eb2f0f694706015d636c688f9ac6" +checksum = "c23047e0cf36ff6904603f499fd13153425cdf5ba47bfbaedbc999da0bd92f4e" dependencies = [ "aes", "arrayref", diff --git a/Cargo.toml b/Cargo.toml index e934f39bab..ba1acb4468 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,6 +241,7 @@ doc-comment = "0.3" dotenvy = "0.15.6" ecdsa = "0.16" ed25519-dalek = "2.1" +encoding_rs = "0.8.35" env_logger = "0.11.6" envy = "0.4" etherparse = "0.13.0" @@ -321,7 +322,7 @@ serde_with = "3.9.0" serde_yaml = "0.9.25" sha2 = "0.10.8" si-scale = "0.2.3" -sphinx-packet = "0.3.1" +sphinx-packet = "=0.3.2" sqlx = "0.7.4" strum = "0.26" strum_macros = "0.26" diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index a3dfdc0661..9663b41d78 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.49" +version = "1.1.50" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 934c0b6db9..9c4b1522ca 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.49" +version = "1.1.50" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index ed987a9a50..351a9b443c 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -6,14 +6,15 @@ pub mod v1; pub mod v2; pub mod v3; pub mod v4; +pub mod v5; mod error; mod util; pub use error::Error; -pub use v4 as latest; +pub use v5 as latest; -pub const CURRENT_VERSION: u8 = 4; +pub const CURRENT_VERSION: u8 = 5; fn make_bincode_serializer() -> impl bincode::Options { use bincode::Options; diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs index 7c7064d174..baaaf8b422 100644 --- a/common/authenticator-requests/src/traits.rs +++ b/common/authenticator-requests/src/traits.rs @@ -8,8 +8,8 @@ use nym_sphinx::addressing::clients::Recipient; use nym_wireguard_types::PeerPublicKey; use crate::{ - v1, v2, v3, - v4::{self, registration::IpPair}, + v1, v2, v3, v4, + v5::{self, registration::IpPair}, Error, }; @@ -19,6 +19,7 @@ pub enum AuthenticatorVersion { V2, V3, V4, + V5, UNKNOWN, } @@ -34,6 +35,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V3 } else if value.version == v4::VERSION { AuthenticatorVersion::V4 + } else if value.version == v5::VERSION { + AuthenticatorVersion::V5 } else { AuthenticatorVersion::UNKNOWN } @@ -68,6 +71,12 @@ impl InitMessage for v4::registration::InitMessage { } } +impl InitMessage for v5::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + pub trait FinalMessage { fn pub_key(&self) -> PeerPublicKey; fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; @@ -138,6 +147,24 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.verify(private_key, nonce) } + fn private_ips(&self) -> IpPair { + self.gateway_client.private_ips.into() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v5::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + fn private_ips(&self) -> IpPair { self.gateway_client.private_ips } @@ -182,29 +209,39 @@ impl TopUpMessage for v4::topup::TopUpMessage { } } +impl TopUpMessage for v5::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + pub enum AuthenticatorRequest { Initial { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, Final { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, QueryBandwidth { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, TopUpBandwidth { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, } @@ -218,7 +255,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final { @@ -227,7 +264,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { @@ -237,7 +274,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -251,20 +288,20 @@ impl From for AuthenticatorRequest { v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + 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: value.reply_to, + 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: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -278,20 +315,20 @@ impl From for AuthenticatorRequest { v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + 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: value.reply_to, + 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: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -299,7 +336,7 @@ impl From for AuthenticatorRequest { Self::TopUpBandwidth { msg: top_up_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -313,20 +350,20 @@ impl From for AuthenticatorRequest { v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + 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: value.reply_to, + 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: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -334,7 +371,42 @@ impl From for AuthenticatorRequest { Self::TopUpBandwidth { msg: top_up_message, protocol: value.protocol, - reply_to: value.reply_to, + 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/v5/conversion.rs b/common/authenticator-requests/src/v5/conversion.rs new file mode 100644 index 0000000000..e3f7da0830 --- /dev/null +++ b/common/authenticator-requests/src/v5/conversion.rs @@ -0,0 +1,478 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; + +use crate::{v4, v5}; + +impl From for v5::request::AuthenticatorRequest { + fn from(authenticator_request: v4::request::AuthenticatorRequest) -> Self { + Self { + protocol: Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: authenticator_request.data.into(), + request_id: authenticator_request.request_id, + } + } +} + +impl From for v5::request::AuthenticatorRequestData { + fn from(authenticator_request_data: v4::request::AuthenticatorRequestData) -> Self { + match authenticator_request_data { + v4::request::AuthenticatorRequestData::Initial(init_msg) => { + v5::request::AuthenticatorRequestData::Initial(init_msg.into()) + } + v4::request::AuthenticatorRequestData::Final(final_msg) => { + v5::request::AuthenticatorRequestData::Final(Box::new((*final_msg).into())) + } + v4::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => { + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + } + v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into()) + } + } + } +} + +impl From for v5::registration::InitMessage { + fn from(init_msg: v4::registration::InitMessage) -> Self { + Self { + pub_key: init_msg.pub_key, + } + } +} + +impl From for v5::registration::FinalMessage { + fn from(final_msg: v4::registration::FinalMessage) -> Self { + Self { + gateway_client: final_msg.gateway_client.into(), + credential: final_msg.credential, + } + } +} + +impl From for v5::registration::GatewayClient { + fn from(gateway_client: v4::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v4::registration::GatewayClient { + fn from(gateway_client: v5::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v5::registration::ClientMac { + fn from(client_mac: v4::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From for v4::registration::ClientMac { + fn from(client_mac: v5::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From> for Box { + fn from(top_up_message: Box) -> Self { + Box::new(v5::topup::TopUpMessage { + pub_key: top_up_message.pub_key, + credential: top_up_message.credential, + }) + } +} + +impl From for v5::response::AuthenticatorResponse { + fn from(value: v4::response::AuthenticatorResponse) -> Self { + Self { + protocol: Protocol { + version: 5, + service_provider_type: value.protocol.service_provider_type, + }, + data: value.data.into(), + } + } +} + +impl From for v5::response::AuthenticatorResponseData { + fn from(authenticator_response_data: v4::response::AuthenticatorResponseData) -> Self { + match authenticator_response_data { + v4::response::AuthenticatorResponseData::PendingRegistration(pending_response) => { + v5::response::AuthenticatorResponseData::PendingRegistration( + pending_response.into(), + ) + } + v4::response::AuthenticatorResponseData::Registered(registered_response) => { + v5::response::AuthenticatorResponseData::Registered(registered_response.into()) + } + v4::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + ), + v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => { + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into()) + } + } + } +} + +impl From for v5::response::RegisteredResponse { + fn from(value: v4::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::response::PendingRegistrationResponse { + fn from(value: v4::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::registration::RegistrationData { + fn from(value: v4::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v4::registration::RegistrationData { + fn from(value: v5::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::response::RemainingBandwidthResponse { + fn from(value: v4::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.map(Into::into), + } + } +} + +impl From for v5::response::TopUpBandwidthResponse { + fn from(value: v4::response::TopUpBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::registration::RegistredData { + fn from(value: v4::registration::RegistredData) -> Self { + Self { + pub_key: value.pub_key, + private_ips: value.private_ips.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::registration::RemainingBandwidthData { + fn from(value: v4::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} + +impl From for v5::registration::IpPair { + fn from(value: v4::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +impl From for v4::registration::IpPair { + fn from(value: v5::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, + }; + + use nym_credentials_interface::CredentialSpendingData; + use nym_crypto::asymmetric::encryption::PrivateKey; + use nym_sphinx::addressing::Recipient; + use nym_wireguard_types::PeerPublicKey; + use x25519_dalek::PublicKey; + + use super::*; + use crate::{ + util::tests::{CREDENTIAL_BYTES, RECIPIENT}, + v4, + }; + + #[test] + fn upgrade_initial_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = v4::request::AuthenticatorRequest::new_initial_request( + v4::registration::InitMessage::new(pub_key), + reply_to, + ); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::Initial(v5::registration::InitMessage { + pub_key + }) + ); + } + + #[test] + fn upgrade_final_req() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v4::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let gateway_client = v4::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let credential = Some(CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap()); + let final_message = v4::registration::FinalMessage { + gateway_client: gateway_client.clone(), + credential: credential.clone(), + }; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = + v4::request::AuthenticatorRequest::new_final_request(final_message, reply_to); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::Final(Box::new( + v5::registration::FinalMessage { + gateway_client: v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v5::registration::IpPair::new(ipv4, ipv6), + nonce + ), + credential + } + )) + ); + } + + #[test] + fn upgrade_query_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = v4::request::AuthenticatorRequest::new_query_request(pub_key, reply_to); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + ); + } + + #[test] + fn upgrade_pending_reg_resp() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v4::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let wg_port = 51822; + let gateway_data = v4::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let registration_data = v4::registration::RegistrationData { + nonce, + gateway_data, + wg_port, + }; + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_pending_registration_success( + registration_data, + request_id, + reply_to, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::PendingRegistration( + v5::response::PendingRegistrationResponse { + request_id, + reply: v5::registration::RegistrationData { + nonce, + gateway_data: v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v5::registration::IpPair::new(ipv4, ipv6), + nonce + ), + wg_port + } + } + ) + ); + } + + #[test] + fn upgrade_registered_resp() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let private_ips = v4::registration::IpPair::new(ipv4, ipv6); + let wg_port = 51822; + let registred_data = v4::registration::RegistredData { + pub_key, + private_ips, + wg_port, + }; + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_registered( + registred_data, + reply_to, + request_id, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse { + request_id, + reply: v5::registration::RegistredData { + wg_port, + pub_key, + private_ips: v5::registration::IpPair::new(ipv4, ipv6) + } + }) + ); + } + + #[test] + fn upgrade_remaining_bandwidth_resp() { + let available_bandwidth = 42; + let remaining_bandwidth_data = Some(v4::registration::RemainingBandwidthData { + available_bandwidth, + }); + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_remaining_bandwidth( + remaining_bandwidth_data, + reply_to, + request_id, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::RemainingBandwidth( + v5::response::RemainingBandwidthResponse { + request_id, + reply: Some(v5::registration::RemainingBandwidthData { + available_bandwidth, + }) + } + ) + ); + } +} diff --git a/common/authenticator-requests/src/v5/mod.rs b/common/authenticator-requests/src/v5/mod.rs new file mode 100644 index 0000000000..2f0df82353 --- /dev/null +++ b/common/authenticator-requests/src/v5/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod conversion; +pub mod registration; +pub mod request; +pub mod response; +pub mod topup; + +pub const VERSION: u8 = 5; diff --git a/common/authenticator-requests/src/v5/registration.rs b/common/authenticator-requests/src/v5/registration.rs new file mode 100644 index 0000000000..9bd5c49e44 --- /dev/null +++ b/common/authenticator-requests/src/v5/registration.rs @@ -0,0 +1,287 @@ +// -2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use base64::{engine::general_purpose, Engine}; +use nym_credentials_interface::CredentialSpendingData; +use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::time::SystemTime; +use std::{fmt, ops::Deref, str::FromStr}; + +#[cfg(feature = "verify")] +use hmac::{Hmac, Mac}; +#[cfg(feature = "verify")] +use nym_crypto::asymmetric::encryption::PrivateKey; +#[cfg(feature = "verify")] +use sha2::Sha256; + +pub type PendingRegistrations = HashMap; +pub type PrivateIPs = HashMap; + +#[cfg(feature = "verify")] +pub type HmacSha256 = Hmac; + +pub type Nonce = u64; +pub type Taken = Option; + +pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct IpPair { + pub ipv4: Ipv4Addr, + pub ipv6: Ipv6Addr, +} + +impl IpPair { + pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl From<(Ipv4Addr, Ipv6Addr)> for IpPair { + fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl fmt::Display for IpPair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.ipv4, self.ipv6) + } +} + +impl From for IpPair { + fn from(value: IpAddr) -> Self { + let (before_last_byte, last_byte) = match value { + std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), + std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), + }; + let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16; + let ipv4 = Ipv4Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0], + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1], + before_last_byte, + last_byte, + ); + let ipv6 = Ipv6Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6], + last_bytes, + ); + IpPair::new(ipv4, ipv6) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InitMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, +} + +impl InitMessage { + pub fn new(pub_key: PeerPublicKey) -> Self { + InitMessage { pub_key } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct FinalMessage { + /// Gateway client data + pub gateway_client: GatewayClient, + + /// Ecash credential + pub credential: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistrationData { + pub nonce: u64, + pub gateway_data: GatewayClient, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistredData { + pub pub_key: PeerPublicKey, + pub private_ips: IpPair, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RemainingBandwidthData { + pub available_bandwidth: i64, +} + +/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct GatewayClient { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Assigned private IPs (v4 and v6) + pub private_ips: IpPair, + + /// Sha256 hmac on the data (alongside the prior nonce) + pub mac: ClientMac, +} + +impl GatewayClient { + #[cfg(feature = "verify")] + pub fn new( + local_secret: &PrivateKey, + remote_public: x25519_dalek::PublicKey, + private_ips: IpPair, + nonce: u64, + ) -> Self { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = x25519_dalek::StaticSecret::from(local_secret.to_bytes()); + let local_public: x25519_dalek::PublicKey = (&static_secret).into(); + + let dh = static_secret.diffie_hellman(&remote_public); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(local_public.as_bytes()); + mac.update(private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + GatewayClient { + pub_key: PeerPublicKey::new(local_public), + private_ips, + mac: ClientMac(mac.finalize().into_bytes().to_vec()), + } + } + + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + #[cfg(feature = "verify")] + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = x25519_dalek::StaticSecret::from(gateway_key.to_bytes()); + + let dh = static_secret.diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| Error::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +// TODO2: rely on our internal crypto/hmac +#[derive(Debug, Clone, PartialEq)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientMac { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = + general_purpose::STANDARD + .decode(s) + .map_err(|source| Error::MalformedClientMac { + mac: s.to_string(), + source, + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientMac { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::encryption; + + #[test] + fn create_ip_pair() { + let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into(); + let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into(); + + assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6)); + } + + #[test] + #[cfg(feature = "verify")] + fn client_request_roundtrip() { + let mut rng = rand::thread_rng(); + + let gateway_key_pair = encryption::KeyPair::new(&mut rng); + let client_key_pair = encryption::KeyPair::new(&mut rng); + + let nonce = 1234567890; + + let client = GatewayClient::new( + client_key_pair.private_key(), + x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()), + IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()), + nonce, + ); + assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) + } +} diff --git a/common/authenticator-requests/src/v5/request.rs b/common/authenticator-requests/src/v5/request.rs new file mode 100644 index 0000000000..4405ee75e0 --- /dev/null +++ b/common/authenticator-requests/src/v5/request.rs @@ -0,0 +1,132 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + registration::{FinalMessage, InitMessage}, + topup::TopUpMessage, +}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::VERSION; + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorRequest { + pub protocol: Protocol, + pub data: AuthenticatorRequestData, + pub request_id: u64, +} + +impl AuthenticatorRequest { + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::Initial(init_message), + request_id, + }, + request_id, + ) + } + + pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::Final(Box::new(final_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::QueryBandwidth(peer_public_key), + request_id, + }, + request_id, + ) + } + + pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)), + request_id, + }, + request_id, + ) + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorRequestData { + Initial(InitMessage), + Final(Box), + QueryBandwidth(PeerPublicKey), + TopUpBandwidth(Box), +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn check_first_bytes_protocol() { + let version = 5; + let data = AuthenticatorRequest { + protocol: Protocol { + version, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: AuthenticatorRequestData::Initial(InitMessage::new( + PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(), + )), + request_id: 1, + }; + let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap(); + assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]); + } +} diff --git a/common/authenticator-requests/src/v5/response.rs b/common/authenticator-requests/src/v5/response.rs new file mode 100644 index 0000000000..044b803d0d --- /dev/null +++ b/common/authenticator-requests/src/v5/response.rs @@ -0,0 +1,132 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::VERSION; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorResponse { + pub protocol: Protocol, + pub data: AuthenticatorResponseData, +} + +impl AuthenticatorResponse { + pub fn new_pending_registration_success( + registration_data: RegistrationData, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse { + reply: registration_data, + request_id, + }), + } + } + + pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::Registered(RegisteredResponse { + reply: registred_data, + request_id, + }), + } + } + + pub fn new_remaining_bandwidth( + remaining_bandwidth_data: Option, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + }), + } + } + + pub fn new_topup_bandwidth( + remaining_bandwidth_data: RemainingBandwidthData, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + }), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn id(&self) -> Option { + match &self.data { + AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id), + AuthenticatorResponseData::Registered(response) => Some(response.request_id), + AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorResponseData { + PendingRegistration(PendingRegistrationResponse), + Registered(RegisteredResponse), + RemainingBandwidth(RemainingBandwidthResponse), + TopUpBandwidth(TopUpBandwidthResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingRegistrationResponse { + pub request_id: u64, + pub reply: RegistrationData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisteredResponse { + pub request_id: u64, + pub reply: RegistredData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RemainingBandwidthResponse { + pub request_id: u64, + pub reply: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopUpBandwidthResponse { + pub request_id: u64, + pub reply: RemainingBandwidthData, +} diff --git a/common/authenticator-requests/src/v5/topup.rs b/common/authenticator-requests/src/v5/topup.rs new file mode 100644 index 0000000000..1163d07f12 --- /dev/null +++ b/common/authenticator-requests/src/v5/topup.rs @@ -0,0 +1,15 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TopUpMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Ecash credential + pub credential: CredentialSpendingData, +} diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 3af0746a8b..92925dcfdf 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -50,7 +50,7 @@ const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0; // define how much to request at once // clients/client-core/src/client/replies/reply_controller.rs const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; -const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 100; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 50; const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index b8417cead9..e0d0884e96 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -33,10 +33,12 @@ pub enum PreparationError { #[error(transparent)] NymTopologyError(#[from] NymTopologyError), - #[error("The received message cannot be sent using a single reply surb. It ended up getting split into {fragments} fragments.")] + #[error("message too long for a single SURB, splitting into {fragments} fragments.")] MessageTooLongForSingleSurb { fragments: usize }, - #[error("Not enough reply SURBs to send the message. We have {available} available and require at least {required}.")] + #[error( + "not enough reply SURBs to send the message, available: {available} required: {required}." + )] NotEnoughSurbs { available: usize, required: usize }, } diff --git a/common/client-core/src/client/replies/reply_controller/mod.rs b/common/client-core/src/client/replies/reply_controller/mod.rs index fb8cdc468b..7b59359407 100644 --- a/common/client-core/src/client/replies/reply_controller/mod.rs +++ b/common/client-core/src/client/replies/reply_controller/mod.rs @@ -746,7 +746,7 @@ where .request_additional_reply_surbs(target, request_size) .await { - warn!("failed to request additional surbs... - {err}") + info!("{err}") } } diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml index 33ff7bc6f8..79a291e827 100644 --- a/common/http-api-client/Cargo.toml +++ b/common/http-api-client/Cargo.toml @@ -21,6 +21,12 @@ serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +# used for decoding text responses (they were already implicitly included) +bytes = { workspace = true } +encoding_rs = { workspace = true } +mime = { workspace = true } + + nym-bin-common = { path = "../bin-common" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] @@ -32,4 +38,4 @@ workspace = true features = ["tokio"] [dev-dependencies] -tokio = { workspace = true, features=["rt", "macros"] } +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 08c704897d..4b2938daa4 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -147,13 +147,13 @@ use thiserror::Error; use tracing::{instrument, warn}; use url::Url; +use http::HeaderMap; +pub use reqwest::IntoUrl; #[cfg(not(target_arch = "wasm32"))] use std::net::SocketAddr; #[cfg(not(target_arch = "wasm32"))] use std::sync::Arc; -pub use reqwest::IntoUrl; - mod user_agent; pub use user_agent::UserAgent; @@ -210,6 +210,12 @@ pub enum HttpClientError { #[error("failed to resolve request. status: '{status}', additional error message: {error}")] EndpointFailure { status: StatusCode, error: E }, + #[error("failed to decode response body: {source} from {content}")] + ResponseDecodeFailure { + source: serde_json::Error, + content: String, + }, + #[cfg(target_arch = "wasm32")] #[error("the request has timed out")] RequestTimeout, @@ -849,6 +855,26 @@ fn sanitize_url, V: AsRef>( url } +fn decode_as_text(bytes: &bytes::Bytes, headers: HeaderMap) -> String { + use encoding_rs::{Encoding, UTF_8}; + use mime::Mime; + + let content_type = headers + .get(http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse::().ok()); + + let encoding_name = content_type + .as_ref() + .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str())) + .unwrap_or("utf-8"); + + let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8); + + let (text, _, _) = encoding.decode(bytes); + text.into_owned() +} + /// Attempt to parse a json object from an HTTP response #[instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> @@ -864,21 +890,23 @@ where return Err(HttpClientError::EmptyResponse { status }); } } + let headers = res.headers().clone(); + tracing::trace!("headers: {:?}", headers); if res.status().is_success() { - #[cfg(debug_assertions)] - { - let text = res.text().await.inspect_err(|err| { - tracing::error!("Couldn't even get response text: {err}"); - })?; - tracing::trace!("Result:\n{:#?}", text); - - serde_json::from_str(&text) - .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) + // internally reqwest is first retrieving bytes and then performing parsing via serde_json + // (and similarly does the same thing for text()) + let full = res.bytes().await?; + match serde_json::from_slice(&full) { + Ok(data) => Ok(data), + Err(err) => { + let content = decode_as_text(&full, headers); + Err(HttpClientError::ResponseDecodeFailure { + source: err, + content, + }) + } } - - #[cfg(not(debug_assertions))] - Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) } else { diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 90bd892d78..a6729e7e5d 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -7,7 +7,7 @@ use nym_sphinx_addressing::clients::Recipient; use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; -use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB}; +use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB, UPDATED_LEGACY_VERSION}; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; use serde::de::{Error as SerdeError, Visitor}; @@ -101,7 +101,8 @@ impl ReplySurb { let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); - let surb_material = SURBMaterial::new(route, delays, destination); + let surb_material = + SURBMaterial::new(route, delays, destination).with_version(UPDATED_LEGACY_VERSION); // this can't fail as we know we have a valid route to gateway and have correct number of delays Ok(ReplySurb { diff --git a/common/pemstore/Cargo.toml b/common/pemstore/Cargo.toml index f3bc33a6ef..f8d13391dd 100644 --- a/common/pemstore/Cargo.toml +++ b/common/pemstore/Cargo.toml @@ -9,3 +9,4 @@ repository = { workspace = true } [dependencies] pem = { workspace = true } +tracing = { workspace = true } \ No newline at end of file diff --git a/common/pemstore/src/lib.rs b/common/pemstore/src/lib.rs index bd36158229..9f6d3e283b 100644 --- a/common/pemstore/src/lib.rs +++ b/common/pemstore/src/lib.rs @@ -6,6 +6,7 @@ use pem::Pem; use std::fs::File; use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; +use tracing::debug; pub mod traits; @@ -46,6 +47,10 @@ where T: PemStorableKey, P: AsRef, { + debug!( + "attempting to load key with the following pem type: {}", + T::pem_type() + ); let key_pem = read_pem_file(path)?; if T::pem_type() != key_pem.tag { diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 585dfd47bf..6bff50d87a 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.47" +version = "1.1.48" edition = "2021" license.workspace = true diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 4c0eb81f35..bd1f9fd601 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -426,6 +426,11 @@ impl FreshHandler { return Ok(2); } + // a v4 gateway will understand v3 requests (aes256gcm-siv) + if client_protocol_version == 3 { + return Ok(3); + } + // we can't handle clients with higher protocol than ours // (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement) if client_protocol_version <= CURRENT_PROTOCOL_VERSION { diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index f95baabd00..aa6b966b7f 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.51" +version = "1.1.53" authors.workspace = true edition = "2021" rust-version.workspace = true @@ -144,4 +144,4 @@ rand_chacha = { workspace = true } sha2 = "0.9" [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/nym-api/src/ecash/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs index bfbaa8c28d..f0038e4869 100644 --- a/nym-api/src/ecash/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -10,7 +10,7 @@ use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use rand::{CryptoRng, RngCore}; use std::path::Path; use thiserror::__private::AsDisplay; -use tracing::warn; +use tracing::{debug, warn}; pub(crate) fn init_bte_keypair( rng: &mut R, @@ -39,17 +39,20 @@ pub(crate) fn load_bte_keypair(config: &config::EcashSigner) -> anyhow::Result anyhow::Result> { + let storage_path = &config.storage_paths.ecash_key_path; + debug!( + "attempting to ecash keypair from {}", + storage_path.display() + ); if !config.storage_paths.ecash_key_path.exists() { + debug!("the provided filepath doesn't exist - the key won't be loaded"); return Ok(None); } - if let Ok(ecash_key) = + let ecash_key = nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) - { - return Ok(Some(ecash_key)); - } - - bail!("ecash key load failure") + .context("failed to load ecash key")?; + Ok(Some(ecash_key)) } // the keys can be considered valid if they were generated for the current dkg epoch diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index 61b5bab2c4..93cc576a85 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -33,14 +33,17 @@ impl DailyMerkleTree { .into_iter() .map(|l| (l.merkle_index, l)) .collect(); + let total_leaves = leaves.len(); let mut sorted_leaves = Vec::new(); for i in 0..leaves.len() { if let Some(next_leaf) = leaves.remove(&i) { sorted_leaves.push(next_leaf); } else { - let lost = leaves.len() - i + 1; - error!("failed to produce consistent merkle tree. there was no leaf with index {i}. at least {lost} leaves got lost") + let lost = total_leaves - i + 1; + error!("failed to produce consistent merkle tree. there was no leaf with index {i}. at least {lost} leaves got lost"); + // we have to drop all data above that height because we can't rebuild the full tree + break; } } diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index a794ca4e98..0cf816ba5b 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.6.0" +version = "1.6.2" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -101,4 +101,4 @@ cargo_metadata = { workspace = true } [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 7a89c5ad26..c5a4a542ed 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -68,7 +68,7 @@ impl Debug { pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30); - const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; + pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; } impl Default for Debug { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 35379a35ef..b2a22e4a9d 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -465,9 +465,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -1487,9 +1487,9 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -3463,8 +3463,11 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bytes", + "encoding_rs", "hickory-resolver", "http 1.1.0", + "mime", "nym-bin-common", "once_cell", "reqwest 0.12.4", @@ -3554,6 +3557,7 @@ name = "nym-pemstore" version = "0.3.0" dependencies = [ "pem", + "tracing", ] [[package]] diff --git a/service-providers/authenticator/src/cli/request.rs b/service-providers/authenticator/src/cli/request.rs index 0a8dfe3cd5..a3f39ddc91 100644 --- a/service-providers/authenticator/src/cli/request.rs +++ b/service-providers/authenticator/src/cli/request.rs @@ -111,16 +111,16 @@ pub(crate) async fn execute(args: &Request) -> Result<(), AuthenticatorError> { let authenticator_recipient = Recipient::from_str(&args.authenticator_recipient)?; let (request, _) = match request_data { AuthenticatorRequestData::Initial(init_message) => { - AuthenticatorRequest::new_initial_request(init_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_initial_request(init_message) } AuthenticatorRequestData::Final(final_message) => { - AuthenticatorRequest::new_final_request(*final_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_final_request(*final_message) } AuthenticatorRequestData::QueryBandwidth(query_message) => { - AuthenticatorRequest::new_query_request(query_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_query_request(query_message) } AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { - AuthenticatorRequest::new_topup_request(*top_up_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_topup_request(*top_up_message) } }; mixnet_client diff --git a/service-providers/authenticator/src/error.rs b/service-providers/authenticator/src/error.rs index 0430a0805b..59a1597147 100644 --- a/service-providers/authenticator/src/error.rs +++ b/service-providers/authenticator/src/error.rs @@ -89,6 +89,9 @@ pub enum AuthenticatorError { #[error("unknown version number")] UnknownVersion, + #[error("missing reply_to for old client")] + MissingReplyToForOldClient, + #[error("{0}")] PublicKey(#[from] nym_wireguard_types::Error), diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index dcd3c8e31e..d88a1185c5 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -21,7 +21,7 @@ use nym_authenticator_requests::{ AuthenticatorRequest, AuthenticatorVersion, FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, }, - v1, v2, v3, v4, CURRENT_VERSION, + v1, v2, v3, v4, v5, CURRENT_VERSION, }; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ecash::EcashManager, @@ -42,7 +42,7 @@ use rand::{prelude::IteratorRandom, thread_rng}; use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; -type AuthenticatorHandleResult = Result<(Vec, Recipient)>; +type AuthenticatorHandleResult = Result<(Vec, Option)>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute pub(crate) struct RegistredAndFree { @@ -155,7 +155,7 @@ impl MixnetListener { init_message: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); @@ -178,7 +178,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -198,7 +198,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -218,7 +218,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -228,12 +228,26 @@ impl MixnetListener { AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone().into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_pending_registration_success( + v5::registration::RegistrationData { nonce: registration_data.nonce, gateway_data: registration_data.gateway_data.clone(), wg_port: registration_data.wg_port, }, request_id, - reply_to, ) .to_bytes() .map_err(|err| { @@ -272,7 +286,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -285,7 +299,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -298,7 +312,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -311,7 +325,19 @@ impl MixnetListener { private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( + v5::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ips: (allowed_ipv4, allowed_ipv6).into(), + wg_port: self.config.authenticator.announced_port, + }, request_id, ) .to_bytes() @@ -360,7 +386,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -380,7 +406,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -400,7 +426,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -410,12 +436,26 @@ impl MixnetListener { AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_pending_registration_success( + v5::registration::RegistrationData { nonce: registration_data.nonce, gateway_data: registration_data.gateway_data, wg_port: registration_data.wg_port, }, request_id, - reply_to, ) .to_bytes() .map_err(|err| { @@ -433,7 +473,7 @@ impl MixnetListener { final_message: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let mut registred_and_free = self.registred_and_free.write().await; let registration_data = registred_and_free @@ -500,7 +540,7 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -511,7 +551,7 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -522,18 +562,28 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( v4::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ips: registration_data.gateway_data.private_ips.into(), + wg_port: registration_data.wg_port, + }, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( + v5::registration::RegistredData { pub_key: registration_data.gateway_data.pub_key, private_ips: registration_data.gateway_data.private_ips, wg_port: registration_data.wg_port, }, - reply_to, request_id, ) .to_bytes() @@ -579,7 +629,7 @@ impl MixnetListener { msg: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let bandwidth_data = self.peer_manager.query_bandwidth(msg).await?; let bytes = match AuthenticatorVersion::from(protocol) { @@ -589,7 +639,7 @@ impl MixnetListener { available_bandwidth: data.available_bandwidth as u64, suspended: false, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -602,7 +652,7 @@ impl MixnetListener { bandwidth_data.map(|data| v2::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -615,7 +665,7 @@ impl MixnetListener { bandwidth_data.map(|data| v3::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -628,7 +678,19 @@ impl MixnetListener { bandwidth_data.map(|data| v4::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v5::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth, + }), request_id, ) .to_bytes() @@ -646,7 +708,7 @@ impl MixnetListener { msg: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let Some(ecash_verifier) = self.ecash_verifier.clone() else { return Err(AuthenticatorError::UnsupportedOperation); @@ -681,11 +743,19 @@ impl MixnetListener { let available_bandwidth = verifier.verify().await?; let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth( + v5::registration::RemainingBandwidthData { + available_bandwidth, + }, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_topup_bandwidth( v4::registration::RemainingBandwidthData { available_bandwidth, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -694,7 +764,7 @@ impl MixnetListener { v3::registration::RemainingBandwidthData { available_bandwidth, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -762,10 +832,10 @@ impl MixnetListener { async fn handle_response( &self, response: Vec, - recipient: Recipient, + recipient: Option, sender_tag: Option, ) -> Result<()> { - let input_message = create_input_message(recipient, sender_tag, response); + let input_message = create_input_message(recipient, sender_tag, response)?; self.mixnet_client .send(input_message) .await @@ -858,6 +928,17 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result { + if request_type == ServiceProviderType::Authenticator as u8 { + v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { + source: err, + }) + .map(Into::into) + } else { + Err(AuthenticatorError::InvalidPacketType(request_type)) + } + } [version, _] => { log::info!("Received packet with invalid version: v{version}"); Err(AuthenticatorError::InvalidPacketVersion(version)) @@ -866,17 +947,30 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result, reply_to_tag: Option, response_packet: Vec, -) -> InputMessage { +) -> Result { let lane = TransmissionLane::General; let packet_type = None; if let Some(reply_to_tag) = reply_to_tag { log::debug!("Creating message using SURB"); - InputMessage::new_reply(reply_to_tag, response_packet, lane, packet_type) - } else { + Ok(InputMessage::new_reply( + reply_to_tag, + response_packet, + lane, + packet_type, + )) + } else if let Some(nym_address) = nym_address { log::debug!("Creating message using nym_address"); - InputMessage::new_regular(nym_address, response_packet, lane, packet_type) + Ok(InputMessage::new_regular( + nym_address, + response_packet, + lane, + packet_type, + )) + } else { + log::error!("No nym-address or sender tag provided"); + Err(AuthenticatorError::MissingReplyToForOldClient) } } diff --git a/service-providers/ip-packet-router/src/clients/client_id.rs b/service-providers/ip-packet-router/src/clients/client_id.rs index 5add99a724..2842f261d3 100644 --- a/service-providers/ip-packet-router/src/clients/client_id.rs +++ b/service-providers/ip-packet-router/src/clients/client_id.rs @@ -7,7 +7,7 @@ use nym_sdk::mixnet::{AnonymousSenderTag, Recipient}; use crate::error::{IpPacketRouterError, Result}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum ConnectedClientId { AnonymousSenderTag(AnonymousSenderTag), NymAddress(Box), diff --git a/service-providers/ip-packet-router/src/clients/connected_clients.rs b/service-providers/ip-packet-router/src/clients/connected_clients.rs index 1957ddaa15..7100048dd8 100644 --- a/service-providers/ip-packet-router/src/clients/connected_clients.rs +++ b/service-providers/ip-packet-router/src/clients/connected_clients.rs @@ -59,23 +59,42 @@ impl ConnectedClients { .any(|client| client.client_id == *client_id) } - //fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { - // self.clients_ipv4_mapping - // .iter() - // .find_map(|(ipv4, connected_client)| { - // if connected_client.client_id == *client_id { - // Some(IpPair::new(*ipv4, connected_client.ipv6)) - // } else { - // None - // } - // }) - //} - // - //fn lookup_client(&self, client_id: &ConnectedClientId) -> Option<&ConnectedClient> { - // self.clients_ipv4_mapping - // .values() - // .find(|connected_client| connected_client.client_id == *client_id) - //} + pub(crate) fn disconnect_client(&mut self, client_id: &ConnectedClientId) { + if let Some(ips) = self.lookup_ip_from_client_id(client_id) { + log::debug!("Disconnect client that requested to do so: {ips}"); + self.disconnect_client_handle(ips); + } + } + + fn disconnect_client_handle(&mut self, ips: IpPair) { + self.clients_ipv4_mapping.remove(&ips.ipv4); + self.clients_ipv6_mapping.remove(&ips.ipv6); + self.tun_listener_connected_client_tx + .send(ConnectedClientEvent::Disconnect(DisconnectEvent(ips))) + .inspect_err(|err| { + log::error!("Failed to send disconnect event: {err}"); + }) + .ok(); + } + + pub(crate) fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { + self.clients_ipv4_mapping + .iter() + .find_map(|(ipv4, connected_client)| { + if connected_client.client_id == *client_id { + Some(IpPair::new(*ipv4, connected_client.ipv6)) + } else { + None + } + }) + } + + #[allow(unused)] + fn lookup_client(&self, client_id: &ConnectedClientId) -> Option<&ConnectedClient> { + self.clients_ipv4_mapping + .values() + .find(|connected_client| connected_client.client_id == *client_id) + } pub(crate) fn connect( &mut self, @@ -163,14 +182,7 @@ impl ConnectedClients { ) { for (ips, _) in &stopped_clients { log::info!("Disconnect stopped client: {ips}"); - self.clients_ipv4_mapping.remove(&ips.ipv4); - self.clients_ipv6_mapping.remove(&ips.ipv6); - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) - .inspect_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); + self.disconnect_client_handle(*ips); } } @@ -180,14 +192,7 @@ impl ConnectedClients { ) { for (ips, _) in &inactive_clients { log::info!("Disconnect inactive client: {ips}"); - self.clients_ipv4_mapping.remove(&ips.ipv4); - self.clients_ipv6_mapping.remove(&ips.ipv6); - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) - .inspect_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); + self.disconnect_client_handle(*ips); } } diff --git a/service-providers/ip-packet-router/src/constants.rs b/service-providers/ip-packet-router/src/constants.rs index 128afa271b..a707199977 100644 --- a/service-providers/ip-packet-router/src/constants.rs +++ b/service-providers/ip-packet-router/src/constants.rs @@ -7,8 +7,8 @@ use std::time::Duration; pub(crate) const DISCONNECT_TIMER_INTERVAL: Duration = Duration::from_secs(10); // We consider a client inactive if it hasn't sent any mixnet packets in this duration -pub(crate) const CLIENT_MIXNET_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60); +pub(crate) const CLIENT_MIXNET_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(60); // We consider a client handler inactive if it hasn't received any packets from the tun device in // this duration -pub(crate) const CLIENT_HANDLER_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(10 * 60); +pub(crate) const CLIENT_HANDLER_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60); diff --git a/service-providers/ip-packet-router/src/messages/response.rs b/service-providers/ip-packet-router/src/messages/response.rs index 6e60d41433..d1bf0d08b4 100644 --- a/service-providers/ip-packet-router/src/messages/response.rs +++ b/service-providers/ip-packet-router/src/messages/response.rs @@ -90,9 +90,6 @@ pub(crate) struct DynamicConnectSuccess { #[derive(Clone, Debug, thiserror::Error)] pub(crate) enum DynamicConnectFailureReason { - #[error("client already connected")] - ClientAlreadyConnected, - #[error("no available ip address")] NoAvailableIp, diff --git a/service-providers/ip-packet-router/src/messages/response/v6.rs b/service-providers/ip-packet-router/src/messages/response/v6.rs index 290f78971b..bf7fc9dbdf 100644 --- a/service-providers/ip-packet-router/src/messages/response/v6.rs +++ b/service-providers/ip-packet-router/src/messages/response/v6.rs @@ -119,9 +119,6 @@ impl From for DynamicConnectResponseReplyV6 { impl From for DynamicConnectFailureReasonV6 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - DynamicConnectFailureReasonV6::RequestedNymAddressAlreadyInUse - } DynamicConnectFailureReason::NoAvailableIp => { DynamicConnectFailureReasonV6::NoAvailableIp } diff --git a/service-providers/ip-packet-router/src/messages/response/v7.rs b/service-providers/ip-packet-router/src/messages/response/v7.rs index 60a3aa6d8a..f2ad4e849f 100644 --- a/service-providers/ip-packet-router/src/messages/response/v7.rs +++ b/service-providers/ip-packet-router/src/messages/response/v7.rs @@ -119,9 +119,6 @@ impl From for DynamicConnectResponseReplyV7 { impl From for DynamicConnectFailureReasonV7 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - DynamicConnectFailureReasonV7::RequestedNymAddressAlreadyInUse - } DynamicConnectFailureReason::NoAvailableIp => { DynamicConnectFailureReasonV7::NoAvailableIp } diff --git a/service-providers/ip-packet-router/src/messages/response/v8.rs b/service-providers/ip-packet-router/src/messages/response/v8.rs index 3f73479a05..99b87075c8 100644 --- a/service-providers/ip-packet-router/src/messages/response/v8.rs +++ b/service-providers/ip-packet-router/src/messages/response/v8.rs @@ -82,9 +82,6 @@ impl From for ConnectResponseReplyV8 { impl From for ConnectFailureReasonV8 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - ConnectFailureReasonV8::ClientAlreadyConnected - } DynamicConnectFailureReason::NoAvailableIp => ConnectFailureReasonV8::NoAvailableIp, DynamicConnectFailureReason::Other(err) => ConnectFailureReasonV8::Other(err), } diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 293718831c..b0957236ac 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -22,9 +22,9 @@ use crate::{ IpPacketRequest, PingRequest, StaticConnectRequest, }, response::{ - DynamicConnectFailureReason, DynamicConnectSuccess, HealthResponse, InfoLevel, - InfoResponse, InfoResponseReply, Response, StaticConnectFailureReason, - StaticConnectResponse, VersionedResponse, + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponse, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, }, ClientVersion, }, @@ -225,7 +225,7 @@ impl MixnetListener { })) } - async fn on_dynamic_connect_request( + fn on_dynamic_connect_request( &mut self, connect_request: DynamicConnectRequest, ) -> PacketHandleResult { @@ -242,13 +242,14 @@ impl MixnetListener { .map(Duration::from_millis) .unwrap_or(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); - if self.connected_clients.is_client_connected(&reply_to) { + if let Some(ips) = self.connected_clients.lookup_ip_from_client_id(&reply_to) { + log::debug!("Reconnecting to the previous session"); return Ok(Some(VersionedResponse { version, reply_to, response: Response::DynamicConnect { request_id, - reply: DynamicConnectFailureReason::ClientAlreadyConnected.into(), + reply: DynamicConnectSuccess { ips }.into(), }, })); } @@ -292,12 +293,47 @@ impl MixnetListener { })) } - fn on_disconnect_request(&self, _disconnect_request: DisconnectRequest) -> PacketHandleResult { - log::info!("Received disconnect request: not implemented, dropping"); - Ok(None) + fn on_disconnect_request( + &mut self, + disconnect_request: DisconnectRequest, + ) -> PacketHandleResult { + log::info!( + "Received disconnect request from {}", + disconnect_request.sent_by + ); + + let version = disconnect_request.version; + let request_id = disconnect_request.request_id; + let client_id = disconnect_request.sent_by; + + // Check if the client is connected + if !self.connected_clients.is_client_connected(&client_id) { + log::info!("Client {} is not connected, cannot disconnect", client_id); + return Ok(Some(VersionedResponse { + version, + reply_to: client_id, + response: Response::Disconnect { + request_id, + reply: DisconnectResponse::Failure(DisconnectFailureReason::ClientNotConnected), + }, + })); + } + + // Disconnect the client + log::info!("Disconnecting client {}", client_id); + self.connected_clients.disconnect_client(&client_id); + + Ok(Some(VersionedResponse { + version, + reply_to: client_id, + response: Response::Disconnect { + request_id, + reply: DisconnectResponse::Success, + }, + })) } - async fn on_ping_request(&self, ping_request: PingRequest) -> PacketHandleResult { + fn on_ping_request(&self, ping_request: PingRequest) -> PacketHandleResult { Ok(Some(VersionedResponse { version: ping_request.version, reply_to: ping_request.sent_by, @@ -307,7 +343,7 @@ impl MixnetListener { })) } - async fn on_health_request(&self, health_request: HealthRequest) -> PacketHandleResult { + fn on_health_request(&self, health_request: HealthRequest) -> PacketHandleResult { Ok(Some(VersionedResponse { version: health_request.version, reply_to: health_request.sent_by, @@ -324,10 +360,10 @@ impl MixnetListener { async fn on_control_request(&mut self, control_request: ControlRequest) -> PacketHandleResult { match control_request { ControlRequest::StaticConnect(r) => self.on_static_connect_request(r).await, - ControlRequest::DynamicConnect(r) => self.on_dynamic_connect_request(r).await, + ControlRequest::DynamicConnect(r) => self.on_dynamic_connect_request(r), ControlRequest::Disconnect(r) => self.on_disconnect_request(r), - ControlRequest::Ping(r) => self.on_ping_request(r).await, - ControlRequest::Health(r) => self.on_health_request(r).await, + ControlRequest::Ping(r) => self.on_ping_request(r), + ControlRequest::Health(r) => self.on_health_request(r), } } diff --git a/service-providers/ip-packet-router/src/tun_listener.rs b/service-providers/ip-packet-router/src/tun_listener.rs index 0d4a99e3b9..072ade7ab0 100644 --- a/service-providers/ip-packet-router/src/tun_listener.rs +++ b/service-providers/ip-packet-router/src/tun_listener.rs @@ -103,7 +103,7 @@ impl TunListener { .update(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))); } } else { - log::info!( + log::debug!( "dropping packet from network: no registered client for destination: {dst_addr}" ); } diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index bbc7080819..207f3960c2 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.50" +version = "1.1.51" authors.workspace = true edition.workspace = true rust-version = "1.70" diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 9163feeb0b..a9b24b4ced 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.49" +version = "1.1.50" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index 57b4de306d..15de5e1610 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.14" +version = "0.1.15" authors.workspace = true repository.workspace = true homepage.workspace = true