new addressing + pipeline unification and routing stubs

This commit is contained in:
Simon Wicky
2026-05-22 11:49:37 +02:00
parent c8f9959d7a
commit 05ed775686
26 changed files with 665 additions and 371 deletions
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_lp_data::packet::frame::LpFrameKind;
use nym_sphinx_addressing::clients::ClientAddress;
use nym_sphinx_addressing::nodes::NymNodeRoutingAddressError;
use nym_sphinx_forwarding::packet::MixPacketFormattingError;
use nym_sphinx_framing::processing::PacketProcessingError;
@@ -34,6 +35,12 @@ pub enum LpDataHandlerError {
#[error("unwrapped a packet into a final hop packet. This is no longer supported")]
FinalHop,
#[error("a mix node received a forward hop addressed to a client; only gateways may forward to clients")]
ClientForwardHopAtMixNode,
#[error("no UDP endpoint registered for client {client_addr}")]
UnknownClientAddress { client_addr: ClientAddress },
#[error("{0}")]
Internal(String),
@@ -8,7 +8,7 @@ use nym_sphinx_params::SphinxKeyRotation;
use crate::node::lp::data::handler::error::LpDataHandlerError;
/// Message types supported by mixnodes.
/// Message types supported by nym-nodes with only mixnode role
#[derive(Debug, Clone, Copy)]
pub enum MixMessage {
Sphinx(SphinxMixMessage),
@@ -1,8 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod gateway;
mod mixnode;
mod mixing;
mod nymnode;
pub use gateway::{ForwardOutfoxMixMessage, ForwardSphinxMixMessage, GatewayMessage};
pub use mixnode::{MixMessage, OutfoxMixMessage, SphinxMixMessage};
pub use mixing::{MixMessage, OutfoxMixMessage, SphinxMixMessage};
pub use nymnode::{ForwardOutfoxMixMessage, ForwardSphinxMixMessage, NymNodeMessage};
@@ -12,26 +12,24 @@ use crate::node::lp::data::handler::{
messages::{MixMessage, SphinxMixMessage},
};
/// Message types supported by gateways. Gateways accept everything mixnodes
/// do (carried as [`GatewayMessage::Mix`]) plus the forward-only variants
/// that bypass sphinx/outfox processing.
/// Message types supported by nym-nodes with a gateway role.
#[derive(Debug, Clone, Copy)]
pub enum GatewayMessage {
pub enum NymNodeMessage {
Mix(MixMessage),
ForwardSphinx(ForwardSphinxMixMessage),
ForwardOutfox(ForwardOutfoxMixMessage),
}
impl GatewayMessage {
impl NymNodeMessage {
pub fn from_frame_header(header: LpFrameHeader) -> Result<Self, LpDataHandlerError> {
match header.kind {
LpFrameKind::SphinxPacket | LpFrameKind::OutfoxPacket => {
Ok(GatewayMessage::Mix(MixMessage::from_frame_header(header)?))
Ok(NymNodeMessage::Mix(MixMessage::from_frame_header(header)?))
}
LpFrameKind::ForwardSphinxPacket => Ok(GatewayMessage::ForwardSphinx(
LpFrameKind::ForwardSphinxPacket => Ok(NymNodeMessage::ForwardSphinx(
header.frame_attributes.try_into()?,
)),
LpFrameKind::ForwardOutfoxPacket => Ok(GatewayMessage::ForwardOutfox(
LpFrameKind::ForwardOutfoxPacket => Ok(NymNodeMessage::ForwardOutfox(
header.frame_attributes.try_into()?,
)),
_ => Err(LpDataHandlerError::UnsupportedLpFrameKind { typ: header.kind }),
@@ -39,30 +37,30 @@ impl GatewayMessage {
}
}
impl From<GatewayMessage> for PacketKind {
fn from(value: GatewayMessage) -> Self {
impl From<NymNodeMessage> for PacketKind {
fn from(value: NymNodeMessage) -> Self {
match value {
GatewayMessage::Mix(msg) => msg.into(),
GatewayMessage::ForwardSphinx(_) => PacketKind::LpSphinx,
GatewayMessage::ForwardOutfox(_) => PacketKind::LpOutfox,
NymNodeMessage::Mix(msg) => msg.into(),
NymNodeMessage::ForwardSphinx(_) => PacketKind::LpSphinx,
NymNodeMessage::ForwardOutfox(_) => PacketKind::LpOutfox,
}
}
}
impl From<MixMessage> for GatewayMessage {
impl From<MixMessage> for NymNodeMessage {
fn from(value: MixMessage) -> Self {
GatewayMessage::Mix(value)
NymNodeMessage::Mix(value)
}
}
impl From<GatewayMessage> for LpFrameHeader {
fn from(value: GatewayMessage) -> Self {
impl From<NymNodeMessage> for LpFrameHeader {
fn from(value: NymNodeMessage) -> Self {
match value {
GatewayMessage::Mix(msg) => msg.into(),
GatewayMessage::ForwardSphinx(msg) => {
NymNodeMessage::Mix(msg) => msg.into(),
NymNodeMessage::ForwardSphinx(msg) => {
LpFrameHeader::new(LpFrameKind::ForwardSphinxPacket, msg)
}
GatewayMessage::ForwardOutfox(msg) => {
NymNodeMessage::ForwardOutfox(msg) => {
LpFrameHeader::new(LpFrameKind::ForwardOutfoxPacket, msg)
}
}
+2 -2
View File
@@ -16,7 +16,7 @@
//!
use crate::node::lp::data::PACKET_BUFFER_SIZE;
use crate::node::lp::data::handler::pipeline::MixnodeDataPipeline;
use crate::node::lp::data::handler::pipeline::NymNodeDataPipeline;
use crate::node::lp::data::shared::SharedLpDataState;
use nym_lp_data::AddressedTimedData;
use nym_lp_data::nymnodes::traits::NymNodeProcessingPipeline;
@@ -215,7 +215,7 @@ impl LpDataHandler {
input_rx: mpsc::Receiver<WorkerInput>,
output_rx: mpsc::SyncSender<WorkerOutput>,
) {
let mut pipeline = MixnodeDataPipeline::new(state.clone(), OsRng);
let mut pipeline = NymNodeDataPipeline::new(state.clone(), OsRng);
while let Ok(input) = input_rx.recv() {
// Blocking is fine, we don't want to unclog ourself and process a new packet that will be dropped anyway
if let Err(e) = output_rx.send(pipeline.process(input.packet, input.timestamp)) {
@@ -1,161 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{net::SocketAddr, sync::Arc, time::Instant};
use nym_lp_data::{
AddressedTimedData, PipelinePayload, TimedData, TimedPayload,
common::traits::{
Framing, FramingUnwrap, Transport, TransportUnwrap, WireUnwrappingPipeline,
WireWrappingPipeline,
},
nymnodes::traits::NymNodeProcessingPipeline,
packet::{EncryptedLpPacket, LpFrame, LpHeader, MalformedLpPacketError, frame::LpFrameHeader},
};
use rand::Rng;
use tracing::warn;
use crate::node::{
lp::data::{
handler::{
messages::GatewayMessage,
pipeline::{MixnodeDataPipeline, wire::WirePipeline},
processing,
},
shared::SharedLpDataState,
},
routing_filter::RoutingFilter,
};
pub(crate) struct GatewayDataPipeline<R> {
state: Arc<SharedLpDataState>,
wire: WirePipeline<R>,
}
impl<R: Rng> GatewayDataPipeline<R> {
pub(crate) fn new(state: Arc<SharedLpDataState>, rng: R) -> Self {
Self {
state: state.clone(),
wire: WirePipeline::new(state, rng),
}
}
}
// Processing logic
impl<R: Rng>
NymNodeProcessingPipeline<
Instant,
EncryptedLpPacket,
GatewayMessage,
GatewayMessage,
SocketAddr,
> for GatewayDataPipeline<R>
{
fn mix(
&mut self,
message_kind: GatewayMessage,
payload: TimedPayload<Instant>,
timestamp: Instant,
) -> Vec<PipelinePayload<Instant, GatewayMessage, SocketAddr>> {
let processing_result = match message_kind {
GatewayMessage::Mix(msg) => {
return MixnodeDataPipeline::<R>::process_mix_packet(
&self.state,
msg,
payload,
timestamp,
)
.into_iter()
.map(|payload| payload.options_transform(Into::into))
.collect();
}
// SW Need to check for bandwidth and routing filter as well
GatewayMessage::ForwardSphinx(metadata) => {
processing::sphinx::process_forward(&self.state, payload, metadata)
}
GatewayMessage::ForwardOutfox(metadata) => {
processing::outfox::process_forward(&self.state, payload, metadata)
}
};
Vec::new()
}
}
// ============== Wire wrap/unwrap: pure delegation to WirePipeline ==============
impl<R: Rng> Framing<Instant, GatewayMessage, SocketAddr> for GatewayDataPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE;
fn to_frame(
&mut self,
payload: PipelinePayload<Instant, GatewayMessage, SocketAddr>,
frame_size: usize,
) -> Vec<AddressedTimedData<Instant, Self::Frame, SocketAddr>> {
let frame = LpFrame {
header: payload.options.into(),
content: payload.data.data.into(),
};
self.wire
.message_to_frame(payload.data.timestamp, frame, payload.dst, frame_size)
}
}
impl<R: Rng> Transport<Instant, EncryptedLpPacket, SocketAddr> for GatewayDataPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpHeader::SIZE;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Instant, Self::Frame, SocketAddr>,
) -> AddressedTimedData<Instant, EncryptedLpPacket, SocketAddr> {
self.wire.frame_to_packet(frame)
}
}
impl<R: Rng> WireWrappingPipeline<Instant, EncryptedLpPacket, GatewayMessage, SocketAddr>
for GatewayDataPipeline<R>
{
fn packet_size(&self) -> usize {
nym_lp_data::packet::MTU
}
}
impl<R: Rng> TransportUnwrap<Instant, EncryptedLpPacket> for GatewayDataPipeline<R> {
type Frame = LpFrame;
type Error = MalformedLpPacketError;
fn packet_to_frame(
&mut self,
packet: EncryptedLpPacket,
timestamp: Instant,
) -> Result<TimedData<Instant, Self::Frame>, Self::Error> {
self.wire.packet_to_frame(packet, timestamp)
}
}
impl<R: Rng> FramingUnwrap<Instant, GatewayMessage> for GatewayDataPipeline<R> {
type Frame = LpFrame;
fn frame_to_message(
&mut self,
frame: TimedData<Instant, Self::Frame>,
) -> Option<(TimedPayload<Instant>, GatewayMessage)> {
let reassembled = self.wire.frame_to_maybe_message(frame)?;
let message_kind = GatewayMessage::from_frame_header(reassembled.data.header)
.inspect_err(|e| warn!("{e}"))
.ok()?;
self.state.message_received(message_kind);
Some((
TimedPayload::new(reassembled.timestamp, reassembled.data.content.to_vec()),
message_kind,
))
}
}
impl<R: Rng> WireUnwrappingPipeline<Instant, EncryptedLpPacket, GatewayMessage>
for GatewayDataPipeline<R>
{
}
@@ -1,12 +1,10 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Per-role data pipelines (mixnode, gateway). The wire wrapping/unwrapping
//! Data pipelines. The wire wrapping/unwrapping
//! shared between them lives in [`wire`].
mod gateway;
mod mixnode;
mod nymnode;
mod wire;
pub(crate) use gateway::GatewayDataPipeline;
pub(crate) use mixnode::MixnodeDataPipeline;
pub(crate) use nymnode::NymNodeDataPipeline;
@@ -12,23 +12,29 @@ use nym_lp_data::{
nymnodes::traits::NymNodeProcessingPipeline,
packet::{EncryptedLpPacket, LpFrame, LpHeader, MalformedLpPacketError, frame::LpFrameHeader},
};
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
use rand::Rng;
use tracing::warn;
use crate::node::{
lp::data::{
handler::{messages::MixMessage, pipeline::wire::WirePipeline, processing},
handler::{
error::LpDataHandlerError,
messages::{MixMessage, NymNodeMessage},
pipeline::wire::WirePipeline,
processing,
},
shared::SharedLpDataState,
},
routing_filter::RoutingFilter,
};
pub(crate) struct MixnodeDataPipeline<R> {
pub(crate) struct NymNodeDataPipeline<R> {
state: Arc<SharedLpDataState>,
wire: WirePipeline<R>,
}
impl<R: Rng> MixnodeDataPipeline<R> {
impl<R: Rng> NymNodeDataPipeline<R> {
pub(crate) fn new(state: Arc<SharedLpDataState>, rng: R) -> Self {
Self {
state: state.clone(),
@@ -36,23 +42,55 @@ impl<R: Rng> MixnodeDataPipeline<R> {
}
}
// Processing logic so gateways can reuse them
// Processing logic for packets supported by mixnode enabled node
pub fn process_mix_packet(
shared_state: &SharedLpDataState,
&mut self,
message_kind: MixMessage,
payload: TimedPayload<Instant>,
_: Instant,
) -> Vec<PipelinePayload<Instant, MixMessage, SocketAddr>> {
let processing_result = match message_kind {
) -> Result<PipelinePayload<Instant, MixMessage, NymNodeRoutingAddress>, LpDataHandlerError>
{
match message_kind {
MixMessage::Sphinx(metadata) => {
processing::sphinx::process(shared_state, payload, metadata)
processing::sphinx::process(&self.state, payload, metadata)
}
MixMessage::Outfox(metadata) => {
processing::outfox::process(shared_state, payload, metadata)
processing::outfox::process(&self.state, payload, metadata)
}
}
}
}
// Processing logic
impl<R: Rng>
NymNodeProcessingPipeline<
Instant,
EncryptedLpPacket,
NymNodeMessage,
NymNodeMessage,
SocketAddr,
> for NymNodeDataPipeline<R>
{
fn mix(
&mut self,
message_kind: NymNodeMessage,
payload: TimedPayload<Instant>,
timestamp: Instant,
) -> Vec<PipelinePayload<Instant, NymNodeMessage, SocketAddr>> {
// Everything specific to a given packet type should happen here
let processing_result = match message_kind {
NymNodeMessage::Mix(msg) => self
.process_mix_packet(msg, payload, timestamp)
.map(|payload| payload.options_transform(Into::into)),
NymNodeMessage::ForwardSphinx(metadata) => {
processing::sphinx::process_forward(&self.state, payload, metadata)
}
NymNodeMessage::ForwardOutfox(metadata) => {
processing::outfox::process_forward(&self.state, payload, metadata)
}
};
shared_state.update_processing_metrics(&processing_result);
self.state.update_processing_metrics(&processing_result);
let packet_to_forward = match processing_result {
Ok(packet) => packet,
@@ -62,45 +100,54 @@ impl<R: Rng> MixnodeDataPipeline<R> {
}
};
let next_hop = packet_to_forward.dst;
if !shared_state.routing_filter.should_route(next_hop.ip()) {
warn!(
event = "packet.dropped.routing_filter",
next_hop = %next_hop,
"dropping packet: egress address does not belong to any known node"
);
shared_state.routing_filter_dropped(next_hop);
Vec::new()
} else {
vec![packet_to_forward]
}
}
}
// Now we are deciding if we are routing the packet and where
// Mixing logic
impl<R: Rng>
NymNodeProcessingPipeline<Instant, EncryptedLpPacket, MixMessage, MixMessage, SocketAddr>
for MixnodeDataPipeline<R>
{
fn mix(
&mut self,
message_kind: MixMessage,
payload: TimedPayload<Instant>,
timestamp: Instant,
) -> Vec<PipelinePayload<Instant, MixMessage, SocketAddr>> {
MixnodeDataPipeline::<R>::process_mix_packet(&self.state, message_kind, payload, timestamp)
match packet_to_forward.dst {
NymNodeRoutingAddress::Node(next_hop) => {
if !self.state.routing_filter.should_route(next_hop.ip()) {
warn!(
event = "packet.dropped.routing_filter",
next_hop = %next_hop,
"dropping packet: egress address does not belong to any known node"
);
self.state.routing_filter_dropped(next_hop);
Vec::new()
} else {
vec![packet_to_forward.with_dst(next_hop)]
}
}
NymNodeRoutingAddress::Client(client_address) => {
if !self.state.processing_config.client_forwarding_enabled {
warn!(
event = "packet.dropped.client_forwarding_disabled",
"dropping packet destined to a client_address on a client_forwarding_disabled node"
);
self.state.client_forwarding_disabled_dropped();
Vec::new()
} else if self.state.is_internal_service_provider(client_address) {
// SW TODO route packet to SP channel, no output (no need for framing and encrytpion, which is why we exit here)
self.state.internal_sp_routed();
Vec::new()
} else {
// SW TODO client address lookup
#[allow(clippy::unwrap_used)] // This is tmp anyway
let client_socket_address = "0.0.0.0:0".parse().unwrap();
vec![packet_to_forward.with_dst(client_socket_address)]
}
}
}
}
}
// ============== Wire wrap/unwrap: pure delegation to WirePipeline ==============
impl<R: Rng> Framing<Instant, MixMessage, SocketAddr> for MixnodeDataPipeline<R> {
impl<R: Rng> Framing<Instant, NymNodeMessage, SocketAddr> for NymNodeDataPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE;
fn to_frame(
&mut self,
payload: PipelinePayload<Instant, MixMessage, SocketAddr>,
payload: PipelinePayload<Instant, NymNodeMessage, SocketAddr>,
frame_size: usize,
) -> Vec<AddressedTimedData<Instant, Self::Frame, SocketAddr>> {
let frame = LpFrame {
@@ -112,7 +159,7 @@ impl<R: Rng> Framing<Instant, MixMessage, SocketAddr> for MixnodeDataPipeline<R>
}
}
impl<R: Rng> Transport<Instant, EncryptedLpPacket, SocketAddr> for MixnodeDataPipeline<R> {
impl<R: Rng> Transport<Instant, EncryptedLpPacket, SocketAddr> for NymNodeDataPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpHeader::SIZE;
@@ -124,15 +171,15 @@ impl<R: Rng> Transport<Instant, EncryptedLpPacket, SocketAddr> for MixnodeDataPi
}
}
impl<R: Rng> WireWrappingPipeline<Instant, EncryptedLpPacket, MixMessage, SocketAddr>
for MixnodeDataPipeline<R>
impl<R: Rng> WireWrappingPipeline<Instant, EncryptedLpPacket, NymNodeMessage, SocketAddr>
for NymNodeDataPipeline<R>
{
fn packet_size(&self) -> usize {
nym_lp_data::packet::MTU
}
}
impl<R: Rng> TransportUnwrap<Instant, EncryptedLpPacket> for MixnodeDataPipeline<R> {
impl<R: Rng> TransportUnwrap<Instant, EncryptedLpPacket> for NymNodeDataPipeline<R> {
type Frame = LpFrame;
type Error = MalformedLpPacketError;
@@ -145,17 +192,32 @@ impl<R: Rng> TransportUnwrap<Instant, EncryptedLpPacket> for MixnodeDataPipeline
}
}
impl<R: Rng> FramingUnwrap<Instant, MixMessage> for MixnodeDataPipeline<R> {
impl<R: Rng> FramingUnwrap<Instant, NymNodeMessage> for NymNodeDataPipeline<R> {
type Frame = LpFrame;
fn frame_to_message(
&mut self,
frame: TimedData<Instant, Self::Frame>,
) -> Option<(TimedPayload<Instant>, MixMessage)> {
) -> Option<(TimedPayload<Instant>, NymNodeMessage)> {
let reassembled = self.wire.frame_to_maybe_message(frame)?;
let message_kind = MixMessage::from_frame_header(reassembled.data.header)
let message_kind = NymNodeMessage::from_frame_header(reassembled.data.header)
.inspect_err(|e| warn!("{e}"))
.ok()?;
// Mix-only nodes must not accept gateway-role frames. Reject before processing
// so a malformed/malicious peer can't drive us through `process_forward`.
if !self.state.processing_config.client_forwarding_enabled
&& !matches!(message_kind, NymNodeMessage::Mix(_))
{
warn!(
event = "packet.dropped.unexpected_forward_frame",
?message_kind,
"dropping unsupported frame on a node with client forwarding disabled"
);
self.state.processing_misc_error();
return None;
}
self.state.message_received(message_kind);
Some((
TimedPayload::new(reassembled.timestamp, reassembled.data.content.to_vec()),
@@ -164,8 +226,8 @@ impl<R: Rng> FramingUnwrap<Instant, MixMessage> for MixnodeDataPipeline<R> {
}
}
impl<R: Rng> WireUnwrappingPipeline<Instant, EncryptedLpPacket, MixMessage>
for MixnodeDataPipeline<R>
impl<R: Rng> WireUnwrappingPipeline<Instant, EncryptedLpPacket, NymNodeMessage>
for NymNodeDataPipeline<R>
{
}
@@ -200,7 +262,7 @@ mod tests {
use crate::node::key_rotation::active_keys::ActiveSphinxKeys;
use crate::node::key_rotation::key::SphinxPrivateKey;
use crate::node::lp::data::handler::messages::{MixMessage, SphinxMixMessage};
use crate::node::lp::data::handler::pipeline::MixnodeDataPipeline;
use crate::node::lp::data::handler::pipeline::NymNodeDataPipeline;
use crate::node::lp::data::shared::{ProcessingConfig, SharedLpDataState};
use crate::node::replay_protection::bloomfilter::{
ReplayProtectionBloomfilters, RotationFilter,
@@ -238,6 +300,7 @@ mod tests {
lp_config: LpConfig::default(),
processing_config: ProcessingConfig {
maximum_packet_delay: TEST_MAX_FORWARD_DELAY,
client_forwarding_enabled: true,
},
sphinx_keys: ActiveSphinxKeys::new_loaded(primary, None),
replay_protection_filter: ReplayProtectionBloomfilters::new(primary_bloom_filter, None),
@@ -253,12 +316,12 @@ mod tests {
/// Returns the pipeline together with the shared state (so tests can
/// inspect metrics or trigger replays directly)
fn mock_pipeline() -> (
MixnodeDataPipeline<DeterministicRng>,
NymNodeDataPipeline<DeterministicRng>,
Arc<SharedLpDataState>,
) {
let mut rng = deterministic_rng();
let state = Arc::new(mock_shared_state(&mut rng));
let pipeline = MixnodeDataPipeline::new(state.clone(), rng);
let pipeline = NymNodeDataPipeline::new(state.clone(), rng);
(pipeline, state)
}
@@ -1,7 +1,7 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Wire wrapping / unwrapping shared by the mixnode and gateway data pipelines.
//! Wire wrapping / unwrapping
//!
//! Everything below the application-message layer (LP packet encode/decode and
//! fragment reassembly) is identical between roles, so it lives here. The
@@ -10,7 +10,7 @@ use tracing::warn;
use crate::node::lp::data::{
handler::{
error::LpDataHandlerError,
messages::{ForwardOutfoxMixMessage, GatewayMessage, MixMessage, OutfoxMixMessage},
messages::{ForwardOutfoxMixMessage, MixMessage, NymNodeMessage, OutfoxMixMessage},
},
shared::SharedLpDataState,
};
@@ -19,7 +19,7 @@ pub(crate) fn process(
shared_state: &SharedLpDataState,
outfox_packet: TimedPayload<Instant>,
metadata: OutfoxMixMessage,
) -> Result<PipelinePayload<Instant, MixMessage, SocketAddr>, LpDataHandlerError> {
) -> Result<PipelinePayload<Instant, MixMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
let TimedPayload {
data: outfox_bytes,
timestamp: arrival_timestamp,
@@ -38,15 +38,49 @@ pub(crate) fn process(
arrival_timestamp, // Outfox doesn't have mixing delays !!!!!
outfox_packet.to_bytes()?, // OutfoxPacket::to_bytes is actually infallible
MixMessage::Outfox(metadata),
NymNodeRoutingAddress::try_from_bytes(&next_address)?.into(),
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
))
}
}
/// Gateway-side outfox processing. See [`super::sphinx::process_forward`] for the
/// equivalent sphinx semantics — `Client(_)` routing addresses are resolved via the
/// gateway's `ClientAddress → SocketAddr` map; `Node(_)` next-hops are forwarded as-is.
// SW Need to check for bandwidth and routing filter as well
pub(crate) fn process_forward(
shared_state: &SharedLpDataState,
outfox_packet: TimedPayload<Instant>,
metadata: ForwardOutfoxMixMessage,
) -> Result<PipelinePayload<Instant, GatewayMessage, SocketAddr>, LpDataHandlerError> {
) -> Result<PipelinePayload<Instant, NymNodeMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
// let TimedPayload {
// data: outfox_bytes,
// timestamp: arrival_timestamp,
// } = outfox_packet;
// let mut outfox_packet = OutfoxPacket::try_from(outfox_bytes.as_slice())?;
// let key = shared_state.resolve_rotation_key(metadata.key_rotation)?;
// let next_address = outfox_packet.decode_next_layer(key.inner().as_ref())?;
// if outfox_packet.is_final_hop() {
// warn!("Gateway received a final-hop outfox packet; clients are the final hop");
// return Err(LpDataHandlerError::FinalHop);
// }
// let routing_address = NymNodeRoutingAddress::try_from_bytes(&next_address)?;
// let destination_socket = match routing_address {
// NymNodeRoutingAddress::Node(socket) => socket,
// NymNodeRoutingAddress::Client(client_addr) => shared_state
// .lookup_client_udp(&client_addr)
// .ok_or(LpDataHandlerError::UnknownClientAddress { client_addr })?,
// };
// Ok(PipelinePayload::new(
// arrival_timestamp,
// outfox_packet.to_bytes()?,
// GatewayMessage::ForwardOutfox(metadata),
// destination_socket,
// ))
todo!()
}
@@ -12,7 +12,7 @@ use tracing::{error, warn};
use crate::node::lp::data::{
handler::{
error::LpDataHandlerError,
messages::{ForwardSphinxMixMessage, GatewayMessage, MixMessage, SphinxMixMessage},
messages::{ForwardSphinxMixMessage, MixMessage, NymNodeMessage, SphinxMixMessage},
},
shared::SharedLpDataState,
};
@@ -21,7 +21,7 @@ pub(crate) fn process(
shared_state: &SharedLpDataState,
sphinx_packet: TimedPayload<Instant>,
metadata: SphinxMixMessage,
) -> Result<PipelinePayload<Instant, MixMessage, SocketAddr>, LpDataHandlerError> {
) -> Result<PipelinePayload<Instant, MixMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
let TimedPayload {
data: sphinx_bytes,
timestamp: arrival_timestamp,
@@ -74,11 +74,12 @@ pub(crate) fn process(
shared_state.excessive_delay_packet();
delay = shared_state.processing_config.maximum_packet_delay;
}
Ok(PipelinePayload::new(
arrival_timestamp + delay,
next_hop_packet.to_bytes(),
MixMessage::Sphinx(metadata),
NymNodeRoutingAddress::try_from(next_hop_address)?.into(),
NymNodeRoutingAddress::try_from(next_hop_address)?,
))
}
nym_sphinx_types::ProcessedPacketData::FinalHop { .. } => {
@@ -88,10 +89,91 @@ pub(crate) fn process(
}
}
/// Gateway-side sphinx processing.
///
/// Differs from [`process`] in two ways:
/// 1. Accepts a `ForwardHop` whose next address is a `Client(ClientAddress)` variant —
/// looks up the connected client's UDP endpoint in `shared_state.client_udp_map` and
/// addresses the outgoing payload to it.
/// 2. Still forwards `Node(SocketAddr)` next-hops normally (e.g., gateway-to-mixnode
/// traffic, gateway-as-relay paths).
///
/// `FinalHop` should never reach here in the new flow (the client is the final sphinx
/// hop on the UDP path) — treat it as a misrouted packet.
///
// SW Need to check for bandwidth and routing filter as well
pub(crate) fn process_forward(
shared_state: &SharedLpDataState,
sphinx_packet: TimedPayload<Instant>,
metadata: ForwardSphinxMixMessage,
) -> Result<PipelinePayload<Instant, GatewayMessage, SocketAddr>, LpDataHandlerError> {
) -> Result<PipelinePayload<Instant, NymNodeMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
// let TimedPayload {
// data: sphinx_bytes,
// timestamp: arrival_timestamp,
// } = sphinx_packet;
// let sphinx_packet = SphinxPacket::from_bytes(&sphinx_bytes)?;
// let key = shared_state.resolve_rotation_key(metadata.key_rotation)?;
// let rotation_id = key.rotation_id();
// let expanded_shared_secret = sphinx_packet
// .header
// .compute_expanded_shared_secret(key.inner().as_ref());
// if !shared_state.replay_protection_filter.disabled() {
// let replay_tag = expanded_shared_secret.replay_tag();
// let Ok(replayed_packet) = shared_state
// .replay_protection_filter
// .check_and_set(rotation_id, replay_tag)
// else {
// error!("CRITICAL FAILURE: replay bloomfilter mutex poisoning!");
// shared_state.shutdown_token.cancel();
// Err(LpDataHandlerError::internal(
// "replay bloomfilter mutex poisoning!",
// ))?
// };
// if replayed_packet {
// warn!(
// event = "packet.dropped.replay",
// rotation_id, "dropping replayed packet"
// );
// Err(PacketProcessingError::PacketReplay)?
// }
// }
// let processed_packet = sphinx_packet.process_with_expanded_secret(&expanded_shared_secret)?;
// match processed_packet.data {
// nym_sphinx_types::ProcessedPacketData::ForwardHop {
// next_hop_packet,
// next_hop_address,
// delay,
// } => {
// let mut delay = delay.to_duration();
// if delay > shared_state.processing_config.maximum_packet_delay {
// shared_state.excessive_delay_packet();
// delay = shared_state.processing_config.maximum_packet_delay;
// }
// let routing_address = NymNodeRoutingAddress::try_from(next_hop_address)?;
// let destination_socket = match routing_address {
// NymNodeRoutingAddress::Node(socket) => socket,
// NymNodeRoutingAddress::Client(client_addr) => shared_state
// .lookup_client_udp(&client_addr)
// .ok_or(LpDataHandlerError::UnknownClientAddress { client_addr })?,
// };
// Ok(PipelinePayload::new(
// arrival_timestamp + delay,
// next_hop_packet.to_bytes(),
// GatewayMessage::ForwardSphinx(metadata),
// destination_socket,
// ))
// }
// nym_sphinx_types::ProcessedPacketData::FinalHop { .. } => {
// warn!("Gateway received a final-hop sphinx packet; clients are the final hop");
// Err(LpDataHandlerError::FinalHop)
// }
// }
todo!()
}
+23 -2
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
config::{Config, LpConfig, NodeModes},
config::{Config, LpConfig},
node::{
key_rotation::active_keys::{ActiveSphinxKeys, SphinxKeyGuard},
lp::data::handler::error::LpDataHandlerError,
@@ -12,6 +12,7 @@ use crate::{
};
use nym_lp_data::{PipelinePayload, fragmentation::reconstruction::MessageReconstructor};
use nym_node_metrics::{NymNodeMetrics, mixnet::PacketKind};
use nym_sphinx_addressing::{ClientAddress, nodes::NymNodeRoutingAddress};
use nym_sphinx_framing::processing::PacketProcessingError;
use nym_sphinx_params::SphinxKeyRotation;
use nym_task::ShutdownToken;
@@ -24,12 +25,14 @@ use tracing::{Span, warn};
#[derive(Clone, Copy)]
pub(crate) struct ProcessingConfig {
pub(crate) maximum_packet_delay: Duration,
pub(crate) client_forwarding_enabled: bool,
}
impl ProcessingConfig {
pub(crate) fn new(config: &Config) -> Self {
ProcessingConfig {
maximum_packet_delay: config.mixnet.debug.maximum_forward_packet_delay,
client_forwarding_enabled: config.modes.expects_client_traffic(),
}
}
}
@@ -53,6 +56,7 @@ pub(crate) struct SharedLpDataState {
pub metrics: NymNodeMetrics,
pub shutdown_token: ShutdownToken,
// SW At some point we will need an `Option<SharedGatewayLpDataState>` somewhere, that will be required for gateways
}
impl SharedLpDataState {
@@ -150,10 +154,22 @@ impl SharedLpDataState {
self.metrics.mixnet.lp_routing_filter_dropped(dst)
}
pub(super) fn client_forwarding_disabled_dropped(&self) {
self.metrics.mixnet.lp_client_forwarding_disabled_dropped()
}
pub(super) fn internal_sp_routed(&self) {
self.metrics.mixnet.lp_internal_sp_routed()
}
pub(super) fn processing_misc_error(&self) {
self.metrics.mixnet.lp_processing_misc_error()
}
pub(super) fn update_processing_metrics(
&self,
processing_result: &Result<
PipelinePayload<Instant, impl Clone + Into<PacketKind>, SocketAddr>,
PipelinePayload<Instant, impl Clone + Into<PacketKind>, NymNodeRoutingAddress>,
LpDataHandlerError,
>,
) {
@@ -174,4 +190,9 @@ impl SharedLpDataState {
}
}
}
// SW Placeholder for SP routing while we don't have gateway state
pub(super) fn is_internal_service_provider(&self, client_address: ClientAddress) -> bool {
false
}
}
@@ -179,6 +179,8 @@ struct LastLpMixnet {
pipeline_overloaded_dropped: usize,
worker_pool_overloaded_dropped: usize,
egress_overloaded_dropped: usize,
client_forwarding_disabled_dropped: usize,
internal_sp_routed: usize,
}
impl LastLpMixnet {
@@ -222,6 +224,11 @@ impl LastLpMixnet {
self.egress_overloaded_dropped,
previous.egress_overloaded_dropped,
),
client_forwarding_disabled_dropped_sec: per_sec(
self.client_forwarding_disabled_dropped,
previous.client_forwarding_disabled_dropped,
),
internal_sp_routed_sec: per_sec(self.internal_sp_routed, previous.internal_sp_routed),
}
}
}
@@ -242,6 +249,8 @@ impl From<&LpMixingStats> for LastLpMixnet {
pipeline_overloaded_dropped: value.pipeline_overloaded_dropped_packets(),
worker_pool_overloaded_dropped: value.worker_pool_overloaded_dropped_packets(),
egress_overloaded_dropped: value.egress_overloaded_dropped_packets(),
client_forwarding_disabled_dropped: value.client_forwarding_disabled_dropped(),
internal_sp_routed: value.internal_sp_routed(),
}
}
}
@@ -313,6 +322,8 @@ pub(crate) struct LpMixnetRateSinceUpdate {
pub(crate) pipeline_overloaded_dropped_sec: f64,
pub(crate) worker_pool_overloaded_dropped_sec: f64,
pub(crate) egress_overloaded_dropped_sec: f64,
pub(crate) client_forwarding_disabled_dropped_sec: f64,
pub(crate) internal_sp_routed_sec: f64,
}
pub(crate) struct WireguardRateSinceUpdate {
@@ -159,6 +159,14 @@ impl OnUpdateMetricsHandler for PrometheusGlobalNodeMetricsRegistryUpdater {
MixnetLpEgressOverloadedDropped,
self.metrics.mixnet.lp.egress_overloaded_dropped_packets() as i64,
);
self.prometheus_wrapper.set(
MixnetLpClientForwardingDisabledDropped,
self.metrics.mixnet.lp.client_forwarding_disabled_dropped() as i64,
);
self.prometheus_wrapper.set(
MixnetLpInternalSpRouted,
self.metrics.mixnet.lp.internal_sp_routed() as i64,
);
// # ENTRY
self.prometheus_wrapper.set(
@@ -330,6 +338,14 @@ impl OnUpdateMetricsHandler for PrometheusGlobalNodeMetricsRegistryUpdater {
MixnetLpEgressOverloadedDroppedRate,
diff.mixnet.lp.egress_overloaded_dropped_sec,
);
self.prometheus_wrapper.set_float(
MixnetLpClientForwardingDisabledDroppedRate,
diff.mixnet.lp.client_forwarding_disabled_dropped_sec,
);
self.prometheus_wrapper.set_float(
MixnetLpInternalSpRoutedRate,
diff.mixnet.lp.internal_sp_routed_sec,
);
// # WIREGUARD
self.prometheus_wrapper
@@ -9,6 +9,7 @@ use nym_mixnet_client::forwarder::{
};
use nym_node_metrics::NymNodeMetrics;
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue};
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
use nym_sphinx_forwarding::packet::MixPacket;
use nym_task::ShutdownToken;
use std::io;
@@ -49,7 +50,11 @@ impl<C, F> PacketForwarder<C, F> {
C: SendWithoutResponse,
F: RoutingFilter,
{
let next_hop = packet.next_hop_address();
// Legacy non-LP path should never get packets adressed to clients
let NymNodeRoutingAddress::Node(next_hop) = packet.next_hop() else {
warn!("client routing address reached legacy forward_packet, this should never happen");
return;
};
if let Err(err) = self.mixnet_client.send_without_response(packet) {
if err.kind() == io::ErrorKind::WouldBlock {
@@ -87,17 +92,23 @@ impl<C, F> PacketForwarder<C, F> {
C: SendWithoutResponse,
F: RoutingFilter,
{
let next_hop = new_packet.packet.next_hop();
// Legacy non-LP path should never get packets adressed to clients
let NymNodeRoutingAddress::Node(next_hop) = new_packet.packet.next_hop() else {
warn!(
event = "packet.dropped.client_variant_at_mix_forward",
next_hop = % new_packet.packet.next_hop(),
"dropping packet: client routing address reached legacy mix forwarder handle_new_packet path, this should never happen"
);
return;
};
if !self.routing_filter.should_route(next_hop.as_ref().ip()) {
if !self.routing_filter.should_route(next_hop.ip()) {
warn!(
event = "packet.dropped.routing_filter",
next_hop = %next_hop,
"dropping packet: egress address does not belong to any known node"
);
self.metrics
.mixnet
.egress_dropped_forward_packet(next_hop.into());
self.metrics.mixnet.egress_dropped_forward_packet(next_hop);
return;
}