basic node LP handler
This commit is contained in:
@@ -19,6 +19,8 @@ use crate::{LpError, replay::ReceivingKeyCounterValidator};
|
||||
use libcrux_psq::handshake::types::{Authenticator, DHPublicKey};
|
||||
use libcrux_psq::session::{Session, SessionBinding};
|
||||
use nym_kkt::keys::EncapsulationKey;
|
||||
use nym_kkt_ciphersuite::{KEM, KEMKeyDigests};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
/// Represents inputs that drive the state machine transitions.
|
||||
@@ -162,6 +164,21 @@ impl LpTransportSession {
|
||||
.as_initiator(InitiatorData::new(remote_protocol_version, remote_peer))
|
||||
}
|
||||
|
||||
/// Helper function to create `PSQHandshakeState` for the handshake initiator for mutual KKT
|
||||
pub fn psq_handshake_initiator_mutual<S>(
|
||||
connection: &'_ mut S,
|
||||
local_peer: LpLocalPeer,
|
||||
remote_peer: LpRemotePeer,
|
||||
remote_protocol_version: u8,
|
||||
) -> Result<PSQHandshakeStateInitiator<'_, S>, LpError>
|
||||
where
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
PSQHandshakeState::new(connection, local_peer)
|
||||
.as_initiator(InitiatorData::new(remote_protocol_version, remote_peer))
|
||||
.set_mutual_kkt()
|
||||
}
|
||||
|
||||
/// Helper function to create `PSQHandshakeState` for the handshake responder
|
||||
pub fn psq_handshake_responder<S>(
|
||||
connection: &'_ mut S,
|
||||
@@ -173,6 +190,19 @@ impl LpTransportSession {
|
||||
PSQHandshakeState::new(connection, local_peer).as_responder(ResponderData::default())
|
||||
}
|
||||
|
||||
/// Helper function to create `PSQHandshakeState` for the handshake responder for mutual KKT
|
||||
pub fn psq_handshake_responder_mutual<S>(
|
||||
connection: &'_ mut S,
|
||||
local_peer: LpLocalPeer,
|
||||
initiator_kem_hashes: BTreeMap<KEM, KEMKeyDigests>,
|
||||
) -> PSQHandshakeStateResponder<'_, S>
|
||||
where
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
PSQHandshakeState::new(connection, local_peer)
|
||||
.as_responder(ResponderData::default().with_initiator_kem_hashes(initiator_kem_hashes))
|
||||
}
|
||||
|
||||
pub fn session_binding(&self) -> &PersistentSessionBinding {
|
||||
&self.session_binding
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ mod tests {
|
||||
use nym_lp::peer::LpLocalPeer;
|
||||
use nym_node::config::{LpConfig, LpDebug};
|
||||
use nym_node::node::GatewayStorage;
|
||||
use nym_node::node::lp::control::handler::LpConnectionHandler;
|
||||
use nym_node::node::lp::control::client_handler::LpConnectionHandler;
|
||||
use nym_node::node::lp::error::LpHandlerError;
|
||||
use nym_node::node::lp::{SharedLpControlState, SharedLpState};
|
||||
use nym_node::wireguard::{PeerManager, PeerRegistrator};
|
||||
|
||||
@@ -16,7 +16,14 @@ pub struct NetworkStats {
|
||||
// the call stack
|
||||
active_egress_mixnet_connections: Arc<AtomicUsize>,
|
||||
|
||||
active_lp_connections: AtomicUsize,
|
||||
// incoming LP control connections from clients
|
||||
active_lp_ingress_client_connections: AtomicUsize,
|
||||
|
||||
// incoming LP control connections from nodes
|
||||
active_lp_ingress_node_connections: AtomicUsize,
|
||||
|
||||
// outgoing LP control connections to nodes
|
||||
active_lp_egress_node_connections: AtomicUsize,
|
||||
}
|
||||
|
||||
impl NetworkStats {
|
||||
@@ -59,15 +66,38 @@ impl NetworkStats {
|
||||
.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn new_lp_connection(&self) {
|
||||
self.active_lp_connections.fetch_add(1, Ordering::Relaxed);
|
||||
pub fn new_ingress_lp_client_connection(&self) {
|
||||
self.active_lp_ingress_client_connections
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn lp_connection_closed(&self) {
|
||||
self.active_lp_connections.fetch_sub(1, Ordering::Relaxed);
|
||||
pub fn closed_ingress_lp_client_connection(&self) {
|
||||
self.active_lp_ingress_client_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn active_lp_connections_count(&self) -> usize {
|
||||
self.active_lp_connections.load(Ordering::Relaxed)
|
||||
pub fn new_ingress_lp_node_connection(&self) {
|
||||
self.active_lp_ingress_node_connections
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn closed_ingress_lp_node_connection(&self) {
|
||||
self.active_lp_ingress_node_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn new_egress_lp_node_connection(&self) {
|
||||
self.active_lp_egress_node_connections
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn closed_egress_lp_node_connection(&self) {
|
||||
self.active_lp_egress_node_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn active_lp_client_connections_count(&self) -> usize {
|
||||
self.active_lp_ingress_client_connections
|
||||
.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
+26
-97
@@ -2,8 +2,11 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::lp::cleanup::TimestampedState;
|
||||
use crate::node::lp::control::{
|
||||
LP_CONNECTION_DURATION_BUCKETS, LP_DURATION_BUCKETS, LpConnectionStats,
|
||||
};
|
||||
use crate::node::lp::error::LpHandlerError;
|
||||
use crate::node::lp::state::SharedLpControlState;
|
||||
use crate::node::lp::state::SharedLpClientControlState;
|
||||
use dashmap::mapref::one::RefMut;
|
||||
use nym_lp::packet::message::LpMessageType;
|
||||
use nym_lp::packet::{EncryptedLpPacket, ForwardPacketData, LpMessage};
|
||||
@@ -13,6 +16,7 @@ use nym_lp::transport::LpHandshakeChannel;
|
||||
use nym_lp::transport::traits::LpTransportChannel;
|
||||
use nym_lp::{LpTransportSession, packet::message::ExpectedResponseSize};
|
||||
use nym_metrics::{add_histogram_obs, inc, inc_by};
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_registration_common::{LpRegistrationRequest, RegistrationStatus};
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
@@ -20,66 +24,15 @@ use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use tracing::*;
|
||||
|
||||
// Histogram buckets for LP operation duration (legacy - used by unused forwarding methods)
|
||||
const LP_DURATION_BUCKETS: &[f64] = &[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];
|
||||
|
||||
// Timeout for forward I/O operations (send + receive on exit stream)
|
||||
// Must be long enough to cover exit gateway processing time
|
||||
const FORWARD_IO_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
// Histogram buckets for LP connection lifecycle duration
|
||||
// LP connections can be very short (registration only: ~1s) or very long (dVPN sessions: hours/days)
|
||||
// Covers full range from seconds to 24 hours
|
||||
const LP_CONNECTION_DURATION_BUCKETS: &[f64] = &[
|
||||
1.0, // 1 second
|
||||
5.0, // 5 seconds
|
||||
10.0, // 10 seconds
|
||||
30.0, // 30 seconds
|
||||
60.0, // 1 minute
|
||||
300.0, // 5 minutes
|
||||
600.0, // 10 minutes
|
||||
1800.0, // 30 minutes
|
||||
3600.0, // 1 hour
|
||||
7200.0, // 2 hours
|
||||
14400.0, // 4 hours
|
||||
28800.0, // 8 hours
|
||||
43200.0, // 12 hours
|
||||
86400.0, // 24 hours
|
||||
];
|
||||
|
||||
/// Connection lifecycle statistics tracking
|
||||
struct ConnectionStats {
|
||||
/// When the connection started
|
||||
start_time: std::time::Instant,
|
||||
/// Total bytes received (including protocol framing)
|
||||
bytes_received: u64,
|
||||
/// Total bytes sent (including protocol framing)
|
||||
bytes_sent: u64,
|
||||
}
|
||||
|
||||
impl ConnectionStats {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
start_time: std::time::Instant::now(),
|
||||
bytes_received: 0,
|
||||
bytes_sent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_bytes_received(&mut self, bytes: usize) {
|
||||
self.bytes_received += bytes as u64;
|
||||
}
|
||||
|
||||
fn record_bytes_sent(&mut self, bytes: usize) {
|
||||
self.bytes_sent += bytes as u64;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LpConnectionHandler<S = TcpStream> {
|
||||
pub struct LpClientConnectionHandler<S = TcpStream> {
|
||||
stream: S,
|
||||
remote_addr: SocketAddr,
|
||||
state: SharedLpControlState,
|
||||
stats: ConnectionStats,
|
||||
state: SharedLpClientControlState,
|
||||
stats: LpConnectionStats,
|
||||
|
||||
// /// Flag indicating whether this is a connection from an entry gateway serving as a proxy
|
||||
// forwarded_connection: bool,
|
||||
@@ -94,7 +47,7 @@ pub struct LpConnectionHandler<S = TcpStream> {
|
||||
exit_stream: Option<(S, SocketAddr)>,
|
||||
}
|
||||
|
||||
impl<S> LpConnectionHandler<S>
|
||||
impl<S> LpClientConnectionHandler<S>
|
||||
where
|
||||
S: LpTransportChannel + LpHandshakeChannel + Unpin,
|
||||
{
|
||||
@@ -102,19 +55,23 @@ where
|
||||
stream: S,
|
||||
// forwarded_connection: bool,
|
||||
remote_addr: SocketAddr,
|
||||
state: SharedLpControlState,
|
||||
state: SharedLpClientControlState,
|
||||
) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
remote_addr,
|
||||
// forwarded_connection,
|
||||
state,
|
||||
stats: ConnectionStats::new(),
|
||||
stats: LpConnectionStats::new(),
|
||||
bound_receiver_idx: None,
|
||||
exit_stream: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn metrics(&self) -> &NymNodeMetrics {
|
||||
&self.state.shared.metrics
|
||||
}
|
||||
|
||||
/// Get the mutable reference to the state machine associated with this client.
|
||||
/// It is vital it's never held across await points or this might lead to a deadlock.
|
||||
fn state_entry_mut(
|
||||
@@ -135,11 +92,12 @@ where
|
||||
/// First packet binds the connection to a receiver_idx (session-affine).
|
||||
/// Binding is set by handle_client_hello() from payload's receiver_index,
|
||||
/// or by validate_or_set_binding() for non-bootstrap first packets.
|
||||
pub async fn handle(mut self) -> Result<(), LpHandlerError> {
|
||||
debug!("Handling LP connection from {}", self.remote_addr);
|
||||
pub async fn handle(&mut self) -> Result<(), LpHandlerError> {
|
||||
let remote = self.remote_addr;
|
||||
debug!("Handling LP connection from {remote}");
|
||||
|
||||
// Track total LP connections handled
|
||||
inc!("lp_connections_total");
|
||||
inc!("lp_client_connections_total");
|
||||
|
||||
// ============================================================
|
||||
// STREAM-ORIENTED PROCESSING: Loop until connection closes
|
||||
@@ -160,18 +118,12 @@ where
|
||||
.await
|
||||
{
|
||||
Err(_timeout) => {
|
||||
debug!(
|
||||
"timed out attempting to complete KTT/PSQ handshake with {}",
|
||||
self.remote_addr
|
||||
);
|
||||
debug!("timed out attempting to complete KTT/PSQ handshake with {remote}",);
|
||||
self.emit_lifecycle_metrics(false);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Err(handshake_failure)) => {
|
||||
debug!(
|
||||
"failed to complete KKT/PSQ handshake with {}: {handshake_failure}",
|
||||
self.remote_addr
|
||||
);
|
||||
debug!("failed to complete KKT/PSQ handshake with {remote}: {handshake_failure}",);
|
||||
self.emit_lifecycle_metrics(false);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -194,7 +146,7 @@ where
|
||||
Err(err) => {
|
||||
if err.is_connection_closed() {
|
||||
// Graceful EOF - client closed connection
|
||||
trace!("Connection closed by {} (EOF)", self.remote_addr);
|
||||
trace!("Connection closed by {remote} (EOF)");
|
||||
break;
|
||||
} else {
|
||||
inc!("lp_errors_receive_packet");
|
||||
@@ -625,30 +577,7 @@ where
|
||||
|
||||
/// Emit connection lifecycle metrics
|
||||
fn emit_lifecycle_metrics(&self, graceful: bool) {
|
||||
// Track connection duration
|
||||
let duration = self.stats.start_time.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"lp_connection_duration_seconds",
|
||||
duration,
|
||||
LP_CONNECTION_DURATION_BUCKETS
|
||||
);
|
||||
|
||||
// Track bytes transferred
|
||||
inc_by!(
|
||||
"lp_connection_bytes_received_total",
|
||||
self.stats.bytes_received as i64
|
||||
);
|
||||
inc_by!(
|
||||
"lp_connection_bytes_sent_total",
|
||||
self.stats.bytes_sent as i64
|
||||
);
|
||||
|
||||
// Track completion type
|
||||
if graceful {
|
||||
inc!("lp_connections_completed_gracefully");
|
||||
} else {
|
||||
inc!("lp_connections_completed_with_error");
|
||||
}
|
||||
self.stats.emit_lifecycle_client_metrics(graceful);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,7 +594,7 @@ mod tests {
|
||||
// ==================== Test Helpers ====================
|
||||
|
||||
/// Create a minimal test state for handler tests
|
||||
async fn create_minimal_test_state() -> SharedLpControlState {
|
||||
async fn create_minimal_test_state() -> SharedLpClientControlState {
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
let mut rng = deterministic_rng();
|
||||
@@ -690,7 +619,7 @@ mod tests {
|
||||
);
|
||||
let lp_peer = LpLocalPeer::new(Ciphersuite::default(), x_keys).with_kem_keys(kem_keys);
|
||||
|
||||
SharedLpControlState {
|
||||
SharedLpClientControlState {
|
||||
local_lp_peer: lp_peer,
|
||||
peer_registrator: None,
|
||||
forward_semaphore,
|
||||
@@ -724,7 +653,7 @@ mod tests {
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
let mut handler = LpClientConnectionHandler::new(stream, remote_addr, state);
|
||||
// Two-phase: receive raw bytes + header, then parse full packet
|
||||
let packet = handler.receive_raw_packet().await?;
|
||||
let header = packet.outer_header();
|
||||
@@ -3,8 +3,12 @@
|
||||
|
||||
use crate::config::LpConfig;
|
||||
use crate::error::NymNodeError;
|
||||
use crate::node::lp::control::handler::LpConnectionHandler;
|
||||
use crate::node::lp::state::SharedLpControlState;
|
||||
use crate::node::lp::control::client_handler::LpClientConnectionHandler;
|
||||
use crate::node::lp::control::node_handler::{
|
||||
InitialLpNodeConnectionHandler, LpNodeConnectionHandler,
|
||||
};
|
||||
use crate::node::lp::directory::{LpNodeDetails, LpNodes};
|
||||
use crate::node::lp::state::{SharedLpClientControlState, SharedLpNodeControlState};
|
||||
use nym_task::ShutdownTracker;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
@@ -15,8 +19,11 @@ pub struct LpControlListener {
|
||||
/// Address to bind to
|
||||
bind_address: SocketAddr,
|
||||
|
||||
/// Shared state for connection handlers
|
||||
handler_state: SharedLpControlState,
|
||||
/// Shared state for clients connection handlers
|
||||
clients_handler_state: SharedLpClientControlState,
|
||||
|
||||
/// Shared state for nodes connection handlers
|
||||
nodes_handler_state: SharedLpNodeControlState,
|
||||
|
||||
/// Shutdown coordination
|
||||
shutdown: ShutdownTracker,
|
||||
@@ -25,18 +32,19 @@ pub struct LpControlListener {
|
||||
impl LpControlListener {
|
||||
pub fn new(
|
||||
bind_address: SocketAddr,
|
||||
handler_state: SharedLpControlState,
|
||||
handler_state: SharedLpClientControlState,
|
||||
shutdown: ShutdownTracker,
|
||||
) -> Self {
|
||||
Self {
|
||||
bind_address,
|
||||
handler_state,
|
||||
shutdown,
|
||||
}
|
||||
todo!()
|
||||
// Self {
|
||||
// bind_address,
|
||||
// handler_state,
|
||||
// shutdown,
|
||||
// }
|
||||
}
|
||||
|
||||
fn lp_config(&self) -> LpConfig {
|
||||
self.handler_state.shared.lp_config
|
||||
self.clients_handler_state.shared.lp_config
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), NymNodeError> {
|
||||
@@ -74,9 +82,50 @@ impl LpControlListener {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_connection(&self, stream: tokio::net::TcpStream, remote_addr: SocketAddr) {
|
||||
// Check connection limit
|
||||
let active_connections = self.active_lp_connections();
|
||||
fn handle_node_connection(
|
||||
&self,
|
||||
stream: tokio::net::TcpStream,
|
||||
remote_addr: SocketAddr,
|
||||
initiator_details: LpNodeDetails,
|
||||
) {
|
||||
debug!("Accepting LP node connection from {remote_addr}");
|
||||
|
||||
// Spawn handler task
|
||||
let mut handler = InitialLpNodeConnectionHandler::new(
|
||||
stream,
|
||||
remote_addr,
|
||||
initiator_details,
|
||||
self.nodes_handler_state.clone(),
|
||||
);
|
||||
|
||||
self.shutdown.try_spawn_named_with_shutdown(
|
||||
async move {
|
||||
let metrics = handler.metrics().clone();
|
||||
|
||||
// Increment connection counter
|
||||
metrics.network.new_ingress_lp_node_connection();
|
||||
|
||||
let result = handler.handle().await;
|
||||
|
||||
// Decrement connection counter
|
||||
metrics.network.closed_ingress_lp_node_connection();
|
||||
|
||||
// Handler emits lifecycle metrics internally on success
|
||||
// For errors, we need to emit them here since handler is consumed
|
||||
if let Err(e) = result {
|
||||
warn!("LP node handler error for {remote_addr}: {e}");
|
||||
// Note: metrics are emitted in handle() for graceful path
|
||||
// On error path, handle() returns early without emitting
|
||||
// So we track errors here
|
||||
}
|
||||
},
|
||||
&format!("LP_NODE::{remote_addr}"),
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_client_connection(&self, stream: tokio::net::TcpStream, remote_addr: SocketAddr) {
|
||||
// Check connection limit (only for clients, nodes must always be allowed regardless of the limit)
|
||||
let active_connections = self.active_client_connections();
|
||||
let max_connections = self.lp_config().debug.max_connections;
|
||||
if active_connections >= max_connections {
|
||||
warn!(
|
||||
@@ -86,45 +135,55 @@ impl LpControlListener {
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Accepting LP connection from {remote_addr} ({active_connections} active connections)"
|
||||
"Accepting LP client connection from {remote_addr} ({active_connections} active connections)"
|
||||
);
|
||||
|
||||
// Increment connection counter
|
||||
self.handler_state
|
||||
.shared
|
||||
.metrics
|
||||
.network
|
||||
.new_lp_connection();
|
||||
|
||||
// Spawn handler task
|
||||
let handler = LpConnectionHandler::new(stream, remote_addr, self.handler_state.clone());
|
||||
let mut handler =
|
||||
LpClientConnectionHandler::new(stream, remote_addr, self.clients_handler_state.clone());
|
||||
|
||||
let metrics = self.handler_state.shared.metrics.clone();
|
||||
self.shutdown.try_spawn_named_with_shutdown(
|
||||
async move {
|
||||
// Increment connection counter
|
||||
handler.metrics().network.new_ingress_lp_client_connection();
|
||||
|
||||
let result = handler.handle().await;
|
||||
// Decrement connection counter
|
||||
handler
|
||||
.metrics()
|
||||
.network
|
||||
.closed_ingress_lp_client_connection();
|
||||
|
||||
// Handler emits lifecycle metrics internally on success
|
||||
// For errors, we need to emit them here since handler is consumed
|
||||
if let Err(e) = result {
|
||||
warn!("LP handler error for {remote_addr}: {e}");
|
||||
warn!("LP client handler error for {remote_addr}: {e}");
|
||||
// Note: metrics are emitted in handle() for graceful path
|
||||
// On error path, handle() returns early without emitting
|
||||
// So we track errors here
|
||||
}
|
||||
|
||||
// Decrement connection counter on exit
|
||||
metrics.network.lp_connection_closed();
|
||||
},
|
||||
&format!("LP::{remote_addr}"),
|
||||
&format!("LP_CLIENT::{remote_addr}"),
|
||||
);
|
||||
}
|
||||
|
||||
fn active_lp_connections(&self) -> usize {
|
||||
self.handler_state
|
||||
fn handle_connection(&self, stream: tokio::net::TcpStream, remote_addr: SocketAddr) {
|
||||
if let Some(initiator_details) = self
|
||||
.nodes_handler_state
|
||||
.nodes
|
||||
.get_node_details(remote_addr.ip())
|
||||
{
|
||||
self.handle_node_connection(stream, remote_addr, initiator_details);
|
||||
} else {
|
||||
self.handle_client_connection(stream, remote_addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn active_client_connections(&self) -> usize {
|
||||
self.clients_handler_state
|
||||
.shared
|
||||
.metrics
|
||||
.network
|
||||
.active_lp_connections_count()
|
||||
.active_lp_client_connections_count()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,119 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod handler;
|
||||
use nym_metrics::{add_histogram_obs, inc, inc_by};
|
||||
|
||||
pub mod client_handler;
|
||||
pub(crate) mod listener;
|
||||
pub mod node_handler;
|
||||
|
||||
// Histogram buckets for LP operation duration (legacy - used by unused forwarding methods)
|
||||
const LP_DURATION_BUCKETS: &[f64] = &[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];
|
||||
|
||||
// Histogram buckets for LP connection lifecycle duration
|
||||
// LP connections can be very short (registration only: ~1s) or very long (dVPN sessions: hours/days)
|
||||
// Covers full range from seconds to 24 hours
|
||||
const LP_CONNECTION_DURATION_BUCKETS: &[f64] = &[
|
||||
1.0, // 1 second
|
||||
5.0, // 5 seconds
|
||||
10.0, // 10 seconds
|
||||
30.0, // 30 seconds
|
||||
60.0, // 1 minute
|
||||
300.0, // 5 minutes
|
||||
600.0, // 10 minutes
|
||||
1800.0, // 30 minutes
|
||||
3600.0, // 1 hour
|
||||
7200.0, // 2 hours
|
||||
14400.0, // 4 hours
|
||||
28800.0, // 8 hours
|
||||
43200.0, // 12 hours
|
||||
86400.0, // 24 hours
|
||||
];
|
||||
|
||||
/// Connection lifecycle statistics tracking
|
||||
struct LpConnectionStats {
|
||||
/// When the connection started
|
||||
start_time: std::time::Instant,
|
||||
/// Total bytes received (including protocol framing)
|
||||
bytes_received: u64,
|
||||
/// Total bytes sent (including protocol framing)
|
||||
bytes_sent: u64,
|
||||
}
|
||||
|
||||
impl LpConnectionStats {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
start_time: std::time::Instant::now(),
|
||||
bytes_received: 0,
|
||||
bytes_sent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn duration(&self) -> std::time::Duration {
|
||||
self.start_time.elapsed()
|
||||
}
|
||||
|
||||
fn record_bytes_received(&mut self, bytes: usize) {
|
||||
self.bytes_received += bytes as u64;
|
||||
}
|
||||
|
||||
fn record_bytes_sent(&mut self, bytes: usize) {
|
||||
self.bytes_sent += bytes as u64;
|
||||
}
|
||||
|
||||
/// Emit connection lifecycle metrics for a client connection
|
||||
fn emit_lifecycle_client_metrics(&self, graceful: bool) {
|
||||
// Track connection duration
|
||||
let duration = self.duration().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"lp_client_connection_duration_seconds",
|
||||
duration,
|
||||
LP_CONNECTION_DURATION_BUCKETS
|
||||
);
|
||||
|
||||
// Track bytes transferred
|
||||
inc_by!(
|
||||
"lp_client_connection_bytes_received_total",
|
||||
self.bytes_received as i64
|
||||
);
|
||||
inc_by!(
|
||||
"lp_client_connection_bytes_sent_total",
|
||||
self.bytes_sent as i64
|
||||
);
|
||||
|
||||
// Track completion type
|
||||
if graceful {
|
||||
inc!("lp_client_connections_completed_gracefully");
|
||||
} else {
|
||||
inc!("lp_client_connections_completed_with_error");
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit connection lifecycle metrics for a node connection
|
||||
fn emit_lifecycle_node_metrics(&self, graceful: bool) {
|
||||
// Track connection duration
|
||||
let duration = self.duration().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"lp_node_connection_duration_seconds",
|
||||
duration,
|
||||
LP_CONNECTION_DURATION_BUCKETS
|
||||
);
|
||||
|
||||
// Track bytes transferred
|
||||
inc_by!(
|
||||
"lp_node_connection_bytes_received_total",
|
||||
self.bytes_received as i64
|
||||
);
|
||||
inc_by!(
|
||||
"lp_node_connection_bytes_sent_total",
|
||||
self.bytes_sent as i64
|
||||
);
|
||||
|
||||
// Track completion type
|
||||
if graceful {
|
||||
inc!("lp_node_connections_completed_gracefully");
|
||||
} else {
|
||||
inc!("lp_node_connections_completed_with_error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::lp::control::LpConnectionStats;
|
||||
use crate::node::lp::directory::LpNodeDetails;
|
||||
use crate::node::lp::error::LpHandlerError;
|
||||
use crate::node::lp::state::SharedLpNodeControlState;
|
||||
use nym_lp::LpTransportSession;
|
||||
use nym_lp::transport::{LpHandshakeChannel, LpTransportChannel};
|
||||
use nym_metrics::inc;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::debug;
|
||||
|
||||
/// Initial connection handler for an LP node before completing the KKT/PSQ handshake.
|
||||
pub struct InitialLpNodeConnectionHandler<S = TcpStream> {
|
||||
stream: S,
|
||||
remote_addr: SocketAddr,
|
||||
initiator_details: LpNodeDetails,
|
||||
|
||||
state: SharedLpNodeControlState,
|
||||
stats: LpConnectionStats,
|
||||
}
|
||||
|
||||
impl<S> InitialLpNodeConnectionHandler<S>
|
||||
where
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
pub fn new(
|
||||
stream: S,
|
||||
remote_addr: SocketAddr,
|
||||
initiator_details: LpNodeDetails,
|
||||
state: SharedLpNodeControlState,
|
||||
) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
remote_addr,
|
||||
initiator_details,
|
||||
state,
|
||||
stats: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn metrics(&self) -> &NymNodeMetrics {
|
||||
&self.state.shared.metrics
|
||||
}
|
||||
|
||||
pub async fn handle(mut self) -> Result<(), LpHandlerError> {
|
||||
// Track total LP connections handled
|
||||
inc!("lp_node_connections_total");
|
||||
let remote = self.remote_addr;
|
||||
|
||||
if self.initiator_details.kem_key_hashes.is_empty() {
|
||||
return Err(LpHandlerError::MissingNodeKEMKeyHashes {
|
||||
node_ip: self.remote_addr.ip(),
|
||||
node_id: self.initiator_details.node_id,
|
||||
});
|
||||
}
|
||||
|
||||
// 1. complete KKT/PSQ handshake before doing anything else.
|
||||
// bail if it takes too long
|
||||
let timeout = self.state.shared.lp_config.debug.handshake_ttl;
|
||||
let local_peer = self.state.local_lp_peer.clone();
|
||||
let stream = &mut self.stream;
|
||||
|
||||
let session = match tokio::time::timeout(timeout, async move {
|
||||
LpTransportSession::psq_handshake_responder_mutual(
|
||||
stream,
|
||||
local_peer,
|
||||
self.initiator_details.kem_key_hashes,
|
||||
)
|
||||
.complete_handshake()
|
||||
.await
|
||||
})
|
||||
.await
|
||||
{
|
||||
Err(_timeout) => {
|
||||
debug!("timed out attempting to complete mutual KTT/PSQ handshake with {remote}");
|
||||
self.stats.emit_lifecycle_node_metrics(false);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Err(handshake_failure)) => {
|
||||
debug!(
|
||||
"failed to complete mutual KKT/PSQ handshake with {remote}: {handshake_failure}"
|
||||
);
|
||||
self.stats.emit_lifecycle_node_metrics(false);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Ok(session)) => session,
|
||||
};
|
||||
|
||||
LpNodeConnectionHandler {
|
||||
stream,
|
||||
remote_addr: remote,
|
||||
state: self.state,
|
||||
stats: self.stats,
|
||||
transport_session: session,
|
||||
}
|
||||
.handle()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection handler for an LP node after completing the KKT/PSQ handshake.
|
||||
pub struct LpNodeConnectionHandler<S = TcpStream> {
|
||||
stream: S,
|
||||
remote_addr: SocketAddr,
|
||||
|
||||
state: SharedLpNodeControlState,
|
||||
stats: LpConnectionStats,
|
||||
transport_session: LpTransportSession,
|
||||
}
|
||||
|
||||
impl<S> LpNodeConnectionHandler<S>
|
||||
where
|
||||
S: LpTransportChannel + Unpin,
|
||||
{
|
||||
async fn handle(mut self) -> Result<(), LpHandlerError> {
|
||||
todo!();
|
||||
|
||||
self.stats.emit_lifecycle_node_metrics(true);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,44 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use nym_lp::peer::DHPublicKey;
|
||||
use nym_lp::{KEM, KEMKeyDigests};
|
||||
use nym_topology::NodeId;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::net::IpAddr;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Wrapper around all known LP nodes
|
||||
pub(crate) struct LpNodes {
|
||||
#[derive(Clone)]
|
||||
pub struct LpNodes {
|
||||
// map between all available ip addresses of other nodes and their details
|
||||
nodes: ArcSwap<HashMap<IpAddr, LpNodeDetails>>,
|
||||
nodes: Arc<ArcSwap<HashMap<IpAddr, LpNodeDetails>>>,
|
||||
}
|
||||
|
||||
struct LpNodeDetails {
|
||||
impl LpNodes {
|
||||
pub(crate) fn is_from_known_node(&self, node_ip: IpAddr) -> bool {
|
||||
self.nodes.load().contains_key(&node_ip)
|
||||
}
|
||||
|
||||
pub(crate) fn get_node_details(&self, node_ip: IpAddr) -> Option<LpNodeDetails> {
|
||||
self.nodes.load().get(&node_ip).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct LpNodeDetails {
|
||||
inner: Arc<LpNodeDetailsInner>,
|
||||
}
|
||||
|
||||
struct LpNodeDetailsInner {
|
||||
kem_key_hashes: BTreeMap<KEM, KEMKeyDigests>,
|
||||
x25519: DHPublicKey,
|
||||
impl Deref for LpNodeDetails {
|
||||
type Target = LpNodeDetailsInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner.deref()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LpNodeDetailsInner {
|
||||
pub(crate) node_id: NodeId,
|
||||
pub(crate) kem_key_hashes: BTreeMap<KEM, KEMKeyDigests>,
|
||||
pub(crate) x25519: DHPublicKey,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ use nym_lp::peer_config::LpReceiverIndex;
|
||||
use nym_lp::session::LpAction;
|
||||
use nym_lp::transport::LpTransportError;
|
||||
use nym_lp::{LpError, packet::MalformedLpPacketError};
|
||||
use std::net::SocketAddr;
|
||||
use nym_topology::NodeId;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -47,6 +48,9 @@ pub enum LpHandlerError {
|
||||
#[error("timed out while attempting to send to/receive from the connection")]
|
||||
ConnectionTimeout,
|
||||
|
||||
#[error("missing KEM key hashes for node {node_id} connected from {node_ip}")]
|
||||
MissingNodeKEMKeyHashes { node_ip: IpAddr, node_id: NodeId },
|
||||
|
||||
#[error("data channel is not yet implemented")]
|
||||
UnimplementedDataChannel,
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
+40
-37
@@ -9,7 +9,7 @@
|
||||
// ## Connection Metrics (via NetworkStats in nym-node-metrics)
|
||||
// - active_lp_connections: Gauge tracking current active LP connections (incremented on accept, decremented on close)
|
||||
//
|
||||
// ## Handler Metrics (in handler.rs)
|
||||
// ## Handler Metrics (in client_handler)
|
||||
// - lp_connections_total: Counter for total LP connections handled
|
||||
// - lp_client_hello_failed: Counter for ClientHello failures (timestamp validation, protocol errors)
|
||||
// - lp_handshakes_success: Counter for successful handshake completions
|
||||
@@ -46,7 +46,7 @@
|
||||
// ## Error Categorization Metrics
|
||||
// - lp_errors_wg_peer_registration: Counter for WireGuard peer registration failures
|
||||
//
|
||||
// ## Connection Lifecycle Metrics (in handler.rs)
|
||||
// ## Connection Lifecycle Metrics (in client_handler)
|
||||
// - lp_connection_duration_seconds: Histogram of connection duration from start to end (buckets: 1s to 24h)
|
||||
// - lp_connection_bytes_received_total: Counter for total bytes received including protocol framing
|
||||
// - lp_connection_bytes_sent_total: Counter for total bytes sent including protocol framing
|
||||
@@ -58,7 +58,7 @@
|
||||
// - lp_states_cleanup_session_removed: Counter for stale sessions removed by cleanup task
|
||||
// - lp_states_cleanup_demoted_removed: Counter for demoted (read-only) sessions removed by cleanup task
|
||||
//
|
||||
// ## Subsession/Rekeying Metrics (in handler.rs)
|
||||
// ## Subsession/Rekeying Metrics (in client_handler)
|
||||
// - lp_subsession_kk2_sent: Counter for SubsessionKK2 responses sent (indicates client initiated rekeying)
|
||||
// - lp_subsession_complete: Counter for successful subsession promotions
|
||||
// - lp_subsession_receiver_index_collision: Counter for subsession receiver_index collisions
|
||||
@@ -83,11 +83,12 @@ use tokio::sync::Semaphore;
|
||||
use tracing::error;
|
||||
|
||||
pub use nym_mixnet_client::forwarder::{MixForwardingReceiver, mix_forwarding_channels};
|
||||
pub use state::{SharedLpControlState, SharedLpDataState, SharedLpState};
|
||||
pub use state::{SharedLpClientControlState, SharedLpDataState, SharedLpState};
|
||||
|
||||
mod cleanup;
|
||||
pub mod control;
|
||||
mod data;
|
||||
pub(crate) mod directory;
|
||||
pub mod error;
|
||||
mod registration;
|
||||
pub mod state;
|
||||
@@ -119,40 +120,42 @@ impl LpSetup {
|
||||
session_states: session_states.clone(),
|
||||
};
|
||||
|
||||
let control_state = SharedLpControlState {
|
||||
local_lp_peer,
|
||||
peer_registrator,
|
||||
forward_semaphore: Arc::new(Semaphore::new(lp_config.debug.max_concurrent_forwards)),
|
||||
shared: shared_lp_state.clone(),
|
||||
};
|
||||
todo!()
|
||||
|
||||
let data_state = SharedLpDataState {
|
||||
outbound_mix_sender: mix_packet_sender,
|
||||
shared: shared_lp_state,
|
||||
};
|
||||
|
||||
let control_listener = LpControlListener::new(
|
||||
lp_config.control_bind_address,
|
||||
control_state,
|
||||
shutdown.clone(),
|
||||
);
|
||||
let data_listener = LpDataListener::new(
|
||||
lp_config.data_bind_address,
|
||||
data_state,
|
||||
shutdown.clone_shutdown_token(),
|
||||
);
|
||||
let cleanup_task = CleanupTask::new(
|
||||
session_states,
|
||||
lp_config.debug,
|
||||
shutdown.clone_shutdown_token(),
|
||||
);
|
||||
|
||||
Ok(LpSetup {
|
||||
control_listener,
|
||||
data_listener,
|
||||
cleanup_task,
|
||||
shutdown,
|
||||
})
|
||||
// let control_state = SharedLpControlState {
|
||||
// local_lp_peer,
|
||||
// peer_registrator,
|
||||
// forward_semaphore: Arc::new(Semaphore::new(lp_config.debug.max_concurrent_forwards)),
|
||||
// shared: shared_lp_state.clone(),
|
||||
// };
|
||||
//
|
||||
// let data_state = SharedLpDataState {
|
||||
// outbound_mix_sender: mix_packet_sender,
|
||||
// shared: shared_lp_state,
|
||||
// };
|
||||
//
|
||||
// let control_listener = LpControlListener::new(
|
||||
// lp_config.control_bind_address,
|
||||
// control_state,
|
||||
// shutdown.clone(),
|
||||
// );
|
||||
// let data_listener = LpDataListener::new(
|
||||
// lp_config.data_bind_address,
|
||||
// data_state,
|
||||
// shutdown.clone_shutdown_token(),
|
||||
// );
|
||||
// let cleanup_task = CleanupTask::new(
|
||||
// session_states,
|
||||
// lp_config.debug,
|
||||
// shutdown.clone_shutdown_token(),
|
||||
// );
|
||||
//
|
||||
// Ok(LpSetup {
|
||||
// control_listener,
|
||||
// data_listener,
|
||||
// cleanup_task,
|
||||
// shutdown,
|
||||
// })
|
||||
}
|
||||
|
||||
pub fn start_tasks(mut self) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::lp::state::SharedLpControlState;
|
||||
use crate::node::lp::state::SharedLpClientControlState;
|
||||
use nym_lp::peer_config::LpReceiverIndex;
|
||||
use nym_metrics::{add_histogram_obs, inc};
|
||||
use nym_registration_common::dvpn::{
|
||||
@@ -29,7 +29,7 @@ const LP_REGISTRATION_DURATION_BUCKETS: &[f64] = &[
|
||||
30.0, // 30s
|
||||
];
|
||||
|
||||
impl SharedLpControlState {
|
||||
impl SharedLpClientControlState {
|
||||
async fn process_dvpn_initial_registration(
|
||||
&self,
|
||||
sender: LpReceiverIndex,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use crate::config::LpConfig;
|
||||
use crate::node::lp::cleanup::TimestampedState;
|
||||
use crate::node::lp::directory::LpNodes;
|
||||
use dashmap::DashMap;
|
||||
use nym_gateway::node::wireguard::PeerRegistrator;
|
||||
use nym_lp::LpTransportSession;
|
||||
@@ -13,9 +14,9 @@ use nym_node_metrics::NymNodeMetrics;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
/// Shared state for LP control connections
|
||||
/// Shared state for LP clients control connections
|
||||
#[derive(Clone)]
|
||||
pub struct SharedLpControlState {
|
||||
pub struct SharedLpClientControlState {
|
||||
/// Encapsulates all required key information of a local Lewes Protocol Peer.
|
||||
pub local_lp_peer: LpLocalPeer,
|
||||
|
||||
@@ -28,12 +29,26 @@ pub struct SharedLpControlState {
|
||||
/// telescope setup. When at capacity, forward requests return an error
|
||||
/// so clients can choose a different gateway.
|
||||
// this is temporary until there is persistent KKT/PSQ session between nodes
|
||||
#[deprecated]
|
||||
pub forward_semaphore: Arc<Semaphore>,
|
||||
|
||||
/// Common shared data
|
||||
pub shared: SharedLpState,
|
||||
}
|
||||
|
||||
/// Shared state for LP nodes control connections
|
||||
#[derive(Clone)]
|
||||
pub struct SharedLpNodeControlState {
|
||||
/// Encapsulates all required key information of a local Lewes Protocol Peer.
|
||||
pub local_lp_peer: LpLocalPeer,
|
||||
|
||||
/// Information about all known LP nodes
|
||||
pub nodes: LpNodes,
|
||||
|
||||
/// Common shared data
|
||||
pub shared: SharedLpState,
|
||||
}
|
||||
|
||||
/// Shared state for LP data connections
|
||||
#[derive(Clone)]
|
||||
pub struct SharedLpDataState {
|
||||
|
||||
Reference in New Issue
Block a user