From cfa17f1ed1872dc201d72d38e380746daa9fbba8 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 20 Apr 2026 12:29:58 +0200 Subject: [PATCH 1/7] v9 bugfix --- service-providers/ip-packet-router/src/messages/request.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/service-providers/ip-packet-router/src/messages/request.rs b/service-providers/ip-packet-router/src/messages/request.rs index 0f8419e5e1a..0c613a2fd46 100644 --- a/service-providers/ip-packet-router/src/messages/request.rs +++ b/service-providers/ip-packet-router/src/messages/request.rs @@ -10,6 +10,7 @@ use nym_ip_packet_requests::{ IpPair, v6::request::IpPacketRequest as IpPacketRequestV6, v7::request::IpPacketRequest as IpPacketRequestV7, v8::request::IpPacketRequest as IpPacketRequestV8, + v9::request::IpPacketRequest as IpPacketRequestV9, }; use nym_sdk::mixnet::ReconstructedMessage; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; @@ -131,14 +132,14 @@ impl TryFrom<&ReconstructedMessage> for IpPacketRequest { Ok(IpPacketRequest::from((request_v8, sender_tag))) } 9 => { - let request_v8 = IpPacketRequestV8::from_reconstructed_message(reconstructed) + let request_v9 = IpPacketRequestV9::from_reconstructed_message(reconstructed) .map_err( |source| IpPacketRouterError::FailedToDeserializeTaggedPacket { source }, )?; let sender_tag = reconstructed .sender_tag .ok_or(IpPacketRouterError::MissingSenderTag)?; - Ok(v9::convert(request_v8, sender_tag)) + Ok(v9::convert(request_v9, sender_tag)) } _ => { log::info!("Received packet with invalid version: v{request_version}"); From 703b3dad13c6dc1aacb19d222f4c4651726e1a7a Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 20 Apr 2026 15:55:11 +0200 Subject: [PATCH 2/7] v9 bugfix --- service-providers/ip-packet-router/src/messages/response.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/service-providers/ip-packet-router/src/messages/response.rs b/service-providers/ip-packet-router/src/messages/response.rs index 4801983f489..deefccc1466 100644 --- a/service-providers/ip-packet-router/src/messages/response.rs +++ b/service-providers/ip-packet-router/src/messages/response.rs @@ -130,7 +130,11 @@ impl VersionedResponse { ClientVersion::V6 => IpPacketResponseV6::try_from(self)?.to_bytes(), ClientVersion::V7 => IpPacketResponseV7::try_from(self)?.to_bytes(), ClientVersion::V8 => IpPacketResponseV8::try_from(self)?.to_bytes(), - ClientVersion::V9 => IpPacketResponseV8::try_from(self)?.to_bytes(), + ClientVersion::V9 => { + let mut resp = IpPacketResponseV8::try_from(self)?; + resp.version = nym_ip_packet_requests::v9::VERSION; + resp.to_bytes() + } } .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err }) } From bcb7319bb18de54a37c66e9aa68c25c2a547cd08 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 20 Apr 2026 20:21:06 +0200 Subject: [PATCH 3/7] more v9 fixes --- nym-ip-packet-client/src/connect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-ip-packet-client/src/connect.rs b/nym-ip-packet-client/src/connect.rs index 0448c0cf40a..e28405d3216 100644 --- a/nym-ip-packet-client/src/connect.rs +++ b/nym-ip-packet-client/src/connect.rs @@ -80,7 +80,7 @@ impl IprClientConnect { } async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result { - let (request, request_id) = IpPacketRequest::new_connect_request(None); + let (request, request_id) = nym_ip_packet_requests::v9::new_connect_request(None); // We use 20 surbs for the connect request because typically the IPR is configured to have // a min threshold of 10 surbs that it reserves for itself to request additional surbs. From a9bbd2e346d866b78eb059eca8de0ef13f11d4cf Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Fri, 1 May 2026 08:26:51 +0200 Subject: [PATCH 4/7] Fix build issues --- common/nym-kkt/src/masked_byte.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/nym-kkt/src/masked_byte.rs b/common/nym-kkt/src/masked_byte.rs index 2bdd29edaee..4d2353246d9 100644 --- a/common/nym-kkt/src/masked_byte.rs +++ b/common/nym-kkt/src/masked_byte.rs @@ -1,4 +1,4 @@ -use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput}; +use nym_crypto::blake3; use crate::error::{ MaskedByteError, @@ -37,7 +37,8 @@ impl MaskedByte { hasher.update(mask); // avoid zero update hasher.update(&[0xFF, byte]); - hasher.finalize_xof_into(&mut output); + let mut xof = hasher.finalize_xof(); + xof.fill(&mut output); Self(output) } @@ -66,7 +67,8 @@ impl MaskedByte { for i in supported_versions { let mut t_hasher = hasher.clone(); t_hasher.update(&[*i]); - t_hasher.finalize_xof_into(&mut buf); + let mut xof = t_hasher.finalize_xof(); + xof.fill(&mut buf); if buf == self.0 { return Ok(*i); } From 46c7eaf4e527bf53ca0b8accff7cf4ad573031f6 Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Fri, 1 May 2026 08:32:49 +0200 Subject: [PATCH 5/7] Pin blake --- Cargo.lock | 4 ++-- Cargo.toml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0377d3b8e5a..93150368879 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,9 +993,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "b17679a8d69b6d7fd9cd9801a536cec9fa5e5970b69f9d4747f70b39b031f5e7" dependencies = [ "arrayref", "arrayvec", diff --git a/Cargo.toml b/Cargo.toml index df0e4b6153f..05529c9a5cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -229,7 +229,9 @@ base85rs = "0.1.3" bincode = "1.3.3" bip39 = { version = "2.0.0", features = ["zeroize"] } bitvec = "1.0.0" -blake3 = "1.7.0" +# Pin: blake3 1.8+ depends on digest 0.11 for `traits-preview`, while workspace hmac/digest stay on 0.10. +# Unpin after upgrading workspace `digest` + `hmac` + `sha2` (and dependents) to the 0.11 stack. +blake3 = { version = "=1.7.0", default-features = true } bloomfilter = "3.0.1" bs58 = "0.5.1" bytecodec = "0.4.15" From 0d64e342dad8b624345445b488f61cd6de43ef87 Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Fri, 1 May 2026 08:50:37 +0200 Subject: [PATCH 6/7] Fixed: anonymous handshake goes out as v8 - replies are accepted as v8 or v9, and the earlier strict client-side mismatch / dropped v9 non-stream behaviour is addressed --- nym-ip-packet-client/src/connect.rs | 2 +- nym-ip-packet-client/src/helpers.rs | 72 +++++++++++++++---- nym-ip-packet-client/src/lib.rs | 9 ++- nym-ip-packet-client/src/listener.rs | 4 +- .../ip-packet-router/src/mixnet_listener.rs | 60 ++++++++++++++-- 5 files changed, 121 insertions(+), 26 deletions(-) diff --git a/nym-ip-packet-client/src/connect.rs b/nym-ip-packet-client/src/connect.rs index e28405d3216..0448c0cf40a 100644 --- a/nym-ip-packet-client/src/connect.rs +++ b/nym-ip-packet-client/src/connect.rs @@ -80,7 +80,7 @@ impl IprClientConnect { } async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result { - let (request, request_id) = nym_ip_packet_requests::v9::new_connect_request(None); + let (request, request_id) = IpPacketRequest::new_connect_request(None); // We use 20 surbs for the connect request because typically the IPR is configured to have // a min threshold of 10 surbs that it reserves for itself to request additional surbs. diff --git a/nym-ip-packet-client/src/helpers.rs b/nym-ip-packet-client/src/helpers.rs index 1b31870ff84..e9898e4026e 100644 --- a/nym-ip-packet-client/src/helpers.rs +++ b/nym-ip-packet-client/src/helpers.rs @@ -1,24 +1,68 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_ip_packet_requests::response_helpers::IprResponseError; use nym_sdk::mixnet::ReconstructedMessage; -use crate::{current::VERSION as CURRENT_VERSION, error::Result}; +use crate::error::{Error, Result}; +fn check_ipr_wire_reply_version(version: u8) -> Result<()> { + if version == 8 || version == 9 { + return Ok(()); + } + if version < 8 { + return Err(Error::ReceivedResponseWithOldVersion { + expected: 8, + received: version, + }); + } + Err(Error::ReceivedResponseWithNewVersion { + expected: 9, + received: version, + }) +} + +/// IPR responses on the wire may be v8 or v9 (identical payload layout; version byte differs). pub(crate) fn check_ipr_message_version(message: &ReconstructedMessage) -> Result<()> { - nym_ip_packet_requests::response_helpers::check_ipr_message_version( - &message.message, - CURRENT_VERSION, - ) - .map_err(|e| match e { - IprResponseError::NoVersionByte => crate::Error::NoVersionInMessage, - IprResponseError::VersionMismatch { expected, received } if received < expected => { - crate::Error::ReceivedResponseWithOldVersion { expected, received } + let version = message + .message + .first() + .copied() + .ok_or(Error::NoVersionInMessage)?; + check_ipr_wire_reply_version(version) +} + +#[cfg(test)] +mod tests { + use super::check_ipr_wire_reply_version; + use crate::Error; + + #[test] + fn wire_reply_accepts_v8_and_v9() { + assert!(check_ipr_wire_reply_version(8).is_ok()); + assert!(check_ipr_wire_reply_version(9).is_ok()); + } + + #[test] + fn wire_reply_rejects_older_than_v8() { + let err = check_ipr_wire_reply_version(7).unwrap_err(); + match err { + Error::ReceivedResponseWithOldVersion { expected, received } => { + assert_eq!(expected, 8); + assert_eq!(received, 7); + } + _ => panic!("unexpected error: {err:?}"), } - IprResponseError::VersionMismatch { expected, received } => { - crate::Error::ReceivedResponseWithNewVersion { expected, received } + } + + #[test] + fn wire_reply_rejects_newer_than_v9() { + let err = check_ipr_wire_reply_version(10).unwrap_err(); + match err { + Error::ReceivedResponseWithNewVersion { expected, received } => { + assert_eq!(expected, 9); + assert_eq!(received, 10); + } + _ => panic!("unexpected error: {err:?}"), } - _ => crate::Error::NoVersionInMessage, - }) + } } diff --git a/nym-ip-packet-client/src/lib.rs b/nym-ip-packet-client/src/lib.rs index 0ab5c2eb24a..f1e682791a5 100644 --- a/nym-ip-packet-client/src/lib.rs +++ b/nym-ip-packet-client/src/lib.rs @@ -1,6 +1,9 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +//! Anonymous IPR connect uses **v8** on the wire so exits that reject non-stream v9 still answer. +//! **v9** is re-exported for code paths that use LP Stream framing. Incoming IPR responses may be **v8 or v9** (same bincode shape). + mod connect; mod error; mod helpers; @@ -10,5 +13,7 @@ pub use connect::IprClientConnect; pub use error::Error; pub use listener::{IprListener, MixnetMessageOutcome}; -// Re-export the currently used version -pub use nym_ip_packet_requests::v9 as current; +pub use nym_ip_packet_requests::v8; +pub use nym_ip_packet_requests::v9; + +pub use v8 as current; diff --git a/nym-ip-packet-client/src/listener.rs b/nym-ip-packet-client/src/listener.rs index 73803001754..ea751b3bcbf 100644 --- a/nym-ip-packet-client/src/listener.rs +++ b/nym-ip-packet-client/src/listener.rs @@ -3,7 +3,7 @@ use bytes::Bytes; use futures::StreamExt; -use nym_ip_packet_requests::{codec::MultiIpPacketCodec, v8::response::ControlResponse}; +use nym_ip_packet_requests::codec::MultiIpPacketCodec; use nym_sdk::mixnet::ReconstructedMessage; use tokio_util::codec::FramedRead; use tracing::{debug, error, info, warn}; @@ -11,7 +11,7 @@ use tracing::{debug, error, info, warn}; use crate::{ current::{ request::{ControlRequest, IpPacketRequest, IpPacketRequestData}, - response::{InfoLevel, IpPacketResponse, IpPacketResponseData}, + response::{ControlResponse, InfoLevel, IpPacketResponse, IpPacketResponseData}, }, helpers::check_ipr_message_version, }; diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index fa25adbf71d..d82196338a4 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -37,6 +37,15 @@ type TunDevice = crate::non_linux_dummy::DummyDevice; #[cfg(target_os = "linux")] type TunDevice = tokio_tun::Tun; +/// v9+ on non-stream sphinx is limited to [`ControlRequest::DynamicConnect`] (handshake). Other +/// v9+ payloads must be sent inside LP Stream frames (`stream_id` is `Some` on dispatch). +fn allows_non_stream_v9_ipr_request(request: &IpPacketRequest) -> bool { + matches!( + request, + IpPacketRequest::Control(ControlRequest::DynamicConnect(_)) + ) +} + // #[cfg(target_os = "linux")] pub(crate) struct MixnetListener { // The configuration for the mixnet listener @@ -560,9 +569,9 @@ impl MixnetListener { /// # Version / transport enforcement /// /// - LP Stream frames (`stream_id` is `Some`) **must** carry v9+ payloads. - /// - Non-stream messages (`stream_id` is `None`) **must** be v8 or lower. - /// - /// Messages that violate these rules are dropped. + /// - Non-stream sphinx (`stream_id` is `None`): v8 or lower for all messages **except** + /// [`ControlRequest::DynamicConnect`], which may use v9 for the anonymous handshake. + /// - Other non-stream v9+ payloads are dropped. async fn on_ipr_message( &mut self, reconstructed: ReconstructedMessage, @@ -577,16 +586,14 @@ impl MixnetListener { req => req, }?; - // Enforce version/transport consistency: - // - LP Stream frames must carry v9+ payloads - // - Non-stream messages must be v8 or lower + // Enforce version/transport consistency (see `on_ipr_message` doc). let version_num = request.version().into_u8(); if stream_id.is_some() && version_num < 9 { log::warn!("LP Stream frame contains v{version_num} payload, expected v9+; dropping",); return Ok(vec![]); } - if stream_id.is_none() && version_num >= 9 { + if stream_id.is_none() && version_num >= 9 && !allows_non_stream_v9_ipr_request(&request) { log::warn!("Non-stream message claims v{version_num}, expected v8 or lower; dropping",); return Ok(vec![]); } @@ -693,6 +700,45 @@ pub(crate) type PacketHandleResult = Result>; #[cfg(test)] mod tests { + use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; + + use super::allows_non_stream_v9_ipr_request; + use crate::{ + clients::ConnectedClientId, + messages::{ + ClientVersion, + request::{ + ControlRequest, DataRequest, DynamicConnectRequest, IpPacketRequest, PingRequest, + }, + }, + }; + + #[test] + fn non_stream_v9_allowed_for_dynamic_connect_only() { + let sent_by = ConnectedClientId::from(AnonymousSenderTag::from_bytes([9u8; 16])); + let dynamic_connect = + IpPacketRequest::Control(ControlRequest::DynamicConnect(DynamicConnectRequest { + version: ClientVersion::V9, + request_id: 1, + sent_by, + buffer_timeout: None, + })); + assert!(allows_non_stream_v9_ipr_request(&dynamic_connect)); + + let data = IpPacketRequest::Data(DataRequest { + version: ClientVersion::V9, + ip_packets: bytes::Bytes::new(), + }); + assert!(!allows_non_stream_v9_ipr_request(&data)); + + let ping = IpPacketRequest::Control(ControlRequest::Ping(PingRequest { + version: ClientVersion::V9, + request_id: 2, + sent_by: ConnectedClientId::from(AnonymousSenderTag::from_bytes([1u8; 16])), + })); + assert!(!allows_non_stream_v9_ipr_request(&ping)); + } + #[test] fn test_lp_stream_frame_detected() { use bytes::BytesMut; From 05076372869f04061aa0c164039a06cdb7503920 Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Fri, 1 May 2026 09:24:03 +0200 Subject: [PATCH 7/7] Fix up helper --- nym-ip-packet-client/src/helpers.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/nym-ip-packet-client/src/helpers.rs b/nym-ip-packet-client/src/helpers.rs index e9898e4026e..c331d04dd00 100644 --- a/nym-ip-packet-client/src/helpers.rs +++ b/nym-ip-packet-client/src/helpers.rs @@ -2,21 +2,31 @@ // SPDX-License-Identifier: GPL-3.0-only use nym_sdk::mixnet::ReconstructedMessage; +use tracing::debug; use crate::error::{Error, Result}; +/// Minimum wire version accepted from the IPR. +const MIN_ACCEPTED_VERSION: u8 = 8; +/// Maximum wire version accepted from the IPR. +const MAX_ACCEPTED_VERSION: u8 = 9; + fn check_ipr_wire_reply_version(version: u8) -> Result<()> { - if version == 8 || version == 9 { + if version >= MIN_ACCEPTED_VERSION && version <= MAX_ACCEPTED_VERSION { + if version == MIN_ACCEPTED_VERSION { + // v8 reply: IPR exit is on the older protocol version, still compatible. + debug!("Received IPR response with wire version v{version} (accepting v8 and v9)"); + } return Ok(()); } - if version < 8 { + if version < MIN_ACCEPTED_VERSION { return Err(Error::ReceivedResponseWithOldVersion { - expected: 8, + expected: MIN_ACCEPTED_VERSION, received: version, }); } Err(Error::ReceivedResponseWithNewVersion { - expected: 9, + expected: MAX_ACCEPTED_VERSION, received: version, }) } @@ -33,7 +43,7 @@ pub(crate) fn check_ipr_message_version(message: &ReconstructedMessage) -> Resul #[cfg(test)] mod tests { - use super::check_ipr_wire_reply_version; + use super::{MAX_ACCEPTED_VERSION, MIN_ACCEPTED_VERSION, check_ipr_wire_reply_version}; use crate::Error; #[test] @@ -47,7 +57,7 @@ mod tests { let err = check_ipr_wire_reply_version(7).unwrap_err(); match err { Error::ReceivedResponseWithOldVersion { expected, received } => { - assert_eq!(expected, 8); + assert_eq!(expected, MIN_ACCEPTED_VERSION); assert_eq!(received, 7); } _ => panic!("unexpected error: {err:?}"), @@ -59,7 +69,7 @@ mod tests { let err = check_ipr_wire_reply_version(10).unwrap_err(); match err { Error::ReceivedResponseWithNewVersion { expected, received } => { - assert_eq!(expected, 9); + assert_eq!(expected, MAX_ACCEPTED_VERSION); assert_eq!(received, 10); } _ => panic!("unexpected error: {err:?}"),