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
This commit is contained in:
@@ -80,7 +80,7 @@ impl IprClientConnect {
|
||||
}
|
||||
|
||||
async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result<u64> {
|
||||
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.
|
||||
|
||||
@@ -1,24 +1,68 @@
|
||||
// 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 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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user