Compare commits

...

7 Commits

Author SHA1 Message Date
Tommy Verrall 0507637286 Fix up helper 2026-05-01 09:24:03 +02:00
Tommy Verrall 0d64e342da 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
2026-05-01 08:50:37 +02:00
Tommy Verrall 46c7eaf4e5 Pin blake 2026-05-01 08:32:49 +02:00
Tommy Verrall a9bbd2e346 Fix build issues 2026-05-01 08:26:51 +02:00
benedettadavico bcb7319bb1 more v9 fixes 2026-05-01 08:18:06 +02:00
benedettadavico 703b3dad13 v9 bugfix 2026-05-01 08:18:06 +02:00
benedettadavico cfa17f1ed1 v9 bugfix 2026-05-01 08:18:05 +02:00
9 changed files with 149 additions and 35 deletions
Generated
+2 -2
View File
@@ -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",
+3 -1
View File
@@ -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"
+5 -3
View File
@@ -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);
}
+69 -15
View File
@@ -1,24 +1,78 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_ip_packet_requests::response_helpers::IprResponseError;
use nym_sdk::mixnet::ReconstructedMessage;
use tracing::debug;
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 }
/// 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 >= 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)");
}
IprResponseError::VersionMismatch { expected, received } => {
crate::Error::ReceivedResponseWithNewVersion { expected, received }
}
_ => crate::Error::NoVersionInMessage,
return Ok(());
}
if version < MIN_ACCEPTED_VERSION {
return Err(Error::ReceivedResponseWithOldVersion {
expected: MIN_ACCEPTED_VERSION,
received: version,
});
}
Err(Error::ReceivedResponseWithNewVersion {
expected: MAX_ACCEPTED_VERSION,
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::{MAX_ACCEPTED_VERSION, MIN_ACCEPTED_VERSION, 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, MIN_ACCEPTED_VERSION);
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, MAX_ACCEPTED_VERSION);
assert_eq!(received, 10);
}
_ => panic!("unexpected error: {err:?}"),
}
}
}
+7 -2
View File
@@ -1,6 +1,9 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// 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;
+2 -2
View File
@@ -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,
};
@@ -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}");
@@ -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 })
}
@@ -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<Option<VersionedResponse>>;
#[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;