8a00ed6071
* Add KKT cryptographic primitives Post-quantum Key Encapsulation Mechanism (KEM) Key Transfer protocol. Enables efficient distribution of post-quantum KEM public keys. Squashed from georgio/noise-psq branch. * Implement LP registration protocol with KKT/PSQ integration Initial implementation of the Lewes Protocol (LP) for gateway registration: - Add nym-lp crate with Noise protocol handshake - Add LP listener to gateway for handling registrations - Add LP client for registration flow - Integrate KKT for post-quantum KEM key exchange - Integrate PSQ for post-quantum PSK derivation - Add Ed25519 authentication throughout - Add docker/localnet support for testing Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com> * Add LP telescoping with nested sessions and subsession support Extends LP protocol with telescoping architecture for nested sessions: - Add nested session support with KKpsk0 rekeying - Add subsession support with collision detection - Implement unified packet format with outer header - Refactor gateway handlers for single-packet forwarding - Add TTL-based state cleanup for stale sessions - Add outer AEAD encryption layer - Refactor registration client for packet-per-connection model * Add gateway-probe localnet mode with WireGuard tunnel support Adds localnet testing mode to gateway-probe for LP development: - Add TestMode enum for different probe configurations - Add --gateway-ip flag for direct gateway testing - Implement two-hop WireGuard tunnel for localnet - Add mock ecash support for testing without real credentials - Add netstack Go bindings for userspace networking - Restructure probe with mode and common modules - Update README with localnet mode documentation * Increase KCP fragment limit from u8 to u16 - Change frg field from u8 to u16 in packet header (25 bytes total) - Update encode/decode to use get_u16_le/put_u16_le - Update Segment struct frg field to u16 - Remove truncating cast in session.rs - Max message size now ~91MB (65,535 fragments × MTU) - Internal protocol only, no interop concerns Nym uses KCP for reliability and multiplexing, not standard real-time use cases. The u8 limit (255 fragments, ~355KB) was insufficient. Addresses: nym-yih9 * Zeroize Ed25519 key material in to_x25519 conversion Wrap hash and x25519_bytes in zeroize::Zeroizing to ensure private key material is cleared from memory after use. Closes: nym-k55g * Return Result from KCP session input() for error detection Change KcpSession::input() to return Result<(), KcpError> so callers can detect invalid packets instead of silently ignoring them. - Add ConvMismatch error variant for conversation ID mismatches - Update driver to propagate errors from session.input() - Update all test and example callers Closes: nym-n0kk * Fix Zeroizing deref in ed25519 to_x25519 conversion The from_bytes() function expects &[u8], need to deref the Zeroizing wrapper to get the inner array. * Add semaphore-based connection limiting for LP packet forwarding Limits concurrent outbound connections when forwarding LP packets to prevent file descriptor exhaustion under high load. Key changes: - Add max_concurrent_forwards config (default 1000) - Add forward_semaphore to LpHandlerState - Acquire semaphore permit before connecting in handle_forward_packet - Return "Gateway at forward capacity" error when at limit This provides load signaling so clients can choose another gateway when the current one is overloaded. Design note: Connection pooling was considered but provides minimal benefit since telescope setup is one-time and targets are distributed across many different gateways. See AIDEV-NOTE in LpHandlerState for full analysis. Closes: nym-xi3m * Return error on session unavailable in handle_subsession_packet Replace .session().ok() with proper error handling to fail fast when session is Closed or Processing after state machine processing. Previously, the code silently continued with outer_key = None, which could cause protocol errors downstream. Closes: nym-8de0 * Use explicit bincode Options helper in nested_session Add bincode_options() helper that returns DefaultOptions with explicit big_endian and varint_encoding configuration. This future-proofs against bincode 1.x/2.x default changes and makes serialization format explicit. Updated all 4 bincode usages in nested_session.rs to use the helper. * Deduplicate outer_key lookup pattern in nested_session.rs Extract common state_machine.session().ok().and_then(...) pattern into two helper methods: - get_send_key() for encryption (outer_aead_key_for_sending) - get_recv_key() for decryption (outer_aead_key) Updated 6 call sites to use the helpers, reducing verbosity. * Add LpConfig struct and AIDEV-NOTE documentation for KKT+PSQ - Create config.rs with LpConfig struct (kem_algorithm, psk_ttl, enable_kkt) - Export LpConfig from lib.rs - Add AIDEV-NOTE to psk.rs explaining: - Why PSQ is embedded in Noise (single round-trip, PSK binding) - KEM migration path (X25519 → MlKem768 → XWing) - Add AIDEV-NOTE to state_machine.rs explaining protocol flow: - KKTExchange → Handshaking → Transport state transitions - PSK derivation formula (ECDH || PSQ || salt) * Add forward_timeout to LP client config Add forward_timeout (30s default) to LpConfig and wrap send_forward_packet's connect_send_receive call with tokio::time::timeout, matching the pattern used by register() with registration_timeout. This prevents indefinite hangs when forwarding packets through entry gateway. * Add negotiated_version field to LpSession Add AtomicU8 field to store the protocol version from handshake packet headers. Includes getter and setter methods for future version negotiation and compatibility checks. - negotiated_version() returns current version (defaults to 1) - set_negotiated_version() allows setting during handshake - Subsessions inherit version 1 (can be enhanced to inherit parent's) * Change MessageType from u16 to u32 Breaking wire protocol change: MessageType field increased from 2 bytes to 4 bytes in LP packets. This future-proofs the message type space and aligns with other u32 fields. Changes: - message.rs: #[repr(u32)], from_u32(), to_u32() - error.rs: InvalidMessageType(u32) - codec.rs: All serialization/deserialization updated to 4-byte msg_type - Cleartext parsing: inner_bytes[4..8], content at [8..] - AEAD parsing: decrypted[4..8], content at [8..] - Serialization: 4 bytes for message type * Various smaller fixes * Refactor LP to stream-oriented TCP processing Gateway (handler.rs): - Add bound_receiver_idx field for session-affine connections - Convert handle() from single-packet to loop with EOF detection - Add validate_or_set_binding() for receiver_idx validation - Set binding in handle_client_hello after collision check - Centralize emit_lifecycle_metrics in main loop only - Add is_connection_closed() helper for graceful EOF Client (client.rs): - Add stream field for persistent TCP connection - Add ensure_connected(), send_packet(), receive_packet(), close() methods - Modify perform_handshake_inner() to use persistent stream - Modify register_with_credential() to use persistent stream - Modify send_forward_packet() to use persistent stream - Keep connect_send_receive() for reference (marked dead_code) This reduces handshake overhead from ~5 TCP connections to 1. Drive-by: Fix log::info! -> info! in wireguard peer_controller.rs * Add persistent exit stream for entry→exit forwarding Entry gateway now maintains a persistent TCP connection to the exit gateway per client session, reusing it for all forward requests from that client. This reduces TCP handshake overhead significantly. Key changes: - Add exit_stream: Option<(TcpStream, SocketAddr)> to LpConnectionHandler - Modify handle_forward_packet() to open on first forward, reuse after - Clear exit_stream on connection errors (auto-reconnect on next forward) - Semaphore only acquired for connection opens, not reuse (sequential access) * Fix code review issues for stream-oriented LP - Add 30s timeout to exit stream I/O operations (nym-df31) Prevents handler from hanging on unresponsive exit gateway - Return error on forward target address mismatch (nym-zegu) Previously warned and proceeded, which could mask bugs - Close client stream on handshake error paths (nym-scvm) Prevents state machine inconsistency on timeout or failure * Add LP registration idempotency and retry logic Make LP registration resilient to network failures that could waste credentials. When registration succeeds on the gateway but the response is lost (e.g., network drop), clients can retry with the same WG key and get the cached result instead of spending another credential. Gateway-side: - Add check_existing_registration() helper that looks up WG peer and returns cached GatewayData if already registered - Add idempotency check in process_registration() dVPN branch - Only return cached response if bandwidth > 0 (ensures registration was actually completed, not just peer created) - Track idempotent registrations with lp_registration_dvpn_idempotent metric Client-side: - Add register_with_retry() to LpRegistrationClient that acquires credential once and retries handshake+registration on failure - Add handshake_and_register_with_retry() to NestedLpSession for exit gateway registration via forwarding - Add exponential backoff with jitter between retry attempts - Verify outer session validity before nested session retry Both retry methods clear state machine before retry to ensure fresh handshake, and reuse the same credential across all attempts. * Add no-mix-acks feature flag to nym-sphinx-framing When enabled, mix nodes skip ack extraction and forwarding entirely. The full payload (including ack portion) is returned as the message. Closes: nym-3wrr * Create nym-lp-speedtest crate scaffold - Created tools/nym-lp-speedtest/ with Cargo.toml - Added main.rs with CLI argument parsing - Created stub modules: client.rs, speedtest.rs, topology.rs - Added to workspace members - Verified compilation with cargo check * Implement topology fetching for nym-lp-speedtest - Add topology.rs with NymTopology integration - Fetch mix nodes and gateways from nym-api - Build GatewayInfo with LP addresses (port 41264) - Provide random_route_to_gateway() for Sphinx routing - Add required Cargo.toml dependencies * Implement LP+Sphinx+KCP client with SURB support - Add send_data() and send_data_with_surbs() methods for mixnet data - Integrate KCP reliable delivery with Sphinx packet construction - Add x25519 encryption keypair for SURB reply mechanism - Wire up main.rs to test LP handshake and data path - Add NymRouteProvider support in topology for SURB construction - Refactor send_data() to delegate to send_data_with_surbs(0) (DRY) The client can now: - Perform LP handshake with gateways - Send data through the mixnet wrapped in KCP + Sphinx packets - Attach SURBs for bidirectional communication - Return encryption keys for decrypting replies * Rename nym-lp-speedtest to nym-lp-client and fix KCP bug - Rename crate from nym-lp-speedtest to nym-lp-client - Fix KCP bug: add driver.update() call before fetch_outgoing() Without update(), KCP never moves segments from snd_queue to snd_buf - Update CLI name, about string, and user agent to match new name * Add LP mixnet mode registration with nym address return - Extend RegistrationMode::Mixnet to include client_ed25519_pubkey and client_x25519_pubkey for nym address construction - Add LpGatewayData struct containing gateway_identity and gateway_sphinx_key for SURB reply routing - Add lp_gateway_data field to LpRegistrationResponse for mixnet mode - Implement success_mixnet() constructor for mixnet registrations - Update gateway registration to insert clients into ActiveClientsStore for SURB reply delivery, matching the websocket flow * Implement LP data handler on UDP:51264 - Add LpDataHandler for UDP data plane (port 51264) - Decrypt LP layer and forward Sphinx packets to mixnet - Add outbound_mix_sender to LpHandlerState - Integrate data handler spawn into LpListener::run() - Add metrics for data packets received/forwarded/errors Implements nym-yzzm * Fix replay protection vulnerability in LP data handler Use state machine process_input() instead of manual decryption to ensure proper replay protection: - Counter check against receiving window - Counter marking after successful decryption Also handle subsession actions gracefully (SendPacket ignored on UDP, clients should use TCP control plane for rekeying). Security fix for nym-yzzm implementation. * feat(ipr): add KcpSessionManager for LP client KCP handling - Add fetch_incoming() and recv() methods to KcpDriver for retrieving reassembled messages - Create KcpSessionManager in ip-packet-router that manages KCP sessions keyed by conv_id (first 4 bytes of KCP packet header) - Store ReplySurbs per session for sending anonymous replies - Implement session timeout (5 min) and max sessions limit (10000) - Add comprehensive tests for session lifecycle and KCP roundtrip * feat(ipr): integrate KcpSessionManager into MixnetListener - Add KcpSessionManager field to MixnetListener struct - Add is_kcp_message() helper to detect KCP-wrapped payloads - Add on_kcp_message() to process LP client KCP messages - Refactor on_reconstructed_message() to route KCP vs regular IPR - Add KCP tick timer (100ms) for session updates and cleanup - Initialize KcpSessionManager in IpPacketRouter::run_service_provider() KCP messages are detected by checking byte 4 for valid KCP commands (81-84), which doesn't conflict with IPR protocol version bytes (6-8) at position 0. Closes: nym-96zl * fix(ipr): prevent KCP detection false positives on IPR messages Add secondary check in is_kcp_message() to exclude messages that match IPR protocol header pattern (version 6-8 at byte 0, ServiceProviderType 0-2 at byte 1). This prevents false positives where IPR messages with byte 4 in range 81-84 would be incorrectly routed to KCP processing. Added 4 unit tests to validate the detection logic. Closes: nym-6f3x * fix(ipr): wrap KCP client responses in KCP before SURB reply - Modify on_kcp_message to handle responses directly instead of returning them - Add handle_kcp_response method that wraps response in KCP and sends via mixnet - Ensures KCP clients receive KCP-wrapped responses for proper reassembly Closes: nym-7oh2 * fix(ipr): send KCP protocol packets in tick instead of just logging - Add get_sender_tag() and fetch_outgoing_for_conv() to KcpSessionManager - Change handle_kcp_tick() to actually send ACKs/retransmissions via mixnet - Reduce KCP tick interval from 100ms to 10ms for better responsiveness This fixes the KCP reliability protocol which was broken because protocol packets (ACKs, retransmissions) were generated but never sent. * feat(lp-client): wrap payload in IpPacketRequest before KCP - Add nym-ip-packet-requests and bytes dependencies - Wrap payload in IpPacketRequest::new_data_request() before sending to KCP - Add LP_DATA_PORT constant (51264) and lp_data_address field to GatewayInfo This ensures IPR can properly parse incoming messages as DataRequest. LP framing (wrapping Sphinx in LP before sending) is a separate task. * feat(lp-client): add LP session management and UDP data plane support - Add wrap_data() and session_id() to LpRegistrationClient for LP packet creation after handshake - Add init_lp_session() and close_lp_session() to SpeedtestClient for managing LP sessions - Extract prepare_sphinx_fragments() helper to reduce code duplication between send_data_with_surbs() and send_data_via_lp() - Add send_data_via_lp() for sending Sphinx packets through LP's UDP data plane (port 51264) The LP session is kept alive after TCP handshake closes, allowing subsequent wrap_data() calls for UDP transmission without re-handshaking. * random formatting * replaced all instances of bincode::serialize and bincode::deserialize with explicit lp_bincode_serialiser() within the LP * additional formatting * removed source of possible panic from nym-kkt invalid KEM mapping will now return an Err rather than panicking * integration test for LP entry registration This includes creation of mocks of various gateway-related components, such as the PeerController * changed ClientHelloData serialisation the old variant using bincode did not produce constant-length output in some cases * Fixed generation of receiver index removes the possible clash with the boostrap id * Integration test for nested LP registration - move `LpTransport` trait definition to shared `nym-lp-transport` crate - make transport layer within `LpConnectionHandler` generic with respect to the forwarding target. it must, however, use the same type as the incoming client connection - extracted explicit `LpConnectionHandler::establish_exit_stream` to more easily modify it in the future to fully protect the channel and disallow using untrusted egress points - fix additional log-string interpolation nits * resolved clippy issues pointed out by clippy 1.91 * added LP discovery into self-described endpoint: - removed changes to the node bonding within the contract - introduced '/api/v1/lewes-protocol' route within nym-node http api - added 'lewes_protocol' field to 'NymNodeData' inside of NymNodeDescription - refactored LpConfig to allow separate bind and announce addresses and used more strict typing * chore: allow unwrap/expect within kkt benchmarking code * chore: downgraded sha2 dep for cosmwasm compatibility * clippy * marking simd calls as unsafe * fixed calls to '_mm_testz_si128' * additional clippy fixes --------- Co-authored-by: Georgio Nicolas <me@georgio.xyz> Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
1309 lines
49 KiB
Rust
1309 lines
49 KiB
Rust
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
use self::helpers::load_x25519_wireguard_keypair;
|
|
use crate::config::helpers::gateway_tasks_config;
|
|
use crate::config::{
|
|
Config, DEFAULT_MIXNET_PORT, GatewayTasksConfig, NodeModes, ServiceProvidersConfig, Wireguard,
|
|
};
|
|
use crate::error::{EntryGatewayError, NymNodeError, ServiceProvidersError};
|
|
use crate::node::description::{load_node_description, save_node_description};
|
|
use crate::node::helpers::{
|
|
DisplayDetails, get_current_rotation_id, load_ed25519_identity_keypair, load_key,
|
|
load_x25519_noise_keypair, store_ed25519_identity_keypair, store_key, store_keypair,
|
|
store_x25519_noise_keypair,
|
|
};
|
|
use crate::node::http::api::api_requests;
|
|
use crate::node::http::helpers::system_info::get_system_info;
|
|
use crate::node::http::state::{AppState, StaticNodeInformation};
|
|
use crate::node::http::{HttpServerConfig, NymNodeHttpServer, NymNodeRouter};
|
|
use crate::node::key_rotation::active_keys::ActiveSphinxKeys;
|
|
use crate::node::key_rotation::controller::KeyRotationController;
|
|
use crate::node::key_rotation::manager::SphinxKeyManager;
|
|
use crate::node::metrics::aggregator::MetricsAggregator;
|
|
use crate::node::metrics::console_logger::ConsoleLogger;
|
|
use crate::node::metrics::handler::client_sessions::GatewaySessionStatsHandler;
|
|
use crate::node::metrics::handler::global_prometheus_updater::PrometheusGlobalNodeMetricsRegistryUpdater;
|
|
use crate::node::metrics::handler::legacy_packet_data::LegacyMixingStatsUpdater;
|
|
use crate::node::metrics::handler::mixnet_data_cleaner::MixnetMetricsCleaner;
|
|
use crate::node::metrics::handler::pending_egress_packets_updater::PendingEgressPacketsUpdater;
|
|
use crate::node::mixnet::SharedFinalHopData;
|
|
use crate::node::mixnet::packet_forwarding::PacketForwarder;
|
|
use crate::node::mixnet::shared::ProcessingConfig;
|
|
use crate::node::nym_apis_client::NymApisClient;
|
|
use crate::node::replay_protection::background_task::ReplayProtectionDiskFlush;
|
|
use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilters;
|
|
use crate::node::replay_protection::manager::ReplayProtectionBloomfiltersManager;
|
|
use crate::node::routing_filter::{OpenFilter, RoutingFilter};
|
|
use crate::node::shared_network::{
|
|
CachedNetwork, CachedTopologyProvider, LocalGatewayNode, NetworkRefresher,
|
|
};
|
|
use nym_bin_common::bin_info;
|
|
use nym_credential_verification::UpgradeModeState;
|
|
use nym_crypto::asymmetric::{ed25519, x25519};
|
|
use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender};
|
|
use nym_mixnet_client::client::ActiveConnections;
|
|
use nym_mixnet_client::forwarder::MixForwardingSender;
|
|
use nym_network_requester::{
|
|
CustomGatewayDetails, GatewayDetails, GatewayRegistration, set_active_gateway,
|
|
setup_fs_gateways_storage, store_gateway_details,
|
|
};
|
|
use nym_node_metrics::NymNodeMetrics;
|
|
use nym_node_metrics::events::MetricEventsSender;
|
|
use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription};
|
|
use nym_noise::config::{NoiseConfig, NoiseNetworkView};
|
|
use nym_noise_keys::VersionedNoiseKey;
|
|
use nym_sphinx_acknowledgements::AckKey;
|
|
use nym_sphinx_addressing::Recipient;
|
|
use nym_task::{ShutdownManager, ShutdownToken, ShutdownTracker};
|
|
use nym_validator_client::UserAgent;
|
|
use nym_verloc::measurements::SharedVerlocStats;
|
|
use nym_verloc::{self, measurements::VerlocMeasurer};
|
|
use nym_wireguard::{WireguardGatewayData, peer_controller::PeerControlRequest};
|
|
use rand::rngs::OsRng;
|
|
use rand::{CryptoRng, RngCore};
|
|
use std::net::SocketAddr;
|
|
use std::ops::Deref;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use tokio::sync::mpsc;
|
|
use tracing::{debug, info, trace};
|
|
use zeroize::Zeroizing;
|
|
|
|
pub mod bonding_information;
|
|
pub mod description;
|
|
pub mod helpers;
|
|
pub(crate) mod http;
|
|
pub(crate) mod key_rotation;
|
|
pub(crate) mod metrics;
|
|
pub(crate) mod mixnet;
|
|
mod nym_apis_client;
|
|
pub(crate) mod replay_protection;
|
|
mod routing_filter;
|
|
mod shared_network;
|
|
|
|
pub struct GatewayTasksData {
|
|
mnemonic: Arc<Zeroizing<bip39::Mnemonic>>,
|
|
client_storage: nym_gateway::node::GatewayStorage,
|
|
stats_storage: nym_gateway::node::PersistentStatsStorage,
|
|
}
|
|
|
|
impl GatewayTasksData {
|
|
pub fn initialise(
|
|
config: &GatewayTasksConfig,
|
|
custom_mnemonic: Option<Zeroizing<bip39::Mnemonic>>,
|
|
) -> Result<(), EntryGatewayError> {
|
|
// SAFETY:
|
|
// this unwrap is fine as 24 word count is a valid argument for generating entropy for a new bip39 mnemonic
|
|
#[allow(clippy::unwrap_used)]
|
|
let mnemonic = Arc::new(
|
|
custom_mnemonic
|
|
.unwrap_or_else(|| Zeroizing::new(bip39::Mnemonic::generate(24).unwrap())),
|
|
);
|
|
config.storage_paths.save_mnemonic_to_file(&mnemonic)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn new(config: &GatewayTasksConfig) -> Result<GatewayTasksData, EntryGatewayError> {
|
|
let client_storage = nym_gateway::node::GatewayStorage::init(
|
|
&config.storage_paths.clients_storage,
|
|
config.debug.message_retrieval_limit,
|
|
)
|
|
.await
|
|
.map_err(nym_gateway::GatewayError::from)?;
|
|
|
|
let stats_storage =
|
|
nym_gateway::node::PersistentStatsStorage::init(&config.storage_paths.stats_storage)
|
|
.await
|
|
.map_err(nym_gateway::GatewayError::from)?;
|
|
|
|
Ok(GatewayTasksData {
|
|
mnemonic: Arc::new(config.storage_paths.load_mnemonic_from_file()?),
|
|
client_storage,
|
|
stats_storage,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct ServiceProvidersData {
|
|
// ideally we'd be storing all the keys here, but unfortunately due to how the service providers
|
|
// are currently implemented, they will be loading the data themselves from the provided paths
|
|
|
|
// those public keys are just convenience wrappers for http builder and details displayer
|
|
nr_ed25519: ed25519::PublicKey,
|
|
nr_x25519: x25519::PublicKey,
|
|
|
|
ipr_ed25519: ed25519::PublicKey,
|
|
ipr_x25519: x25519::PublicKey,
|
|
|
|
// TODO: those should be moved to WG section
|
|
auth_ed25519: ed25519::PublicKey,
|
|
auth_x25519: x25519::PublicKey,
|
|
}
|
|
|
|
impl ServiceProvidersData {
|
|
fn initialise_client_keys<R: RngCore + CryptoRng>(
|
|
rng: &mut R,
|
|
typ: &str,
|
|
ed25519_paths: nym_pemstore::KeyPairPath,
|
|
x25519_paths: nym_pemstore::KeyPairPath,
|
|
ack_key_path: &Path,
|
|
) -> Result<(), ServiceProvidersError> {
|
|
let ed25519_keys = ed25519::KeyPair::new(rng);
|
|
let x25519_keys = x25519::KeyPair::new(rng);
|
|
let aes128ctr_key = AckKey::new(rng);
|
|
|
|
store_keypair(
|
|
&ed25519_keys,
|
|
&ed25519_paths,
|
|
format!("{typ}-ed25519-identity"),
|
|
)?;
|
|
store_keypair(&x25519_keys, &x25519_paths, format!("{typ}-x25519-dh"))?;
|
|
store_key(&aes128ctr_key, ack_key_path, format!("{typ}-ack-key"))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn initialise_client_gateway_storage(
|
|
storage_path: &Path,
|
|
registration: &GatewayRegistration,
|
|
) -> Result<(), ServiceProvidersError> {
|
|
// insert all required information into the gateways store
|
|
// (I hate that we have to do it, but that's currently the simplest thing to do)
|
|
let storage = setup_fs_gateways_storage(storage_path).await?;
|
|
store_gateway_details(&storage, registration).await?;
|
|
set_active_gateway(&storage, ®istration.gateway_id().to_base58_string()).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn initialise_network_requester<R: RngCore + CryptoRng>(
|
|
rng: &mut R,
|
|
config: &ServiceProvidersConfig,
|
|
registration: &GatewayRegistration,
|
|
) -> Result<(), ServiceProvidersError> {
|
|
trace!("initialising network requester keys");
|
|
Self::initialise_client_keys(
|
|
rng,
|
|
"network-requester",
|
|
config
|
|
.storage_paths
|
|
.network_requester
|
|
.ed25519_identity_storage_paths(),
|
|
config
|
|
.storage_paths
|
|
.network_requester
|
|
.x25519_diffie_hellman_storage_paths(),
|
|
&config.storage_paths.network_requester.ack_key_file,
|
|
)?;
|
|
Self::initialise_client_gateway_storage(
|
|
&config.storage_paths.network_requester.gateway_registrations,
|
|
registration,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn initialise_ip_packet_router_requester<R: RngCore + CryptoRng>(
|
|
rng: &mut R,
|
|
config: &ServiceProvidersConfig,
|
|
registration: &GatewayRegistration,
|
|
) -> Result<(), ServiceProvidersError> {
|
|
trace!("initialising ip packet router keys");
|
|
Self::initialise_client_keys(
|
|
rng,
|
|
"ip-packet-router",
|
|
config
|
|
.storage_paths
|
|
.ip_packet_router
|
|
.ed25519_identity_storage_paths(),
|
|
config
|
|
.storage_paths
|
|
.ip_packet_router
|
|
.x25519_diffie_hellman_storage_paths(),
|
|
&config.storage_paths.ip_packet_router.ack_key_file,
|
|
)?;
|
|
Self::initialise_client_gateway_storage(
|
|
&config.storage_paths.ip_packet_router.gateway_registrations,
|
|
registration,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn initialise_authenticator<R: RngCore + CryptoRng>(
|
|
rng: &mut R,
|
|
config: &ServiceProvidersConfig,
|
|
registration: &GatewayRegistration,
|
|
) -> Result<(), ServiceProvidersError> {
|
|
trace!("initialising authenticator keys");
|
|
Self::initialise_client_keys(
|
|
rng,
|
|
"authenticator",
|
|
config
|
|
.storage_paths
|
|
.authenticator
|
|
.ed25519_identity_storage_paths(),
|
|
config
|
|
.storage_paths
|
|
.authenticator
|
|
.x25519_diffie_hellman_storage_paths(),
|
|
&config.storage_paths.authenticator.ack_key_file,
|
|
)?;
|
|
Self::initialise_client_gateway_storage(
|
|
&config.storage_paths.authenticator.gateway_registrations,
|
|
registration,
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn initialise(
|
|
config: &ServiceProvidersConfig,
|
|
public_key: ed25519::PublicKey,
|
|
) -> Result<(), ServiceProvidersError> {
|
|
// generate all the keys for NR, IPR and AUTH
|
|
let mut rng = OsRng;
|
|
|
|
let gateway_details = GatewayDetails::Custom(CustomGatewayDetails::new(public_key)).into();
|
|
|
|
// NR:
|
|
Self::initialise_network_requester(&mut rng, config, &gateway_details).await?;
|
|
|
|
// IPR:
|
|
Self::initialise_ip_packet_router_requester(&mut rng, config, &gateway_details).await?;
|
|
|
|
// Authenticator
|
|
Self::initialise_authenticator(&mut rng, config, &gateway_details).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn new(config: &ServiceProvidersConfig) -> Result<ServiceProvidersData, ServiceProvidersError> {
|
|
let nr_paths = &config.storage_paths.network_requester;
|
|
let nr_ed25519 = load_key(
|
|
&nr_paths.public_ed25519_identity_key_file,
|
|
"network requester ed25519",
|
|
)?;
|
|
|
|
let nr_x25519 = load_key(
|
|
&nr_paths.public_x25519_diffie_hellman_key_file,
|
|
"network requester x25519",
|
|
)?;
|
|
|
|
let ipr_paths = &config.storage_paths.ip_packet_router;
|
|
let ipr_ed25519 = load_key(
|
|
&ipr_paths.public_ed25519_identity_key_file,
|
|
"ip packet router ed25519",
|
|
)?;
|
|
|
|
let ipr_x25519 = load_key(
|
|
&ipr_paths.public_x25519_diffie_hellman_key_file,
|
|
"ip packet router x25519",
|
|
)?;
|
|
|
|
let auth_paths = &config.storage_paths.authenticator;
|
|
let auth_ed25519 = load_key(
|
|
&auth_paths.public_ed25519_identity_key_file,
|
|
"authenticator ed25519",
|
|
)?;
|
|
|
|
let auth_x25519 = load_key(
|
|
&auth_paths.public_x25519_diffie_hellman_key_file,
|
|
"authenticator x25519",
|
|
)?;
|
|
|
|
Ok(ServiceProvidersData {
|
|
nr_ed25519,
|
|
nr_x25519,
|
|
ipr_ed25519,
|
|
ipr_x25519,
|
|
auth_ed25519,
|
|
auth_x25519,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct WireguardData {
|
|
inner: WireguardGatewayData,
|
|
peer_rx: mpsc::Receiver<PeerControlRequest>,
|
|
use_userspace: bool,
|
|
}
|
|
|
|
impl WireguardData {
|
|
pub(crate) fn new(config: &Wireguard) -> Result<Self, NymNodeError> {
|
|
let (inner, peer_rx) = WireguardGatewayData::new(
|
|
config.clone().into(),
|
|
Arc::new(load_x25519_wireguard_keypair(
|
|
&config.storage_paths.x25519_wireguard_storage_paths(),
|
|
)?),
|
|
);
|
|
Ok(WireguardData {
|
|
inner,
|
|
peer_rx,
|
|
use_userspace: config.use_userspace,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn initialise(config: &Wireguard) -> Result<(), ServiceProvidersError> {
|
|
let mut rng = OsRng;
|
|
let x25519_keys = x25519::KeyPair::new(&mut rng);
|
|
|
|
store_keypair(
|
|
&x25519_keys,
|
|
&config.storage_paths.x25519_wireguard_storage_paths(),
|
|
"wg-x25519-dh",
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl From<WireguardData> for nym_wireguard::WireguardData {
|
|
fn from(value: WireguardData) -> Self {
|
|
nym_wireguard::WireguardData {
|
|
inner: value.inner,
|
|
peer_rx: value.peer_rx,
|
|
use_userspace: value.use_userspace,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct NymNode {
|
|
config: Config,
|
|
accepted_operator_terms_and_conditions: bool,
|
|
shutdown_manager: ShutdownManager,
|
|
|
|
description: NodeDescription,
|
|
|
|
metrics: NymNodeMetrics,
|
|
|
|
verloc_stats: SharedVerlocStats,
|
|
|
|
entry_gateway: GatewayTasksData,
|
|
|
|
upgrade_mode_state: UpgradeModeState,
|
|
|
|
#[allow(dead_code)]
|
|
service_providers: ServiceProvidersData,
|
|
|
|
wireguard: Option<WireguardData>,
|
|
|
|
ed25519_identity_keys: Arc<ed25519::KeyPair>,
|
|
sphinx_key_manager: Option<SphinxKeyManager>,
|
|
|
|
// to be used when noise is integrated
|
|
#[allow(dead_code)]
|
|
x25519_noise_keys: Arc<x25519::KeyPair>,
|
|
}
|
|
|
|
impl NymNode {
|
|
pub(crate) async fn initialise(
|
|
config: &Config,
|
|
custom_mnemonic: Option<Zeroizing<bip39::Mnemonic>>,
|
|
) -> Result<(), NymNodeError> {
|
|
debug!("initialising nym-node with id: {}", config.id);
|
|
let mut rng = OsRng;
|
|
|
|
// global initialisation
|
|
let ed25519_identity_keys = ed25519::KeyPair::new(&mut rng);
|
|
let x25519_noise_keys = x25519::KeyPair::new(&mut rng);
|
|
let current_rotation_id =
|
|
get_current_rotation_id(&config.mixnet.nym_api_urls, &config.mixnet.nyxd_urls).await?;
|
|
let _ = SphinxKeyManager::initialise_new(
|
|
&mut rng,
|
|
current_rotation_id,
|
|
&config.storage_paths.keys.primary_x25519_sphinx_key_file,
|
|
&config.storage_paths.keys.secondary_x25519_sphinx_key_file,
|
|
)?;
|
|
|
|
trace!("attempting to store ed25519 identity keypair");
|
|
store_ed25519_identity_keypair(
|
|
&ed25519_identity_keys,
|
|
&config.storage_paths.keys.ed25519_identity_storage_paths(),
|
|
)?;
|
|
|
|
trace!("attempting to store x25519 noise keypair");
|
|
store_x25519_noise_keypair(
|
|
&x25519_noise_keys,
|
|
&config.storage_paths.keys.x25519_noise_storage_paths(),
|
|
)?;
|
|
|
|
trace!("creating description file");
|
|
save_node_description(
|
|
&config.storage_paths.description,
|
|
&NodeDescription::default(),
|
|
)?;
|
|
|
|
// entry gateway initialisation
|
|
GatewayTasksData::initialise(&config.gateway_tasks, custom_mnemonic)?;
|
|
|
|
// service providers initialisation
|
|
ServiceProvidersData::initialise(
|
|
&config.service_providers,
|
|
*ed25519_identity_keys.public_key(),
|
|
)
|
|
.await?;
|
|
|
|
// wireguard initialisation
|
|
WireguardData::initialise(&config.wireguard)?;
|
|
|
|
config.save()
|
|
}
|
|
|
|
pub(crate) async fn new(config: Config) -> Result<Self, NymNodeError> {
|
|
let wireguard_data = WireguardData::new(&config.wireguard)?;
|
|
let current_rotation_id =
|
|
get_current_rotation_id(&config.mixnet.nym_api_urls, &config.mixnet.nyxd_urls).await?;
|
|
|
|
Ok(NymNode {
|
|
ed25519_identity_keys: Arc::new(load_ed25519_identity_keypair(
|
|
&config.storage_paths.keys.ed25519_identity_storage_paths(),
|
|
)?),
|
|
sphinx_key_manager: Some(SphinxKeyManager::try_load_or_regenerate(
|
|
current_rotation_id,
|
|
&config.storage_paths.keys.primary_x25519_sphinx_key_file,
|
|
&config.storage_paths.keys.secondary_x25519_sphinx_key_file,
|
|
)?),
|
|
x25519_noise_keys: Arc::new(load_x25519_noise_keypair(
|
|
&config.storage_paths.keys.x25519_noise_storage_paths(),
|
|
)?),
|
|
description: load_node_description(&config.storage_paths.description)?,
|
|
metrics: NymNodeMetrics::new(),
|
|
verloc_stats: Default::default(),
|
|
entry_gateway: GatewayTasksData::new(&config.gateway_tasks).await?,
|
|
upgrade_mode_state: UpgradeModeState::new(
|
|
config.gateway_tasks.upgrade_mode.attester_public_key,
|
|
),
|
|
service_providers: ServiceProvidersData::new(&config.service_providers)?,
|
|
wireguard: Some(wireguard_data),
|
|
config,
|
|
accepted_operator_terms_and_conditions: false,
|
|
shutdown_manager: ShutdownManager::build_new_default()
|
|
.map_err(|source| NymNodeError::ShutdownSignalFailure { source })?,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn shutdown_tracker(&self) -> &ShutdownTracker {
|
|
self.shutdown_manager.shutdown_tracker()
|
|
}
|
|
|
|
pub(crate) fn shutdown_token(&self) -> ShutdownToken {
|
|
self.shutdown_manager.clone_shutdown_token()
|
|
}
|
|
|
|
pub(crate) fn config(&self) -> &Config {
|
|
&self.config
|
|
}
|
|
|
|
pub(crate) fn with_accepted_operator_terms_and_conditions(
|
|
mut self,
|
|
accepted_operator_terms_and_conditions: bool,
|
|
) -> Self {
|
|
self.accepted_operator_terms_and_conditions = accepted_operator_terms_and_conditions;
|
|
self
|
|
}
|
|
|
|
fn exit_network_requester_address(&self) -> Recipient {
|
|
Recipient::new(
|
|
self.service_providers.nr_ed25519,
|
|
self.service_providers.nr_x25519,
|
|
*self.ed25519_identity_keys.public_key(),
|
|
)
|
|
}
|
|
|
|
fn exit_ip_packet_router_address(&self) -> Recipient {
|
|
Recipient::new(
|
|
self.service_providers.ipr_ed25519,
|
|
self.service_providers.ipr_x25519,
|
|
*self.ed25519_identity_keys.public_key(),
|
|
)
|
|
}
|
|
|
|
fn exit_authenticator_address(&self) -> Recipient {
|
|
Recipient::new(
|
|
self.service_providers.auth_ed25519,
|
|
self.service_providers.auth_x25519,
|
|
*self.ed25519_identity_keys.public_key(),
|
|
)
|
|
}
|
|
|
|
fn x25519_wireguard_key(&self) -> Result<x25519::PublicKey, NymNodeError> {
|
|
let wg_data = self
|
|
.wireguard
|
|
.as_ref()
|
|
.ok_or(NymNodeError::WireguardDataUnavailable)?;
|
|
|
|
Ok(*wg_data.inner.keypair().public_key())
|
|
}
|
|
|
|
pub(crate) fn display_details(&self) -> Result<DisplayDetails, NymNodeError> {
|
|
let sphinx_keys = self.sphinx_keys()?;
|
|
Ok(DisplayDetails {
|
|
current_modes: self.config.modes,
|
|
description: self.description.clone(),
|
|
ed25519_identity_key: self.ed25519_identity_key().to_base58_string(),
|
|
x25519_primary_sphinx_key: sphinx_keys.keys.primary().deref().into(),
|
|
x25519_secondary_sphinx_key: sphinx_keys.keys.secondary().map(|g| g.deref().into()),
|
|
x25519_noise_key: self.x25519_noise_key().to_base58_string(),
|
|
x25519_wireguard_key: self.x25519_wireguard_key()?.to_base58_string(),
|
|
exit_network_requester_address: self.exit_network_requester_address().to_string(),
|
|
exit_ip_packet_router_address: self.exit_ip_packet_router_address().to_string(),
|
|
exit_authenticator_address: self.exit_authenticator_address().to_string(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn modes(&self) -> NodeModes {
|
|
self.config.modes
|
|
}
|
|
|
|
pub(crate) fn ed25519_identity_key(&self) -> &ed25519::PublicKey {
|
|
self.ed25519_identity_keys.public_key()
|
|
}
|
|
|
|
pub(crate) fn x25519_noise_key(&self) -> &x25519::PublicKey {
|
|
self.x25519_noise_keys.public_key()
|
|
}
|
|
|
|
#[track_caller]
|
|
pub(crate) fn active_sphinx_keys(&self) -> Result<ActiveSphinxKeys, NymNodeError> {
|
|
Ok(self.sphinx_keys()?.keys.clone())
|
|
}
|
|
|
|
async fn build_network_refresher(&self) -> Result<NetworkRefresher, NymNodeError> {
|
|
NetworkRefresher::initialise_new(
|
|
self.config.debug.testnet,
|
|
Self::user_agent(),
|
|
self.config.mixnet.nym_api_urls.clone(),
|
|
self.config.debug.topology_cache_ttl,
|
|
self.config.debug.routing_nodes_check_interval,
|
|
self.shutdown_manager.clone_shutdown_token(),
|
|
)
|
|
.await
|
|
}
|
|
|
|
fn as_gateway_topology_node(&self) -> Result<LocalGatewayNode, NymNodeError> {
|
|
let ip_addresses = self.config.host.public_ips.clone();
|
|
|
|
let Some(ip) = ip_addresses.first() else {
|
|
return Err(NymNodeError::NoPublicIps);
|
|
};
|
|
|
|
let mix_port = self
|
|
.config
|
|
.mixnet
|
|
.announce_port
|
|
.unwrap_or(DEFAULT_MIXNET_PORT);
|
|
let mix_host = SocketAddr::new(*ip, mix_port);
|
|
|
|
let clients_ws_port = self
|
|
.config
|
|
.gateway_tasks
|
|
.announce_ws_port
|
|
.unwrap_or(self.config.gateway_tasks.ws_bind_address.port());
|
|
|
|
Ok(LocalGatewayNode {
|
|
active_sphinx_keys: self.active_sphinx_keys()?.clone(),
|
|
mix_host,
|
|
identity_key: *self.ed25519_identity_key(),
|
|
entry: nym_topology::EntryDetails {
|
|
ip_addresses,
|
|
clients_ws_port,
|
|
hostname: self.config.host.hostname.clone(),
|
|
clients_wss_port: self.config.gateway_tasks.announce_wss_port,
|
|
},
|
|
})
|
|
}
|
|
|
|
async fn start_gateway_tasks(
|
|
&mut self,
|
|
cached_network: CachedNetwork,
|
|
metrics_sender: MetricEventsSender,
|
|
active_clients_store: ActiveClientsStore,
|
|
mix_packet_sender: MixForwardingSender,
|
|
) -> Result<(), NymNodeError> {
|
|
let config = gateway_tasks_config(&self.config);
|
|
|
|
let topology_provider = Box::new(CachedTopologyProvider::new(
|
|
self.as_gateway_topology_node()?,
|
|
cached_network,
|
|
self.config.gateway_tasks.debug.minimum_mix_performance,
|
|
));
|
|
|
|
let mut gateway_tasks_builder = GatewayTasksBuilder::new(
|
|
config.gateway,
|
|
self.ed25519_identity_keys.clone(),
|
|
self.entry_gateway.client_storage.clone(),
|
|
mix_packet_sender,
|
|
metrics_sender,
|
|
self.metrics.clone(),
|
|
self.entry_gateway.mnemonic.clone(),
|
|
Self::user_agent(),
|
|
self.upgrade_mode_state.clone(),
|
|
self.shutdown_tracker().clone(),
|
|
);
|
|
|
|
// start task for watching the changes in upgrade mode attestation
|
|
let upgrade_check_request_sender = if let Some(upgrade_mode_watcher) =
|
|
gateway_tasks_builder.try_build_upgrade_mode_watcher()
|
|
{
|
|
let req_sender = upgrade_mode_watcher.request_sender();
|
|
upgrade_mode_watcher.start();
|
|
req_sender
|
|
} else {
|
|
UpgradeModeCheckRequestSender::new_empty()
|
|
};
|
|
|
|
// create the common state for subtasks relying on the upgrade mode information
|
|
// (i.e. everything that'd require ticket/bandwidth processing)
|
|
let upgrade_mode_common_state =
|
|
gateway_tasks_builder.build_upgrade_mode_common_state(upgrade_check_request_sender);
|
|
|
|
// if we're running in entry mode, start the websocket
|
|
if self.modes().entry {
|
|
info!(
|
|
"starting the clients websocket... on {}",
|
|
self.config.gateway_tasks.ws_bind_address
|
|
);
|
|
let mut websocket = gateway_tasks_builder
|
|
.build_websocket_listener(
|
|
active_clients_store.clone(),
|
|
upgrade_mode_common_state.clone(),
|
|
)
|
|
.await?;
|
|
self.shutdown_tracker()
|
|
.try_spawn_named(async move { websocket.run().await }, "EntryWebsocket");
|
|
|
|
// Set WireGuard data early so LP listener can access it
|
|
// (LP listener needs wg_peer_controller for dVPN registrations)
|
|
if self.config.wireguard.enabled {
|
|
let Some(wg_data) = self.wireguard.take() else {
|
|
return Err(NymNodeError::WireguardDataUnavailable);
|
|
};
|
|
gateway_tasks_builder.set_wireguard_data(wg_data.into());
|
|
}
|
|
|
|
// Start LP listener if enabled
|
|
info!(
|
|
"starting the LP listener on {} (data handler on: {})",
|
|
self.config.gateway_tasks.lp.control_bind_address,
|
|
self.config.gateway_tasks.lp.data_bind_address,
|
|
);
|
|
let mut lp_listener = gateway_tasks_builder
|
|
.build_lp_listener(active_clients_store.clone())
|
|
.await?;
|
|
self.shutdown_tracker()
|
|
.try_spawn_named(async move { lp_listener.run().await }, "LpListener");
|
|
} else {
|
|
info!("node not running in entry mode: the websocket and LP will remain closed");
|
|
}
|
|
|
|
// if we're running in exit mode, start the IPR and NR
|
|
if self.modes().exit {
|
|
info!("starting the exit service providers: NR + IPR");
|
|
gateway_tasks_builder.set_network_requester_opts(config.nr_opts);
|
|
gateway_tasks_builder.set_ip_packet_router_opts(config.ipr_opts);
|
|
|
|
let exit_sps = gateway_tasks_builder.build_exit_service_providers(
|
|
topology_provider.clone(),
|
|
topology_provider.clone(),
|
|
)?;
|
|
|
|
// note, this has all the joinhandles for when we want to use joinset
|
|
let (started_nr, started_ipr) = exit_sps.start_service_providers().await?;
|
|
active_clients_store.insert_embedded(started_nr.handle);
|
|
active_clients_store.insert_embedded(started_ipr.handle);
|
|
info!("started NR at: {}", started_nr.on_start_data.address);
|
|
info!("started IPR at: {}", started_ipr.on_start_data.address);
|
|
} else {
|
|
info!(
|
|
"node not running in exit mode: the exit service providers (NR + IPR) will remain unavailable"
|
|
);
|
|
}
|
|
|
|
// if we're running wireguard, start the authenticator
|
|
// and the actual wireguard listener
|
|
if self.config.wireguard.enabled {
|
|
info!(
|
|
"starting the wireguard tasks: authenticator service provider + wireguard peer controller"
|
|
);
|
|
|
|
gateway_tasks_builder.set_authenticator_opts(config.auth_opts);
|
|
|
|
let authenticator = gateway_tasks_builder
|
|
.build_wireguard_authenticator(upgrade_mode_common_state.clone(), topology_provider)
|
|
.await?;
|
|
let started_authenticator = authenticator.start_service_provider().await?;
|
|
active_clients_store.insert_embedded(started_authenticator.handle);
|
|
|
|
info!(
|
|
"started authenticator at: {}",
|
|
started_authenticator.on_start_data.address
|
|
);
|
|
|
|
gateway_tasks_builder
|
|
.try_start_wireguard(upgrade_mode_common_state)
|
|
.await
|
|
.map_err(NymNodeError::GatewayTasksStartupFailure)?;
|
|
} else {
|
|
info!(
|
|
"node not running with wireguard: authenticator service provider and wireguard will remain unavailable"
|
|
);
|
|
}
|
|
|
|
// start task for removing stale and un-retrieved client messages
|
|
let mut stale_messages_cleaner = gateway_tasks_builder.build_stale_messages_cleaner();
|
|
let shutdown_token = self.shutdown_token();
|
|
self.shutdown_tracker().try_spawn_named(
|
|
async move { stale_messages_cleaner.run(shutdown_token).await },
|
|
"StaleMessagesCleaner",
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn build_http_server(&self) -> Result<NymNodeHttpServer, NymNodeError> {
|
|
let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails {
|
|
location: self.config.host.location,
|
|
announce_ports: AnnouncePorts {
|
|
verloc_port: self.config.verloc.announce_port,
|
|
mix_port: self.config.mixnet.announce_port,
|
|
},
|
|
accepted_operator_terms_and_conditions: self.accepted_operator_terms_and_conditions,
|
|
};
|
|
|
|
// mixnode info
|
|
let mixnode_details = api_requests::v1::mixnode::models::Mixnode {};
|
|
|
|
// entry gateway info
|
|
let wireguard = if self.config.wireguard.enabled {
|
|
#[allow(deprecated)]
|
|
Some(api_requests::v1::gateway::models::Wireguard {
|
|
port: self.config.wireguard.announced_tunnel_port,
|
|
tunnel_port: self.config.wireguard.announced_tunnel_port,
|
|
metadata_port: self.config.wireguard.announced_metadata_port,
|
|
public_key: self.x25519_wireguard_key()?.to_string(),
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
let mixnet_websockets = Some(api_requests::v1::gateway::models::WebSockets {
|
|
ws_port: self
|
|
.config
|
|
.gateway_tasks
|
|
.announce_ws_port
|
|
.unwrap_or(self.config.gateway_tasks.ws_bind_address.port()),
|
|
wss_port: self.config.gateway_tasks.announce_wss_port,
|
|
});
|
|
let gateway_details = api_requests::v1::gateway::models::Gateway {
|
|
enforces_zk_nyms: self.config.gateway_tasks.enforce_zk_nyms,
|
|
client_interfaces: api_requests::v1::gateway::models::ClientInterfaces {
|
|
wireguard,
|
|
mixnet_websockets,
|
|
},
|
|
};
|
|
|
|
// exit gateway info
|
|
let nr_details = api_requests::v1::network_requester::models::NetworkRequester {
|
|
encoded_identity_key: self.service_providers.nr_ed25519.to_base58_string(),
|
|
encoded_x25519_key: self.service_providers.nr_x25519.to_base58_string(),
|
|
address: self.exit_network_requester_address().to_string(),
|
|
};
|
|
|
|
let ipr_details = api_requests::v1::ip_packet_router::models::IpPacketRouter {
|
|
encoded_identity_key: self.service_providers.ipr_ed25519.to_base58_string(),
|
|
encoded_x25519_key: self.service_providers.ipr_x25519.to_base58_string(),
|
|
address: self.exit_ip_packet_router_address().to_string(),
|
|
};
|
|
|
|
let auth_details = api_requests::v1::authenticator::models::Authenticator {
|
|
encoded_identity_key: self.service_providers.auth_ed25519.to_base58_string(),
|
|
encoded_x25519_key: self.service_providers.auth_x25519.to_base58_string(),
|
|
address: self.exit_authenticator_address().to_string(),
|
|
};
|
|
|
|
let exit_policy_details =
|
|
api_requests::v1::network_requester::exit_policy::models::UsedExitPolicy {
|
|
enabled: true,
|
|
upstream_source: self
|
|
.config
|
|
.service_providers
|
|
.upstream_exit_policy_url
|
|
.to_string(),
|
|
last_updated: 0,
|
|
// TODO: this will require some refactoring to actually retrieve the data from the embedded providers
|
|
policy: None,
|
|
};
|
|
|
|
let lp_details = api_requests::v1::lewes_protocol::models::LewesProtocol {
|
|
enabled: self.modes().entry,
|
|
control_port: self.config.gateway_tasks.lp.announced_control_port(),
|
|
data_port: self.config.gateway_tasks.lp.announced_data_port(),
|
|
};
|
|
|
|
let mut config = HttpServerConfig::new()
|
|
.with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref())
|
|
.with_mixnode_details(mixnode_details)
|
|
.with_gateway_details(gateway_details)
|
|
.with_network_requester_details(nr_details)
|
|
.with_ip_packet_router_details(ipr_details)
|
|
.with_authenticator_details(auth_details)
|
|
.with_used_exit_policy(exit_policy_details)
|
|
.with_description(self.description.clone())
|
|
.with_auxiliary_details(auxiliary_details)
|
|
.with_prometheus_bearer_token(self.config.http.access_token.clone())
|
|
.with_lewes_protocol(lp_details);
|
|
|
|
if self.config.http.expose_system_info {
|
|
config = config.with_system_info(get_system_info(
|
|
self.config.http.expose_system_hardware,
|
|
self.config.http.expose_crypto_hardware,
|
|
))
|
|
}
|
|
if self.config.modes.mixnode {
|
|
config.api.v1_config.node.roles.mixnode_enabled = true;
|
|
}
|
|
|
|
if self.config.modes.entry {
|
|
config.api.v1_config.node.roles.gateway_enabled = true
|
|
}
|
|
|
|
if self.config.modes.exit {
|
|
config.api.v1_config.node.roles.network_requester_enabled = true;
|
|
config.api.v1_config.node.roles.ip_packet_router_enabled = true;
|
|
}
|
|
|
|
if let Some(path) = &self.config.gateway_tasks.storage_paths.bridge_client_params {
|
|
config = config.with_bridge_client_params_file(path);
|
|
}
|
|
|
|
let x25519_versioned_noise_key = if self.config.mixnet.debug.unsafe_disable_noise {
|
|
None
|
|
} else {
|
|
Some(VersionedNoiseKey {
|
|
supported_version: nym_noise::LATEST_NOISE_VERSION,
|
|
x25519_pubkey: *self.x25519_noise_keys.public_key(),
|
|
})
|
|
};
|
|
|
|
let app_state = AppState::new(
|
|
StaticNodeInformation {
|
|
ed25519_identity_keys: self.ed25519_identity_keys.clone(),
|
|
x25519_versioned_noise_key,
|
|
ip_addresses: self.config.host.public_ips.clone(),
|
|
hostname: self.config.host.hostname.clone(),
|
|
},
|
|
self.active_sphinx_keys()?.clone(),
|
|
self.metrics.clone(),
|
|
self.verloc_stats.clone(),
|
|
self.config
|
|
.gateway_tasks
|
|
.upgrade_mode
|
|
.attestation_url
|
|
.clone(),
|
|
self.upgrade_mode_state.clone(),
|
|
self.config.http.node_load_cache_ttl,
|
|
);
|
|
|
|
Ok(NymNodeRouter::new(config, app_state)
|
|
.build_server(&self.config.http.bind_address)
|
|
.await?)
|
|
}
|
|
|
|
fn user_agent() -> UserAgent {
|
|
bin_info!().into()
|
|
}
|
|
|
|
async fn try_refresh_remote_nym_api_cache(
|
|
&self,
|
|
client: &NymApisClient,
|
|
) -> Result<(), NymNodeError> {
|
|
info!("attempting to request described cache refresh from nym-api(s)...");
|
|
|
|
client
|
|
.broadcast_force_refresh(self.ed25519_identity_keys.private_key())
|
|
.await;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn start_verloc_measurements(&self) {
|
|
info!(
|
|
"Starting the [verloc] round-trip-time measurer on {} ...",
|
|
self.config.verloc.bind_address
|
|
);
|
|
|
|
let mut base_agent = Self::user_agent();
|
|
base_agent.application = format!("{}-verloc", base_agent.application);
|
|
let config = nym_verloc::measurements::ConfigBuilder::new(
|
|
self.config.mixnet.nym_api_urls.clone(),
|
|
base_agent,
|
|
)
|
|
.listening_address(self.config.verloc.bind_address)
|
|
.packets_per_node(self.config.verloc.debug.packets_per_node)
|
|
.connection_timeout(self.config.verloc.debug.connection_timeout)
|
|
.packet_timeout(self.config.verloc.debug.packet_timeout)
|
|
.delay_between_packets(self.config.verloc.debug.delay_between_packets)
|
|
.tested_nodes_batch_size(self.config.verloc.debug.tested_nodes_batch_size)
|
|
.testing_interval(self.config.verloc.debug.testing_interval)
|
|
.retry_timeout(self.config.verloc.debug.retry_timeout)
|
|
.build();
|
|
|
|
let mut verloc_measurer = VerlocMeasurer::new(
|
|
config,
|
|
self.ed25519_identity_keys.clone(),
|
|
self.shutdown_manager.clone_shutdown_token(),
|
|
);
|
|
verloc_measurer.set_shared_state(self.verloc_stats.clone());
|
|
self.shutdown_manager
|
|
.try_spawn_named(async move { verloc_measurer.run().await }, "VerlocMeasurer");
|
|
}
|
|
|
|
pub(crate) fn setup_metrics_backend(
|
|
&self,
|
|
active_clients_store: ActiveClientsStore,
|
|
active_egress_mixnet_connections: ActiveConnections,
|
|
) -> MetricEventsSender {
|
|
info!("setting up node metrics...");
|
|
|
|
// aggregator (to listen for any metrics events)
|
|
let mut metrics_aggregator =
|
|
MetricsAggregator::new(self.config.metrics.debug.aggregator_update_rate);
|
|
|
|
// >>>> START: register all relevant handlers for custom events
|
|
|
|
// legacy metrics updater on the deprecated endpoint
|
|
metrics_aggregator.register_handler(
|
|
LegacyMixingStatsUpdater::new(self.metrics.clone()),
|
|
self.config.metrics.debug.legacy_mixing_metrics_update_rate,
|
|
);
|
|
|
|
// stats for gateway client sessions (websocket-related information)
|
|
metrics_aggregator.register_handler(
|
|
GatewaySessionStatsHandler::new(
|
|
self.metrics.clone(),
|
|
self.entry_gateway.stats_storage.clone(),
|
|
),
|
|
self.config.metrics.debug.clients_sessions_update_rate,
|
|
);
|
|
|
|
// handler for periodically cleaning up stale recipient/sender data
|
|
metrics_aggregator.register_handler(
|
|
MixnetMetricsCleaner::new(self.metrics.clone()),
|
|
self.config.metrics.debug.stale_mixnet_metrics_cleaner_rate,
|
|
);
|
|
|
|
// handler for updating the value of forward/final hop packets pending delivery
|
|
metrics_aggregator.register_handler(
|
|
PendingEgressPacketsUpdater::new(
|
|
self.metrics.clone(),
|
|
active_clients_store,
|
|
active_egress_mixnet_connections,
|
|
),
|
|
self.config.metrics.debug.pending_egress_packets_update_rate,
|
|
);
|
|
|
|
// handler for updating the prometheus registry from the global atomic metrics counters
|
|
// such as number of packets received
|
|
metrics_aggregator.register_handler(
|
|
PrometheusGlobalNodeMetricsRegistryUpdater::new(self.metrics.clone()),
|
|
self.config
|
|
.metrics
|
|
.debug
|
|
.global_prometheus_counters_update_rate,
|
|
);
|
|
|
|
// handler for handling prometheus metrics events
|
|
// metrics_aggregator.register_handler(PrometheusEventsHandler{}, None);
|
|
|
|
// note: we're still measuring things such as number of mixed packets,
|
|
// but since they're stored as atomic integers, they are incremented directly at source
|
|
// rather than going through event pipeline
|
|
// should we need custom mixnet events, we can add additional handler for that. that's not a problem
|
|
|
|
// >>>> END: register all relevant handlers
|
|
|
|
// console logger to preserve old mixnode functionalities
|
|
if self.config.metrics.debug.log_stats_to_console {
|
|
let mut console_logger = ConsoleLogger::new(
|
|
self.config.metrics.debug.console_logging_update_interval,
|
|
self.metrics.clone(),
|
|
);
|
|
|
|
self.shutdown_tracker().try_spawn_named_with_shutdown(
|
|
async move { console_logger.run().await },
|
|
"ConsoleLogger",
|
|
);
|
|
}
|
|
|
|
let events_sender = metrics_aggregator.sender();
|
|
|
|
// spawn the aggregator task
|
|
let shutdown_token = self.shutdown_token();
|
|
self.shutdown_tracker().try_spawn_named(
|
|
async move { metrics_aggregator.run(shutdown_token).await },
|
|
"MetricsAggregator",
|
|
);
|
|
|
|
events_sender
|
|
}
|
|
|
|
pub(crate) async fn setup_replay_detection(
|
|
&self,
|
|
) -> Result<ReplayProtectionBloomfiltersManager, NymNodeError> {
|
|
if self.config.mixnet.replay_protection.debug.unsafe_disabled {
|
|
return Ok(ReplayProtectionBloomfiltersManager::new_disabled(
|
|
self.metrics.clone(),
|
|
));
|
|
}
|
|
|
|
// create the background task for the bloomfilter
|
|
// to reset it and flush it to disk
|
|
let sphinx_keys = self.sphinx_keys()?;
|
|
let mut replay_detection_background = ReplayProtectionDiskFlush::new(
|
|
&self.config,
|
|
sphinx_keys.keys.primary_key_rotation_id(),
|
|
sphinx_keys.keys.secondary_key_rotation_id(),
|
|
self.metrics.clone(),
|
|
self.shutdown_manager.clone_shutdown_token(),
|
|
)
|
|
.await?;
|
|
|
|
let bloomfilters_manager = replay_detection_background.bloomfilters_manager();
|
|
self.shutdown_manager.try_spawn_named(
|
|
async move { replay_detection_background.run().await },
|
|
"ReplayDetection",
|
|
);
|
|
Ok(bloomfilters_manager)
|
|
}
|
|
|
|
// I'm assuming this will be needed in other places, so it's explicitly extracted
|
|
fn setup_nym_apis_client(&self) -> Result<NymApisClient, NymNodeError> {
|
|
NymApisClient::new(
|
|
&self.config.mixnet.nym_api_urls,
|
|
self.shutdown_manager.clone_shutdown_token(),
|
|
)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn sphinx_keys(&self) -> Result<&SphinxKeyManager, NymNodeError> {
|
|
self.sphinx_key_manager
|
|
.as_ref()
|
|
.ok_or(NymNodeError::ConsumedSphinxKeys)
|
|
}
|
|
|
|
fn take_managed_sphinx_keys(&mut self) -> Result<SphinxKeyManager, NymNodeError> {
|
|
self.sphinx_key_manager
|
|
.take()
|
|
.ok_or(NymNodeError::ConsumedSphinxKeys)
|
|
}
|
|
|
|
pub(crate) async fn setup_key_rotation(
|
|
&mut self,
|
|
nym_apis_client: NymApisClient,
|
|
replay_protection_manager: ReplayProtectionBloomfiltersManager,
|
|
) -> Result<(), NymNodeError> {
|
|
let managed_keys = self.take_managed_sphinx_keys()?;
|
|
let rotation_state = nym_apis_client.get_key_rotation_info().await?;
|
|
|
|
let rotation_controller = KeyRotationController::new(
|
|
&self.config,
|
|
rotation_state.into(),
|
|
nym_apis_client,
|
|
replay_protection_manager,
|
|
managed_keys,
|
|
self.shutdown_manager.clone_shutdown_token(),
|
|
);
|
|
|
|
rotation_controller.start();
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn start_mixnet_listener<F>(
|
|
&self,
|
|
active_clients_store: &ActiveClientsStore,
|
|
replay_protection_bloomfilter: ReplayProtectionBloomfilters,
|
|
routing_filter: F,
|
|
noise_config: NoiseConfig,
|
|
) -> Result<(MixForwardingSender, ActiveConnections), NymNodeError>
|
|
where
|
|
F: RoutingFilter + Send + Sync + 'static,
|
|
{
|
|
let processing_config = ProcessingConfig::new(&self.config);
|
|
|
|
// we're ALWAYS listening for mixnet packets, either for forward or final hops (or both)
|
|
info!(
|
|
"Starting the mixnet listener... on {} (forward: {}, final hop: {}))",
|
|
self.config.mixnet.bind_address,
|
|
processing_config.forward_hop_processing_enabled,
|
|
processing_config.final_hop_processing_enabled
|
|
);
|
|
|
|
let mixnet_client_config = nym_mixnet_client::Config::new(
|
|
self.config.mixnet.debug.packet_forwarding_initial_backoff,
|
|
self.config.mixnet.debug.packet_forwarding_maximum_backoff,
|
|
self.config.mixnet.debug.initial_connection_timeout,
|
|
self.config.mixnet.debug.maximum_connection_buffer_size,
|
|
self.config.mixnet.debug.use_legacy_packet_encoding,
|
|
);
|
|
let mixnet_client = nym_mixnet_client::Client::new(
|
|
mixnet_client_config,
|
|
noise_config.clone(),
|
|
self.metrics
|
|
.network
|
|
.active_egress_mixnet_connections_counter(),
|
|
);
|
|
let active_connections = mixnet_client.active_connections();
|
|
|
|
let mut packet_forwarder =
|
|
PacketForwarder::new(mixnet_client, routing_filter, self.metrics.clone());
|
|
let mix_packet_sender = packet_forwarder.sender();
|
|
|
|
let shutdown_token = self.shutdown_token();
|
|
|
|
self.shutdown_tracker().try_spawn_named(
|
|
async move { packet_forwarder.run(shutdown_token).await },
|
|
"PacketForwarder",
|
|
);
|
|
|
|
let final_hop_data = SharedFinalHopData::new(
|
|
active_clients_store.clone(),
|
|
self.entry_gateway.client_storage.clone(),
|
|
);
|
|
|
|
let shared = mixnet::SharedData::new(
|
|
processing_config,
|
|
self.active_sphinx_keys()?,
|
|
replay_protection_bloomfilter,
|
|
mix_packet_sender.clone(),
|
|
final_hop_data,
|
|
noise_config,
|
|
self.metrics.clone(),
|
|
self.shutdown_token(),
|
|
);
|
|
|
|
let mut mixnet_listener = mixnet::Listener::new(self.config.mixnet.bind_address, shared);
|
|
|
|
let shutdown_token = self.shutdown_token();
|
|
self.shutdown_tracker().try_spawn_named(
|
|
async move { mixnet_listener.run(shutdown_token).await },
|
|
"MixnetListener",
|
|
);
|
|
|
|
Ok((mix_packet_sender, active_connections))
|
|
}
|
|
|
|
pub(crate) async fn run_minimal_mixnet_processing(mut self) -> Result<(), NymNodeError> {
|
|
let noise_config = NoiseConfig::new(
|
|
self.x25519_noise_keys.clone(),
|
|
NoiseNetworkView::new_empty(),
|
|
self.config.mixnet.debug.initial_connection_timeout,
|
|
)
|
|
.with_unsafe_disabled(true);
|
|
|
|
self.start_mixnet_listener(
|
|
&ActiveClientsStore::new(),
|
|
ReplayProtectionBloomfilters::new_disabled(),
|
|
OpenFilter,
|
|
noise_config,
|
|
)
|
|
.await?;
|
|
|
|
self.shutdown_manager.close_tracker();
|
|
self.shutdown_manager.run_until_shutdown().await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn start_nym_node_tasks(mut self) -> Result<ShutdownManager, NymNodeError> {
|
|
info!(
|
|
"starting Nym Node {} with the following modes: mixnode: {}, entry: {}, exit: {}, wireguard: {}",
|
|
self.ed25519_identity_key(),
|
|
self.config.modes.mixnode,
|
|
self.config.modes.entry,
|
|
self.config.modes.exit,
|
|
self.config.wireguard.enabled
|
|
);
|
|
debug!("config: {:#?}", self.config);
|
|
|
|
let http_server = self.build_http_server().await?;
|
|
let bind_address = self.config.http.bind_address;
|
|
let server_shutdown = self.shutdown_manager.clone_shutdown_token();
|
|
|
|
self.shutdown_manager.try_spawn_named(
|
|
async move {
|
|
info!("starting NymNodeHTTPServer on {bind_address}");
|
|
http_server
|
|
.with_graceful_shutdown(async move { server_shutdown.cancelled().await })
|
|
.await
|
|
},
|
|
"HttpApi",
|
|
);
|
|
|
|
let nym_apis_client = self.setup_nym_apis_client()?;
|
|
|
|
self.try_refresh_remote_nym_api_cache(&nym_apis_client)
|
|
.await?;
|
|
self.start_verloc_measurements();
|
|
|
|
let network_refresher = self.build_network_refresher().await?;
|
|
let active_clients_store = ActiveClientsStore::new();
|
|
|
|
let bloomfilters_manager = self.setup_replay_detection().await?;
|
|
|
|
let noise_config = nym_noise::config::NoiseConfig::new(
|
|
self.x25519_noise_keys.clone(),
|
|
network_refresher.noise_view(),
|
|
self.config.mixnet.debug.initial_connection_timeout,
|
|
)
|
|
.with_unsafe_disabled(self.config.mixnet.debug.unsafe_disable_noise);
|
|
|
|
let (mix_packet_sender, active_egress_mixnet_connections) = self
|
|
.start_mixnet_listener(
|
|
&active_clients_store,
|
|
bloomfilters_manager.bloomfilters(),
|
|
network_refresher.routing_filter(),
|
|
noise_config,
|
|
)
|
|
.await?;
|
|
|
|
let metrics_sender = self.setup_metrics_backend(
|
|
active_clients_store.clone(),
|
|
active_egress_mixnet_connections,
|
|
);
|
|
|
|
self.start_gateway_tasks(
|
|
network_refresher.cached_network(),
|
|
metrics_sender,
|
|
active_clients_store,
|
|
mix_packet_sender,
|
|
)
|
|
.await?;
|
|
|
|
self.setup_key_rotation(nym_apis_client, bloomfilters_manager)
|
|
.await?;
|
|
|
|
network_refresher.start();
|
|
self.shutdown_manager.close_tracker();
|
|
|
|
Ok(self.shutdown_manager)
|
|
}
|
|
|
|
pub(crate) async fn run(mut self) -> Result<(), NymNodeError> {
|
|
let mut shutdown_signals = self.shutdown_manager.detach_shutdown_signals();
|
|
|
|
// listen for shutdown signal in case we received it when attempting to spawn all the tasks
|
|
tokio::select! {
|
|
_ = shutdown_signals.wait_for_signal() => {
|
|
info!("received shutdown signal during setup - exiting");
|
|
// ideally we'd also do some cleanup here, but currently there's no easy way to access the handles
|
|
return Ok(())
|
|
}
|
|
startup_result = self.start_nym_node_tasks() => {
|
|
let mut shutdown_manager = startup_result?;
|
|
shutdown_manager.replace_shutdown_signals(shutdown_signals);
|
|
shutdown_manager.run_until_shutdown().await;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|