IPR request types v8 (#5498)

* IPR v8 request/response types

* Remove signature for when we use sender tags

* Remove unused

* Address some review comments

* Update license to GPL-3.0 for IPR

Since the IPR can run as a binary, make sure it's license is GPL-3.0

* update cargo deny

* Add back support for v6

* Tidy responses

* Clippy

* Fix compilation

* Conversions

* Conversions

* Split response conversion

* request split

* Complete conversion switch

* Remove commented out code

* rustfmt

* Remove unused conversions

* Remove unused TryFrom

* use from
This commit is contained in:
Jon Häggblad
2025-02-27 15:21:55 +01:00
committed by GitHub
parent e6f6e1342f
commit 856dbfe1ac
54 changed files with 2939 additions and 1038 deletions
Generated
+1 -2
View File
@@ -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",
+9 -9
View File
@@ -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()
}
@@ -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<Vec<u8>, SignatureError>;
fn request_as_bytes(&self) -> Result<Vec<u8>, 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(),
@@ -1,69 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{v6, v7};
impl From<v7::response::StaticConnectFailureReason> 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<v7::response::DynamicConnectFailureReason> 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<v7::response::InfoResponseReply> 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<v7::response::InfoLevel> 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,
}
}
}
-1
View File
@@ -1,4 +1,3 @@
pub mod conversion;
pub mod request;
pub mod response;
@@ -1,125 +0,0 @@
use time::OffsetDateTime;
use crate::{v6, v7};
impl From<v6::request::IpPacketRequest> for v7::request::IpPacketRequest {
fn from(ip_packet_request: v6::request::IpPacketRequest) -> Self {
Self {
version: 7,
data: ip_packet_request.data.into(),
}
}
}
impl From<v6::request::IpPacketRequestData> 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<v6::request::StaticConnectRequest> 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<v6::request::DynamicConnectRequest> 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<v6::request::DisconnectRequest> for v7::request::SignedDisconnectRequest {
fn from(disconnect_request: v6::request::DisconnectRequest) -> Self {
Self {
request: disconnect_request.into(),
signature: None,
}
}
}
impl From<v6::request::DisconnectRequest> 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<v6::request::DataRequest> for v7::request::DataRequest {
fn from(data_request: v6::request::DataRequest) -> Self {
Self {
ip_packets: data_request.ip_packets,
}
}
}
impl From<v6::request::PingRequest> 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<v6::request::HealthRequest> 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(),
}
}
}
-2
View File
@@ -1,6 +1,4 @@
pub mod conversion;
pub mod request;
pub mod response;
pub mod signature;
pub const VERSION: u8 = 7;
+53 -33
View File
@@ -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<f64>,
buffer_timeout: Option<u64>,
) -> (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<f64>,
buffer_timeout: Option<u64>,
) -> (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<Vec<u8>, 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<Self, bincode::Error> {
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<identity::Signature> {
match self {
@@ -202,9 +222,9 @@ impl IpPacketRequestData {
pub fn signable_request(&self) -> Option<Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, SignatureError> {
fn request_as_bytes(&self) -> Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, SignatureError> {
fn request_as_bytes(&self) -> Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, SignatureError> {
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError> {
self.request
.to_bytes()
.map_err(|error| SignatureError::RequestSerializationError {
+4
View File
@@ -0,0 +1,4 @@
pub mod request;
pub mod response;
pub const VERSION: u8 = 8;
+536
View File
@@ -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<ControlRequest>),
}
#[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<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 SignedStaticConnectRequest {
pub request: StaticConnectRequest,
pub signature: Option<ed25519::Signature>,
}
// 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<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 SignedDynamicConnectRequest {
pub request: DynamicConnectRequest,
pub signature: Option<ed25519::Signature>,
}
// 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<ed25519::Signature>,
}
// 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<Recipient>,
signing_keypair: Arc<ed25519::KeyPair>,
},
}
// 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<Recipient>),
}
impl IpPacketRequest {
pub fn new_connect_request(
ips: Option<IpPair>,
buffer_timeout: Option<u64>,
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<u64> {
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<Vec<u8>, bincode::Error> {
use bincode::Options;
crate::make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Recipient> 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<ReturnAddress> 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]),
})
);
}
}
@@ -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<ControlResponse>),
}
#[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<HealthResponse>),
// 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<bool>,
}
#[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<u64> {
match &self.data {
IpPacketResponseData::Data(_) => None,
IpPacketResponseData::Control(response) => response.id(),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
}
impl IpPacketResponseData {
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
}
impl ControlResponse {
fn id(&self) -> Option<u64> {
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,
}
}
}
+1
View File
@@ -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,
+13 -6
View File
@@ -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<identity::KeyPair>,
pub(crate) identity_keys: Arc<ed25519::KeyPair>,
/// 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::KeyPair>,
identity_keys: Arc<ed25519::KeyPair>,
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<ed25519::KeyPair> {
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
}
@@ -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<u8>, recipient: Recipient) -> Result<()> {
let input_message =
InputMessage::new_regular(recipient, response, TransmissionLane::General, None);
async fn handle_response(
&self,
response: Vec<u8>,
recipient: Recipient,
sender_tag: Option<AnonymousSenderTag>,
) -> 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<Authentic
}
}
}
fn create_input_message(
nym_address: Recipient,
reply_to_tag: Option<AnonymousSenderTag>,
response_packet: Vec<u8>,
) -> 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)
}
}
@@ -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"] }
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-only
use crate::cli::CliIpPacketRouterClient;
use nym_bin_common::output_format::OutputFormat;
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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::{
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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::{
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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::{
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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::{
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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;
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-only
use crate::cli::CliIpPacketRouterClient;
use nym_bin_common::output_format::OutputFormat;
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-only
use crate::cli::CliIpPacketRouterClient;
use nym_bin_common::output_format::OutputFormat;
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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::{
@@ -0,0 +1,58 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<Recipient>),
}
impl ConnectedClientId {
pub(crate) fn into_nym_address(self) -> Result<Recipient> {
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<Recipient> for ConnectedClientId {
fn from(nym_address: Recipient) -> Self {
ConnectedClientId::NymAddress(Box::new(nym_address))
}
}
impl From<AnonymousSenderTag> for ConnectedClientId {
fn from(tag: AnonymousSenderTag) -> Self {
ConnectedClientId::AnonymousSenderTag(tag)
}
}
impl TryFrom<(SentBy, Option<AnonymousSenderTag>)> for ConnectedClientId {
type Error = IpPacketRouterError;
fn try_from((sent_by, sender_tag): (SentBy, Option<AnonymousSenderTag>)) -> Result<Self> {
match sent_by {
SentBy::NymAddress(nym_address) => Ok(ConnectedClientId::NymAddress(nym_address)),
SentBy::AnonymousSenderTag => sender_tag
.map(ConnectedClientId::AnonymousSenderTag)
.ok_or(IpPacketRouterError::InvalidReplyTo),
}
}
}
@@ -1,14 +1,25 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<Vec<u8>>,
forward_from_tun_rx: mpsc::UnboundedReceiver<Vec<u8>>,
// 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<Vec<u8>>,
tokio::sync::oneshot::Sender<()>,
mpsc::UnboundedSender<Vec<u8>>,
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<Vec<u8>> {
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) => {
@@ -0,0 +1,257 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<Ipv4Addr, ConnectedClient>,
clients_ipv6_mapping: HashMap<Ipv6Addr, ConnectedClient>,
// Notify the tun listener when a new client connects or disconnects
tun_listener_connected_client_tx: mpsc::UnboundedSender<ConnectedClientEvent>,
}
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<IpPair> {
// 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<Vec<u8>>,
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<IpPair> {
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<oneshot::Sender<()>>,
}
#[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<RwLock<Instant>>,
// 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<CloseTx>,
// Handle for the connected client handler
pub(crate) handle: Arc<tokio::task::JoinHandle<()>>,
}
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<ConnectEvent>),
}
pub(crate) struct DisconnectEvent(pub(crate) IpPair);
pub(crate) struct ConnectEvent {
pub(crate) ips: IpPair,
pub(crate) forward_from_tun_tx: mpsc::UnboundedSender<Vec<u8>>,
}
@@ -0,0 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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,
};
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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;
@@ -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()
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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()
@@ -1,5 +1,5 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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};
@@ -1,5 +1,5 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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,
@@ -1,3 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::time::Duration;
// We routinely check if any clients needs to be disconnected at this interval
@@ -1,8 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<T> = std::result::Result<T, IpPacketRouterError>;
@@ -1,3 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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,
+16 -7
View File
@@ -1,17 +1,26 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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};
@@ -1,3 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(target_os = "linux")]
mod cli;
@@ -0,0 +1,22 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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,
}
}
}
@@ -0,0 +1,144 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<u64>,
}
#[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<u64>,
}
#[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<Self, Self::Error> {
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"),
}
}
}
@@ -0,0 +1,100 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<IpPacketRequestV6> 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(),
}
}
}
@@ -0,0 +1,100 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<IpPacketRequestV7> 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(),
}
}
}
@@ -0,0 +1,180 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<AnonymousSenderTag>)> for IpPacketRequest {
type Error = IpPacketRouterError;
fn try_from(
(request, sender_tag): (IpPacketRequestV8, Option<AnonymousSenderTag>),
) -> Result<Self, Self::Error> {
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<AnonymousSenderTag>, ClientVersion)> for ControlRequest {
type Error = IpPacketRouterError;
fn try_from(
(request, sender_tag, version): (
ControlRequestV8,
Option<AnonymousSenderTag>,
ClientVersion,
),
) -> Result<Self, Self::Error> {
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<AnonymousSenderTag>,
ClientVersion,
)> for StaticConnectRequest
{
type Error = IpPacketRouterError;
fn try_from(
(request, sender_tag, version): (
StaticConnectRequestV8,
Option<AnonymousSenderTag>,
ClientVersion,
),
) -> Result<Self, Self::Error> {
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<AnonymousSenderTag>,
ClientVersion,
)> for DynamicConnectRequest
{
type Error = IpPacketRouterError;
fn try_from(
(request, sender_tag, version): (
DynamicConnectRequestV8,
Option<AnonymousSenderTag>,
ClientVersion,
),
) -> Result<Self, Self::Error> {
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<AnonymousSenderTag>,
ClientVersion,
)> for DisconnectRequest
{
type Error = IpPacketRouterError;
fn try_from(
(request, sender_tag, version): (
DisconnectRequestV8,
Option<AnonymousSenderTag>,
ClientVersion,
),
) -> Result<Self, Self::Error> {
Ok(Self {
version,
request_id: request.request_id,
sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?,
})
}
}
impl TryFrom<(PingRequestV8, Option<AnonymousSenderTag>, ClientVersion)> for PingRequest {
type Error = IpPacketRouterError;
fn try_from(
(request, sender_tag, version): (PingRequestV8, Option<AnonymousSenderTag>, ClientVersion),
) -> Result<Self, Self::Error> {
Ok(Self {
version,
request_id: request.request_id,
sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?,
})
}
}
impl TryFrom<(HealthRequestV8, Option<AnonymousSenderTag>, ClientVersion)> for HealthRequest {
type Error = IpPacketRouterError;
fn try_from(
(request, sender_tag, version): (
HealthRequestV8,
Option<AnonymousSenderTag>,
ClientVersion,
),
) -> Result<Self, Self::Error> {
Ok(Self {
version,
request_id: request.request_id,
sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?,
})
}
}
@@ -0,0 +1,190 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<HealthResponse>,
},
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<bool>,
}
impl VersionedResponse {
pub(crate) fn try_into_bytes(self) -> Result<Vec<u8>> {
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<StaticConnectFailureReason> for StaticConnectResponse {
fn from(failure: StaticConnectFailureReason) -> Self {
StaticConnectResponse::Failure(failure)
}
}
impl From<DynamicConnectSuccess> for DynamicConnectResponse {
fn from(success: DynamicConnectSuccess) -> Self {
DynamicConnectResponse::Success(success)
}
}
impl From<DynamicConnectFailureReason> for DynamicConnectResponse {
fn from(failure: DynamicConnectFailureReason) -> Self {
DynamicConnectResponse::Failure(failure)
}
}
@@ -0,0 +1,188 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<VersionedResponse> for IpPacketResponseV6 {
type Error = IpPacketRouterError;
fn try_from(response: VersionedResponse) -> std::result::Result<Self, Self::Error> {
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<StaticConnectResponse> for StaticConnectResponseReplyV6 {
fn from(response: StaticConnectResponse) -> Self {
match response {
StaticConnectResponse::Success => StaticConnectResponseReplyV6::Success,
StaticConnectResponse::Failure(err) => {
StaticConnectResponseReplyV6::Failure(err.into())
}
}
}
}
impl From<StaticConnectFailureReason> 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<DynamicConnectResponse> 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<DynamicConnectFailureReason> 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<DisconnectResponse> for DisconnectResponseReplyV6 {
fn from(response: DisconnectResponse) -> Self {
match response {
DisconnectResponse::Success => DisconnectResponseReplyV6::Success,
DisconnectResponse::Failure(err) => DisconnectResponseReplyV6::Failure(err.into()),
}
}
}
impl From<DisconnectFailureReason> for DisconnectFailureReasonV6 {
fn from(reason: DisconnectFailureReason) -> Self {
match reason {
DisconnectFailureReason::ClientNotConnected => {
DisconnectFailureReasonV6::RequestedNymAddressNotConnected
}
DisconnectFailureReason::Other(err) => DisconnectFailureReasonV6::Other(err),
}
}
}
impl From<HealthResponse> for HealthResponseReplyV6 {
fn from(response: HealthResponse) -> Self {
HealthResponseReplyV6 {
build_info: response.build_info,
routable: response.routable,
}
}
}
impl From<InfoResponseReply> 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<InfoLevel> for InfoLevelV6 {
fn from(level: InfoLevel) -> Self {
match level {
InfoLevel::Info => InfoLevelV6::Info,
InfoLevel::Warn => InfoLevelV6::Warn,
InfoLevel::Error => InfoLevelV6::Error,
}
}
}
@@ -0,0 +1,188 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<VersionedResponse> for IpPacketResponseV7 {
type Error = IpPacketRouterError;
fn try_from(response: VersionedResponse) -> std::result::Result<Self, Self::Error> {
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<StaticConnectResponse> for StaticConnectResponseReplyV7 {
fn from(response: StaticConnectResponse) -> Self {
match response {
StaticConnectResponse::Success => StaticConnectResponseReplyV7::Success,
StaticConnectResponse::Failure(err) => {
StaticConnectResponseReplyV7::Failure(err.into())
}
}
}
}
impl From<StaticConnectFailureReason> 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<DynamicConnectResponse> 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<DynamicConnectFailureReason> 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<DisconnectResponse> for DisconnectResponseReplyV7 {
fn from(response: DisconnectResponse) -> Self {
match response {
DisconnectResponse::Success => DisconnectResponseReplyV7::Success,
DisconnectResponse::Failure(err) => DisconnectResponseReplyV7::Failure(err.into()),
}
}
}
impl From<DisconnectFailureReason> for DisconnectFailureReasonV7 {
fn from(reason: DisconnectFailureReason) -> Self {
match reason {
DisconnectFailureReason::ClientNotConnected => {
DisconnectFailureReasonV7::RequestedNymAddressNotConnected
}
DisconnectFailureReason::Other(err) => DisconnectFailureReasonV7::Other(err),
}
}
}
impl From<HealthResponse> for HealthResponseReplyV7 {
fn from(response: HealthResponse) -> Self {
HealthResponseReplyV7 {
build_info: response.build_info,
routable: response.routable,
}
}
}
impl From<InfoResponseReply> 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<InfoLevel> for InfoLevelV7 {
fn from(level: InfoLevel) -> Self {
match level {
InfoLevel::Info => InfoLevelV7::Info,
InfoLevel::Warn => InfoLevelV7::Warn,
InfoLevel::Error => InfoLevelV7::Error,
}
}
}
@@ -0,0 +1,183 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<VersionedResponse> 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<StaticConnectResponse> for StaticConnectResponseReplyV8 {
fn from(reply: StaticConnectResponse) -> Self {
match reply {
StaticConnectResponse::Success => StaticConnectResponseReplyV8::Success,
StaticConnectResponse::Failure(err) => {
StaticConnectResponseReplyV8::Failure(err.into())
}
}
}
}
impl From<StaticConnectFailureReason> 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<DynamicConnectResponse> 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<DynamicConnectFailureReason> 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<DisconnectResponse> for DisconnectResponseReplyV8 {
fn from(reply: DisconnectResponse) -> Self {
match reply {
DisconnectResponse::Success => DisconnectResponseReplyV8::Success,
DisconnectResponse::Failure(err) => DisconnectResponseReplyV8::Failure(err.into()),
}
}
}
impl From<DisconnectFailureReason> for DisconnectFailureReasonV8 {
fn from(reason: DisconnectFailureReason) -> Self {
match reason {
DisconnectFailureReason::ClientNotConnected => {
DisconnectFailureReasonV8::ClientNotConnected
}
DisconnectFailureReason::Other(err) => DisconnectFailureReasonV8::Other(err),
}
}
}
impl From<HealthResponse> for HealthResponseReplyV8 {
fn from(response: HealthResponse) -> Self {
HealthResponseReplyV8 {
build_info: response.build_info,
routable: response.routable,
}
}
}
impl From<InfoResponseReply> 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<InfoLevel> for InfoLevelV8 {
fn from(level: InfoLevel) -> Self {
match level {
InfoLevel::Info => InfoLevelV8::Info,
InfoLevel::Warn => InfoLevelV8::Warn,
InfoLevel::Error => InfoLevelV8::Error,
}
}
}
@@ -1,3 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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;
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::io::Error;
use std::pin::Pin;
@@ -1,5 +1,5 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-only
use std::net::SocketAddr;
@@ -1,5 +1,5 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-only
use crate::config::Config;
use crate::error::IpPacketRouterError;
@@ -1,3 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<Vec<u8>>,
pub(crate) ips: IpPair,
struct ConnectedClientMirror {
forward_from_tun_tx: mpsc::UnboundedSender<Vec<u8>>,
ips: IpPair,
}
pub(crate) struct ConnectedClientsListener {
clients_ipv4: HashMap<Ipv4Addr, ConnectedClientMirror>,
clients_ipv6: HashMap<Ipv6Addr, ConnectedClientMirror>,
connected_client_rx:
tokio::sync::mpsc::UnboundedReceiver<mixnet_listener::ConnectedClientEvent>,
connected_client_rx: mpsc::UnboundedReceiver<ConnectedClientEvent>,
}
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<ConnectedClientEvent>) -> 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!(
@@ -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<u8>,
) -> 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)
}
}
}