diff --git a/Cargo.lock b/Cargo.lock index d9ee2ed197..dac0f0ff20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5955,7 +5955,6 @@ dependencies = [ "reqwest 0.12.4", "serde", "serde_json", - "tap", "thiserror 2.0.11", "time", "tokio", @@ -6289,7 +6288,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "1.0.0-rc.8" +version = "1.0.2" dependencies = [ "ammonia", "anyhow", diff --git a/common/ip-packet-requests/src/lib.rs b/common/ip-packet-requests/src/lib.rs index 1da258c256..977a277446 100644 --- a/common/ip-packet-requests/src/lib.rs +++ b/common/ip-packet-requests/src/lib.rs @@ -2,24 +2,18 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::net::{Ipv4Addr, Ipv6Addr}; -// The current version of the protocol. -// The idea here is that we add new request response types at least one version before we start -// using them. -// Also, depending on the version in the client connect message the IPR could respond with a -// matching older version. -pub use v6::request; -pub use v6::response; - pub mod codec; +pub mod sign; pub mod v6; pub mod v7; +pub mod v8; // version 3: initial version // version 4: IPv6 support // version 5: Add severity level to info response // version 6: Increase the available IPs // version 7: Add signature support (for the future) -pub const CURRENT_VERSION: u8 = 6; +// version 8: Anonymous sends #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct IpPair { @@ -45,3 +39,9 @@ fn make_bincode_serializer() -> impl bincode::Options { .with_big_endian() .with_varint_encoding() } + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} diff --git a/common/ip-packet-requests/src/v7/signature.rs b/common/ip-packet-requests/src/sign.rs similarity index 62% rename from common/ip-packet-requests/src/v7/signature.rs rename to common/ip-packet-requests/src/sign.rs index e11de693b8..e678a9dc90 100644 --- a/common/ip-packet-requests/src/v7/signature.rs +++ b/common/ip-packet-requests/src/sign.rs @@ -1,6 +1,7 @@ use std::time::Duration; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::ed25519; +use time::OffsetDateTime; // For reply protection, if a request is older than this, it will be rejected const MAX_REQUEST_AGE: Duration = Duration::from_secs(10); @@ -22,29 +23,37 @@ pub enum SignatureError { #[error("signature verification failed")] VerificationFailed { message: String, - error: identity::SignatureError, + error: ed25519::SignatureError, }, } pub trait SignedRequest { - fn identity(&self) -> &identity::PublicKey; + fn identity(&self) -> Option<&ed25519::PublicKey>; - fn request(&self) -> Result, SignatureError>; + fn request_as_bytes(&self) -> Result, SignatureError>; - fn signature(&self) -> Option<&identity::Signature>; + fn signature(&self) -> Option<&ed25519::Signature>; - fn timestamp(&self) -> time::OffsetDateTime; + fn timestamp(&self) -> OffsetDateTime; fn verify(&self) -> Result<(), SignatureError> { + let identity = match self.identity() { + Some(identity) => identity, + None => { + // If we are not revealing our identity, we don't need to verify anything + return Ok(()); + } + }; + if let Some(signature) = self.signature() { // First check that the request is recent enough - if time::OffsetDateTime::now_utc() - self.timestamp() > MAX_REQUEST_AGE { + if OffsetDateTime::now_utc() - self.timestamp() > MAX_REQUEST_AGE { return Err(SignatureError::RequestOutOfDate); } - let request_as_bytes = self.request()?; + let request_as_bytes = self.request_as_bytes()?; - self.identity() + identity .verify(request_as_bytes, signature) .map_err(|error| SignatureError::VerificationFailed { message: "signature verification failed".to_string(), diff --git a/common/ip-packet-requests/src/v6/conversion.rs b/common/ip-packet-requests/src/v6/conversion.rs deleted file mode 100644 index be2327b2e6..0000000000 --- a/common/ip-packet-requests/src/v6/conversion.rs +++ /dev/null @@ -1,69 +0,0 @@ -// 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 ae7394c02d..8f3f1e3955 100644 --- a/common/ip-packet-requests/src/v6/mod.rs +++ b/common/ip-packet-requests/src/v6/mod.rs @@ -1,4 +1,3 @@ -pub mod conversion; pub mod request; pub mod response; diff --git a/common/ip-packet-requests/src/v7/conversion.rs b/common/ip-packet-requests/src/v7/conversion.rs deleted file mode 100644 index 04b0244b05..0000000000 --- a/common/ip-packet-requests/src/v7/conversion.rs +++ /dev/null @@ -1,125 +0,0 @@ -use time::OffsetDateTime; - -use crate::{v6, v7}; - -impl From for v7::request::IpPacketRequest { - fn from(ip_packet_request: v6::request::IpPacketRequest) -> Self { - Self { - version: 7, - data: ip_packet_request.data.into(), - } - } -} - -impl From for v7::request::IpPacketRequestData { - fn from(ip_packet_request_data: v6::request::IpPacketRequestData) -> Self { - match ip_packet_request_data { - v6::request::IpPacketRequestData::StaticConnect(r) => { - v7::request::IpPacketRequestData::StaticConnect( - v7::request::SignedStaticConnectRequest { - request: r.into(), - signature: None, - }, - ) - } - v6::request::IpPacketRequestData::DynamicConnect(r) => { - v7::request::IpPacketRequestData::DynamicConnect( - v7::request::SignedDynamicConnectRequest { - request: r.into(), - signature: None, - }, - ) - } - v6::request::IpPacketRequestData::Disconnect(r) => { - v7::request::IpPacketRequestData::Disconnect(v7::request::SignedDisconnectRequest { - request: r.into(), - signature: None, - }) - } - v6::request::IpPacketRequestData::Data(r) => { - v7::request::IpPacketRequestData::Data(r.into()) - } - v6::request::IpPacketRequestData::Ping(r) => { - v7::request::IpPacketRequestData::Ping(r.into()) - } - v6::request::IpPacketRequestData::Health(r) => { - v7::request::IpPacketRequestData::Health(r.into()) - } - } - } -} - -impl From for v7::request::StaticConnectRequest { - fn from(static_connect_request: v6::request::StaticConnectRequest) -> Self { - Self { - request_id: static_connect_request.request_id, - ips: static_connect_request.ips, - reply_to: static_connect_request.reply_to, - reply_to_hops: static_connect_request.reply_to_hops, - reply_to_avg_mix_delays: static_connect_request.reply_to_avg_mix_delays, - buffer_timeout: static_connect_request.buffer_timeout, - timestamp: OffsetDateTime::now_utc(), - } - } -} - -#[allow(deprecated)] -impl From for v7::request::DynamicConnectRequest { - fn from(dynamic_connect_request: v6::request::DynamicConnectRequest) -> Self { - Self { - request_id: dynamic_connect_request.request_id, - reply_to: dynamic_connect_request.reply_to, - reply_to_hops: dynamic_connect_request.reply_to_hops, - reply_to_avg_mix_delays: dynamic_connect_request.reply_to_avg_mix_delays, - buffer_timeout: dynamic_connect_request.buffer_timeout, - timestamp: OffsetDateTime::now_utc(), - } - } -} - -impl From for v7::request::SignedDisconnectRequest { - fn from(disconnect_request: v6::request::DisconnectRequest) -> Self { - Self { - request: disconnect_request.into(), - signature: None, - } - } -} - -impl From for v7::request::DisconnectRequest { - fn from(disconnect_request: v6::request::DisconnectRequest) -> Self { - Self { - request_id: disconnect_request.request_id, - reply_to: disconnect_request.reply_to, - timestamp: OffsetDateTime::now_utc(), - } - } -} - -impl From for v7::request::DataRequest { - fn from(data_request: v6::request::DataRequest) -> Self { - Self { - ip_packets: data_request.ip_packets, - } - } -} - -impl From for v7::request::PingRequest { - fn from(ping_request: v6::request::PingRequest) -> Self { - Self { - request_id: ping_request.request_id, - reply_to: ping_request.reply_to, - timestamp: OffsetDateTime::now_utc(), - } - } -} - -impl From for v7::request::HealthRequest { - fn from(health_request: v6::request::HealthRequest) -> Self { - Self { - request_id: health_request.request_id, - reply_to: health_request.reply_to, - timestamp: OffsetDateTime::now_utc(), - } - } -} diff --git a/common/ip-packet-requests/src/v7/mod.rs b/common/ip-packet-requests/src/v7/mod.rs index e0e61133f1..f15da6711a 100644 --- a/common/ip-packet-requests/src/v7/mod.rs +++ b/common/ip-packet-requests/src/v7/mod.rs @@ -1,6 +1,4 @@ -pub mod conversion; pub mod request; pub mod response; -pub mod signature; pub const VERSION: u8 = 7; diff --git a/common/ip-packet-requests/src/v7/request.rs b/common/ip-packet-requests/src/v7/request.rs index 43b71bb362..2313c16942 100644 --- a/common/ip-packet-requests/src/v7/request.rs +++ b/common/ip-packet-requests/src/v7/request.rs @@ -1,22 +1,18 @@ +use std::fmt; + use nym_crypto::asymmetric::identity; use nym_sphinx::addressing::clients::Recipient; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use crate::{make_bincode_serializer, IpPair}; - -use super::{ - signature::{SignatureError, SignedRequest}, - VERSION, +use crate::{ + sign::{SignatureError, SignedRequest}, + IpPair, }; -fn generate_random() -> u64 { - use rand::RngCore; - let mut rng = rand::rngs::OsRng; - rng.next_u64() -} +use super::VERSION; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct IpPacketRequest { pub version: u8, pub data: IpPacketRequestData, @@ -30,7 +26,7 @@ impl IpPacketRequest { reply_to_avg_mix_delays: Option, buffer_timeout: Option, ) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -58,7 +54,7 @@ impl IpPacketRequest { reply_to_avg_mix_delays: Option, buffer_timeout: Option, ) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -79,7 +75,7 @@ impl IpPacketRequest { } pub fn new_disconnect_request(reply_to: Recipient) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -104,7 +100,7 @@ impl IpPacketRequest { } pub fn new_ping(reply_to: Recipient) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -119,7 +115,7 @@ impl IpPacketRequest { } pub fn new_health_request(reply_to: Recipient) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -155,16 +151,27 @@ impl IpPacketRequest { } } + pub fn verify(&self) -> Result<(), SignatureError> { + match &self.data { + IpPacketRequestData::StaticConnect(request) => request.verify(), + IpPacketRequestData::DynamicConnect(request) => request.verify(), + IpPacketRequestData::Disconnect(request) => request.verify(), + IpPacketRequestData::Data(_) => Ok(()), + IpPacketRequestData::Ping(_) => Ok(()), + IpPacketRequestData::Health(_) => Ok(()), + } + } + pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - make_bincode_serializer().serialize(self) + crate::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) + crate::make_bincode_serializer().deserialize(&message.message) } } @@ -179,6 +186,19 @@ pub enum IpPacketRequestData { Health(HealthRequest), } +impl fmt::Display for IpPacketRequestData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpPacketRequestData::StaticConnect(_) => write!(f, "StaticConnect"), + IpPacketRequestData::DynamicConnect(_) => write!(f, "DynamicConnect"), + IpPacketRequestData::Disconnect(_) => write!(f, "Disconnect"), + IpPacketRequestData::Data(_) => write!(f, "Data"), + IpPacketRequestData::Ping(_) => write!(f, "Ping"), + IpPacketRequestData::Health(_) => write!(f, "Health"), + } + } +} + impl IpPacketRequestData { pub fn add_signature(&mut self, signature: identity::Signature) -> Option { match self { @@ -202,9 +222,9 @@ impl IpPacketRequestData { 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::StaticConnect(request) => Some(request.request_as_bytes()), + IpPacketRequestData::DynamicConnect(request) => Some(request.request_as_bytes()), + IpPacketRequestData::Disconnect(request) => Some(request.request_as_bytes()), IpPacketRequestData::Data(_) => None, IpPacketRequestData::Ping(_) => None, IpPacketRequestData::Health(_) => None, @@ -242,7 +262,7 @@ pub struct StaticConnectRequest { impl StaticConnectRequest { pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - make_bincode_serializer().serialize(self) + crate::make_bincode_serializer().serialize(self) } } @@ -253,11 +273,11 @@ pub struct SignedStaticConnectRequest { } impl SignedRequest for SignedStaticConnectRequest { - fn identity(&self) -> &identity::PublicKey { - self.request.reply_to.identity() + fn identity(&self) -> Option<&identity::PublicKey> { + Some(self.request.reply_to.identity()) } - fn request(&self) -> Result, SignatureError> { + fn request_as_bytes(&self) -> Result, SignatureError> { self.request .to_bytes() .map_err(|error| SignatureError::RequestSerializationError { @@ -306,7 +326,7 @@ pub struct DynamicConnectRequest { impl DynamicConnectRequest { pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - make_bincode_serializer().serialize(self) + crate::make_bincode_serializer().serialize(self) } } @@ -317,11 +337,11 @@ pub struct SignedDynamicConnectRequest { } impl SignedRequest for SignedDynamicConnectRequest { - fn identity(&self) -> &identity::PublicKey { - self.request.reply_to.identity() + fn identity(&self) -> Option<&identity::PublicKey> { + Some(self.request.reply_to.identity()) } - fn request(&self) -> Result, SignatureError> { + fn request_as_bytes(&self) -> Result, SignatureError> { self.request .to_bytes() .map_err(|error| SignatureError::RequestSerializationError { @@ -355,7 +375,7 @@ pub struct DisconnectRequest { impl DisconnectRequest { pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - make_bincode_serializer().serialize(self) + crate::make_bincode_serializer().serialize(self) } } @@ -366,11 +386,11 @@ pub struct SignedDisconnectRequest { } impl SignedRequest for SignedDisconnectRequest { - fn identity(&self) -> &identity::PublicKey { - self.request.reply_to.identity() + fn identity(&self) -> Option<&identity::PublicKey> { + Some(self.request.reply_to.identity()) } - fn request(&self) -> Result, SignatureError> { + fn request_as_bytes(&self) -> Result, SignatureError> { self.request .to_bytes() .map_err(|error| SignatureError::RequestSerializationError { diff --git a/common/ip-packet-requests/src/v8/mod.rs b/common/ip-packet-requests/src/v8/mod.rs new file mode 100644 index 0000000000..70d1147b3a --- /dev/null +++ b/common/ip-packet-requests/src/v8/mod.rs @@ -0,0 +1,4 @@ +pub mod request; +pub mod response; + +pub const VERSION: u8 = 8; diff --git a/common/ip-packet-requests/src/v8/request.rs b/common/ip-packet-requests/src/v8/request.rs new file mode 100644 index 0000000000..15cf47827f --- /dev/null +++ b/common/ip-packet-requests/src/v8/request.rs @@ -0,0 +1,536 @@ +use std::{fmt, sync::Arc}; + +use nym_crypto::asymmetric::ed25519; +use nym_sphinx::addressing::Recipient; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use crate::{ + sign::{SignatureError, SignedRequest}, + IpPair, +}; + +use super::VERSION; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct IpPacketRequest { + pub version: u8, + pub data: IpPacketRequestData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum IpPacketRequestData { + Data(DataRequest), + Control(Box), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum ControlRequest { + StaticConnect(SignedStaticConnectRequest), + DynamicConnect(SignedDynamicConnectRequest), + Disconnect(SignedDisconnectRequest), + Ping(PingRequest), + Health(HealthRequest), +} + +// A data request is when the client wants to send an IP packet to a destination. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DataRequest { + pub ip_packets: bytes::Bytes, +} + +// A static connect request is when the client provides the internal IP address it will use on the +// ip packet router. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct StaticConnectRequest { + pub request_id: u64, + + // The requested internal IP addresses. + pub ips: IpPair, + + // The maximum time in milliseconds the IPR should wait when filling up a mix packet + // with ip packets. + pub buffer_timeout: Option, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, + + pub sender: SentBy, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedStaticConnectRequest { + pub request: StaticConnectRequest, + pub signature: Option, +} + +// A dynamic connect request is when the client does not provide the internal IP address it will use +// on the ip packet router, and instead requests one to be assigned to it. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DynamicConnectRequest { + pub request_id: u64, + + // The maximum time in milliseconds the IPR should wait when filling up a mix packet + // with ip packets. + pub buffer_timeout: Option, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, + + pub sender: SentBy, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedDynamicConnectRequest { + pub request: DynamicConnectRequest, + pub signature: Option, +} + +// A disconnect request is when the client wants to disconnect from the ip packet router and free +// up the allocated IP address. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DisconnectRequest { + pub request_id: u64, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, + + pub sender: SentBy, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedDisconnectRequest { + pub request: DisconnectRequest, + pub signature: Option, +} + +// A ping request is when the client wants to check if the ip packet router is still alive. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PingRequest { + pub request_id: u64, + + pub sender: SentBy, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct HealthRequest { + pub request_id: u64, + + pub sender: SentBy, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +// When constructing the request, use this return address. It has the keypair to be able to sign +// the request if we reveal the sender. +#[derive(Clone, Debug)] +pub enum ReturnAddress { + AnonymousSenderTag, + NymAddress { + reply_to: Box, + signing_keypair: Arc, + }, +} + +// The serialized sender field in the request, that does not contain the keypair. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum SentBy { + AnonymousSenderTag, + NymAddress(Box), +} + +impl IpPacketRequest { + pub fn new_connect_request( + ips: Option, + buffer_timeout: Option, + return_address: ReturnAddress, + ) -> Result<(Self, u64), SignatureError> { + let request_id = rand::random(); + let timestamp = OffsetDateTime::now_utc(); + let sender = return_address.clone().into(); + let request = if let Some(ips) = ips { + let request = StaticConnectRequest { + request_id, + ips, + buffer_timeout, + timestamp, + sender, + }; + let signature = return_address + .signing_key() + .map(|keypair| { + request + .to_bytes() + .map(|bytes| keypair.private_key().sign(bytes)) + }) + .transpose()?; + ControlRequest::StaticConnect(SignedStaticConnectRequest { request, signature }) + } else { + let request = DynamicConnectRequest { + request_id, + buffer_timeout, + timestamp, + sender, + }; + let signature = return_address + .signing_key() + .map(|keypair| { + request + .to_bytes() + .map(|bytes| keypair.private_key().sign(bytes)) + }) + .transpose()?; + ControlRequest::DynamicConnect(SignedDynamicConnectRequest { request, signature }) + }; + let request = Self { + version: VERSION, + data: IpPacketRequestData::Control(Box::new(request)), + }; + Ok((request, request_id)) + } + + pub fn new_disconnect_request( + return_address: ReturnAddress, + ) -> Result<(Self, u64), SignatureError> { + let request_id = rand::random(); + let timestamp = OffsetDateTime::now_utc(); + let sender = return_address.clone().into(); + let request = DisconnectRequest { + request_id, + timestamp, + sender, + }; + let signature = return_address + .signing_key() + .map(|keypair| { + request + .to_bytes() + .map(|bytes| keypair.private_key().sign(bytes)) + }) + .transpose()?; + let request = Self { + version: VERSION, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Disconnect( + SignedDisconnectRequest { request, signature }, + ))), + }; + Ok((request, request_id)) + } + + pub fn new_data_request(ip_packets: bytes::Bytes) -> Self { + Self { + version: VERSION, + data: IpPacketRequestData::Data(DataRequest { ip_packets }), + } + } + + pub fn new_ping(return_address: ReturnAddress) -> (Self, u64) { + let request_id = rand::random(); + let timestamp = OffsetDateTime::now_utc(); + let sender = return_address.into(); + let ping_request = PingRequest { + request_id, + sender, + timestamp, + }; + let request = Self { + version: VERSION, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Ping(ping_request))), + }; + (request, request_id) + } + + pub fn new_health_request(return_address: ReturnAddress) -> (Self, u64) { + let request_id = rand::random(); + let timestamp = OffsetDateTime::now_utc(); + let sender = return_address.into(); + let health_request = HealthRequest { + request_id, + sender, + timestamp, + }; + let request = Self { + version: VERSION, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Health(health_request))), + }; + (request, request_id) + } + + pub fn id(&self) -> Option { + match self.data { + IpPacketRequestData::Control(ref c) => Some(c.id()), + IpPacketRequestData::Data(_) => None, + } + } + + pub fn verify(&self) -> Result<(), SignatureError> { + match &self.data { + IpPacketRequestData::Control(c) => c.verify(), + IpPacketRequestData::Data(_) => Ok(()), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + crate::make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + crate::make_bincode_serializer().deserialize(&message.message) + } +} + +impl fmt::Display for IpPacketRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "IpPacketRequest {{ version: {}, data: {} }}", + self.version, self.data + ) + } +} + +impl fmt::Display for IpPacketRequestData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpPacketRequestData::Data(_) => write!(f, "Data"), + IpPacketRequestData::Control(c) => write!(f, "Control({})", c), + } + } +} + +impl ControlRequest { + fn id(&self) -> u64 { + match self { + ControlRequest::StaticConnect(request) => request.request.request_id, + ControlRequest::DynamicConnect(request) => request.request.request_id, + ControlRequest::Disconnect(request) => request.request.request_id, + ControlRequest::Ping(request) => request.request_id, + ControlRequest::Health(request) => request.request_id, + } + } + + fn verify(&self) -> Result<(), SignatureError> { + match self { + ControlRequest::StaticConnect(request) => request.verify(), + ControlRequest::DynamicConnect(request) => request.verify(), + ControlRequest::Disconnect(request) => request.verify(), + ControlRequest::Ping(_) => Ok(()), + ControlRequest::Health(_) => Ok(()), + } + } +} + +impl fmt::Display for ControlRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ControlRequest::StaticConnect(_) => write!(f, "StaticConnect"), + ControlRequest::DynamicConnect(_) => write!(f, "DynamicConnect"), + ControlRequest::Disconnect(_) => write!(f, "Disconnect"), + ControlRequest::Ping(_) => write!(f, "Ping"), + ControlRequest::Health(_) => write!(f, "Health"), + } + } +} + +impl StaticConnectRequest { + pub fn to_bytes(&self) -> Result, SignatureError> { + use bincode::Options; + crate::make_bincode_serializer() + .serialize(self) + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } +} + +impl SignedRequest for SignedStaticConnectRequest { + fn request_as_bytes(&self) -> Result, SignatureError> { + self.request.to_bytes() + } + + fn timestamp(&self) -> OffsetDateTime { + self.request.timestamp + } + + fn identity(&self) -> Option<&ed25519::PublicKey> { + self.request.sender.identity() + } + + fn signature(&self) -> Option<&ed25519::Signature> { + self.signature.as_ref() + } +} + +impl DynamicConnectRequest { + pub fn to_bytes(&self) -> Result, SignatureError> { + use bincode::Options; + crate::make_bincode_serializer() + .serialize(self) + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } +} + +impl SignedRequest for SignedDynamicConnectRequest { + fn request_as_bytes(&self) -> Result, SignatureError> { + self.request.to_bytes() + } + + fn timestamp(&self) -> OffsetDateTime { + self.request.timestamp + } + + fn identity(&self) -> Option<&ed25519::PublicKey> { + self.request.sender.identity() + } + + fn signature(&self) -> Option<&ed25519::Signature> { + self.signature.as_ref() + } +} + +impl DisconnectRequest { + pub fn to_bytes(&self) -> Result, SignatureError> { + use bincode::Options; + crate::make_bincode_serializer() + .serialize(self) + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } +} + +impl SignedRequest for SignedDisconnectRequest { + fn request_as_bytes(&self) -> Result, SignatureError> { + self.request.to_bytes() + } + + fn timestamp(&self) -> OffsetDateTime { + self.request.timestamp + } + + fn identity(&self) -> Option<&ed25519::PublicKey> { + self.request.sender.identity() + } + + fn signature(&self) -> Option<&ed25519::Signature> { + self.signature.as_ref() + } +} + +impl SentBy { + fn identity(&self) -> Option<&ed25519::PublicKey> { + match self { + SentBy::AnonymousSenderTag => None, + SentBy::NymAddress(recipient) => Some(recipient.identity()), + } + } +} + +impl From for SentBy { + fn from(recipient: Recipient) -> Self { + SentBy::NymAddress(Box::new(recipient)) + } +} + +impl ReturnAddress { + fn signing_key(&self) -> Option<&ed25519::KeyPair> { + match self { + ReturnAddress::AnonymousSenderTag => None, + ReturnAddress::NymAddress { + signing_keypair, .. + } => Some(signing_keypair.as_ref()), + } + } +} + +impl From for SentBy { + fn from(return_address: ReturnAddress) -> Self { + match return_address { + ReturnAddress::AnonymousSenderTag => SentBy::AnonymousSenderTag, + ReturnAddress::NymAddress { reply_to, .. } => SentBy::NymAddress(reply_to), + } + } +} + +#[cfg(test)] +mod tests { + use time::macros::datetime; + + use super::*; + use std::net::{Ipv4Addr, Ipv6Addr}; + use std::str::FromStr; + + #[test] + fn check_size_of_request() { + let connect = IpPacketRequest { + version: 4, + data: IpPacketRequestData::Control(Box::new(ControlRequest::StaticConnect( + SignedStaticConnectRequest { + request: StaticConnectRequest { + request_id: 123, + ips: IpPair::new( + Ipv4Addr::from_str("10.0.0.1").unwrap(), + Ipv6Addr::from_str("fc00::1").unwrap(), + ), + buffer_timeout: None, + timestamp: datetime!(2024-01-01 12:59:59.5 UTC), + sender: SentBy::AnonymousSenderTag, + }, + signature: None, + }, + ))), + }; + assert_eq!(connect.to_bytes().unwrap().len(), 42); + } + + #[test] + fn check_size_of_data() { + let data = IpPacketRequest { + version: 4, + data: IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1u8; 32]), + }), + }; + assert_eq!(data.to_bytes().unwrap().len(), 35); + } + + #[test] + fn serialize_and_deserialize_data_request() { + let data = IpPacketRequest { + version: 4, + data: IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]), + }), + }; + + let serialized = data.to_bytes().unwrap(); + let deserialized = IpPacketRequest::from_reconstructed_message( + &nym_sphinx::receiver::ReconstructedMessage { + message: serialized, + sender_tag: None, + }, + ) + .unwrap(); + + assert_eq!(deserialized.version, 4); + assert_eq!( + deserialized.data, + IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]), + }) + ); + } +} diff --git a/common/ip-packet-requests/src/v8/response.rs b/common/ip-packet-requests/src/v8/response.rs new file mode 100644 index 0000000000..a919d2110f --- /dev/null +++ b/common/ip-packet-requests/src/v8/response.rs @@ -0,0 +1,259 @@ +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, IpPair}; + +use super::VERSION; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct IpPacketResponse { + pub version: u8, + pub data: IpPacketResponseData, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum IpPacketResponseData { + Data(DataResponse), + Control(Box), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DataResponse { + pub ip_packet: bytes::Bytes, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ControlResponse { + // Response for a static connect request + StaticConnect(StaticConnectResponse), + + // Response for a dynamic connect request + DynamicConnect(DynamicConnectResponse), + + // Response for a disconnect initiqated by the client + Disconnect(DisconnectResponse), + + // Message from the server that the client got disconnected without the client initiating it + UnrequestedDisconnect(UnrequestedDisconnect), + + // Response to ping request + Pong(PongResponse), + + // Response for a health request + Health(Box), + + // Info response. This can be anything from informative messages to errors + Info(InfoResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StaticConnectResponse { + pub request_id: u64, + pub reply: StaticConnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum StaticConnectResponseReply { + Success, + Failure(StaticConnectFailureReason), +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum StaticConnectFailureReason { + #[error("requested ip address is already in use")] + RequestedIpAlreadyInUse, + + #[error("client is already connected")] + ClientAlreadyConnected, + + #[error("request timestamp is out of date")] + OutOfDateTimestamp, + + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DynamicConnectResponse { + pub request_id: u64, + pub reply: DynamicConnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum DynamicConnectResponseReply { + Success(DynamicConnectSuccess), + Failure(DynamicConnectFailureReason), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DynamicConnectSuccess { + pub ips: IpPair, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum DynamicConnectFailureReason { + #[error("client is already connected")] + ClientAlreadyConnected, + + #[error("no available ip address")] + NoAvailableIp, + + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DisconnectResponse { + pub request_id: u64, + pub reply: DisconnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum DisconnectResponseReply { + Success, + Failure(DisconnectFailureReason), +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum DisconnectFailureReason { + #[error("client is not connected")] + ClientNotConnected, + + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UnrequestedDisconnect { + pub reason: UnrequestedDisconnectReason, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum UnrequestedDisconnectReason { + #[error("client mixnet traffic timeout")] + ClientMixnetTrafficTimeout, + + #[error("client tun traffic timeout")] + ClientTunTrafficTimeout, + + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PongResponse { + pub request_id: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HealthResponse { + pub request_id: u64, + pub reply: HealthResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HealthResponseReply { + // Return the binary build information of the IPR + pub build_info: BinaryBuildInformationOwned, + + // Return if the IPR has performed a successful routing test. + pub routable: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InfoResponse { + pub request_id: u64, + pub reply: InfoResponseReply, + pub level: InfoLevel, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum InfoResponseReply { + #[error("{msg}")] + Generic { msg: String }, + + #[error( + "version mismatch: response is v{request_version} and response is v{response_version}" + )] + VersionMismatch { + request_version: u8, + response_version: u8, + }, + + #[error("destination failed exit policy filter check: {dst}")] + ExitPolicyFilterCheckFailed { dst: String }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum InfoLevel { + Info, + Warn, + Error, +} + +impl IpPacketResponse { + pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self { + Self { + version: VERSION, + data: IpPacketResponseData::Data(DataResponse { ip_packet }), + } + } + + pub fn id(&self) -> Option { + match &self.data { + IpPacketResponseData::Data(_) => None, + IpPacketResponseData::Control(response) => response.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) + } +} + +impl IpPacketResponseData { + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +impl ControlResponse { + fn id(&self) -> Option { + match self { + ControlResponse::StaticConnect(response) => Some(response.request_id), + ControlResponse::DynamicConnect(response) => Some(response.request_id), + ControlResponse::Disconnect(response) => Some(response.request_id), + ControlResponse::UnrequestedDisconnect(_) => None, + ControlResponse::Pong(response) => Some(response.request_id), + ControlResponse::Health(response) => Some(response.request_id), + ControlResponse::Info(response) => Some(response.request_id), + } + } +} + +impl StaticConnectResponseReply { + pub fn is_success(&self) -> bool { + match self { + StaticConnectResponseReply::Success => true, + StaticConnectResponseReply::Failure(_) => false, + } + } +} + +impl DynamicConnectResponseReply { + pub fn is_success(&self) -> bool { + match self { + DynamicConnectResponseReply::Success(_) => true, + DynamicConnectResponseReply::Failure(_) => false, + } + } +} diff --git a/deny.toml b/deny.toml index 4da5bf5c59..5bd402997e 100644 --- a/deny.toml +++ b/deny.toml @@ -122,6 +122,7 @@ exceptions = [ { allow = ["GPL-3.0"], crate = "nym-network-requester" }, { allow = ["GPL-3.0"], crate = "nym-node" }, { allow = ["GPL-3.0"], crate = "nym-validator-rewarder" }, + { allow = ["GPL-3.0"], crate = "nym-ip-packet-router" }, ] # Some crates don't have (easily) machine readable licensing information, diff --git a/sdk/rust/nym-sdk/src/mixnet/native_client.rs b/sdk/rust/nym-sdk/src/mixnet/native_client.rs index 33e0a7023b..fe542d9eb7 100644 --- a/sdk/rust/nym-sdk/src/mixnet/native_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/native_client.rs @@ -3,7 +3,7 @@ use crate::mixnet::traits::MixnetMessageSender; use crate::{Error, Result}; use async_trait::async_trait; use futures::{ready, Stream, StreamExt}; -use log::error; +use log::{debug, error}; use nym_client_core::client::base_client::GatewayConnection; use nym_client_core::client::mix_traffic::ClientRequestSender; use nym_client_core::client::{ @@ -12,7 +12,7 @@ use nym_client_core::client::{ received_buffer::ReconstructedMessagesReceiver, }; use nym_client_core::config::ForgetMe; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::ClientRequest; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::{params::PacketType, receiver::ReconstructedMessage}; @@ -32,7 +32,7 @@ pub struct MixnetClient { /// The nym address of this connected client. pub(crate) nym_address: Recipient, - pub(crate) identity_keys: Arc, + pub(crate) identity_keys: Arc, /// Input to the client from the users perspective. This can be either data to send or control /// messages. @@ -67,7 +67,7 @@ impl MixnetClient { #[allow(clippy::too_many_arguments)] pub(crate) fn new( nym_address: Recipient, - identity_keys: Arc, + identity_keys: Arc, client_input: ClientInput, client_output: ClientOutput, client_state: ClientState, @@ -125,8 +125,13 @@ impl MixnetClient { self.client_request_sender.clone() } + /// Get the client's identity keys. + pub fn identity_keypair(&self) -> Arc { + self.identity_keys.clone() + } + /// Sign a message with the client's private identity key. - pub fn sign(&self, data: &[u8]) -> identity::Signature { + pub fn sign(&self, data: &[u8]) -> ed25519::Signature { self.identity_keys.private_key().sign(data) } @@ -274,7 +279,9 @@ impl Stream for MixnetClient { } Poll::Ready(Some(next)) } else { - error!("the reconstructed messages vector is empty - please let the developers know if you see this message"); + // I *think* this happens for SURBs, but I'm not 100% sure. Nonetheless it's + // beneign, but let's log it here anyway as a reminder + debug!("the reconstructed messages vector is empty"); cx.waker().wake_by_ref(); Poll::Pending } diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index fb3e57378a..dcd3c8e31e 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -30,7 +30,9 @@ use nym_credential_verification::{ use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; -use nym_sdk::mixnet::{InputMessage, MixnetMessageSender, Recipient, TransmissionLane}; +use nym_sdk::mixnet::{ + AnonymousSenderTag, InputMessage, MixnetMessageSender, Recipient, TransmissionLane, +}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::TaskHandle; @@ -757,9 +759,13 @@ impl MixnetListener { } // When an incoming mixnet message triggers a response that we send back. - async fn handle_response(&self, response: Vec, recipient: Recipient) -> Result<()> { - let input_message = - InputMessage::new_regular(recipient, response, TransmissionLane::General, None); + async fn handle_response( + &self, + response: Vec, + recipient: Recipient, + sender_tag: Option, + ) -> Result<()> { + let input_message = create_input_message(recipient, sender_tag, response); self.mixnet_client .send(input_message) .await @@ -782,9 +788,10 @@ impl MixnetListener { } msg = self.mixnet_client.next() => { if let Some(msg) = msg { + let sender_tag = msg.sender_tag; match self.on_reconstructed_message(msg).await { Ok((response, recipient)) => { - if let Err(err) = self.handle_response(response, recipient).await { + if let Err(err) = self.handle_response(response, recipient, sender_tag).await { log::error!("Mixnet listener failed to handle response: {err}"); } } @@ -857,3 +864,19 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result, + response_packet: Vec, +) -> InputMessage { + 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 { + log::debug!("Creating message using nym_address"); + InputMessage::new_regular(nym_address, response_packet, lane, packet_type) + } +} diff --git a/service-providers/ip-packet-router/Cargo.toml b/service-providers/ip-packet-router/Cargo.toml index 9a322dcaec..fc7303a872 100644 --- a/service-providers/ip-packet-router/Cargo.toml +++ b/service-providers/ip-packet-router/Cargo.toml @@ -6,7 +6,7 @@ repository.workspace = true homepage.workspace = true documentation.workspace = true edition.workspace = true -license.workspace = true +license = "GPL-3.0" [dependencies] anyhow.workspace = true @@ -38,7 +38,6 @@ rand = { workspace = true } reqwest.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -tap.workspace = true thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } diff --git a/service-providers/ip-packet-router/src/cli/add_gateway.rs b/service-providers/ip-packet-router/src/cli/add_gateway.rs index 251e2cb1a3..ef23413c68 100644 --- a/service-providers/ip-packet-router/src/cli/add_gateway.rs +++ b/service-providers/ip-packet-router/src/cli/add_gateway.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_bin_common::output_format::OutputFormat; diff --git a/service-providers/ip-packet-router/src/cli/ecash/import_coin_index_signatures.rs b/service-providers/ip-packet-router/src/cli/ecash/import_coin_index_signatures.rs index a06f9e70b8..0a9a53dab5 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/import_coin_index_signatures.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/import_coin_index_signatures.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_import_coin_index_signatures::{ diff --git a/service-providers/ip-packet-router/src/cli/ecash/import_credential.rs b/service-providers/ip-packet-router/src/cli/ecash/import_credential.rs index d37a8fa066..85209b933e 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/import_credential.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/import_credential.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_import_credential::{ diff --git a/service-providers/ip-packet-router/src/cli/ecash/import_expiration_date_signatures.rs b/service-providers/ip-packet-router/src/cli/ecash/import_expiration_date_signatures.rs index 7c338d05cb..28ea667345 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/import_expiration_date_signatures.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/import_expiration_date_signatures.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_import_expiration_date_signatures::{ diff --git a/service-providers/ip-packet-router/src/cli/ecash/import_master_verification_key.rs b/service-providers/ip-packet-router/src/cli/ecash/import_master_verification_key.rs index 654be2ea18..bff9d3ea62 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/import_master_verification_key.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/import_master_verification_key.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_import_master_verification_key::{ diff --git a/service-providers/ip-packet-router/src/cli/ecash/mod.rs b/service-providers/ip-packet-router/src/cli/ecash/mod.rs index 33a4ddd667..e06a563f84 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/mod.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/mod.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use clap::{Args, Subcommand}; use nym_client_core::cli_helpers::client_import_coin_index_signatures::CommonClientImportCoinIndexSignaturesArgs; diff --git a/service-providers/ip-packet-router/src/cli/ecash/show_ticketbooks.rs b/service-providers/ip-packet-router/src/cli/ecash/show_ticketbooks.rs index b3abea30f7..0c9ebad722 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/show_ticketbooks.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/show_ticketbooks.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_bin_common::output_format::OutputFormat; diff --git a/service-providers/ip-packet-router/src/cli/list_gateways.rs b/service-providers/ip-packet-router/src/cli/list_gateways.rs index 3050233c87..8ddb9a985b 100644 --- a/service-providers/ip-packet-router/src/cli/list_gateways.rs +++ b/service-providers/ip-packet-router/src/cli/list_gateways.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_bin_common::output_format::OutputFormat; diff --git a/service-providers/ip-packet-router/src/cli/switch_gateway.rs b/service-providers/ip-packet-router/src/cli/switch_gateway.rs index 4a365a4bdb..cb53983fb8 100644 --- a/service-providers/ip-packet-router/src/cli/switch_gateway.rs +++ b/service-providers/ip-packet-router/src/cli/switch_gateway.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_switch_gateway::{ diff --git a/service-providers/ip-packet-router/src/clients/client_id.rs b/service-providers/ip-packet-router/src/clients/client_id.rs new file mode 100644 index 0000000000..038efbda76 --- /dev/null +++ b/service-providers/ip-packet-router/src/clients/client_id.rs @@ -0,0 +1,58 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::fmt; + +use nym_ip_packet_requests::v8::request::SentBy; +use nym_sdk::mixnet::{AnonymousSenderTag, Recipient}; + +use crate::error::{IpPacketRouterError, Result}; + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum ConnectedClientId { + AnonymousSenderTag(AnonymousSenderTag), + NymAddress(Box), +} + +impl ConnectedClientId { + pub(crate) fn into_nym_address(self) -> Result { + match self { + ConnectedClientId::NymAddress(nym_address) => Ok(*nym_address), + ConnectedClientId::AnonymousSenderTag(_) => Err(IpPacketRouterError::InvalidReplyTo), + } + } +} + +impl fmt::Display for ConnectedClientId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConnectedClientId::NymAddress(nym_address) => write!(f, "{nym_address}"), + ConnectedClientId::AnonymousSenderTag(tag) => write!(f, "{tag}"), + } + } +} + +impl From for ConnectedClientId { + fn from(nym_address: Recipient) -> Self { + ConnectedClientId::NymAddress(Box::new(nym_address)) + } +} + +impl From for ConnectedClientId { + fn from(tag: AnonymousSenderTag) -> Self { + ConnectedClientId::AnonymousSenderTag(tag) + } +} + +impl TryFrom<(SentBy, Option)> for ConnectedClientId { + type Error = IpPacketRouterError; + + fn try_from((sent_by, sender_tag): (SentBy, Option)) -> Result { + match sent_by { + SentBy::NymAddress(nym_address) => Ok(ConnectedClientId::NymAddress(nym_address)), + SentBy::AnonymousSenderTag => sender_tag + .map(ConnectedClientId::AnonymousSenderTag) + .ok_or(IpPacketRouterError::InvalidReplyTo), + } + } +} diff --git a/service-providers/ip-packet-router/src/connected_client_handler.rs b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs similarity index 62% rename from service-providers/ip-packet-router/src/connected_client_handler.rs rename to service-providers/ip-packet-router/src/clients/connected_client_handler.rs index a1904eba5f..a865a3a75f 100644 --- a/service-providers/ip-packet-router/src/connected_client_handler.rs +++ b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs @@ -1,14 +1,25 @@ -// Copyright 2024 - Nym Technologies SA +// Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use std::time::Duration; + use bytes::Bytes; -use nym_ip_packet_requests::codec::MultiIpPacketCodec; -use nym_sdk::mixnet::{MixnetMessageSender, Recipient}; +use nym_ip_packet_requests::{ + codec::MultiIpPacketCodec, v6::response::IpPacketResponse as IpPacketResponseV6, + v7::response::IpPacketResponse as IpPacketResponseV7, + v8::response::IpPacketResponse as IpPacketResponseV8, +}; +use nym_sdk::mixnet::MixnetMessageSender; +use tokio::{ + sync::{mpsc, oneshot}, + time::interval, +}; use crate::{ + clients::ConnectedClientId, constants::CLIENT_HANDLER_ACTIVITY_TIMEOUT, error::{IpPacketRouterError, Result}, - mixnet_listener::SupportedClientVersion, + messages::ClientVersion, util::create_message::create_input_message, }; @@ -19,17 +30,17 @@ 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 client that sent the packets + sent_by: ConnectedClientId, // Channel to receive packets from the tun_listener - forward_from_tun_rx: tokio::sync::mpsc::UnboundedReceiver>, + forward_from_tun_rx: 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<()>, + close_rx: oneshot::Receiver<()>, // Interval to check for activity timeout activity_timeout: tokio::time::Interval, @@ -38,31 +49,33 @@ pub(crate) struct ConnectedClientHandler { encoder: MultiIpPacketCodec, // The version of the client - client_version: SupportedClientVersion, + client_version: ClientVersion, } impl ConnectedClientHandler { pub(crate) fn start( - reply_to: Recipient, - buffer_timeout: std::time::Duration, - client_version: SupportedClientVersion, + client_id: ConnectedClientId, + buffer_timeout: Duration, + client_version: ClientVersion, mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender, ) -> ( - tokio::sync::mpsc::UnboundedSender>, - tokio::sync::oneshot::Sender<()>, + mpsc::UnboundedSender>, + oneshot::Sender<()>, tokio::task::JoinHandle<()>, ) { - let (close_tx, close_rx) = tokio::sync::oneshot::channel(); - let (forward_from_tun_tx, forward_from_tun_rx) = tokio::sync::mpsc::unbounded_channel(); + log::debug!("Starting connected client handler for: {}", client_id); + log::debug!("client version: {:?}", client_version); + let (close_tx, close_rx) = oneshot::channel(); + let (forward_from_tun_tx, forward_from_tun_rx) = mpsc::unbounded_channel(); // Reset so that we don't get the first tick immediately - let mut activity_timeout = tokio::time::interval(CLIENT_HANDLER_ACTIVITY_TIMEOUT); + let mut activity_timeout = interval(CLIENT_HANDLER_ACTIVITY_TIMEOUT); activity_timeout.reset(); let encoder = MultiIpPacketCodec::new(buffer_timeout); let connected_client_handler = ConnectedClientHandler { - nym_address: reply_to, + sent_by: client_id, forward_from_tun_rx, mixnet_client_sender, close_rx, @@ -80,20 +93,18 @@ impl ConnectedClientHandler { (forward_from_tun_tx, close_tx, handle) } - async fn send_packets_to_mixnet(&mut self, packets: Bytes) -> Result<()> { - 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() - } + async fn create_ip_packet(&self, packets: Bytes) -> Result> { + match self.client_version { + ClientVersion::V6 => IpPacketResponseV6::new_ip_packet(packets).to_bytes(), + ClientVersion::V7 => IpPacketResponseV7::new_ip_packet(packets).to_bytes(), + ClientVersion::V8 => IpPacketResponseV8::new_ip_packet(packets).to_bytes(), } - .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err })?; + .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err }) + } - let input_message = create_input_message(self.nym_address, response_packet); + async fn send_packets_to_mixnet(&mut self, packets: Bytes) -> Result<()> { + let response_packet = self.create_ip_packet(packets).await?; + let input_message = create_input_message(&self.sent_by, response_packet); self.mixnet_client_sender .send(input_message) @@ -123,17 +134,20 @@ impl ConnectedClientHandler { loop { tokio::select! { _ = &mut self.close_rx => { - log::info!("client handler stopping: received close: {}", self.nym_address); + log::info!("client handler stopping: received close: {}", self.sent_by); break; }, _ = self.activity_timeout.tick() => { - log::info!("client handler stopping: activity timeout: {}", self.nym_address); + log::info!("client handler stopping: activity timeout: {}", self.sent_by); break; }, - Some(packets) = self.encoder.buffer_timeout() => { - if let Err(err) = self.handle_buffer_timeout(packets).await { - log::error!("client handler: failed to handle buffer timeout: {err}"); - } + packets = self.encoder.buffer_timeout() => match packets { + Some(packets) => { + if let Err(err) = self.handle_buffer_timeout(packets).await { + log::error!("client handler: failed to handle buffer timeout: {err}"); + } + }, + None => log::trace!("no packets to send"), }, packet = self.forward_from_tun_rx.recv() => match packet { Some(packet) => { diff --git a/service-providers/ip-packet-router/src/clients/connected_clients.rs b/service-providers/ip-packet-router/src/clients/connected_clients.rs new file mode 100644 index 0000000000..1957ddaa15 --- /dev/null +++ b/service-providers/ip-packet-router/src/clients/connected_clients.rs @@ -0,0 +1,257 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::Arc, + time::Instant, +}; + +use nym_ip_packet_requests::IpPair; +use tokio::sync::{mpsc, oneshot, RwLock}; + +use crate::{ + constants::CLIENT_MIXNET_INACTIVITY_TIMEOUT, + error::{IpPacketRouterError, Result}, + tun_listener, +}; + +use super::ConnectedClientId; + +pub(crate) struct ConnectedClients { + // The set of connected clients + clients_ipv4_mapping: HashMap, + clients_ipv6_mapping: HashMap, + + // Notify the tun listener when a new client connects or disconnects + tun_listener_connected_client_tx: mpsc::UnboundedSender, +} + +impl ConnectedClients { + pub(crate) fn new() -> (Self, tun_listener::ConnectedClientsListener) { + let (connected_client_tx, connected_client_rx) = mpsc::unbounded_channel(); + ( + Self { + clients_ipv4_mapping: Default::default(), + clients_ipv6_mapping: Default::default(), + tun_listener_connected_client_tx: connected_client_tx, + }, + tun_listener::ConnectedClientsListener::new(connected_client_rx), + ) + } + + pub(crate) fn is_ip_connected(&self, ips: &IpPair) -> bool { + self.clients_ipv4_mapping.contains_key(&ips.ipv4) + || self.clients_ipv6_mapping.contains_key(&ips.ipv6) + } + + pub(crate) fn get_client_from_ip_mut(&mut self, ip: &IpAddr) -> Option<&mut ConnectedClient> { + match ip { + IpAddr::V4(ip) => self.clients_ipv4_mapping.get_mut(ip), + IpAddr::V6(ip) => self.clients_ipv6_mapping.get_mut(ip), + } + } + + pub(crate) fn is_client_connected(&self, client_id: &ConnectedClientId) -> bool { + self.clients_ipv4_mapping + .values() + .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 connect( + &mut self, + ips: IpPair, + client_id: ConnectedClientId, + forward_from_tun_tx: mpsc::UnboundedSender>, + close_tx: oneshot::Sender<()>, + handle: tokio::task::JoinHandle<()>, + ) { + // The map of connected clients that the mixnet listener keeps track of. It monitors + // activity and disconnects clients that have been inactive for too long. + let client = ConnectedClient { + client_id: client_id.clone(), + ipv6: ips.ipv6, + last_activity: Arc::new(RwLock::new(Instant::now())), + close_tx: Arc::new(CloseTx { + client_id, + inner: Some(close_tx), + }), + handle: Arc::new(handle), + }; + + log::info!("Inserting {} and {}", ips.ipv4, ips.ipv6); + self.clients_ipv4_mapping.insert(ips.ipv4, client.clone()); + self.clients_ipv6_mapping.insert(ips.ipv6, client); + + // Send the connected client info to the tun listener, which will use it to forward packets + // to the connected client handler. + self.tun_listener_connected_client_tx + .send(ConnectedClientEvent::Connect(Box::new(ConnectEvent { + ips, + forward_from_tun_tx, + }))) + .inspect_err(|err| { + log::error!("Failed to send connected client event: {err}"); + }) + .ok(); + } + + pub(crate) async fn update_activity(&mut self, ips: &IpPair) -> Result<()> { + if let Some(client) = self.clients_ipv4_mapping.get(&ips.ipv4) { + *client.last_activity.write().await = Instant::now(); + Ok(()) + } else { + Err(IpPacketRouterError::FailedToUpdateClientActivity) + } + } + + // Identify connected client handlers that have stopped without being told to stop + pub(crate) fn get_finished_client_handlers(&mut self) -> Vec<(IpPair, ConnectedClientId)> { + self.clients_ipv4_mapping + .iter_mut() + .filter_map(|(ip, connected_client)| { + if connected_client.handle.is_finished() { + Some(( + IpPair::new(*ip, connected_client.ipv6), + connected_client.client_id.clone(), + )) + } else { + None + } + }) + .collect() + } + + pub(crate) async fn get_inactive_clients(&mut self) -> Vec<(IpPair, ConnectedClientId)> { + let now = Instant::now(); + let mut ret = vec![]; + for (ip, connected_client) in self.clients_ipv4_mapping.iter() { + if now.duration_since(*connected_client.last_activity.read().await) + > CLIENT_MIXNET_INACTIVITY_TIMEOUT + { + ret.push(( + IpPair::new(*ip, connected_client.ipv6), + connected_client.client_id.clone(), + )) + } + } + ret + } + + pub(crate) fn disconnect_stopped_client_handlers( + &mut self, + stopped_clients: Vec<(IpPair, ConnectedClientId)>, + ) { + 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(); + } + } + + pub(crate) fn disconnect_inactive_clients( + &mut self, + inactive_clients: Vec<(IpPair, ConnectedClientId)>, + ) { + 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(); + } + } + + pub(crate) fn find_new_ip(&self) -> Option { + crate::util::generate_new_ip::find_new_ips( + &self.clients_ipv4_mapping, + &self.clients_ipv6_mapping, + ) + } +} + +pub(crate) struct CloseTx { + // pub(crate) nym_address: Recipient, + pub(crate) client_id: ConnectedClientId, + // Send to connected clients listener to stop. This is option only because we need to take + // ownership of it when the client is dropped. + pub(crate) inner: Option>, +} + +#[derive(Clone)] +pub(crate) struct ConnectedClient { + // The nym address of the connected client that we are communicating with on the other side of + // the mixnet + // pub(crate) nym_address: Recipient, + pub(crate) client_id: ConnectedClientId, + + // The assigned IPv6 address of this client + pub(crate) ipv6: Ipv6Addr, + + // Keep track of last activity so we can disconnect inactive clients + pub(crate) last_activity: Arc>, + + // Channel to send close signal to the connected client handler + // This is currently unused because the disconnect command it not yet implemented. + #[allow(unused)] + pub(crate) close_tx: Arc, + + // Handle for the connected client handler + pub(crate) handle: Arc>, +} + +impl ConnectedClient { + pub(crate) async fn update_activity(&self) { + *self.last_activity.write().await = Instant::now(); + } +} + +impl Drop for CloseTx { + fn drop(&mut self) { + log::debug!("signal to close client: {}", self.client_id); + if let Some(close_tx) = self.inner.take() { + close_tx.send(()).ok(); + } + } +} + +pub(crate) enum ConnectedClientEvent { + Disconnect(DisconnectEvent), + Connect(Box), +} + +pub(crate) struct DisconnectEvent(pub(crate) IpPair); + +pub(crate) struct ConnectEvent { + pub(crate) ips: IpPair, + pub(crate) forward_from_tun_tx: mpsc::UnboundedSender>, +} diff --git a/service-providers/ip-packet-router/src/clients/mod.rs b/service-providers/ip-packet-router/src/clients/mod.rs new file mode 100644 index 0000000000..fc6b39a359 --- /dev/null +++ b/service-providers/ip-packet-router/src/clients/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod client_id; +mod connected_client_handler; +mod connected_clients; + +pub(crate) use client_id::ConnectedClientId; +pub(crate) use connected_client_handler::ConnectedClientHandler; +pub(crate) use connected_clients::{ + ConnectEvent, ConnectedClientEvent, ConnectedClients, DisconnectEvent, +}; diff --git a/service-providers/ip-packet-router/src/config/helpers.rs b/service-providers/ip-packet-router/src/config/helpers.rs index e6abda1d98..6dd7209b87 100644 --- a/service-providers/ip-packet-router/src/config/helpers.rs +++ b/service-providers/ip-packet-router/src/config/helpers.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::config::default_config_filepath; use crate::config::old_config_v1::ConfigV1; diff --git a/service-providers/ip-packet-router/src/config/mod.rs b/service-providers/ip-packet-router/src/config/mod.rs index 1c1e155959..1f7aad3cc2 100644 --- a/service-providers/ip-packet-router/src/config/mod.rs +++ b/service-providers/ip-packet-router/src/config/mod.rs @@ -199,6 +199,7 @@ impl Default for IpPacketRouter { fn default() -> Self { IpPacketRouter { disable_poisson_rate: true, + #[allow(clippy::expect_used)] upstream_exit_policy_url: Some( mainnet::EXIT_POLICY_URL .parse() diff --git a/service-providers/ip-packet-router/src/config/old_config_v1.rs b/service-providers/ip-packet-router/src/config/old_config_v1.rs index 34399af4cf..b7cddacf00 100644 --- a/service-providers/ip-packet-router/src/config/old_config_v1.rs +++ b/service-providers/ip-packet-router/src/config/old_config_v1.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::config::persistence::IpPacketRouterPaths; use crate::config::Config; @@ -76,6 +76,7 @@ impl Default for IpPacketRouterV1 { fn default() -> Self { IpPacketRouterV1 { disable_poisson_rate: true, + #[allow(clippy::expect_used)] upstream_exit_policy_url: Some( mainnet::EXIT_POLICY_URL .parse() diff --git a/service-providers/ip-packet-router/src/config/persistence.rs b/service-providers/ip-packet-router/src/config/persistence.rs index 7f60a98f8c..2bc1cdfc6e 100644 --- a/service-providers/ip-packet-router/src/config/persistence.rs +++ b/service-providers/ip-packet-router/src/config/persistence.rs @@ -1,5 +1,5 @@ // Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use nym_client_core::config::disk_persistence::CommonClientPaths; use serde::{Deserialize, Serialize}; diff --git a/service-providers/ip-packet-router/src/config/template.rs b/service-providers/ip-packet-router/src/config/template.rs index ab5db7bbe8..5338b4b497 100644 --- a/service-providers/ip-packet-router/src/config/template.rs +++ b/service-providers/ip-packet-router/src/config/template.rs @@ -1,5 +1,5 @@ // Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only pub(crate) const CONFIG_TEMPLATE: &str = // While using normal toml marshalling would have been way simpler with less overhead, diff --git a/service-providers/ip-packet-router/src/constants.rs b/service-providers/ip-packet-router/src/constants.rs index bfee848df2..128afa271b 100644 --- a/service-providers/ip-packet-router/src/constants.rs +++ b/service-providers/ip-packet-router/src/constants.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use std::time::Duration; // We routinely check if any clients needs to be disconnected at this interval diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index 7c8a4590d5..0a81bde0ed 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -1,8 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use std::net::SocketAddr; pub use nym_client_core::error::ClientCoreError; use nym_exit_policy::PolicyError; use nym_id::NymIdError; +use nym_ip_packet_requests::sign::SignatureError; #[derive(thiserror::Error, Debug)] pub enum IpPacketRouterError { @@ -90,12 +94,13 @@ pub enum IpPacketRouterError { EmptyPacket, #[error("failed to verify request: {source}")] - FailedToVerifyRequest { - source: nym_ip_packet_requests::v7::signature::SignatureError, - }, + FailedToVerifyRequest { source: SignatureError }, #[error("client is connected with an invalid version: {version}")] InvalidConnectedClientVersion { version: u8 }, + + #[error("invalid reply-to address in the response")] + InvalidReplyTo, } pub type Result = std::result::Result; diff --git a/service-providers/ip-packet-router/src/ip_packet_router.rs b/service-providers/ip-packet-router/src/ip_packet_router.rs index a0f185cc76..4e976e96f9 100644 --- a/service-providers/ip-packet-router/src/ip_packet_router.rs +++ b/service-providers/ip-packet-router/src/ip_packet_router.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + #![cfg_attr(not(target_os = "linux"), allow(dead_code))] #![cfg_attr(not(target_os = "linux"), allow(unused_imports))] @@ -11,11 +14,7 @@ use nym_client_core::{ use nym_sdk::mixnet::Recipient; use nym_task::{TaskClient, TaskHandle}; -use crate::{ - config::Config, - error::IpPacketRouterError, - request_filter::{self, RequestFilter}, -}; +use crate::{config::Config, error::IpPacketRouterError, request_filter::RequestFilter}; pub struct OnStartData { // to add more fields as required @@ -127,7 +126,10 @@ impl IpPacketRouter { pub async fn run_service_provider(self) -> Result<(), IpPacketRouterError> { // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). - use crate::{mixnet_listener, tun_listener}; + use crate::{ + clients::ConnectedClients, mixnet_listener::MixnetListener, + request_filter::RequestFilter, tun_listener::TunListener, + }; let task_handle: TaskHandle = self.shutdown.map(Into::into).unwrap_or_default(); // Connect to the mixnet @@ -157,19 +159,19 @@ impl IpPacketRouter { // Channel used by the IpPacketRouter to signal connected and disconnected clients to the // TunListener - let (connected_clients, connected_clients_rx) = mixnet_listener::ConnectedClients::new(); + let (connected_clients, connected_clients_rx) = ConnectedClients::new(); - let tun_listener = tun_listener::TunListener { + let tun_listener = TunListener { tun_reader, task_client: task_handle.get_handle(), connected_clients: connected_clients_rx, }; tun_listener.start(); - let request_filter = request_filter::RequestFilter::new(&self.config).await?; + let request_filter = RequestFilter::new(&self.config).await?; request_filter.start_update_tasks().await; - let mixnet_listener = mixnet_listener::MixnetListener { + let mixnet_listener = MixnetListener { _config: self.config, request_filter: request_filter.clone(), tun_writer, diff --git a/service-providers/ip-packet-router/src/lib.rs b/service-providers/ip-packet-router/src/lib.rs index cb4a84ec65..fe66577b22 100644 --- a/service-providers/ip-packet-router/src/lib.rs +++ b/service-providers/ip-packet-router/src/lib.rs @@ -1,17 +1,26 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::panic)] #![cfg_attr(not(target_os = "linux"), allow(dead_code))] #![cfg_attr(not(target_os = "linux"), allow(unused_imports))] -pub use crate::config::Config; -pub use ip_packet_router::{IpPacketRouter, OnStartData}; - pub mod config; -mod connected_client_handler; -mod constants; pub mod error; +pub mod request_filter; + +pub(crate) mod messages; +pub(crate) mod non_linux_dummy; + +mod clients; +mod constants; mod ip_packet_router; mod mixnet_client; mod mixnet_listener; -pub(crate) mod non_linux_dummy; -pub mod request_filter; mod tun_listener; mod util; + +pub use crate::config::Config; +pub use ip_packet_router::{IpPacketRouter, OnStartData}; diff --git a/service-providers/ip-packet-router/src/main.rs b/service-providers/ip-packet-router/src/main.rs index c8f9fd0e1b..17e5040ab3 100644 --- a/service-providers/ip-packet-router/src/main.rs +++ b/service-providers/ip-packet-router/src/main.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + #[cfg(target_os = "linux")] mod cli; diff --git a/service-providers/ip-packet-router/src/messages/mod.rs b/service-providers/ip-packet-router/src/messages/mod.rs new file mode 100644 index 0000000000..a695fc5e2f --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod request; +pub(crate) mod response; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum ClientVersion { + V6, + V7, + V8, +} + +impl ClientVersion { + pub(crate) fn into_u8(self) -> u8 { + match self { + ClientVersion::V6 => 6, + ClientVersion::V7 => 7, + ClientVersion::V8 => 8, + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/request.rs b/service-providers/ip-packet-router/src/messages/request.rs new file mode 100644 index 0000000000..3b781275a7 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/request.rs @@ -0,0 +1,144 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod v6; +mod v7; +mod v8; + +use nym_ip_packet_requests::{ + v6::request::IpPacketRequest as IpPacketRequestV6, + v7::request::IpPacketRequest as IpPacketRequestV7, + v8::request::IpPacketRequest as IpPacketRequestV8, IpPair, +}; +use nym_sdk::mixnet::ReconstructedMessage; +use std::fmt; + +use crate::{clients::ConnectedClientId, error::IpPacketRouterError}; + +use super::ClientVersion; + +// The internal representation of the request after deserialization, valid for all versions +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum IpPacketRequest { + Data(DataRequest), + Control(ControlRequest), +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DataRequest { + pub(crate) version: ClientVersion, + pub(crate) ip_packets: bytes::Bytes, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum ControlRequest { + StaticConnect(StaticConnectRequest), + DynamicConnect(DynamicConnectRequest), + Disconnect(DisconnectRequest), + Ping(PingRequest), + Health(HealthRequest), +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct StaticConnectRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, + pub(crate) ips: IpPair, + pub(crate) buffer_timeout: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DynamicConnectRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, + pub(crate) buffer_timeout: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DisconnectRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct PingRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct HealthRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, +} + +impl TryFrom<&ReconstructedMessage> for IpPacketRequest { + type Error = IpPacketRouterError; + + fn try_from(reconstructed: &ReconstructedMessage) -> Result { + let request_version = *reconstructed + .message + .first() + .ok_or(IpPacketRouterError::EmptyPacket)?; + + match request_version { + 6 => { + let request_v6 = IpPacketRequestV6::from_reconstructed_message(reconstructed) + .map_err( + |source| IpPacketRouterError::FailedToDeserializeTaggedPacket { source }, + )?; + Ok(IpPacketRequest::from(request_v6)) + } + 7 => { + let request_v7 = IpPacketRequestV7::from_reconstructed_message(reconstructed) + .map_err( + |source| IpPacketRouterError::FailedToDeserializeTaggedPacket { source }, + )?; + request_v7 + .verify() + .map_err(|source| IpPacketRouterError::FailedToVerifyRequest { source })?; + Ok(IpPacketRequest::from(request_v7)) + } + 8 => { + let request_v8 = IpPacketRequestV8::from_reconstructed_message(reconstructed) + .map_err( + |source| IpPacketRouterError::FailedToDeserializeTaggedPacket { source }, + )?; + request_v8 + .verify() + .map_err(|source| IpPacketRouterError::FailedToVerifyRequest { source })?; + IpPacketRequest::try_from((request_v8, reconstructed.sender_tag)) + } + _ => { + log::info!("Received packet with invalid version: v{request_version}"); + Err(IpPacketRouterError::InvalidPacketVersion(request_version)) + } + } + } +} + +impl fmt::Display for IpPacketRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpPacketRequest::Data(_) => write!(f, "Data"), + IpPacketRequest::Control(control) => write!(f, "{control}"), + } + } +} + +impl fmt::Display for ControlRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ControlRequest::StaticConnect(_) => write!(f, "StaticConnect"), + ControlRequest::DynamicConnect(_) => write!(f, "DynamicConnect"), + ControlRequest::Disconnect(_) => write!(f, "Disconnect"), + ControlRequest::Ping(_) => write!(f, "Ping"), + ControlRequest::Health(_) => write!(f, "Health"), + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/request/v6.rs b/service-providers/ip-packet-router/src/messages/request/v6.rs new file mode 100644 index 0000000000..d6b26475c7 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/request/v6.rs @@ -0,0 +1,100 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v6::request::{ + DataRequest as DataRequestV6, DisconnectRequest as DisconnectRequestV6, + DynamicConnectRequest as DynamicConnectRequestV6, HealthRequest as HealthRequestV6, + IpPacketRequest as IpPacketRequestV6, IpPacketRequestData as IpPacketRequestDataV6, + PingRequest as PingRequestV6, StaticConnectRequest as StaticConnectRequestV6, +}; + +use super::{ + ClientVersion, ControlRequest, DataRequest, DisconnectRequest, DynamicConnectRequest, + HealthRequest, IpPacketRequest, PingRequest, StaticConnectRequest, +}; + +impl From for IpPacketRequest { + fn from(request: IpPacketRequestV6) -> Self { + let version = ClientVersion::V6; + match request.data { + IpPacketRequestDataV6::Data(inner) => Self::Data((inner, version).into()), + IpPacketRequestDataV6::StaticConnect(inner) => { + Self::Control(ControlRequest::StaticConnect((inner, version).into())) + } + IpPacketRequestDataV6::DynamicConnect(inner) => { + Self::Control(ControlRequest::DynamicConnect((inner, version).into())) + } + IpPacketRequestDataV6::Disconnect(inner) => { + Self::Control(ControlRequest::Disconnect((inner, version).into())) + } + IpPacketRequestDataV6::Ping(inner) => { + Self::Control(ControlRequest::Ping((inner, version).into())) + } + IpPacketRequestDataV6::Health(inner) => { + Self::Control(ControlRequest::Health((inner, version).into())) + } + } + } +} + +impl From<(DataRequestV6, ClientVersion)> for DataRequest { + fn from((request, version): (DataRequestV6, ClientVersion)) -> Self { + Self { + version, + ip_packets: request.ip_packets, + } + } +} + +impl From<(StaticConnectRequestV6, ClientVersion)> for StaticConnectRequest { + fn from((request, version): (StaticConnectRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + ips: request.ips, + buffer_timeout: request.buffer_timeout, + } + } +} + +impl From<(DynamicConnectRequestV6, ClientVersion)> for DynamicConnectRequest { + fn from((request, version): (DynamicConnectRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + buffer_timeout: request.buffer_timeout, + } + } +} + +impl From<(DisconnectRequestV6, ClientVersion)> for DisconnectRequest { + fn from((request, version): (DisconnectRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} + +impl From<(PingRequestV6, ClientVersion)> for PingRequest { + fn from((request, version): (PingRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} + +impl From<(HealthRequestV6, ClientVersion)> for HealthRequest { + fn from((request, version): (HealthRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/request/v7.rs b/service-providers/ip-packet-router/src/messages/request/v7.rs new file mode 100644 index 0000000000..1582de66c2 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/request/v7.rs @@ -0,0 +1,100 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v7::request::{ + DataRequest as DataRequestV7, DisconnectRequest as DisconnectRequestV7, + DynamicConnectRequest as DynamicConnectRequestV7, HealthRequest as HealthRequestV7, + IpPacketRequest as IpPacketRequestV7, IpPacketRequestData as IpPacketRequestDataV7, + PingRequest as PingRequestV7, StaticConnectRequest as StaticConnectRequestV7, +}; + +use super::{ + ClientVersion, ControlRequest, DataRequest, DisconnectRequest, DynamicConnectRequest, + HealthRequest, IpPacketRequest, PingRequest, StaticConnectRequest, +}; + +impl From for IpPacketRequest { + fn from(request: IpPacketRequestV7) -> Self { + let version = ClientVersion::V7; + match request.data { + IpPacketRequestDataV7::Data(inner) => Self::Data((inner, version).into()), + IpPacketRequestDataV7::StaticConnect(inner) => Self::Control( + ControlRequest::StaticConnect((inner.request, version).into()), + ), + IpPacketRequestDataV7::DynamicConnect(inner) => Self::Control( + ControlRequest::DynamicConnect((inner.request, version).into()), + ), + IpPacketRequestDataV7::Disconnect(inner) => { + Self::Control(ControlRequest::Disconnect((inner.request, version).into())) + } + IpPacketRequestDataV7::Ping(inner) => { + Self::Control(ControlRequest::Ping((inner, version).into())) + } + IpPacketRequestDataV7::Health(inner) => { + Self::Control(ControlRequest::Health((inner, version).into())) + } + } + } +} + +impl From<(DataRequestV7, ClientVersion)> for DataRequest { + fn from((request, version): (DataRequestV7, ClientVersion)) -> Self { + Self { + version, + ip_packets: request.ip_packets, + } + } +} + +impl From<(StaticConnectRequestV7, ClientVersion)> for StaticConnectRequest { + fn from((request, version): (StaticConnectRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + ips: request.ips, + buffer_timeout: request.buffer_timeout, + } + } +} + +impl From<(DynamicConnectRequestV7, ClientVersion)> for DynamicConnectRequest { + fn from((request, version): (DynamicConnectRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + buffer_timeout: request.buffer_timeout, + } + } +} + +impl From<(DisconnectRequestV7, ClientVersion)> for DisconnectRequest { + fn from((request, version): (DisconnectRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} + +impl From<(PingRequestV7, ClientVersion)> for PingRequest { + fn from((request, version): (PingRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} + +impl From<(HealthRequestV7, ClientVersion)> for HealthRequest { + fn from((request, version): (HealthRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/request/v8.rs b/service-providers/ip-packet-router/src/messages/request/v8.rs new file mode 100644 index 0000000000..e7190faae7 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/request/v8.rs @@ -0,0 +1,180 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v8::request::{ + ControlRequest as ControlRequestV8, DataRequest as DataRequestV8, + DisconnectRequest as DisconnectRequestV8, DynamicConnectRequest as DynamicConnectRequestV8, + HealthRequest as HealthRequestV8, IpPacketRequest as IpPacketRequestV8, + IpPacketRequestData as IpPacketRequestDataV8, PingRequest as PingRequestV8, + StaticConnectRequest as StaticConnectRequestV8, +}; +use nym_sdk::mixnet::AnonymousSenderTag; + +use crate::error::IpPacketRouterError; + +use super::{ + ClientVersion, ConnectedClientId, ControlRequest, DataRequest, DisconnectRequest, + DynamicConnectRequest, HealthRequest, IpPacketRequest, PingRequest, StaticConnectRequest, +}; + +impl TryFrom<(IpPacketRequestV8, Option)> for IpPacketRequest { + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag): (IpPacketRequestV8, Option), + ) -> Result { + let version = ClientVersion::V8; + Ok(match request.data { + IpPacketRequestDataV8::Data(inner) => Self::Data((inner, version).into()), + IpPacketRequestDataV8::Control(inner) => { + Self::Control((*inner, sender_tag, version).try_into()?) + } + }) + } +} + +impl From<(DataRequestV8, ClientVersion)> for DataRequest { + fn from((request, version): (DataRequestV8, ClientVersion)) -> Self { + Self { + version, + ip_packets: request.ip_packets, + } + } +} + +impl TryFrom<(ControlRequestV8, Option, ClientVersion)> for ControlRequest { + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + ControlRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(match request { + ControlRequestV8::StaticConnect(inner) => { + ControlRequest::StaticConnect((inner.request, sender_tag, version).try_into()?) + } + ControlRequestV8::DynamicConnect(inner) => { + ControlRequest::DynamicConnect((inner.request, sender_tag, version).try_into()?) + } + ControlRequestV8::Disconnect(inner) => { + ControlRequest::Disconnect((inner.request, sender_tag, version).try_into()?) + } + ControlRequestV8::Ping(inner) => { + ControlRequest::Ping((inner, sender_tag, version).try_into()?) + } + ControlRequestV8::Health(inner) => { + ControlRequest::Health((inner, sender_tag, version).try_into()?) + } + }) + } +} + +impl + TryFrom<( + StaticConnectRequestV8, + Option, + ClientVersion, + )> for StaticConnectRequest +{ + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + StaticConnectRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + ips: request.ips, + buffer_timeout: request.buffer_timeout, + }) + } +} + +impl + TryFrom<( + DynamicConnectRequestV8, + Option, + ClientVersion, + )> for DynamicConnectRequest +{ + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + DynamicConnectRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + buffer_timeout: request.buffer_timeout, + }) + } +} + +impl + TryFrom<( + DisconnectRequestV8, + Option, + ClientVersion, + )> for DisconnectRequest +{ + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + DisconnectRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + }) + } +} + +impl TryFrom<(PingRequestV8, Option, ClientVersion)> for PingRequest { + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): (PingRequestV8, Option, ClientVersion), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + }) + } +} + +impl TryFrom<(HealthRequestV8, Option, ClientVersion)> for HealthRequest { + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + HealthRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + }) + } +} diff --git a/service-providers/ip-packet-router/src/messages/response.rs b/service-providers/ip-packet-router/src/messages/response.rs new file mode 100644 index 0000000000..3743323166 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/response.rs @@ -0,0 +1,190 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod v6; +mod v7; +mod v8; + +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use nym_ip_packet_requests::{ + v6::response::IpPacketResponse as IpPacketResponseV6, + v7::response::IpPacketResponse as IpPacketResponseV7, + v8::response::IpPacketResponse as IpPacketResponseV8, IpPair, +}; + +use crate::{ + clients::ConnectedClientId, + error::{IpPacketRouterError, Result}, +}; + +use super::ClientVersion; + +pub(crate) struct VersionedResponse { + pub(crate) version: ClientVersion, + pub(crate) reply_to: ConnectedClientId, + pub(crate) response: Response, +} + +#[derive(Debug, Clone)] +pub(crate) enum Response { + StaticConnect { + request_id: u64, + reply: StaticConnectResponse, + }, + DynamicConnect { + request_id: u64, + reply: DynamicConnectResponse, + }, + // Disconnect is not yet implemented + #[allow(unused)] + Disconnect { + request_id: u64, + reply: DisconnectResponse, + }, + Pong { + request_id: u64, + }, + Health { + request_id: u64, + reply: Box, + }, + Info { + request_id: u64, + reply: InfoResponse, + }, +} + +#[derive(Debug, Clone)] +pub(crate) enum StaticConnectResponse { + Success, + Failure(StaticConnectFailureReason), +} + +#[derive(thiserror::Error, Debug, Clone)] +pub(crate) enum StaticConnectFailureReason { + #[error("requested ip address is already in use")] + RequestedIpAlreadyInUse, + + #[error("client already connected")] + ClientAlreadyConnected, + + #[allow(unused)] + #[error("request timestamp is out of date")] + OutOfDateTimestamp, + + #[allow(unused)] + #[error("{0}")] + Other(String), +} + +#[derive(Debug, Clone)] +pub(crate) enum DynamicConnectResponse { + Success(DynamicConnectSuccess), + Failure(DynamicConnectFailureReason), +} + +#[derive(Debug, Clone)] +pub(crate) struct DynamicConnectSuccess { + pub(crate) ips: IpPair, +} + +#[derive(Clone, Debug, thiserror::Error)] +pub(crate) enum DynamicConnectFailureReason { + #[error("client already connected")] + ClientAlreadyConnected, + + #[error("no available ip address")] + NoAvailableIp, + + #[allow(unused)] + #[error("{0}")] + Other(String), +} + +// Disconnect is not yet implemented +#[allow(unused)] +#[derive(Debug, Clone)] +pub(crate) enum DisconnectResponse { + Success, + Failure(DisconnectFailureReason), +} + +// Disconnect is not yet implemented +#[allow(unused)] +#[derive(Debug, Clone, thiserror::Error)] +pub(crate) enum DisconnectFailureReason { + #[error("requested client is not currently connected")] + ClientNotConnected, + + #[error("{0}")] + Other(String), +} + +#[derive(Debug, Clone)] +pub(crate) struct HealthResponse { + pub(crate) build_info: BinaryBuildInformationOwned, + pub(crate) routable: Option, +} + +impl VersionedResponse { + pub(crate) fn try_into_bytes(self) -> Result> { + match self.version { + ClientVersion::V6 => IpPacketResponseV6::try_from(self)?.to_bytes(), + ClientVersion::V7 => IpPacketResponseV7::try_from(self)?.to_bytes(), + ClientVersion::V8 => IpPacketResponseV8::from(self).to_bytes(), + } + .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err }) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct InfoResponse { + pub(crate) reply: InfoResponseReply, + pub(crate) level: InfoLevel, +} + +#[derive(Clone, Debug, thiserror::Error)] +pub(crate) enum InfoResponseReply { + #[allow(unused)] + #[error("{msg}")] + Generic { msg: String }, + + #[allow(unused)] + #[error( + "version mismatch: response is v{request_version} and response is v{response_version}" + )] + VersionMismatch { + request_version: u8, + response_version: u8, + }, + + #[error("destination failed exit policy filter check: {dst}")] + ExitPolicyFilterCheckFailed { dst: String }, +} + +#[derive(Clone, Debug)] +pub(crate) enum InfoLevel { + #[allow(unused)] + Info, + Warn, + #[allow(unused)] + Error, +} + +impl From for StaticConnectResponse { + fn from(failure: StaticConnectFailureReason) -> Self { + StaticConnectResponse::Failure(failure) + } +} + +impl From for DynamicConnectResponse { + fn from(success: DynamicConnectSuccess) -> Self { + DynamicConnectResponse::Success(success) + } +} + +impl From for DynamicConnectResponse { + fn from(failure: DynamicConnectFailureReason) -> Self { + DynamicConnectResponse::Failure(failure) + } +} diff --git a/service-providers/ip-packet-router/src/messages/response/v6.rs b/service-providers/ip-packet-router/src/messages/response/v6.rs new file mode 100644 index 0000000000..290f78971b --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/response/v6.rs @@ -0,0 +1,188 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v6::response::{ + DisconnectFailureReason as DisconnectFailureReasonV6, + DisconnectResponse as DisconnectResponseV6, + DisconnectResponseReply as DisconnectResponseReplyV6, + DynamicConnectFailureReason as DynamicConnectFailureReasonV6, + DynamicConnectResponse as DynamicConnectResponseV6, + DynamicConnectResponseReply as DynamicConnectResponseReplyV6, + DynamicConnectSuccess as DynamicConnectSuccessV6, HealthResponse as HealthResponseV6, + HealthResponseReply as HealthResponseReplyV6, InfoLevel as InfoLevelV6, + InfoResponse as InfoResponseV6, InfoResponseReply as InfoResponseReplyV6, + IpPacketResponse as IpPacketResponseV6, IpPacketResponseData as IpPacketResponseDataV6, + PongResponse as PongResponseV6, StaticConnectFailureReason as StaticConnectFailureReasonV6, + StaticConnectResponse as StaticConnectResponseV6, + StaticConnectResponseReply as StaticConnectResponseReplyV6, +}; + +use crate::error::IpPacketRouterError; + +use super::{ + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectResponse, DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, +}; + +impl TryFrom for IpPacketResponseV6 { + type Error = IpPacketRouterError; + + fn try_from(response: VersionedResponse) -> std::result::Result { + let version = response.version.into_u8(); + let reply_to = response.reply_to.into_nym_address()?; + let data = match response.response { + Response::StaticConnect { request_id, reply } => { + IpPacketResponseDataV6::StaticConnect(StaticConnectResponseV6 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::DynamicConnect { request_id, reply } => { + IpPacketResponseDataV6::DynamicConnect(DynamicConnectResponseV6 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::Disconnect { request_id, reply } => { + IpPacketResponseDataV6::Disconnect(DisconnectResponseV6 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::Pong { request_id } => IpPacketResponseDataV6::Pong(PongResponseV6 { + request_id, + reply_to, + }), + Response::Health { request_id, reply } => { + IpPacketResponseDataV6::Health(HealthResponseV6 { + request_id, + reply_to, + reply: (*reply).into(), + }) + } + Response::Info { request_id, reply } => IpPacketResponseDataV6::Info(InfoResponseV6 { + request_id, + reply_to, + reply: reply.reply.into(), + level: reply.level.into(), + }), + }; + Ok(IpPacketResponseV6 { version, data }) + } +} + +impl From for StaticConnectResponseReplyV6 { + fn from(response: StaticConnectResponse) -> Self { + match response { + StaticConnectResponse::Success => StaticConnectResponseReplyV6::Success, + StaticConnectResponse::Failure(err) => { + StaticConnectResponseReplyV6::Failure(err.into()) + } + } + } +} + +impl From for StaticConnectFailureReasonV6 { + fn from(reason: StaticConnectFailureReason) -> Self { + match reason { + StaticConnectFailureReason::RequestedIpAlreadyInUse => { + StaticConnectFailureReasonV6::RequestedIpAlreadyInUse + } + StaticConnectFailureReason::ClientAlreadyConnected => { + StaticConnectFailureReasonV6::RequestedNymAddressAlreadyInUse + } + StaticConnectFailureReason::OutOfDateTimestamp => { + StaticConnectFailureReasonV6::Other("unexpected timestamp".to_string()) + } + StaticConnectFailureReason::Other(err) => StaticConnectFailureReasonV6::Other(err), + } + } +} + +impl From for DynamicConnectResponseReplyV6 { + fn from(response: DynamicConnectResponse) -> Self { + match response { + DynamicConnectResponse::Success(DynamicConnectSuccess { ips }) => { + DynamicConnectResponseReplyV6::Success(DynamicConnectSuccessV6 { ips }) + } + DynamicConnectResponse::Failure(err) => { + DynamicConnectResponseReplyV6::Failure(err.into()) + } + } + } +} + +impl From for DynamicConnectFailureReasonV6 { + fn from(reason: DynamicConnectFailureReason) -> Self { + match reason { + DynamicConnectFailureReason::ClientAlreadyConnected => { + DynamicConnectFailureReasonV6::RequestedNymAddressAlreadyInUse + } + DynamicConnectFailureReason::NoAvailableIp => { + DynamicConnectFailureReasonV6::NoAvailableIp + } + DynamicConnectFailureReason::Other(err) => DynamicConnectFailureReasonV6::Other(err), + } + } +} + +impl From for DisconnectResponseReplyV6 { + fn from(response: DisconnectResponse) -> Self { + match response { + DisconnectResponse::Success => DisconnectResponseReplyV6::Success, + DisconnectResponse::Failure(err) => DisconnectResponseReplyV6::Failure(err.into()), + } + } +} + +impl From for DisconnectFailureReasonV6 { + fn from(reason: DisconnectFailureReason) -> Self { + match reason { + DisconnectFailureReason::ClientNotConnected => { + DisconnectFailureReasonV6::RequestedNymAddressNotConnected + } + DisconnectFailureReason::Other(err) => DisconnectFailureReasonV6::Other(err), + } + } +} + +impl From for HealthResponseReplyV6 { + fn from(response: HealthResponse) -> Self { + HealthResponseReplyV6 { + build_info: response.build_info, + routable: response.routable, + } + } +} + +impl From for InfoResponseReplyV6 { + fn from(reply: InfoResponseReply) -> Self { + match reply { + InfoResponseReply::Generic { msg } => InfoResponseReplyV6::Generic { msg }, + InfoResponseReply::VersionMismatch { + request_version, + response_version, + } => InfoResponseReplyV6::VersionMismatch { + request_version, + response_version, + }, + InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => { + InfoResponseReplyV6::ExitPolicyFilterCheckFailed { dst } + } + } + } +} + +impl From for InfoLevelV6 { + fn from(level: InfoLevel) -> Self { + match level { + InfoLevel::Info => InfoLevelV6::Info, + InfoLevel::Warn => InfoLevelV6::Warn, + InfoLevel::Error => InfoLevelV6::Error, + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/response/v7.rs b/service-providers/ip-packet-router/src/messages/response/v7.rs new file mode 100644 index 0000000000..60a3aa6d8a --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/response/v7.rs @@ -0,0 +1,188 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v7::response::{ + DisconnectFailureReason as DisconnectFailureReasonV7, + DisconnectResponse as DisconnectResponseV7, + DisconnectResponseReply as DisconnectResponseReplyV7, + DynamicConnectFailureReason as DynamicConnectFailureReasonV7, + DynamicConnectResponse as DynamicConnectResponseV7, + DynamicConnectResponseReply as DynamicConnectResponseReplyV7, + DynamicConnectSuccess as DynamicConnectSuccessV7, HealthResponse as HealthResponseV7, + HealthResponseReply as HealthResponseReplyV7, InfoLevel as InfoLevelV7, + InfoResponse as InfoResponseV7, InfoResponseReply as InfoResponseReplyV7, + IpPacketResponse as IpPacketResponseV7, IpPacketResponseData as IpPacketResponseDataV7, + PongResponse as PongResponseV7, StaticConnectFailureReason as StaticConnectFailureReasonV7, + StaticConnectResponse as StaticConnectResponseV7, + StaticConnectResponseReply as StaticConnectResponseReplyV7, +}; + +use crate::error::IpPacketRouterError; + +use super::{ + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectResponse, DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, +}; + +impl TryFrom for IpPacketResponseV7 { + type Error = IpPacketRouterError; + + fn try_from(response: VersionedResponse) -> std::result::Result { + let version = response.version.into_u8(); + let reply_to = response.reply_to.into_nym_address()?; + let data = match response.response { + Response::StaticConnect { request_id, reply } => { + IpPacketResponseDataV7::StaticConnect(StaticConnectResponseV7 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::DynamicConnect { request_id, reply } => { + IpPacketResponseDataV7::DynamicConnect(DynamicConnectResponseV7 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::Disconnect { request_id, reply } => { + IpPacketResponseDataV7::Disconnect(DisconnectResponseV7 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::Pong { request_id } => IpPacketResponseDataV7::Pong(PongResponseV7 { + request_id, + reply_to, + }), + Response::Health { request_id, reply } => { + IpPacketResponseDataV7::Health(HealthResponseV7 { + request_id, + reply_to, + reply: (*reply).into(), + }) + } + Response::Info { request_id, reply } => IpPacketResponseDataV7::Info(InfoResponseV7 { + request_id, + reply_to, + reply: reply.reply.into(), + level: reply.level.into(), + }), + }; + Ok(IpPacketResponseV7 { version, data }) + } +} + +impl From for StaticConnectResponseReplyV7 { + fn from(response: StaticConnectResponse) -> Self { + match response { + StaticConnectResponse::Success => StaticConnectResponseReplyV7::Success, + StaticConnectResponse::Failure(err) => { + StaticConnectResponseReplyV7::Failure(err.into()) + } + } + } +} + +impl From for StaticConnectFailureReasonV7 { + fn from(reason: StaticConnectFailureReason) -> Self { + match reason { + StaticConnectFailureReason::RequestedIpAlreadyInUse => { + StaticConnectFailureReasonV7::RequestedIpAlreadyInUse + } + StaticConnectFailureReason::ClientAlreadyConnected => { + StaticConnectFailureReasonV7::RequestedNymAddressAlreadyInUse + } + StaticConnectFailureReason::OutOfDateTimestamp => { + StaticConnectFailureReasonV7::OutOfDateTimestamp + } + StaticConnectFailureReason::Other(err) => StaticConnectFailureReasonV7::Other(err), + } + } +} + +impl From for DynamicConnectResponseReplyV7 { + fn from(response: DynamicConnectResponse) -> Self { + match response { + DynamicConnectResponse::Success(DynamicConnectSuccess { ips }) => { + DynamicConnectResponseReplyV7::Success(DynamicConnectSuccessV7 { ips }) + } + DynamicConnectResponse::Failure(err) => { + DynamicConnectResponseReplyV7::Failure(err.into()) + } + } + } +} + +impl From for DynamicConnectFailureReasonV7 { + fn from(reason: DynamicConnectFailureReason) -> Self { + match reason { + DynamicConnectFailureReason::ClientAlreadyConnected => { + DynamicConnectFailureReasonV7::RequestedNymAddressAlreadyInUse + } + DynamicConnectFailureReason::NoAvailableIp => { + DynamicConnectFailureReasonV7::NoAvailableIp + } + DynamicConnectFailureReason::Other(err) => DynamicConnectFailureReasonV7::Other(err), + } + } +} + +impl From for DisconnectResponseReplyV7 { + fn from(response: DisconnectResponse) -> Self { + match response { + DisconnectResponse::Success => DisconnectResponseReplyV7::Success, + DisconnectResponse::Failure(err) => DisconnectResponseReplyV7::Failure(err.into()), + } + } +} + +impl From for DisconnectFailureReasonV7 { + fn from(reason: DisconnectFailureReason) -> Self { + match reason { + DisconnectFailureReason::ClientNotConnected => { + DisconnectFailureReasonV7::RequestedNymAddressNotConnected + } + DisconnectFailureReason::Other(err) => DisconnectFailureReasonV7::Other(err), + } + } +} + +impl From for HealthResponseReplyV7 { + fn from(response: HealthResponse) -> Self { + HealthResponseReplyV7 { + build_info: response.build_info, + routable: response.routable, + } + } +} + +impl From for InfoResponseReplyV7 { + fn from(reply: InfoResponseReply) -> Self { + match reply { + InfoResponseReply::Generic { msg } => InfoResponseReplyV7::Generic { msg }, + InfoResponseReply::VersionMismatch { + request_version, + response_version, + } => InfoResponseReplyV7::VersionMismatch { + request_version, + response_version, + }, + InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => { + InfoResponseReplyV7::ExitPolicyFilterCheckFailed { dst } + } + } + } +} + +impl From for InfoLevelV7 { + fn from(level: InfoLevel) -> Self { + match level { + InfoLevel::Info => InfoLevelV7::Info, + InfoLevel::Warn => InfoLevelV7::Warn, + InfoLevel::Error => InfoLevelV7::Error, + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/response/v8.rs b/service-providers/ip-packet-router/src/messages/response/v8.rs new file mode 100644 index 0000000000..5ad984e355 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/response/v8.rs @@ -0,0 +1,183 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v8::response::{ + ControlResponse as ControlResponseV8, DisconnectFailureReason as DisconnectFailureReasonV8, + DisconnectResponse as DisconnectResponseV8, + DisconnectResponseReply as DisconnectResponseReplyV8, + DynamicConnectFailureReason as DynamicConnectFailureReasonV8, + DynamicConnectResponse as DynamicConnectResponseV8, + DynamicConnectResponseReply as DynamicConnectResponseReplyV8, + DynamicConnectSuccess as DynamicConnectSuccessV8, HealthResponse as HealthResponseV8, + HealthResponseReply as HealthResponseReplyV8, InfoLevel as InfoLevelV8, + InfoResponse as InfoResponseV8, InfoResponseReply as InfoResponseReplyV8, + IpPacketResponse as IpPacketResponseV8, IpPacketResponseData as IpPacketResponseDataV8, + PongResponse as PongResponseV8, StaticConnectFailureReason as StaticConnectFailureReasonV8, + StaticConnectResponse as StaticConnectResponseV8, + StaticConnectResponseReply as StaticConnectResponseReplyV8, +}; + +use super::{ + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectResponse, DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, +}; + +impl From for IpPacketResponseV8 { + fn from(response: VersionedResponse) -> Self { + let version = response.version.into_u8(); + let data = + match response.response { + Response::StaticConnect { request_id, reply } => IpPacketResponseDataV8::Control( + Box::new(ControlResponseV8::StaticConnect(StaticConnectResponseV8 { + request_id, + reply: reply.into(), + })), + ), + Response::DynamicConnect { request_id, reply } => { + IpPacketResponseDataV8::Control(Box::new(ControlResponseV8::DynamicConnect( + DynamicConnectResponseV8 { + request_id, + reply: reply.into(), + }, + ))) + } + Response::Disconnect { request_id, reply } => IpPacketResponseDataV8::Control( + Box::new(ControlResponseV8::Disconnect(DisconnectResponseV8 { + request_id, + reply: reply.into(), + })), + ), + Response::Pong { request_id } => IpPacketResponseDataV8::Control(Box::new( + ControlResponseV8::Pong(PongResponseV8 { request_id }), + )), + Response::Health { request_id, reply } => IpPacketResponseDataV8::Control( + Box::new(ControlResponseV8::Health(Box::new(HealthResponseV8 { + request_id, + reply: (*reply).into(), + }))), + ), + Response::Info { request_id, reply } => IpPacketResponseDataV8::Control(Box::new( + ControlResponseV8::Info(InfoResponseV8 { + request_id, + reply: reply.reply.into(), + level: reply.level.into(), + }), + )), + }; + + IpPacketResponseV8 { version, data } + } +} + +impl From for StaticConnectResponseReplyV8 { + fn from(reply: StaticConnectResponse) -> Self { + match reply { + StaticConnectResponse::Success => StaticConnectResponseReplyV8::Success, + StaticConnectResponse::Failure(err) => { + StaticConnectResponseReplyV8::Failure(err.into()) + } + } + } +} + +impl From for StaticConnectFailureReasonV8 { + fn from(reason: StaticConnectFailureReason) -> Self { + match reason { + StaticConnectFailureReason::RequestedIpAlreadyInUse => { + StaticConnectFailureReasonV8::RequestedIpAlreadyInUse + } + StaticConnectFailureReason::ClientAlreadyConnected => { + StaticConnectFailureReasonV8::ClientAlreadyConnected + } + StaticConnectFailureReason::OutOfDateTimestamp => { + StaticConnectFailureReasonV8::OutOfDateTimestamp + } + StaticConnectFailureReason::Other(err) => StaticConnectFailureReasonV8::Other(err), + } + } +} + +impl From for DynamicConnectResponseReplyV8 { + fn from(reply: DynamicConnectResponse) -> Self { + match reply { + DynamicConnectResponse::Success(DynamicConnectSuccess { ips }) => { + DynamicConnectResponseReplyV8::Success(DynamicConnectSuccessV8 { ips }) + } + DynamicConnectResponse::Failure(err) => { + DynamicConnectResponseReplyV8::Failure(err.into()) + } + } + } +} + +impl From for DynamicConnectFailureReasonV8 { + fn from(reason: DynamicConnectFailureReason) -> Self { + match reason { + DynamicConnectFailureReason::ClientAlreadyConnected => { + DynamicConnectFailureReasonV8::ClientAlreadyConnected + } + DynamicConnectFailureReason::NoAvailableIp => { + DynamicConnectFailureReasonV8::NoAvailableIp + } + DynamicConnectFailureReason::Other(err) => DynamicConnectFailureReasonV8::Other(err), + } + } +} + +impl From for DisconnectResponseReplyV8 { + fn from(reply: DisconnectResponse) -> Self { + match reply { + DisconnectResponse::Success => DisconnectResponseReplyV8::Success, + DisconnectResponse::Failure(err) => DisconnectResponseReplyV8::Failure(err.into()), + } + } +} + +impl From for DisconnectFailureReasonV8 { + fn from(reason: DisconnectFailureReason) -> Self { + match reason { + DisconnectFailureReason::ClientNotConnected => { + DisconnectFailureReasonV8::ClientNotConnected + } + DisconnectFailureReason::Other(err) => DisconnectFailureReasonV8::Other(err), + } + } +} + +impl From for HealthResponseReplyV8 { + fn from(response: HealthResponse) -> Self { + HealthResponseReplyV8 { + build_info: response.build_info, + routable: response.routable, + } + } +} + +impl From for InfoResponseReplyV8 { + fn from(reply: InfoResponseReply) -> Self { + match reply { + InfoResponseReply::Generic { msg } => InfoResponseReplyV8::Generic { msg }, + InfoResponseReply::VersionMismatch { + request_version, + response_version, + } => InfoResponseReplyV8::VersionMismatch { + request_version, + response_version, + }, + InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => { + InfoResponseReplyV8::ExitPolicyFilterCheckFailed { dst } + } + } + } +} + +impl From for InfoLevelV8 { + fn from(level: InfoLevel) -> Self { + match level { + InfoLevel::Info => InfoLevelV8::Info, + InfoLevel::Warn => InfoLevelV8::Warn, + InfoLevel::Error => InfoLevelV8::Error, + } + } +} diff --git a/service-providers/ip-packet-router/src/mixnet_client.rs b/service-providers/ip-packet-router/src/mixnet_client.rs index 178832a164..43f5f00038 100644 --- a/service-providers/ip-packet-router/src/mixnet_client.rs +++ b/service-providers/ip-packet-router/src/mixnet_client.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use nym_client_core::{config::disk_persistence::CommonClientPaths, TopologyProvider}; use nym_sdk::{GatewayTransceiver, NymNetworkDetails}; use nym_task::TaskClient; diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 2bd5fdeaaa..293718831c 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -1,389 +1,37 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + 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, - v7::{ - self, - request::{ - DataRequest, DisconnectRequest, DynamicConnectRequest, IpPacketRequest, - IpPacketRequestData, StaticConnectRequest, - }, - signature::SignedRequest, - }, - IpPair, -}; -use nym_sdk::mixnet::{MixnetMessageSender, Recipient}; +use nym_ip_packet_requests::codec::MultiIpPacketCodec; +use nym_sdk::mixnet::MixnetMessageSender; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::TaskHandle; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::sync::Arc; -use std::{collections::HashMap, net::SocketAddr}; -use tap::TapFallible; +use std::{net::SocketAddr, time::Duration}; use tokio::io::AsyncWriteExt; -use tokio::sync::RwLock; use tokio_util::codec::Decoder; use crate::{ + clients::{ConnectedClientHandler, ConnectedClients}, config::Config, - connected_client_handler, - constants::{CLIENT_MIXNET_INACTIVITY_TIMEOUT, DISCONNECT_TIMER_INTERVAL}, + constants::DISCONNECT_TIMER_INTERVAL, error::{IpPacketRouterError, Result}, - request_filter::{self}, - tun_listener, - util::generate_new_ip, - util::{ - create_message::create_input_message, - parse_ip::{parse_packet, ParsedPacket}, + messages::{ + request::{ + ControlRequest, DataRequest, DisconnectRequest, DynamicConnectRequest, HealthRequest, + IpPacketRequest, PingRequest, StaticConnectRequest, + }, + response::{ + DynamicConnectFailureReason, DynamicConnectSuccess, HealthResponse, InfoLevel, + InfoResponse, InfoResponseReply, Response, StaticConnectFailureReason, + StaticConnectResponse, VersionedResponse, + }, + ClientVersion, }, + request_filter::RequestFilter, + util::parse_ip::ParsedPacket, }; -pub(crate) struct ConnectedClients { - // The set of connected clients - clients_ipv4_mapping: HashMap, - clients_ipv6_mapping: HashMap, - - // Notify the tun listener when a new client connects or disconnects - tun_listener_connected_client_tx: tokio::sync::mpsc::UnboundedSender, -} - -impl ConnectedClients { - pub(crate) fn new() -> (Self, tun_listener::ConnectedClientsListener) { - let (connected_client_tx, connected_client_rx) = tokio::sync::mpsc::unbounded_channel(); - ( - Self { - clients_ipv4_mapping: Default::default(), - clients_ipv6_mapping: Default::default(), - tun_listener_connected_client_tx: connected_client_tx, - }, - tun_listener::ConnectedClientsListener::new(connected_client_rx), - ) - } - - fn is_ip_connected(&self, ips: &IpPair) -> bool { - self.clients_ipv4_mapping.contains_key(&ips.ipv4) - || self.clients_ipv6_mapping.contains_key(&ips.ipv6) - } - - fn get_client_from_ip_mut(&mut self, ip: &IpAddr) -> Option<&mut ConnectedClient> { - match ip { - IpAddr::V4(ip) => self.clients_ipv4_mapping.get_mut(ip), - IpAddr::V6(ip) => self.clients_ipv6_mapping.get_mut(ip), - } - } - - fn is_nym_address_connected(&self, nym_address: &Recipient) -> bool { - self.clients_ipv4_mapping - .values() - .any(|client| client.nym_address == *nym_address) - } - - fn lookup_ip_from_nym_address(&self, nym_address: &Recipient) -> Option { - self.clients_ipv4_mapping - .iter() - .find_map(|(ipv4, connected_client)| { - if connected_client.nym_address == *nym_address { - Some(IpPair::new(*ipv4, connected_client.ipv6)) - } else { - None - } - }) - } - - #[allow(dead_code)] - fn lookup_client_from_nym_address(&self, nym_address: &Recipient) -> Option<&ConnectedClient> { - self.clients_ipv4_mapping - .iter() - .find_map(|(_, connected_client)| { - if connected_client.nym_address == *nym_address { - Some(connected_client) - } else { - None - } - }) - } - - fn connect( - &mut self, - ips: IpPair, - nym_address: Recipient, - forward_from_tun_tx: tokio::sync::mpsc::UnboundedSender>, - close_tx: tokio::sync::oneshot::Sender<()>, - handle: tokio::task::JoinHandle<()>, - ) { - // The map of connected clients that the mixnet listener keeps track of. It monitors - // activity and disconnects clients that have been inactive for too long. - let client = ConnectedClient { - nym_address, - ipv6: ips.ipv6, - last_activity: Arc::new(RwLock::new(std::time::Instant::now())), - _close_tx: Arc::new(CloseTx { - nym_address, - inner: Some(close_tx), - }), - handle: Arc::new(handle), - }; - log::info!("Inserting {} and {}", ips.ipv4, ips.ipv6); - self.clients_ipv4_mapping.insert(ips.ipv4, client.clone()); - self.clients_ipv6_mapping.insert(ips.ipv6, client); - // Send the connected client info to the tun listener, which will use it to forward packets - // to the connected client handler. - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Connect(Box::new(ConnectEvent { - ips, - forward_from_tun_tx, - }))) - .tap_err(|err| { - log::error!("Failed to send connected client event: {err}"); - }) - .ok(); - } - - async fn update_activity(&mut self, ips: &IpPair) -> Result<()> { - if let Some(client) = self.clients_ipv4_mapping.get(&ips.ipv4) { - *client.last_activity.write().await = std::time::Instant::now(); - Ok(()) - } else { - Err(IpPacketRouterError::FailedToUpdateClientActivity) - } - } - - // Identify connected client handlers that have stopped without being told to stop - fn get_finished_client_handlers(&mut self) -> Vec<(IpPair, Recipient)> { - self.clients_ipv4_mapping - .iter_mut() - .filter_map(|(ip, connected_client)| { - if connected_client.handle.is_finished() { - Some(( - IpPair::new(*ip, connected_client.ipv6), - connected_client.nym_address, - )) - } else { - None - } - }) - .collect() - } - - async fn get_inactive_clients(&mut self) -> Vec<(IpPair, Recipient)> { - let now = std::time::Instant::now(); - let mut ret = vec![]; - for (ip, connected_client) in self.clients_ipv4_mapping.iter() { - if now.duration_since(*connected_client.last_activity.read().await) - > CLIENT_MIXNET_INACTIVITY_TIMEOUT - { - ret.push(( - IpPair::new(*ip, connected_client.ipv6), - connected_client.nym_address, - )) - } - } - ret - } - - fn disconnect_stopped_client_handlers(&mut self, stopped_clients: Vec<(IpPair, Recipient)>) { - 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))) - .tap_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); - } - } - - fn disconnect_inactive_clients(&mut self, inactive_clients: Vec<(IpPair, Recipient)>) { - 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))) - .tap_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); - } - } - - fn find_new_ip(&self) -> Option { - generate_new_ip::find_new_ips(&self.clients_ipv4_mapping, &self.clients_ipv6_mapping) - } -} - -pub(crate) struct CloseTx { - pub(crate) nym_address: Recipient, - // Send to connected clients listener to stop. This is option only because we need to take - // ownership of it when the client is dropped. - pub(crate) inner: Option>, -} - -#[derive(Clone)] -pub(crate) struct ConnectedClient { - // The nym address of the connected client that we are communicating with on the other side of - // the mixnet - pub(crate) nym_address: Recipient, - - // The assigned IPv6 address of this client - pub(crate) ipv6: Ipv6Addr, - - // Keep track of last activity so we can disconnect inactive clients - pub(crate) last_activity: Arc>, - - pub(crate) _close_tx: Arc, - - // Handle for the connected client handler - pub(crate) handle: Arc>, -} - -impl ConnectedClient { - async fn update_activity(&self) { - *self.last_activity.write().await = std::time::Instant::now(); - } -} - -impl Drop for CloseTx { - fn drop(&mut self) { - log::debug!("signal to close client: {}", self.nym_address); - if let Some(close_tx) = self.inner.take() { - close_tx.send(()).ok(); - } - } -} - -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(not(target_os = "linux"))] type TunDevice = crate::non_linux_dummy::DummyDevice; @@ -396,7 +44,7 @@ pub(crate) struct MixnetListener { pub(crate) _config: Config, // The request filter that we use to check if a packet should be forwarded - pub(crate) request_filter: request_filter::RequestFilter, + pub(crate) request_filter: RequestFilter, // The TUN device that we use to send and receive packets from the internet pub(crate) tun_writer: tokio::io::WriteHalf, @@ -414,192 +62,23 @@ pub(crate) struct MixnetListener { // #[cfg(target_os = "linux")] impl MixnetListener { - // Receving a static connect request from a client with an IP provided that we assign to them, - // if it's available. If it's not available, we send a failure response. - async fn on_static_connect_request( - &mut self, - connect_request: StaticConnectRequest, - client_version: SupportedClientVersion, - ) -> PacketHandleResult { - log::info!( - "Received static connect request from {sender_address}", - sender_address = connect_request.reply_to - ); - - let request_id = connect_request.request_id; - let requested_ips = connect_request.ips; - let reply_to = connect_request.reply_to; - // TODO: add to connect request - let buffer_timeout = nym_ip_packet_requests::codec::BUFFER_TIMEOUT; - // TODO: ignoring reply_to_avg_mix_delays for now - - // Check that the IP is available in the set of connected clients - let is_ip_taken = self.connected_clients.is_ip_connected(&requested_ips); - - // Check that the nym address isn't already registered - let is_nym_address_taken = self.connected_clients.is_nym_address_connected(&reply_to); - - match (is_ip_taken, is_nym_address_taken) { - (true, true) => { - log::info!("Connecting an already connected client"); - if self - .connected_clients - .update_activity(&requested_ips) - .await - .is_err() - { - log::error!("Failed to update activity for client"); - }; - Ok(Some(Response::new_static_connect_success( - request_id, - reply_to, - client_version, - ))) - } - (false, false) => { - log::info!("Connecting a new client"); - - // Spawn the ConnectedClientHandler for the new client - let (forward_from_tun_tx, close_tx, handle) = - connected_client_handler::ConnectedClientHandler::start( - reply_to, - buffer_timeout, - client_version, - self.mixnet_client.split_sender(), - ); - - // Register the new client in the set of connected clients - self.connected_clients.connect( - requested_ips, - reply_to, - forward_from_tun_tx, - close_tx, - handle, - ); - Ok(Some(Response::new_static_connect_success( - request_id, - reply_to, - client_version, - ))) - } - (true, false) => { - log::info!("Requested IP is not available"); - 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(Response::new_static_connect_failure( - request_id, - reply_to, - StaticConnectFailureReason::RequestedNymAddressAlreadyInUse, - client_version, - ))) - } - } - } - - async fn on_dynamic_connect_request( - &mut self, - connect_request: DynamicConnectRequest, - client_version: SupportedClientVersion, - ) -> PacketHandleResult { - log::info!( - "Received dynamic connect request from {sender_address}", - sender_address = connect_request.reply_to - ); - - let request_id = connect_request.request_id; - let reply_to = connect_request.reply_to; - // TODO: add to connect request - let buffer_timeout = nym_ip_packet_requests::codec::BUFFER_TIMEOUT; - // TODO: ignoring reply_to_avg_mix_delays for now - - // Check if it's the same client connecting again, then we just reuse the same IP - // TODO: this is problematic. Until we sign connect requests this means you can spam people - // with return traffic - - if let Some(existing_ips) = self.connected_clients.lookup_ip_from_nym_address(&reply_to) { - log::info!("Found existing client for nym address"); - if self - .connected_clients - .update_activity(&existing_ips) - .await - .is_err() - { - log::error!("Failed to update activity for client"); - } - 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(Response::new_dynamic_connect_failure( - request_id, - reply_to, - DynamicConnectFailureReason::NoAvailableIp, - client_version, - ))); - }; - - // Spawn the ConnectedClientHandler for the new client - let (forward_from_tun_tx, close_tx, handle) = - connected_client_handler::ConnectedClientHandler::start( - reply_to, - buffer_timeout, - client_version, - self.mixnet_client.split_sender(), - ); - - // Register the new client in the set of connected clients - self.connected_clients - .connect(new_ips, reply_to, forward_from_tun_tx, close_tx, handle); - Ok(Some(Response::new_dynamic_connect_success( - request_id, - reply_to, - new_ips, - client_version, - ))) - } - - 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, - client_version: SupportedClientVersion, + version: ClientVersion, ) -> PacketHandleResult { log::trace!("Received data request"); // We don't forward packets that we are not able to parse. BUT, there might be a good // reason to still forward them. // - // For example, if we are running in a mode where we are only supposed to forward - // packets to a specific destination, we might want to forward them anyway. - // // TODO: look into this let ParsedPacket { packet_type, src_addr, dst_addr, dst, - } = parse_packet(ip_packet)?; + } = crate::util::parse_ip::parse_packet(ip_packet)?; let dst_str = dst.map_or(dst_addr.to_string(), |dst| dst.to_string()); log::debug!("Received packet: {packet_type}: {src_addr} -> {dst_str}"); @@ -621,14 +100,19 @@ impl MixnetListener { Ok(None) } else { log::info!("Denied filter check: {dst}"); - Ok(Some(Response::new_data_info_response( - connected_client.nym_address, - InfoResponseReply::ExitPolicyFilterCheckFailed { - dst: dst.to_string(), + Ok(Some(VersionedResponse { + version, + reply_to: connected_client.client_id.clone(), + response: Response::Info { + request_id: 0, + reply: InfoResponse { + reply: InfoResponseReply::ExitPolicyFilterCheckFailed { + dst: dst.to_string(), + }, + level: InfoLevel::Warn, + }, }, - InfoLevel::Warn, - client_version, - ))) + })) } } else { // If the client is not connected, just drop the packet silently @@ -640,19 +124,213 @@ 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, client_version).await; + let result = self.handle_packet(&packet, data_request.version).await; responses.push(result); } Ok(responses) } + // Receving a static connect request from a client with an IP provided that we assign to them, + // if it's available. If it's not available, we send a failure response. + async fn on_static_connect_request( + &mut self, + connect_request: StaticConnectRequest, + ) -> PacketHandleResult { + log::info!( + "Received static connect request from {}", + connect_request.sent_by + ); + + let version = connect_request.version; + let sent_by = connect_request.sent_by; + let request_id = connect_request.request_id; + let requested_ips = connect_request.ips; + let buffer_timeout = connect_request + .buffer_timeout + .map(Duration::from_millis) + .unwrap_or(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); + + // Check that the IP is available in the set of connected clients + let is_ip_taken = self.connected_clients.is_ip_connected(&requested_ips); + + // Check that the client_id address isn't already registered + let is_client_id_taken = self.connected_clients.is_client_connected(&sent_by); + + let response = match (is_ip_taken, is_client_id_taken) { + (true, true) => { + log::info!("Connecting an already connected client"); + if self + .connected_clients + .update_activity(&requested_ips) + .await + .is_err() + { + log::error!("Failed to update activity for client"); + }; + Response::StaticConnect { + request_id, + reply: StaticConnectResponse::Success, + } + } + (false, false) => { + log::info!("Connecting a new client"); + + // Spawn the ConnectedClientHandler for the new client + let (forward_from_tun_tx, close_tx, handle) = ConnectedClientHandler::start( + sent_by.clone(), + buffer_timeout, + version, + self.mixnet_client.split_sender(), + ); + + // Register the new client in the set of connected clients + self.connected_clients.connect( + requested_ips, + sent_by.clone(), + forward_from_tun_tx, + close_tx, + handle, + ); + Response::StaticConnect { + request_id, + reply: StaticConnectResponse::Success, + } + } + (true, false) => { + log::info!("Requested IP is not available"); + Response::StaticConnect { + request_id, + reply: StaticConnectFailureReason::RequestedIpAlreadyInUse.into(), + } + } + (false, true) => { + log::info!("Nym address is already registered"); + Response::StaticConnect { + request_id, + reply: StaticConnectFailureReason::ClientAlreadyConnected.into(), + } + } + }; + + Ok(Some(VersionedResponse { + version, + reply_to: sent_by, + response, + })) + } + + async fn on_dynamic_connect_request( + &mut self, + connect_request: DynamicConnectRequest, + ) -> PacketHandleResult { + log::info!( + "Received dynamic connect request from {}", + connect_request.sent_by + ); + + let version = connect_request.version; + let request_id = connect_request.request_id; + let reply_to = connect_request.sent_by; + let buffer_timeout = connect_request + .buffer_timeout + .map(Duration::from_millis) + .unwrap_or(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); + + if self.connected_clients.is_client_connected(&reply_to) { + return Ok(Some(VersionedResponse { + version, + reply_to, + response: Response::DynamicConnect { + request_id, + reply: DynamicConnectFailureReason::ClientAlreadyConnected.into(), + }, + })); + } + + let Some(new_ips) = self.connected_clients.find_new_ip() else { + log::info!("No available IP address"); + return Ok(Some(VersionedResponse { + version, + reply_to, + response: Response::DynamicConnect { + request_id, + reply: DynamicConnectFailureReason::NoAvailableIp.into(), + }, + })); + }; + + // Spawn the ConnectedClientHandler for the new client + let (forward_from_tun_tx, close_tx, handle) = ConnectedClientHandler::start( + reply_to.clone(), + buffer_timeout, + version, + self.mixnet_client.split_sender(), + ); + + // Register the new client in the set of connected clients + self.connected_clients.connect( + new_ips, + reply_to.clone(), + forward_from_tun_tx, + close_tx, + handle, + ); + + Ok(Some(VersionedResponse { + version, + reply_to, + response: Response::DynamicConnect { + request_id, + reply: DynamicConnectSuccess { ips: new_ips }.into(), + }, + })) + } + + fn on_disconnect_request(&self, _disconnect_request: DisconnectRequest) -> PacketHandleResult { + log::info!("Received disconnect request: not implemented, dropping"); + Ok(None) + } + + async fn on_ping_request(&self, ping_request: PingRequest) -> PacketHandleResult { + Ok(Some(VersionedResponse { + version: ping_request.version, + reply_to: ping_request.sent_by, + response: Response::Pong { + request_id: ping_request.request_id, + }, + })) + } + + async fn on_health_request(&self, health_request: HealthRequest) -> PacketHandleResult { + Ok(Some(VersionedResponse { + version: health_request.version, + reply_to: health_request.sent_by, + response: Response::Health { + request_id: health_request.request_id, + reply: Box::new(HealthResponse { + build_info: nym_bin_common::bin_info_owned!(), + routable: None, + }), + }, + })) + } + + 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::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, + } + } + fn on_version_mismatch( &self, _version: u8, @@ -669,52 +347,27 @@ impl MixnetListener { reconstructed: ReconstructedMessage, ) -> Result> { log::debug!( - "Received message with sender_tag: {:?}", - reconstructed.sender_tag + "Received message with sender_tag: {}", + reconstructed + .sender_tag + .map(|tag| tag.to_string()) + .unwrap_or("missing".to_owned()) ); - let (request, client_version) = match deserialize_request(&reconstructed) { + // First deserialize the request + let request = match IpPacketRequest::try_from(&reconstructed) { Err(IpPacketRouterError::InvalidPacketVersion(version)) => { + log::debug!("Received packet with invalid version: v{version}"); return Ok(vec![self.on_version_mismatch(version, &reconstructed)]); } req => req, }?; - match request.data { - IpPacketRequestData::StaticConnect(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, client_version) - .await, - ]) - } - IpPacketRequestData::DynamicConnect(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, client_version) - .await, - ]) - } - IpPacketRequestData::Disconnect(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, client_version) - ]) - } - IpPacketRequestData::Data(data_request) => { - self.on_data_request(data_request, client_version).await - } - IpPacketRequestData::Ping(_) => { - log::info!("Received ping request: not implemented, dropping"); - Ok(vec![]) - } - IpPacketRequestData::Health(_) => { - log::info!("Received health request: not implemented, dropping"); - Ok(vec![]) - } + log::debug!("Received request: {request}"); + + match request { + IpPacketRequest::Data(request) => self.on_data_request(request).await, + IpPacketRequest::Control(request) => Ok(vec![self.on_control_request(request).await]), } } @@ -738,15 +391,12 @@ 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: Response) -> Result<()> { - let Some(recipient) = response.recipient() else { - log::error!("No recipient in response packet, this should NOT happen!"); - return Err(IpPacketRouterError::NoRecipientInResponse); - }; + async fn handle_response(&self, response: VersionedResponse) -> Result<()> { + let send_to = response.reply_to.clone(); + let response_bytes = response.try_into_bytes()?; + let input_message = + crate::util::create_message::create_input_message(&send_to, response_bytes); - let response_packet = response.to_bytes()?; - - let input_message = create_input_message(*recipient, response_packet); self.mixnet_client .send(input_message) .await @@ -807,77 +457,4 @@ impl MixnetListener { } } -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 - 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, - client_version: SupportedClientVersion, -) -> Result<()> { - if let Err(err) = request.verify() { - // 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(()) -} - -pub(crate) enum ConnectedClientEvent { - Disconnect(DisconnectEvent), - Connect(Box), -} - -pub(crate) struct DisconnectEvent(pub(crate) IpPair); - -pub(crate) struct ConnectEvent { - pub(crate) ips: IpPair, - pub(crate) forward_from_tun_tx: tokio::sync::mpsc::UnboundedSender>, -} +pub(crate) type PacketHandleResult = Result>; diff --git a/service-providers/ip-packet-router/src/non_linux_dummy.rs b/service-providers/ip-packet-router/src/non_linux_dummy.rs index 4369f72689..abac0ad1d3 100644 --- a/service-providers/ip-packet-router/src/non_linux_dummy.rs +++ b/service-providers/ip-packet-router/src/non_linux_dummy.rs @@ -1,5 +1,5 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only use std::io::Error; use std::pin::Pin; diff --git a/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs b/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs index 9dcecbdea3..3ebc68e774 100644 --- a/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs +++ b/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs @@ -1,5 +1,5 @@ // Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use std::net::SocketAddr; diff --git a/service-providers/ip-packet-router/src/request_filter/mod.rs b/service-providers/ip-packet-router/src/request_filter/mod.rs index 599a00a1d2..5e2f359570 100644 --- a/service-providers/ip-packet-router/src/request_filter/mod.rs +++ b/service-providers/ip-packet-router/src/request_filter/mod.rs @@ -1,5 +1,5 @@ // Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::config::Config; use crate::error::IpPacketRouterError; diff --git a/service-providers/ip-packet-router/src/tun_listener.rs b/service-providers/ip-packet-router/src/tun_listener.rs index ce3a70ca68..0d4a99e3b9 100644 --- a/service-providers/ip-packet-router/src/tun_listener.rs +++ b/service-providers/ip-packet-router/src/tun_listener.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -5,34 +8,27 @@ use nym_ip_packet_requests::IpPair; use nym_task::TaskClient; #[cfg(target_os = "linux")] use tokio::io::AsyncReadExt; +use tokio::sync::mpsc; -use crate::{ - error::Result, - mixnet_listener::{self}, - util::parse_ip::parse_dst_addr, -}; +use crate::clients::{ConnectEvent, ConnectedClientEvent, DisconnectEvent}; +use crate::{error::Result, util::parse_ip::parse_dst_addr}; // The TUN listener keeps a local map of the connected clients that has its state updated by the // mixnet listener. Basically it's just so that we don't have to have mutexes around shared state. // It's even ok if this is slightly out of date -pub(crate) struct ConnectedClientMirror { - pub(crate) forward_from_tun_tx: tokio::sync::mpsc::UnboundedSender>, - pub(crate) ips: IpPair, +struct ConnectedClientMirror { + forward_from_tun_tx: mpsc::UnboundedSender>, + ips: IpPair, } pub(crate) struct ConnectedClientsListener { clients_ipv4: HashMap, clients_ipv6: HashMap, - connected_client_rx: - tokio::sync::mpsc::UnboundedReceiver, + connected_client_rx: mpsc::UnboundedReceiver, } impl ConnectedClientsListener { - pub(crate) fn new( - connected_client_rx: tokio::sync::mpsc::UnboundedReceiver< - mixnet_listener::ConnectedClientEvent, - >, - ) -> Self { + pub(crate) fn new(connected_client_rx: mpsc::UnboundedReceiver) -> Self { ConnectedClientsListener { clients_ipv4: HashMap::new(), clients_ipv6: HashMap::new(), @@ -40,17 +36,17 @@ impl ConnectedClientsListener { } } - pub(crate) fn get(&self, ip: &IpAddr) -> Option<&ConnectedClientMirror> { + fn get(&self, ip: &IpAddr) -> Option<&ConnectedClientMirror> { match ip { IpAddr::V4(ip) => self.clients_ipv4.get(ip), IpAddr::V6(ip) => self.clients_ipv6.get(ip), } } - pub(crate) fn update(&mut self, event: mixnet_listener::ConnectedClientEvent) { + pub(crate) fn update(&mut self, event: ConnectedClientEvent) { match event { - mixnet_listener::ConnectedClientEvent::Connect(connected_event) => { - let mixnet_listener::ConnectEvent { + ConnectedClientEvent::Connect(connected_event) => { + let ConnectEvent { ips, forward_from_tun_tx, } = *connected_event; @@ -70,9 +66,7 @@ impl ConnectedClientsListener { }, ); } - mixnet_listener::ConnectedClientEvent::Disconnect( - mixnet_listener::DisconnectEvent(ips), - ) => { + ConnectedClientEvent::Disconnect(DisconnectEvent(ips)) => { log::trace!("Disconnect client: {ips}"); self.clients_ipv4.remove(&ips.ipv4); self.clients_ipv6.remove(&ips.ipv6); @@ -106,9 +100,7 @@ impl TunListener { if forward_from_tun_tx.send(packet).is_err() { log::warn!("Failed to forward packet to connected client {dst_addr}: disconnecting it from tun listener"); self.connected_clients - .update(mixnet_listener::ConnectedClientEvent::Disconnect( - mixnet_listener::DisconnectEvent(*ips), - )); + .update(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))); } } else { log::info!( diff --git a/service-providers/ip-packet-router/src/util/create_message.rs b/service-providers/ip-packet-router/src/util/create_message.rs index dca63c21df..c2800c0516 100644 --- a/service-providers/ip-packet-router/src/util/create_message.rs +++ b/service-providers/ip-packet-router/src/util/create_message.rs @@ -1,11 +1,20 @@ -use nym_sdk::mixnet::{InputMessage, Recipient}; +use nym_sdk::mixnet::InputMessage; use nym_task::connections::TransmissionLane; +use crate::clients::ConnectedClientId; + pub(crate) fn create_input_message( - nym_address: Recipient, + recipient: &ConnectedClientId, response_packet: Vec, ) -> InputMessage { let lane = TransmissionLane::General; let packet_type = None; - InputMessage::new_regular(nym_address, response_packet, lane, packet_type) + match recipient { + ConnectedClientId::NymAddress(recipient) => { + InputMessage::new_regular(**recipient, response_packet, lane, packet_type) + } + ConnectedClientId::AnonymousSenderTag(tag) => { + InputMessage::new_reply(*tag, response_packet, lane, packet_type) + } + } }