Max/sdk stream wrapper (#6320)

* Replace MixnetStream with LP framing
- Replace custom header with LpFrameHeader
- Added sequence number for message ordering

* IPR: support LP Stream-framed client connections
- Detect and route LP Stream frames in mixnet_listener
- Wrap inline responses in LP Stream frames
- Thread stream_id to ConnectedClientHandler for TUN responses

* sdk: add ipr_wrapper module with IpMixStream
- IpMixStream wraps MixnetStream for IPR tunnel over mixnet
- LP Stream framing handled automatically by MixnetStream
- Gateway discovery, connect handshake, IP packet send/receive

* sdk: remove superseded stream_wrapper module

* Trim obvious comments, add architecture.md stub

* sdk: add missing deps and fix warnings

* Cut down architecture diagram until finished with rest of the code, leaving stubs

* sdk: refactor IpMixStream, extract shared helpers

- Extract gateway discovery and connect response parsing
- Add recv() to MixnetStream, remove 64KB read buffer
- Simplify IpMixStream constructor

* Fix SphinxStream renames missed during rebase

* Add IpPacketResponse::from_bytes() for stream-based deserialization

* Clean up ip_packet_client: delete stale connect.rs, take raw bytes not ReconstructedMessage

* Clippy

* Delete unused ip_packet_client modules

- Remove helpers.rs (ICMP utilities moved to example)
- Remove error.rs (errors consolidated into sdk/error.rs)
- Remove README.md
- Update module root to only export discovery + listener

* Simplify listener, IpMixStream, and network_env

- Collapse IprListener struct into standalone handle_ipr_response()
- Move check_ipr_message_version() into listener.rs
- Remove IpMixStream test module (moved to example)
- Remove parse_network() and commented-out Sandbox arms
- Return Result from find_workspace_root() instead of panicking
- Add IprTunnelDisconnected and WorkspaceRootNotFound error variants

* Refactor IPR stream handling and document seq conventions
- Inline stream_id tracking (remove current_stream_id field)
- Re-export encode_stream_frame from clients module
- Document seq=0 reservation for inline control responses
- Document data-path counter starting at 1 with skip-on-wrap

* Add ipr_tunnel example for integration testing
- ICMP ping through IPR with --gateway flag for targeting specific exits
- Move pnet_packet from dependencies to dev-dependencies

* Add message reordering to stream router
- Buffer out-of-order messages per-stream using BTreeMap
- Drain contiguous sequences individually to preserve message boundaries
- Drop duplicate/old sequence numbers with a warning
- Remove dead_code allow on StreamFrame::sequence_num

* Clean up comments and fill architecture.md
- Remove separator line comments
- Update stale comments about ordering not being implemented
- Remove collapsible_if allows, use let-else instead
- Fill in architecture.md data flow and connection lifecycle

* Simplify ipr_tunnel example to minimal smoke test
- Single ping instead of multi-ping loop
- Remove identifier and PING_COUNT
- Collapse ICMP helpers into single build_icmp_ping function

* Add dual-stack IPv6 ping and rename gateway → ipr
- Rename --gateway flag to --ipr and new_with_gateway() to new_with_ipr()
- Add ICMPv6 ping to ipr_tunnel example for dual-stack smoke test
- Tighten echo reply validation (protocol field check, diagnostic output)
- Document IP allocation (subnets, static vs dynamic, client keying) in architecture.md
- Promote LP Stream Open handshake log to INFO

* Tweak subnet comment in docs

* Don't stop IPR listener on decode failure
- Change break to continue so garbage packets can't kill the listener
- Remaining valid packets in the bundle are still processed

* Fix license headers and use workspace dep for pnet_packet
- Switch GPL-3.0 to Apache-2.0 on all SDK library files
- Add missing license headers to 7 files
- Use workspace version for pnet_packet dependency

* Document IP pool isolation from WG/LP dVPN pool
- IPR uses 10.0.0.0/16 on nymtun, WG uses 10.1.0.0/16 on nymwg
- Reference constants.rs as source of truth

* Remove network_env.rs and simplify IpMixStream API
  - Default to mainnet via setup_env(None) instead of requiring env param
  - Remove NetworkEnvironment enum and workspace root detection
  - Remove WorkspaceRootNotFound error variant
  - Update ipr_tunnel example to match new signatures

* Use weighted random selection for IPR gateway discovery
  - Replace max_by_key with choose_weighted biased by performance score
  - Prevents all clients converging on a single highest-performing IPR

* Cap stream reorder buffer to prevent unbounded memory growth
- Add MAX_REORDER_BUFFER (256) to limit per-stream pending messages:
	- buffer overflows = skip ahead to lowest buffered seq and drain
	- protects against malicious senders that deliberately skip sequence numbers

* Extract shared IPR response helpers into nym-ip-packet-requests
  - Add response_helpers module with version check, connect response
    parsing, and control response dispatch
  - SDK ip_packet_client now delegates to shared module
  - Monorepo nym-ip-packet-client uses shared version check and
    connect response parsing
  - Fix doc comment attributing fork to nym-vpn-client

* Extract ICMP test helpers into nym-ip-packet-requests
  - Add icmp_utils module behind test-utils feature flag
  - Move build_icmp_ping, build_icmpv6_ping, is_echo_reply_v4/v6 from
    example
  - Update ipr_tunnel example to use shared helpers

* Add protocol v9 LP-framed transport marker

- Add v9 module (re-exports v8, VERSION=9)
- Accept v9 requests and responses in IPR
- Switch SDK IpMixStream to send v9

* Log protocol version in dynamic connect requests

* Remove KCP from IPR and fix unwrap_or_default in SDK
- Remove all KCP session management from ip-packet-router (replaced by
  LP Stream framing)
- Drop nym-kcp dependency and KcpError variant from IPR
- Replace unwrap_or_default with ok_or(Error::NoNymAPIUrl) in
  IpMixStream::new()

* Add v9 protocol wrapper constructors and enforce version/transport
consistency
- Add v9::new_connect_request(), new_data_request(),
  new_ip_packet_response() to centralise version stamping
- Replace manual protocol.version overrides in SDK and IPR with v9
  wrapper calls
- Bump nym-ip-packet-client current re-export from v8 to v9
- Enforce LP Stream frames must carry v9+ payloads, non-stream must be
  v8 or lower

* Filter IPR exit nodes by minimum v9-compatible release version
- Define MIN_RELEASE_VERSION (1.30.0) in ip-packet-requests/v9 alongside protocol constants
- Add semver-based filtering in SDK gateway discovery to skip nodes below v9 threshold
- Add semver dependency to ip-packet-requests and nym-sdk

* Use numeric version comparison for transport/version enforcement
- Compare version as u8 instead of enum equality so future v10+ is handled correctly
- Remove unused `use super::*` import left over from KCP test removal
This commit is contained in:
mfahampshire
2026-03-27 20:35:26 +00:00
committed by GitHub
parent cc799b69d3
commit c07ef0253d
35 changed files with 1338 additions and 873 deletions
+11 -34
View File
@@ -11,14 +11,10 @@ use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error};
use nym_ip_packet_requests::response_helpers::{self, IprResponseError};
use crate::{
current::{
request::IpPacketRequest,
response::{
ConnectResponse, ConnectResponseReply, ControlResponse, IpPacketResponse,
IpPacketResponseData,
},
},
current::{request::IpPacketRequest, response::IpPacketResponse},
error::{Error, Result},
helpers::check_ipr_message_version,
};
@@ -101,32 +97,6 @@ impl IprClientConnect {
Ok(request_id)
}
async fn handle_connect_response(&self, response: ConnectResponse) -> Result<IpPair> {
debug!("Handling dynamic connect response");
match response.reply {
ConnectResponseReply::Success(r) => Ok(r.ips),
ConnectResponseReply::Failure(reason) => Err(Error::ConnectRequestDenied { reason }),
}
}
async fn handle_ip_packet_router_response(&self, response: IpPacketResponse) -> Result<IpPair> {
let control_response = match response.data {
IpPacketResponseData::Control(control_response) => control_response,
_ => {
error!("Received non-control response while waiting for connect response");
return Err(Error::UnexpectedConnectResponse);
}
};
match *control_response {
ControlResponse::Connect(resp) => self.handle_connect_response(resp).await,
response => {
error!("Unexpected response: {response:?}");
Err(Error::UnexpectedConnectResponse)
}
}
}
async fn listen_for_connect_response(&mut self, request_id: u64) -> Result<IpPair> {
// Connecting is basically synchronous from the perspective of the mixnet client, so it's safe
// to just grab ahold of the mutex and keep it until we get the response.
@@ -173,7 +143,14 @@ impl IprClientConnect {
if response.id() == Some(request_id) {
tracing::debug!("Got response with matching id");
return self.handle_ip_packet_router_response(response).await;
// Replaces local handle_ip_packet_router_response() + handle_connect_response()
return response_helpers::parse_connect_response(response)
.map_err(|e| match e {
IprResponseError::ConnectDenied(reason) => {
Error::ConnectRequestDenied { reason }
}
_ => Error::UnexpectedConnectResponse,
});
}
}
}
+15 -21
View File
@@ -1,30 +1,24 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::cmp::Ordering;
use nym_ip_packet_requests::response_helpers::IprResponseError;
use nym_sdk::mixnet::ReconstructedMessage;
use crate::{Error, current::VERSION as CURRENT_VERSION, error::Result};
use crate::{current::VERSION as CURRENT_VERSION, error::Result};
pub(crate) fn check_ipr_message_version(message: &ReconstructedMessage) -> Result<()> {
// Assuming it's a IPR message, it will have a version as its first byte
if let Some(version) = message.message.first() {
match version.cmp(&CURRENT_VERSION) {
Ordering::Greater => Err(Error::ReceivedResponseWithNewVersion {
expected: CURRENT_VERSION,
received: *version,
}),
Ordering::Less => Err(Error::ReceivedResponseWithOldVersion {
expected: CURRENT_VERSION,
received: *version,
}),
Ordering::Equal => {
// We're good
Ok(())
}
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 }
}
} else {
Err(Error::NoVersionInMessage)
}
IprResponseError::VersionMismatch { expected, received } => {
crate::Error::ReceivedResponseWithNewVersion { expected, received }
}
_ => crate::Error::NoVersionInMessage,
})
}
+1 -1
View File
@@ -11,4 +11,4 @@ pub use error::Error;
pub use listener::{IprListener, MixnetMessageOutcome};
// Re-export the currently used version
pub use nym_ip_packet_requests::v8 as current;
pub use nym_ip_packet_requests::v9 as current;