diff --git a/nym-ip-packet-client/src/connect.rs b/nym-ip-packet-client/src/connect.rs index e28405d321..0448c0cf40 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 1b31870ff8..e9898e4026 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}; -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 } - } - IprResponseError::VersionMismatch { expected, received } => { - crate::Error::ReceivedResponseWithNewVersion { expected, received } - } - _ => crate::Error::NoVersionInMessage, +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<()> { + 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:?}"), + } + } + + #[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:?}"), + } + } +} diff --git a/nym-ip-packet-client/src/lib.rs b/nym-ip-packet-client/src/lib.rs index 0ab5c2eb24..f1e682791a 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 7380300175..ea751b3bcb 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 fa25adbf71..d82196338a 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;