diff --git a/Cargo.lock b/Cargo.lock index 0bc53266deb..0822026fe5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4999,23 +4999,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "nym-network-statistics" -version = "1.1.34" -dependencies = [ - "dirs 4.0.0", - "log", - "nym-bin-common", - "nym-statistics-common", - "nym-task", - "pretty_env_logger", - "rocket", - "serde", - "sqlx", - "thiserror", - "tokio", -] - [[package]] name = "nym-node" version = "1.1.4" diff --git a/common/ip-packet-requests/src/v6/conversion.rs b/common/ip-packet-requests/src/v6/conversion.rs new file mode 100644 index 00000000000..be2327b2e6b --- /dev/null +++ b/common/ip-packet-requests/src/v6/conversion.rs @@ -0,0 +1,69 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{v6, v7}; + +impl From for v6::response::StaticConnectFailureReason { + fn from(failure: v7::response::StaticConnectFailureReason) -> Self { + match failure { + v7::response::StaticConnectFailureReason::RequestedIpAlreadyInUse => { + v6::response::StaticConnectFailureReason::RequestedIpAlreadyInUse + } + v7::response::StaticConnectFailureReason::RequestedNymAddressAlreadyInUse => { + v6::response::StaticConnectFailureReason::RequestedNymAddressAlreadyInUse + } + v7::response::StaticConnectFailureReason::OutOfDateTimestamp => { + v6::response::StaticConnectFailureReason::Other("out of date timestamp".to_string()) + } + v7::response::StaticConnectFailureReason::Other(reason) => { + v6::response::StaticConnectFailureReason::Other(reason) + } + } + } +} + +impl From for v6::response::DynamicConnectFailureReason { + fn from(failure: v7::response::DynamicConnectFailureReason) -> Self { + match failure { + v7::response::DynamicConnectFailureReason::RequestedNymAddressAlreadyInUse => { + v6::response::DynamicConnectFailureReason::RequestedNymAddressAlreadyInUse + } + v7::response::DynamicConnectFailureReason::NoAvailableIp => { + v6::response::DynamicConnectFailureReason::NoAvailableIp + } + v7::response::DynamicConnectFailureReason::Other(err) => { + v6::response::DynamicConnectFailureReason::Other(err) + } + } + } +} + +impl From for v6::response::InfoResponseReply { + fn from(reply: v7::response::InfoResponseReply) -> Self { + match reply { + v7::response::InfoResponseReply::Generic { msg } => { + v6::response::InfoResponseReply::Generic { msg } + } + v7::response::InfoResponseReply::VersionMismatch { + request_version, + response_version, + } => v6::response::InfoResponseReply::VersionMismatch { + request_version, + response_version, + }, + v7::response::InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => { + v6::response::InfoResponseReply::ExitPolicyFilterCheckFailed { dst } + } + } + } +} + +impl From for v6::response::InfoLevel { + fn from(level: v7::response::InfoLevel) -> Self { + match level { + v7::response::InfoLevel::Info => v6::response::InfoLevel::Info, + v7::response::InfoLevel::Warn => v6::response::InfoLevel::Warn, + v7::response::InfoLevel::Error => v6::response::InfoLevel::Error, + } + } +} diff --git a/common/ip-packet-requests/src/v6/mod.rs b/common/ip-packet-requests/src/v6/mod.rs index 11a7aee8dcb..73ffb1815cd 100644 --- a/common/ip-packet-requests/src/v6/mod.rs +++ b/common/ip-packet-requests/src/v6/mod.rs @@ -1,3 +1,4 @@ +pub mod conversion; pub mod request; pub mod response; diff --git a/common/ip-packet-requests/src/v7/request.rs b/common/ip-packet-requests/src/v7/request.rs index 7f8c1b73983..24f9819c2a2 100644 --- a/common/ip-packet-requests/src/v7/request.rs +++ b/common/ip-packet-requests/src/v7/request.rs @@ -198,6 +198,17 @@ impl IpPacketRequestData { | IpPacketRequestData::Health(_) => None, } } + + pub fn signable_request(&self) -> Option, SignatureError>> { + match self { + IpPacketRequestData::StaticConnect(request) => Some(request.request()), + IpPacketRequestData::DynamicConnect(request) => Some(request.request()), + IpPacketRequestData::Disconnect(request) => Some(request.request()), + IpPacketRequestData::Data(_) => None, + IpPacketRequestData::Ping(_) => None, + IpPacketRequestData::Health(_) => None, + } + } } // A static connect request is when the client provides the internal IP address it will use on the diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index 04c03659c08..1d2fd0fcc17 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -67,6 +67,7 @@ pub use nym_credential_storage::{ ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage, models::StoredIssuedCredential, storage::Storage as CredentialStorage, }; +pub use nym_crypto::asymmetric::ed25519; pub use nym_network_defaults::NymNetworkDetails; pub use nym_socks5_client_core::config::Socks5; pub use nym_sphinx::{ diff --git a/service-providers/ip-packet-router/src/connected_client_handler.rs b/service-providers/ip-packet-router/src/connected_client_handler.rs index c4bfdd8c383..8754fb83943 100644 --- a/service-providers/ip-packet-router/src/connected_client_handler.rs +++ b/service-providers/ip-packet-router/src/connected_client_handler.rs @@ -2,12 +2,13 @@ // SPDX-License-Identifier: GPL-3.0-only use bytes::Bytes; -use nym_ip_packet_requests::{codec::MultiIpPacketCodec, v6::response::IpPacketResponse}; +use nym_ip_packet_requests::codec::MultiIpPacketCodec; use nym_sdk::mixnet::{MixnetMessageSender, Recipient}; use crate::{ constants::CLIENT_HANDLER_ACTIVITY_TIMEOUT, error::{IpPacketRouterError, Result}, + mixnet_listener::SupportedClientVersion, util::create_message::create_input_message, }; @@ -18,13 +19,29 @@ use crate::{ // This handler is spawned as a task, and it listens to IP packets passed from the tun_listener, // encodes it, and then sends to mixnet. pub(crate) struct ConnectedClientHandler { + // The address of the client that this handler is connected to nym_address: Recipient, + + // The number of hops the packet should take before reaching the client mix_hops: Option, + + // Channel to receive packets from the tun_listener forward_from_tun_rx: tokio::sync::mpsc::UnboundedReceiver>, + + // Channel to send packets to the mixnet mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender, + + // Channel to receive close signal close_rx: tokio::sync::oneshot::Receiver<()>, + + // Interval to check for activity timeout activity_timeout: tokio::time::Interval, + + // Encoder to bundle multiple packets into a single one encoder: MultiIpPacketCodec, + + // The version of the client + client_version: SupportedClientVersion, } impl ConnectedClientHandler { @@ -32,6 +49,7 @@ impl ConnectedClientHandler { reply_to: Recipient, reply_to_hops: Option, buffer_timeout: std::time::Duration, + client_version: SupportedClientVersion, mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender, ) -> ( tokio::sync::mpsc::UnboundedSender>, @@ -55,6 +73,7 @@ impl ConnectedClientHandler { close_rx, activity_timeout, encoder, + client_version, }; let handle = tokio::spawn(async move { @@ -67,9 +86,18 @@ impl ConnectedClientHandler { } async fn send_packets_to_mixnet(&mut self, packets: Bytes) -> Result<()> { - let response_packet = IpPacketResponse::new_ip_packet(packets) - .to_bytes() - .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err })?; + let response_packet = match self.client_version { + SupportedClientVersion::V6 => { + nym_ip_packet_requests::v6::response::IpPacketResponse::new_ip_packet(packets) + .to_bytes() + } + SupportedClientVersion::V7 => { + nym_ip_packet_requests::v7::response::IpPacketResponse::new_ip_packet(packets) + .to_bytes() + } + } + .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err })?; + let input_message = create_input_message(self.nym_address, response_packet, self.mix_hops); self.mixnet_client_sender diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index ff9f85cff5e..6a7e5393d7f 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -96,6 +96,9 @@ pub enum IpPacketRouterError { FailedToVerifyRequest { source: nym_ip_packet_requests::v7::signature::SignatureError, }, + + #[error("client is connected with an invalid version: {version}")] + InvalidConnectedClientVersion { version: u8 }, } pub type Result = std::result::Result; diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 4e41f3d996e..914cd03b36a 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -4,22 +4,19 @@ use std::{collections::HashMap, net::SocketAddr}; use bytes::{Bytes, BytesMut}; use futures::StreamExt; +use nym_ip_packet_requests::v7::response::{ + DynamicConnectFailureReason, InfoLevel, InfoResponseReply, StaticConnectFailureReason, +}; use nym_ip_packet_requests::{ codec::MultiIpPacketCodec, - v6::{ - self, - response::{ - DynamicConnectFailureReason, InfoLevel, InfoResponseReply, IpPacketResponse, - StaticConnectFailureReason, - }, - }, + v6, v7::{ self, request::{ DataRequest, DisconnectRequest, DynamicConnectRequest, IpPacketRequest, IpPacketRequestData, StaticConnectRequest, }, - signature::{SignatureError, SignedRequest}, + signature::SignedRequest, }, IpPair, }; @@ -266,7 +263,132 @@ impl Drop for CloseTx { } } -type PacketHandleResult = Result>; +type PacketHandleResult = Result>; + +#[derive(Debug, Clone)] +enum Response { + V6(v6::response::IpPacketResponse), + V7(v7::response::IpPacketResponse), +} + +impl Response { + fn recipient(&self) -> Option<&Recipient> { + match self { + Response::V6(response) => response.recipient(), + Response::V7(response) => response.recipient(), + } + } + + fn new_static_connect_success( + request_id: u64, + reply_to: Recipient, + client_version: SupportedClientVersion, + ) -> Self { + match client_version { + SupportedClientVersion::V6 => Response::V6( + v6::response::IpPacketResponse::new_static_connect_success(request_id, reply_to), + ), + SupportedClientVersion::V7 => Response::V7( + v7::response::IpPacketResponse::new_static_connect_success(request_id, reply_to), + ), + } + } + + fn new_static_connect_failure( + request_id: u64, + reply_to: Recipient, + reason: StaticConnectFailureReason, + client_version: SupportedClientVersion, + ) -> Self { + match client_version { + SupportedClientVersion::V6 => { + Response::V6(v6::response::IpPacketResponse::new_static_connect_failure( + request_id, + reply_to, + reason.into(), + )) + } + SupportedClientVersion::V7 => { + Response::V7(v7::response::IpPacketResponse::new_static_connect_failure( + request_id, reply_to, reason, + )) + } + } + } + + fn new_dynamic_connect_success( + request_id: u64, + reply_to: Recipient, + ips: IpPair, + client_version: SupportedClientVersion, + ) -> Self { + match client_version { + SupportedClientVersion::V6 => { + Response::V6(v6::response::IpPacketResponse::new_dynamic_connect_success( + request_id, reply_to, ips, + )) + } + SupportedClientVersion::V7 => { + Response::V7(v7::response::IpPacketResponse::new_dynamic_connect_success( + request_id, reply_to, ips, + )) + } + } + } + + fn new_dynamic_connect_failure( + request_id: u64, + reply_to: Recipient, + reason: DynamicConnectFailureReason, + client_version: SupportedClientVersion, + ) -> Self { + match client_version { + SupportedClientVersion::V6 => { + Response::V6(v6::response::IpPacketResponse::new_dynamic_connect_failure( + request_id, + reply_to, + reason.into(), + )) + } + SupportedClientVersion::V7 => { + Response::V7(v7::response::IpPacketResponse::new_dynamic_connect_failure( + request_id, reply_to, reason, + )) + } + } + } + + fn new_data_info_response( + reply_to: Recipient, + reply: InfoResponseReply, + level: InfoLevel, + client_version: SupportedClientVersion, + ) -> Self { + match client_version { + SupportedClientVersion::V6 => { + Response::V6(v6::response::IpPacketResponse::new_data_info_response( + reply_to, + reply.into(), + level.into(), + )) + } + SupportedClientVersion::V7 => Response::V7( + v7::response::IpPacketResponse::new_data_info_response(reply_to, reply, level), + ), + } + } + + fn to_bytes(&self) -> Result> { + match self { + Response::V6(response) => response.to_bytes(), + Response::V7(response) => response.to_bytes(), + } + .map_err(|err| { + log::error!("Failed to serialize response packet"); + IpPacketRouterError::FailedToSerializeResponsePacket { source: err } + }) + } +} #[cfg(target_os = "linux")] pub(crate) struct MixnetListener { @@ -297,6 +419,7 @@ impl MixnetListener { async fn on_static_connect_request( &mut self, connect_request: StaticConnectRequest, + client_version: SupportedClientVersion, ) -> PacketHandleResult { log::info!( "Received static connect request from {sender_address}", @@ -328,8 +451,10 @@ impl MixnetListener { { log::error!("Failed to update activity for client"); }; - Ok(Some(IpPacketResponse::new_static_connect_success( - request_id, reply_to, + Ok(Some(Response::new_static_connect_success( + request_id, + reply_to, + client_version, ))) } (false, false) => { @@ -341,6 +466,7 @@ impl MixnetListener { reply_to, reply_to_hops, buffer_timeout, + client_version, self.mixnet_client.split_sender(), ); @@ -353,24 +479,28 @@ impl MixnetListener { close_tx, handle, ); - Ok(Some(IpPacketResponse::new_static_connect_success( - request_id, reply_to, + Ok(Some(Response::new_static_connect_success( + request_id, + reply_to, + client_version, ))) } (true, false) => { log::info!("Requested IP is not available"); - Ok(Some(IpPacketResponse::new_static_connect_failure( + Ok(Some(Response::new_static_connect_failure( request_id, reply_to, StaticConnectFailureReason::RequestedIpAlreadyInUse, + client_version, ))) } (false, true) => { log::info!("Nym address is already registered"); - Ok(Some(IpPacketResponse::new_static_connect_failure( + Ok(Some(Response::new_static_connect_failure( request_id, reply_to, StaticConnectFailureReason::RequestedNymAddressAlreadyInUse, + client_version, ))) } } @@ -379,6 +509,7 @@ impl MixnetListener { async fn on_dynamic_connect_request( &mut self, connect_request: DynamicConnectRequest, + client_version: SupportedClientVersion, ) -> PacketHandleResult { log::info!( "Received dynamic connect request from {sender_address}", @@ -406,19 +537,21 @@ impl MixnetListener { { log::error!("Failed to update activity for client"); } - return Ok(Some(IpPacketResponse::new_dynamic_connect_success( + return Ok(Some(Response::new_dynamic_connect_success( request_id, reply_to, existing_ips, + client_version, ))); } let Some(new_ips) = self.connected_clients.find_new_ip() else { log::info!("No available IP address"); - return Ok(Some(IpPacketResponse::new_dynamic_connect_failure( + return Ok(Some(Response::new_dynamic_connect_failure( request_id, reply_to, DynamicConnectFailureReason::NoAvailableIp, + client_version, ))); }; @@ -428,6 +561,7 @@ impl MixnetListener { reply_to, reply_to_hops, buffer_timeout, + client_version, self.mixnet_client.split_sender(), ); @@ -440,17 +574,28 @@ impl MixnetListener { close_tx, handle, ); - Ok(Some(IpPacketResponse::new_dynamic_connect_success( - request_id, reply_to, new_ips, + Ok(Some(Response::new_dynamic_connect_success( + request_id, + reply_to, + new_ips, + client_version, ))) } - fn on_disconnect_request(&self, _disconnect_request: DisconnectRequest) -> PacketHandleResult { + fn on_disconnect_request( + &self, + _disconnect_request: DisconnectRequest, + _client_version: SupportedClientVersion, + ) -> PacketHandleResult { log::info!("Received disconnect request: not implemented, dropping"); Ok(None) } - async fn handle_packet(&mut self, ip_packet: &Bytes) -> PacketHandleResult { + async fn handle_packet( + &mut self, + ip_packet: &Bytes, + client_version: SupportedClientVersion, + ) -> PacketHandleResult { log::trace!("Received data request"); // We don't forward packets that we are not able to parse. BUT, there might be a good @@ -487,12 +632,13 @@ impl MixnetListener { Ok(None) } else { log::info!("Denied filter check: {dst}"); - Ok(Some(IpPacketResponse::new_data_info_response( + Ok(Some(Response::new_data_info_response( connected_client.nym_address, InfoResponseReply::ExitPolicyFilterCheckFailed { dst: dst.to_string(), }, InfoLevel::Warn, + client_version, ))) } } else { @@ -505,13 +651,14 @@ impl MixnetListener { async fn on_data_request( &mut self, data_request: DataRequest, + client_version: SupportedClientVersion, ) -> Result> { let mut responses = Vec::new(); let mut decoder = MultiIpPacketCodec::new(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); let mut bytes = BytesMut::new(); bytes.extend_from_slice(&data_request.ip_packets); while let Ok(Some(packet)) = decoder.decode(&mut bytes) { - let result = self.handle_packet(&packet).await; + let result = self.handle_packet(&packet, client_version).await; responses.push(result); } Ok(responses) @@ -519,26 +666,13 @@ impl MixnetListener { fn on_version_mismatch( &self, - version: u8, - reconstructed: &ReconstructedMessage, + _version: u8, + _reconstructed: &ReconstructedMessage, ) -> PacketHandleResult { - // If it's possible to parse, do so and return back a response, otherwise just drop - let (id, recipient) = - v6::request::IpPacketRequest::from_reconstructed_message(reconstructed) - .ok() - .and_then(|request| { - request - .recipient() - .map(|recipient| (request.id().unwrap_or(0), *recipient)) - }) - .ok_or(IpPacketRouterError::InvalidPacketVersion(version))?; - - Ok(Some(IpPacketResponse::new_version_mismatch( - id, - recipient, - version, - nym_ip_packet_requests::CURRENT_VERSION, - ))) + // Just drop it. In the future we might want to return a response here, if for example + // the client is connecting with a version that is older than the currently supported + // ones. + Ok(None) } async fn on_reconstructed_message( @@ -550,7 +684,7 @@ impl MixnetListener { reconstructed.sender_tag ); - let request = match deserialize_request(&reconstructed) { + let (request, client_version) = match deserialize_request(&reconstructed) { Err(IpPacketRouterError::InvalidPacketVersion(version)) => { return Ok(vec![self.on_version_mismatch(version, &reconstructed)]); } @@ -559,21 +693,31 @@ impl MixnetListener { match request.data { IpPacketRequestData::StaticConnect(signed_connect_request) => { - verify_signed_request(&signed_connect_request)?; + verify_signed_request(&signed_connect_request, client_version)?; let connect_request = signed_connect_request.request; - Ok(vec![self.on_static_connect_request(connect_request).await]) + Ok(vec![ + self.on_static_connect_request(connect_request, client_version) + .await, + ]) } IpPacketRequestData::DynamicConnect(signed_connect_request) => { - verify_signed_request(&signed_connect_request)?; + verify_signed_request(&signed_connect_request, client_version)?; let connect_request = signed_connect_request.request; - Ok(vec![self.on_dynamic_connect_request(connect_request).await]) + Ok(vec![ + self.on_dynamic_connect_request(connect_request, client_version) + .await, + ]) } IpPacketRequestData::Disconnect(signed_disconnect_request) => { - verify_signed_request(&signed_disconnect_request)?; + verify_signed_request(&signed_disconnect_request, client_version)?; let disconnect_request = signed_disconnect_request.request; - Ok(vec![self.on_disconnect_request(disconnect_request)]) + Ok(vec![ + self.on_disconnect_request(disconnect_request, client_version) + ]) + } + IpPacketRequestData::Data(data_request) => { + self.on_data_request(data_request, client_version).await } - IpPacketRequestData::Data(data_request) => self.on_data_request(data_request).await, IpPacketRequestData::Ping(_) => { log::info!("Received ping request: not implemented, dropping"); Ok(vec![]) @@ -605,16 +749,13 @@ impl MixnetListener { // When an incoming mixnet message triggers a response that we send back, such as during // connect handshake. - async fn handle_response(&self, response: IpPacketResponse) -> Result<()> { + async fn handle_response(&self, response: Response) -> Result<()> { let Some(recipient) = response.recipient() else { log::error!("No recipient in response packet, this should NOT happen!"); return Err(IpPacketRouterError::NoRecipientInResponse); }; - let response_packet = response.to_bytes().map_err(|err| { - log::error!("Failed to serialize response packet"); - IpPacketRouterError::FailedToSerializeResponsePacket { source: err } - })?; + let response_packet = response.to_bytes()?; // We could avoid this lookup if we check this when we create the response. let mix_hops = if let Some(c) = self @@ -687,33 +828,65 @@ impl MixnetListener { } } -fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result { +fn deserialize_request( + reconstructed: &ReconstructedMessage, +) -> Result<(IpPacketRequest, SupportedClientVersion)> { let request_version = *reconstructed .message .first() .ok_or(IpPacketRouterError::EmptyPacket)?; // Check version of the request and convert to the latest version if necessary - match request_version { - 6 => v6::request::IpPacketRequest::from_reconstructed_message(reconstructed) - .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err }) - .map(|r| r.into()), - 7 => v7::request::IpPacketRequest::from_reconstructed_message(reconstructed) - .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err }), + let request = match request_version { + 6 => nym_ip_packet_requests::v6::request::IpPacketRequest::from_reconstructed_message( + reconstructed, + ) + .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err }) + .map(|r| r.into()), + 7 => nym_ip_packet_requests::v7::request::IpPacketRequest::from_reconstructed_message( + reconstructed, + ) + .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err }), _ => { log::info!("Received packet with invalid version: v{request_version}"); Err(IpPacketRouterError::InvalidPacketVersion(request_version)) } + }; + + let Some(request_version) = SupportedClientVersion::new(request_version) else { + return Err(IpPacketRouterError::InvalidPacketVersion(request_version)); + }; + + // Tag the request with the version of the request + request.map(|r| (r, request_version)) +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum SupportedClientVersion { + V6, + V7, +} + +impl SupportedClientVersion { + fn new(request_version: u8) -> Option { + match request_version { + 6 => Some(SupportedClientVersion::V6), + 7 => Some(SupportedClientVersion::V7), + _ => None, + } } } -fn verify_signed_request(request: &impl SignedRequest) -> Result<()> { +fn verify_signed_request( + request: &impl SignedRequest, + client_version: SupportedClientVersion, +) -> Result<()> { if let Err(err) = request.verify() { - // Once we start to require clients to send v7 requests, we will enfore checking - // signatures. Until then, we only check if they are present. - if !matches!(err, SignatureError::MissingSignature) { - return Err(IpPacketRouterError::FailedToVerifyRequest { source: err }); + // If the client is V6, we don't care about missing signature + if client_version == SupportedClientVersion::V6 { + return Ok(()); } + return Err(IpPacketRouterError::FailedToVerifyRequest { source: err }); } Ok(()) }