Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 5 additions & 3 deletions common/nym-kkt/src/masked_byte.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput};
use nym_crypto::blake3;

use crate::error::{
MaskedByteError,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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);
}
Expand Down
82 changes: 68 additions & 14 deletions nym-ip-packet-client/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -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};

/// 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)");
}
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<()> {
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::{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:?}"),
}
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, MAX_ACCEPTED_VERSION);
assert_eq!(received, 10);
}
_ => panic!("unexpected error: {err:?}"),
}
_ => crate::Error::NoVersionInMessage,
})
}
}
9 changes: 7 additions & 2 deletions nym-ip-packet-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
4 changes: 2 additions & 2 deletions nym-ip-packet-client/src/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

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};

use crate::{
current::{
request::{ControlRequest, IpPacketRequest, IpPacketRequestData},
response::{InfoLevel, IpPacketResponse, IpPacketResponseData},
response::{ControlResponse, InfoLevel, IpPacketResponse, IpPacketResponseData},
},
helpers::check_ipr_message_version,
};
Expand Down
5 changes: 3 additions & 2 deletions service-providers/ip-packet-router/src/messages/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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}");
Expand Down
6 changes: 5 additions & 1 deletion service-providers/ip-packet-router/src/messages/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down
60 changes: 53 additions & 7 deletions service-providers/ip-packet-router/src/mixnet_listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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![]);
}
Expand Down Expand Up @@ -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;
Expand Down
Loading