From 114d92f93fc375d12bd7ea1b3ea862e53c0d020f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 20 Sep 2023 15:47:05 +0100 Subject: [PATCH] Feature/gateway inbuilt nr (#3877) * changed NymConfigTemplate trait to call 'template' by reference * network requester lib * introduced generic parameter to 'MixTrafficController' to allow non-remote gateways * allowing for custom gateway sender * types cleanup * minor gateway cmds refactor + initial NR work * wip * running a NR inside gateway note: this NR isnt tied to the gateway yet * rebase fixes * propagating same shutdown handle * wip * starting NR with appropriate local transceiver * fixed premature shutdown * wiring up PacketRouter * both ends wired together * actually working so much cleanup to do now * started removing dead code * wip * temp: hardcode gateway * further cleanup * fixed build of other binaries * setup-network-requester subcmd * overriding NR config in gateway init/run * wip making it wasm-compatible [again] * refactored 'GatewaySetup' * clippy and friends * removed debug code * rust 1.72 lints * ensuring local gateway is available + some comments * correctly putting network requester data in the same underlying details struct * improved gateway errors * changed 'network_requester_config' deserialization * missing clap annotation for 'enabled' flag in 'setup-network-requester' command * saving config file after 'setup-network-requester' * removed dead code * review comments * make embedded NR wait for gateway to come online (for at most 70min) * fixed shutdown on successful gateway wait * updated NR config override --- Cargo.lock | 22 +- Cargo.toml | 1 + clients/native/src/client/config/mod.rs | 2 +- .../native/src/client/config/persistence.rs | 2 +- clients/native/src/client/mod.rs | 147 +---- clients/native/src/commands/init.rs | 42 +- clients/native/src/commands/mod.rs | 2 +- clients/socks5/src/commands/init.rs | 36 +- clients/socks5/src/commands/mod.rs | 2 +- clients/socks5/src/config/mod.rs | 2 +- clients/socks5/src/config/persistence.rs | 2 +- common/bin-common/src/output_format/mod.rs | 10 + common/client-core/Cargo.toml | 3 +- .../client-core/src/client/base_client/mod.rs | 251 ++++++--- .../base_client/storage/gateway_details.rs | 133 ++++- .../src/client/base_client/storage/mod.rs | 3 - .../client-core/src/client/key_manager/mod.rs | 59 +- .../src/client/key_manager/persistence.rs | 26 +- .../{mix_traffic.rs => mix_traffic/mod.rs} | 53 +- .../src/client/mix_traffic/transceiver.rs | 262 +++++++++ .../acknowledgement_listener.rs | 2 +- .../acknowledgement_control/mod.rs | 12 +- .../src/client/real_messages_control/mod.rs | 6 +- .../client-core/src/client/received_buffer.rs | 6 +- .../src/client/topology_control/mod.rs | 55 ++ .../src/config/disk_persistence/keys_paths.rs | 2 +- .../src/config/disk_persistence/mod.rs | 4 +- common/client-core/src/config/mod.rs | 22 + .../src/config/old_config_v1_1_20_2.rs | 2 +- common/client-core/src/error.rs | 36 +- common/client-core/src/init/helpers.rs | 29 +- common/client-core/src/init/mod.rs | 508 +++++------------- common/client-core/src/init/types.rs | 331 ++++++++++++ common/client-core/src/lib.rs | 5 + .../client-libs/gateway-client/src/client.rs | 85 +-- .../client-libs/gateway-client/src/error.rs | 10 + common/client-libs/gateway-client/src/lib.rs | 11 +- .../gateway-client/src/packet_router.rs | 109 ++-- .../gateway-client/src/socket_state.rs | 1 + .../client-libs/gateway-client/src/traits.rs | 96 ++++ .../mixnet-client/src/forwarder.rs | 2 +- common/config/src/lib.rs | 11 +- common/credential-utils/src/utils.rs | 2 +- common/crypto/src/asymmetric/identity/mod.rs | 7 + common/network-defaults/src/mainnet.rs | 17 +- common/socks5-client-core/Cargo.toml | 1 + .../src/config/old_config_v1_1_13.rs | 2 +- common/socks5-client-core/src/lib.rs | 20 +- .../src/connection_controller.rs | 9 +- common/task/src/lib.rs | 2 +- common/task/src/manager.rs | 152 +++++- common/topology/src/error.rs | 3 + common/topology/src/lib.rs | 8 +- common/types/src/gateway.rs | 56 +- common/wasm/client-core/src/config/mod.rs | 10 + .../wasm/client-core/src/config/override.rs | 8 + common/wasm/client-core/src/helpers.rs | 16 +- common/wasm/client-core/src/lib.rs | 6 +- .../src/storage/core_client_traits.rs | 9 +- .../src/tutorials/cosmos-service/lib.md | 1 - gateway/Cargo.toml | 5 +- gateway/src/commands/helpers.rs | 277 ++++++++++ gateway/src/commands/init.rs | 165 ++++-- gateway/src/commands/mod.rs | 148 +---- gateway/src/commands/node_details.rs | 13 +- gateway/src/commands/run.rs | 79 ++- .../src/commands/setup_network_requester.rs | 109 ++++ gateway/src/commands/sign.rs | 33 +- gateway/src/commands/upgrade_helpers.rs | 66 +++ gateway/src/config/mod.rs | 68 ++- gateway/src/config/old_config_v1_1_20.rs | 37 +- gateway/src/config/old_config_v1_1_28.rs | 224 ++++++++ gateway/src/config/persistence/paths.rs | 46 +- gateway/src/config/template.rs | 9 +- gateway/src/error.rs | 72 ++- gateway/src/main.rs | 3 + .../node/client_handling/active_clients.rs | 122 ++++- .../embedded_network_requester/mod.rs | 89 +++ gateway/src/node/client_handling/mod.rs | 1 + .../connection_handler/authenticated.rs | 13 +- .../websocket/connection_handler/fresh.rs | 42 +- .../client_handling/websocket/listener.rs | 2 +- gateway/src/node/helpers.rs | 148 +++++ .../receiver/connection_handler.rs | 89 ++- .../node/mixnet_handling/receiver/listener.rs | 2 +- gateway/src/node/mod.rs | 243 ++++++--- gateway/src/node/storage/error.rs | 4 +- gateway/src/node/storage/inboxes.rs | 10 +- gateway/src/support/config.rs | 5 +- mixnode/src/config/mod.rs | 2 +- nym-api/src/network_monitor/monitor/sender.rs | 64 ++- nym-api/src/support/config/mod.rs | 2 +- nym-browser-extension/storage/src/storage.rs | 6 + nym-connect/desktop/Cargo.lock | 5 +- .../desktop/src-tauri/src/config/mod.rs | 32 +- .../src-tauri/src/config/persistence.rs | 2 +- .../desktop/src-tauri/src/config/upgrade.rs | 2 +- nym-connect/desktop/src-tauri/src/state.rs | 8 +- nym-connect/desktop/src-tauri/src/tasks.rs | 5 +- sdk/lib/socks5-listener/src/config/mod.rs | 2 +- .../socks5-listener/src/config/persistence.rs | 4 +- sdk/lib/socks5-listener/src/lib.rs | 26 +- sdk/rust/nym-sdk/examples/bandwidth.rs | 3 +- sdk/rust/nym-sdk/examples/builder.rs | 1 - .../nym-sdk/examples/builder_with_storage.rs | 1 - .../examples/custom_topology_provider.rs | 1 - .../examples/manually_handle_storage.rs | 1 - sdk/rust/nym-sdk/examples/socks5.rs | 3 +- sdk/rust/nym-sdk/examples/surb-reply.rs | 1 - sdk/rust/nym-sdk/src/bandwidth.rs | 1 - sdk/rust/nym-sdk/src/error.rs | 11 + sdk/rust/nym-sdk/src/lib.rs | 8 + sdk/rust/nym-sdk/src/mixnet/client.rs | 220 ++++++-- sdk/rust/nym-sdk/src/mixnet/native_client.rs | 26 +- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 20 +- .../network-requester/Cargo.toml | 4 + .../src/allowed_hosts/filter.rs | 8 + .../network-requester/src/cli/init.rs | 46 +- .../network-requester/src/cli/mod.rs | 57 +- .../network-requester/src/cli/run.rs | 13 +- .../network-requester/src/config/mod.rs | 29 +- .../src/config/persistence.rs | 4 +- .../network-requester/src/core.rs | 246 ++++++--- .../network-requester/src/error.rs | 5 +- .../network-requester/src/lib.rs | 27 + .../network-requester/src/main.rs | 3 +- tools/nym-nr-query/src/main.rs | 4 +- wasm/client/src/client.rs | 13 +- wasm/mix-fetch/src/client.rs | 14 +- wasm/node-tester/src/tester.rs | 51 +- 130 files changed, 4219 insertions(+), 1620 deletions(-) rename common/client-core/src/client/{mix_traffic.rs => mix_traffic/mod.rs} (74%) create mode 100644 common/client-core/src/client/mix_traffic/transceiver.rs create mode 100644 common/client-core/src/init/types.rs create mode 100644 common/client-libs/gateway-client/src/traits.rs create mode 100644 gateway/src/commands/helpers.rs create mode 100644 gateway/src/commands/setup_network_requester.rs create mode 100644 gateway/src/commands/upgrade_helpers.rs create mode 100644 gateway/src/config/old_config_v1_1_28.rs create mode 100644 gateway/src/node/client_handling/embedded_network_requester/mod.rs create mode 100644 gateway/src/node/helpers.rs create mode 100644 service-providers/network-requester/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index bd4608b55d..d03f6b2517 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2347,19 +2347,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "4.0.2" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" -dependencies = [ - "cfg-if", - "num_cpus", -] - -[[package]] -name = "dashmap" -version = "5.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -6264,7 +6254,7 @@ dependencies = [ "async-trait", "base64 0.21.4", "cfg-if", - "dashmap 5.5.0", + "dashmap", "dirs 4.0.0", "futures", "gloo-timers", @@ -6562,7 +6552,7 @@ dependencies = [ "bs58 0.4.0", "clap 4.3.21", "colored", - "dashmap 4.0.2", + "dashmap", "dirs 4.0.0", "dotenvy", "futures", @@ -6579,6 +6569,7 @@ dependencies = [ "nym-mixnet-client", "nym-mixnode-common", "nym-network-defaults", + "nym-network-requester", "nym-pemstore", "nym-sphinx", "nym-statistics-common", @@ -7115,6 +7106,7 @@ dependencies = [ name = "nym-socks5-client-core" version = "0.1.0" dependencies = [ + "anyhow", "dirs 4.0.0", "futures", "log", @@ -7734,7 +7726,7 @@ checksum = "8b3a2a91fdbfdd4d212c0dcc2ab540de2c2bcbbd90be17de7a7daf8822d010c1" dependencies = [ "async-trait", "crossbeam-channel", - "dashmap 5.5.0", + "dashmap", "fnv", "futures-channel", "futures-executor", diff --git a/Cargo.toml b/Cargo.toml index 92fc1e6113..e37ea29c84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ cw2 = { version = "=1.1.0" } cw3 = { version = "=1.1.0" } cw4 = { version = "=1.1.0" } cw-controllers = { version = "=1.1.0" } +dashmap = "5.5.3" dotenvy = "0.15.6" futures = "0.3.28" generic-array = "0.14.7" diff --git a/clients/native/src/client/config/mod.rs b/clients/native/src/client/config/mod.rs index dd819471d8..4f57387ceb 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -67,7 +67,7 @@ pub struct Config { } impl NymConfigTemplate for Config { - fn template() -> &'static str { + fn template(&self) -> &'static str { CONFIG_TEMPLATE } } diff --git a/clients/native/src/client/config/persistence.rs b/clients/native/src/client/config/persistence.rs index d84c956d8f..a61913a2f7 100644 --- a/clients/native/src/client/config/persistence.rs +++ b/clients/native/src/client/config/persistence.rs @@ -14,7 +14,7 @@ pub struct ClientPaths { impl ClientPaths { pub fn new_default>(base_data_directory: P) -> Self { ClientPaths { - common_paths: CommonClientPaths::new_default(base_data_directory), + common_paths: CommonClientPaths::new_base(base_data_directory), } } } diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 1e47ec9bf4..f262d5e26b 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -4,28 +4,19 @@ use crate::client::config::Config; use crate::error::ClientError; use crate::websocket; -use futures::channel::mpsc; use log::*; use nym_client_core::client::base_client::non_wasm_helpers::default_query_dkg_client_from_config; use nym_client_core::client::base_client::storage::OnDiskPersistent; use nym_client_core::client::base_client::{ BaseClientBuilder, ClientInput, ClientOutput, ClientState, }; -use nym_client_core::client::inbound_messages::InputMessage; -use nym_client_core::client::received_buffer::{ - ReceivedBufferMessage, ReceivedBufferRequestSender, ReconstructedMessagesReceiver, -}; -use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::params::PacketType; -use nym_task::connections::TransmissionLane; -use nym_task::TaskManager; +use nym_task::TaskHandle; use nym_validator_client::QueryHttpRpcNyxdClient; use std::error::Error; use std::path::PathBuf; -use tokio::sync::watch::error::SendError; pub use nym_sphinx::addressing::clients::Recipient; -pub use nym_sphinx::receiver::ReconstructedMessage; pub mod config; @@ -92,7 +83,7 @@ impl SocketClient { pub async fn run_socket_forever(self) -> Result<(), Box> { let shutdown = self.start_socket().await?; - let res = shutdown.catch_interrupt().await; + let res = shutdown.wait_for_shutdown().await; log::info!("Stopping nym-client"); res } @@ -125,7 +116,7 @@ impl SocketClient { Ok(base_client) } - pub async fn start_socket(self) -> Result { + pub async fn start_socket(self) -> Result { if !self.config.socket.socket_type.is_websocket() { return Err(ClientError::InvalidSocketMode); } @@ -144,141 +135,13 @@ impl SocketClient { client_output, client_state, &self_address, - started_client.task_manager.subscribe(), + started_client.task_handle.get_handle(), packet_type, ); info!("Client startup finished!"); info!("The address of this client is: {self_address}"); - Ok(started_client.task_manager) - } - - pub async fn start_direct(self) -> Result { - if self.config.socket.socket_type.is_websocket() { - return Err(ClientError::InvalidSocketMode); - } - - let base_builder = self.create_base_client_builder().await?; - let packet_type = self.config.base.debug.traffic.packet_type; - let mut started_client = base_builder.start_base().await?; - let address = started_client.address; - let client_input = started_client.client_input.register_producer(); - let client_output = started_client.client_output.register_consumer(); - - // register our receiver - let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); - - // tell the buffer to start sending stuff to us - client_output - .received_buffer_request_sender - .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( - reconstructed_sender, - )) - .expect("the buffer request failed!"); - - Ok(DirectClient { - client_input, - _received_buffer_request_sender: client_output.received_buffer_request_sender, - reconstructed_receiver, - address, - shutdown_notifier: started_client.task_manager, - packet_type, - }) - } -} - -pub struct DirectClient { - client_input: ClientInput, - // make sure to not drop the channel - _received_buffer_request_sender: ReceivedBufferRequestSender, - reconstructed_receiver: ReconstructedMessagesReceiver, - address: Recipient, - - // we need to keep reference to this guy otherwise things will start dropping - shutdown_notifier: TaskManager, - packet_type: PacketType, -} - -impl DirectClient { - pub fn address(&self) -> &Recipient { - &self.address - } - - pub fn signal_shutdown(&self) -> Result<(), SendError<()>> { - self.shutdown_notifier.signal_shutdown() - } - - pub async fn wait_for_shutdown(&mut self) { - self.shutdown_notifier.wait_for_shutdown().await - } - - /// EXPERIMENTAL DIRECT RUST API - /// It's untested and there are absolutely no guarantees about it (but seems to have worked - /// well enough in local tests) - pub async fn send_regular_message(&mut self, recipient: Recipient, message: Vec) { - let lane = TransmissionLane::General; - let input_msg = InputMessage::new_regular(recipient, message, lane, Some(self.packet_type)); - - self.client_input - .input_sender - .send(input_msg) - .await - .expect("InputMessageReceiver has stopped receiving!"); - } - - /// EXPERIMENTAL DIRECT RUST API - /// It's untested and there are absolutely no guarantees about it (but seems to have worked - /// well enough in local tests) - pub async fn send_anonymous_message( - &mut self, - recipient: Recipient, - message: Vec, - reply_surbs: u32, - ) { - let lane = TransmissionLane::General; - let input_msg = InputMessage::new_anonymous( - recipient, - message, - reply_surbs, - lane, - Some(self.packet_type), - ); - - self.client_input - .input_sender - .send(input_msg) - .await - .expect("InputMessageReceiver has stopped receiving!"); - } - - /// EXPERIMENTAL DIRECT RUST API - /// It's untested and there are absolutely no guarantees about it (but seems to have worked - /// well enough in local tests) - pub async fn send_reply(&mut self, recipient_tag: AnonymousSenderTag, message: Vec) { - let lane = TransmissionLane::General; - let input_msg = - InputMessage::new_reply(recipient_tag, message, lane, Some(self.packet_type)); - - self.client_input - .input_sender - .send(input_msg) - .await - .expect("InputMessageReceiver has stopped receiving!"); - } - - /// EXPERIMENTAL DIRECT RUST API - /// It's untested and there are absolutely no guarantees about it (but seems to have worked - /// well enough in local tests) - /// Note: it waits for the first occurrence of messages being sent to ourselves. If you expect multiple - /// messages, you might have to call this function repeatedly. - // TODO: I guess this should really return something that `impl Stream` - pub async fn wait_for_messages(&mut self) -> Vec { - use futures::StreamExt; - - self.reconstructed_receiver - .next() - .await - .expect("buffer controller seems to have somehow died!") + Ok(started_client.task_handle) } } diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 784ffe8c74..f403a3e5dc 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -15,8 +15,9 @@ use nym_bin_common::output_format::OutputFormat; use nym_client_core::client::base_client::storage::gateway_details::OnDiskGatewayDetails; use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::config::GatewayEndpointConfig; +use nym_client_core::error::ClientCoreError; use nym_client_core::init::helpers::current_gateways; -use nym_client_core::init::GatewaySetup; +use nym_client_core::init::types::{GatewayDetails, GatewaySelectionSpecification, GatewaySetup}; use nym_crypto::asymmetric::identity; use nym_sphinx::addressing::clients::Recipient; use nym_topology::NymTopology; @@ -114,7 +115,7 @@ impl From for OverrideConfig { #[derive(Debug, Serialize)] pub struct InitResults { #[serde(flatten)] - client_core: nym_client_core::init::InitResults, + client_core: nym_client_core::init::types::InitResults, client_listening_port: u16, client_address: String, } @@ -122,7 +123,11 @@ pub struct InitResults { impl InitResults { fn new(config: &Config, address: &Recipient, gateway: &GatewayEndpointConfig) -> Self { Self { - client_core: nym_client_core::init::InitResults::new(&config.base, address, gateway), + client_core: nym_client_core::init::types::InitResults::new( + &config.base, + address, + gateway, + ), client_listening_port: config.socket.listening_port, client_address: address.to_string(), } @@ -162,7 +167,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), ClientError> { // re-registering if wanted. let user_wants_force_register = args.force_register_gateway; if user_wants_force_register { - eprintln!("Instructed to force registering gateway. This might overwrite keys!"); + eprintln!("Instructed to force registering gateway. This will overwrite keys!"); } // If the client was already initialized, don't generate new keys and don't re-register with @@ -172,7 +177,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), ClientError> { // Attempt to use a user-provided gateway, if possible let user_chosen_gateway_id = args.gateway; - let gateway_setup = GatewaySetup::new_fresh( + let selection_spec = GatewaySelectionSpecification::new( user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(args.latency_based_selection), ); @@ -186,7 +191,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), ClientError> { let details_store = OnDiskGatewayDetails::new(&config.storage_paths.common_paths.gateway_details); - let network_gateways = if let Some(hardcoded_topology) = args + let available_gateways = if let Some(hardcoded_topology) = args .custom_mixnet .map(NymTopology::new_from_file) .transpose()? @@ -198,16 +203,16 @@ pub(crate) async fn execute(args: Init) -> Result<(), ClientError> { current_gateways(&mut rng, &config.base.client.nym_api_urls).await? }; - let init_details = nym_client_core::init::setup_gateway_from( - gateway_setup, - &key_store, - &details_store, - register_gateway, - Some(&network_gateways), - ) - .await - .tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))? - .details; + let gateway_setup = GatewaySetup::New { + specification: selection_spec, + available_gateways, + overwrite_data: register_gateway, + }; + + let init_details = + nym_client_core::init::setup_gateway(gateway_setup, &key_store, &details_store) + .await + .tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?; let config_save_location = config.default_location(); config.save_to_default_location().tap_err(|_| { @@ -222,7 +227,10 @@ pub(crate) async fn execute(args: Init) -> Result<(), ClientError> { eprintln!("Client configuration completed.\n"); - let init_results = InitResults::new(&config, &address, &init_details.gateway_details); + let GatewayDetails::Configured(gateway_details) = init_details.gateway_details else { + return Err(ClientCoreError::UnexpectedPersistedCustomGatewayDetails)?; + }; + let init_results = InitResults::new(&config, &address, &gateway_details); println!("{}", args.output.format(&init_results)); Ok(()) diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 4c440c4d99..3ebe929b16 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -133,7 +133,7 @@ fn persist_gateway_details( source: Box::new(source), }) })?; - let persisted_details = PersistedGatewayDetails::new(details, &shared_keys); + let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; details_store .store_to_disk(&persisted_details) .map_err(|source| { diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index 4672b0dae9..3b570da674 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -14,8 +14,9 @@ use nym_bin_common::output_format::OutputFormat; use nym_client_core::client::base_client::storage::gateway_details::OnDiskGatewayDetails; use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::config::GatewayEndpointConfig; +use nym_client_core::error::ClientCoreError; use nym_client_core::init::helpers::current_gateways; -use nym_client_core::init::GatewaySetup; +use nym_client_core::init::types::{GatewayDetails, GatewaySelectionSpecification, GatewaySetup}; use nym_crypto::asymmetric::identity; use nym_sphinx::addressing::clients::Recipient; use nym_topology::NymTopology; @@ -113,7 +114,7 @@ impl From for OverrideConfig { #[derive(Debug, Serialize)] pub struct InitResults { #[serde(flatten)] - client_core: nym_client_core::init::InitResults, + client_core: nym_client_core::init::types::InitResults, socks5_listening_port: u16, client_address: String, } @@ -121,7 +122,7 @@ pub struct InitResults { impl InitResults { fn new(config: &Config, address: &Recipient, gateway: &GatewayEndpointConfig) -> Self { Self { - client_core: nym_client_core::init::InitResults::new( + client_core: nym_client_core::init::types::InitResults::new( &config.core.base, address, gateway, @@ -176,7 +177,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), Socks5ClientError> { // Attempt to use a user-provided gateway, if possible let user_chosen_gateway_id = args.gateway; - let gateway_setup = GatewaySetup::new_fresh( + let selection_spec = GatewaySelectionSpecification::new( user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(args.latency_based_selection), ); @@ -193,7 +194,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), Socks5ClientError> { let details_store = OnDiskGatewayDetails::new(&config.storage_paths.common_paths.gateway_details); - let network_gateways = if let Some(hardcoded_topology) = args + let available_gateways = if let Some(hardcoded_topology) = args .custom_mixnet .map(NymTopology::new_from_file) .transpose()? @@ -205,16 +206,16 @@ pub(crate) async fn execute(args: Init) -> Result<(), Socks5ClientError> { current_gateways(&mut rng, &config.core.base.client.nym_api_urls).await? }; - let init_details = nym_client_core::init::setup_gateway_from( - gateway_setup, - &key_store, - &details_store, - register_gateway, - Some(&network_gateways), - ) - .await - .tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))? - .details; + let gateway_setup = GatewaySetup::New { + specification: selection_spec, + available_gateways, + overwrite_data: register_gateway, + }; + + let init_details = + nym_client_core::init::setup_gateway(gateway_setup, &key_store, &details_store) + .await + .tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?; // TODO: ask the service provider we specified for its interface version and set it in the config @@ -229,7 +230,10 @@ pub(crate) async fn execute(args: Init) -> Result<(), Socks5ClientError> { let address = init_details.client_address()?; - let init_results = InitResults::new(&config, &address, &init_details.gateway_details); + let GatewayDetails::Configured(gateway_details) = init_details.gateway_details else { + return Err(ClientCoreError::UnexpectedPersistedCustomGatewayDetails)?; + }; + let init_results = InitResults::new(&config, &address, &gateway_details); println!("{}", args.output.format(&init_results)); Ok(()) diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index b071ed5574..1f42cdc905 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -175,7 +175,7 @@ fn persist_gateway_details( source: Box::new(source), }) })?; - let persisted_details = PersistedGatewayDetails::new(details, &shared_keys); + let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; details_store .store_to_disk(&persisted_details) .map_err(|source| { diff --git a/clients/socks5/src/config/mod.rs b/clients/socks5/src/config/mod.rs index f4f74942cf..6f9c0c7fa4 100644 --- a/clients/socks5/src/config/mod.rs +++ b/clients/socks5/src/config/mod.rs @@ -62,7 +62,7 @@ pub struct Config { } impl NymConfigTemplate for Config { - fn template() -> &'static str { + fn template(&self) -> &'static str { CONFIG_TEMPLATE } } diff --git a/clients/socks5/src/config/persistence.rs b/clients/socks5/src/config/persistence.rs index 6df47997c9..696ae289e0 100644 --- a/clients/socks5/src/config/persistence.rs +++ b/clients/socks5/src/config/persistence.rs @@ -14,7 +14,7 @@ pub struct SocksClientPaths { impl SocksClientPaths { pub fn new_default>(base_data_directory: P) -> Self { SocksClientPaths { - common_paths: CommonClientPaths::new_default(base_data_directory), + common_paths: CommonClientPaths::new_base(base_data_directory), } } } diff --git a/common/bin-common/src/output_format/mod.rs b/common/bin-common/src/output_format/mod.rs index 386cc267a1..d3d57504ec 100644 --- a/common/bin-common/src/output_format/mod.rs +++ b/common/bin-common/src/output_format/mod.rs @@ -32,4 +32,14 @@ impl OutputFormat { OutputFormat::Json => serde_json::to_string(data).unwrap(), } } + + #[cfg(feature = "output_format")] + pub fn to_stdout(&self, data: &T) { + println!("{}", self.format(data)) + } + + #[cfg(feature = "output_format")] + pub fn to_stderr(&self, data: &T) { + eprintln!("{}", self.format(data)) + } } diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 22420b8731..a98eb783ab 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -11,7 +11,7 @@ rust-version = "1.66" async-trait = { workspace = true } base64 = "0.21.2" cfg-if = "1.0.0" -dashmap = "5.4.0" +dashmap = { workspace = true } dirs = "4.0" futures = { workspace = true } humantime-serde = "1.0" @@ -35,7 +35,6 @@ nym-config = { path = "../config" } nym-crypto = { path = "../crypto" } nym-explorer-client = { path = "../../explorer-api/explorer-client" } nym-gateway-client = { path = "../client-libs/gateway-client" } -#gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] } nym-gateway-requests = { path = "../../gateway/gateway-requests" } nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nym-sphinx = { path = "../nymsphinx" } diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index fe10b19418..07d676a6f3 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -8,6 +8,7 @@ use crate::client::base_client::storage::MixnetClientStorage; use crate::client::cover_traffic_stream::LoopCoverTrafficStream; use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; use crate::client::key_manager::persistence::KeyStore; +use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway}; use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; use crate::client::real_messages_control; use crate::client::real_messages_control::RealMessagesController; @@ -25,16 +26,18 @@ use crate::client::topology_control::{ }; use crate::config::{Config, DebugConfig}; use crate::error::ClientCoreError; -use crate::init::{setup_gateway, GatewaySetup, InitialisationDetails, InitialisationResult}; +use crate::init::{ + setup_gateway, + types::{GatewayDetails, GatewaySetup, InitialisationResult}, +}; use crate::{config, spawn_future}; use futures::channel::mpsc; -use log::{debug, info}; +use log::{debug, error, info}; use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::storage::Storage as CredentialStorage; -use nym_crypto::asymmetric::{encryption, identity}; +use nym_crypto::asymmetric::encryption; use nym_gateway_client::{ - AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, - MixnetMessageSender, + AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, }; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; @@ -42,10 +45,11 @@ use nym_sphinx::addressing::nodes::NodeIdentity; use nym_sphinx::params::PacketType; use nym_sphinx::receiver::{ReconstructedMessage, SphinxMessageReceiver}; use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; -use nym_task::{TaskClient, TaskManager}; +use nym_task::{TaskClient, TaskHandle}; use nym_topology::provider_trait::TopologyProvider; use nym_topology::HardcodedTopologyProvider; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; +use std::fmt::Debug; use std::path::Path; use std::sync::Arc; use url::Url; @@ -157,7 +161,12 @@ pub struct BaseClientBuilder<'a, C, S: MixnetClientStorage> { config: &'a Config, client_store: S, dkg_query_client: Option, + + wait_for_gateway: bool, custom_topology_provider: Option>, + custom_gateway_transceiver: Option>, + shutdown: Option, + setup_method: GatewaySetup, } @@ -175,7 +184,10 @@ where config: base_config, client_store, dkg_query_client, + wait_for_gateway: false, custom_topology_provider: None, + custom_gateway_transceiver: None, + shutdown: None, setup_method: GatewaySetup::MustLoad, } } @@ -186,6 +198,12 @@ where self } + #[must_use] + pub fn with_wait_for_gateway(mut self, wait_for_gateway: bool) -> Self { + self.wait_for_gateway = wait_for_gateway; + self + } + #[must_use] pub fn with_topology_provider( mut self, @@ -195,6 +213,18 @@ where self } + #[must_use] + pub fn with_gateway_transceiver(mut self, sender: Box) -> Self { + self.custom_gateway_transceiver = Some(sender); + self + } + + #[must_use] + pub fn with_shutdown(mut self, shutdown: TaskClient) -> Self { + self.shutdown = Some(shutdown); + self + } + pub fn with_stored_topology>( mut self, file: P, @@ -206,13 +236,13 @@ where // note: do **NOT** make this method public as its only valid usage is from within `start_base` // because it relies on the crypto keys being already loaded - fn mix_address(details: &InitialisationDetails) -> Recipient { + fn mix_address(details: &InitialisationResult) -> Recipient { Recipient::new( *details.managed_keys.identity_public_key(), *details.managed_keys.encryption_public_key(), // TODO: below only works under assumption that gateway address == gateway id // (which currently is true) - NodeIdentity::from_base58_string(&details.gateway_details.gateway_id).unwrap(), + NodeIdentity::from_base58_string(details.gateway_details.gateway_id()).unwrap(), ) } @@ -299,50 +329,37 @@ where config: &Config, initialisation_result: InitialisationResult, bandwidth_controller: Option>, - mixnet_message_sender: MixnetMessageSender, - ack_sender: AcknowledgementSender, + packet_router: PacketRouter, shutdown: TaskClient, ) -> Result, ClientCoreError> where ::StorageError: Send + Sync + 'static, ::StorageError: Send + Sync + 'static, { - let managed_keys = initialisation_result.details.managed_keys; + let managed_keys = initialisation_result.managed_keys; + let GatewayDetails::Configured(gateway_config) = initialisation_result.gateway_details + else { + return Err(ClientCoreError::UnexpectedPersistedCustomGatewayDetails); + }; let mut gateway_client = if let Some(existing_client) = initialisation_result.authenticated_ephemeral_client { - existing_client.upgrade( - mixnet_message_sender, - ack_sender, - config.debug.gateway_connection.gateway_response_timeout, - bandwidth_controller, - shutdown, - ) + existing_client.upgrade(packet_router, bandwidth_controller, shutdown) } else { - let gateway_config = initialisation_result.details.gateway_details; - - let gateway_address = gateway_config.gateway_listener.clone(); - let gateway_id = gateway_config.gateway_id; - - // TODO: in theory, at this point, this should be infallible - let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) - .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; - + let cfg = gateway_config.try_into()?; GatewayClient::new( - gateway_address, + cfg, managed_keys.identity_keypair(), - gateway_identity, Some(managed_keys.must_get_gateway_shared_key()), - mixnet_message_sender, - ack_sender, - config.debug.gateway_connection.gateway_response_timeout, + packet_router, bandwidth_controller, shutdown, ) + .with_disabled_credentials_mode(config.client.disabled_credentials_mode) + .with_response_timeout(config.debug.gateway_connection.gateway_response_timeout) }; let gateway_id = gateway_client.gateway_identity(); - gateway_client.set_disabled_credentials_mode(config.client.disabled_credentials_mode); let shared_key = gateway_client .authenticate_and_start() @@ -355,11 +372,48 @@ where } })?; - managed_keys.ensure_gateway_key(shared_key); + managed_keys.ensure_gateway_key(Some(shared_key)); Ok(gateway_client) } + async fn setup_gateway_transceiver( + custom_gateway_transceiver: Option>, + config: &Config, + initialisation_result: InitialisationResult, + bandwidth_controller: Option>, + packet_router: PacketRouter, + mut shutdown: TaskClient, + ) -> Result, ClientCoreError> + where + ::StorageError: Send + Sync + 'static, + ::StorageError: Send + Sync + 'static, + { + // if we have setup custom gateway sender and persisted details agree with it, return it + if let Some(mut custom_gateway_transceiver) = custom_gateway_transceiver { + return if !initialisation_result.gateway_details.is_custom() { + Err(ClientCoreError::CustomGatewaySelectionExpected) + } else { + // and make sure to invalidate the task client so we wouldn't cause premature shutdown + shutdown.mark_as_success(); + custom_gateway_transceiver.set_packet_router(packet_router)?; + Ok(custom_gateway_transceiver) + }; + } + + // otherwise, setup normal gateway client, etc + let gateway_client = Self::start_gateway_client( + config, + initialisation_result, + bandwidth_controller, + packet_router, + shutdown, + ) + .await?; + + Ok(Box::new(RemoteGateway::new(gateway_client))) + } + fn setup_topology_provider( custom_provider: Option>, provider_from_config: config::TopologyStructure, @@ -387,6 +441,8 @@ where topology_provider: Box, topology_config: config::Topology, topology_accessor: TopologyAccessor, + local_gateway: &NodeIdentity, + wait_for_gateway: bool, mut shutdown: TaskClient, ) -> Result<(), ClientCoreError> { let topology_refresher_config = @@ -410,6 +466,32 @@ where return Err(ClientCoreError::InsufficientNetworkTopology(err)); } + let gateway_wait_timeout = if wait_for_gateway { + Some(topology_config.max_startup_gateway_waiting_period) + } else { + None + }; + + if let Err(err) = topology_refresher + .ensure_contains_gateway(local_gateway) + .await + { + if let Some(waiting_timeout) = gateway_wait_timeout { + if let Err(err) = topology_refresher + .wait_for_gateway(local_gateway, waiting_timeout) + .await + { + error!( + "the gateway did not come back online within the specified timeout: {err}" + ); + return Err(err.into()); + } + } else { + error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}"); + return Err(err.into()); + } + } + if topology_config.disable_refreshing { // if we're not spawning the refresher, don't cause shutdown immediately info!("The topology refesher is not going to be started"); @@ -424,19 +506,12 @@ where Ok(()) } - // controller for sending packets to mixnet (either real traffic or cover traffic) - // TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership - // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for - // requests? fn start_mix_traffic_controller( - gateway_client: GatewayClient, + gateway_transceiver: Box, shutdown: TaskClient, - ) -> BatchMixMessageSender - where - ::StorageError: Send + Sync + 'static, - { + ) -> BatchMixMessageSender { info!("Starting mix traffic controller..."); - let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); + let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_transceiver); mix_traffic_controller.start_with_shutdown(shutdown); mix_tx } @@ -473,21 +548,12 @@ where setup_method: GatewaySetup, key_store: &S::KeyStore, details_store: &S::GatewayDetailsStore, - overwrite_data: bool, - validator_servers: Option<&[Url]>, ) -> Result where ::StorageError: Sync + Send, ::StorageError: Sync + Send, { - setup_gateway( - setup_method, - key_store, - details_store, - overwrite_data, - validator_servers, - ) - .await + setup_gateway(setup_method, key_store, details_store).await } pub async fn start_base(mut self) -> Result @@ -505,17 +571,11 @@ where self.setup_method, self.client_store.key_store(), self.client_store.gateway_details_store(), - false, - Some(&self.config.client.nym_api_urls), ) .await?; let (reply_storage_backend, credential_store) = self.client_store.into_runtime_stores(); - let bandwidth_controller = self - .dkg_query_client - .map(|client| BandwidthController::new(credential_store, client)); - // channels for inter-component communication // TODO: make the channels be internally created by the relevant components // rather than creating them here, so say for example the buffer controller would create the request channels @@ -536,31 +596,25 @@ where let shared_topology_accessor = TopologyAccessor::new(); // Shutdown notifier for signalling tasks to stop - let task_manager = TaskManager::default(); + let shutdown = self + .shutdown + .map(Into::::into) + .unwrap_or_default() + .name_if_unnamed("BaseNymClient"); // channels responsible for dealing with reply-related fun let (reply_controller_sender, reply_controller_receiver) = reply_controller::requests::new_control_channels(); - let self_address = Self::mix_address(&init_res.details); - let ack_key = init_res.details.managed_keys.ack_key(); - let encryption_keys = init_res.details.managed_keys.encryption_keypair(); + let self_address = Self::mix_address(&init_res); + let ack_key = init_res.managed_keys.ack_key(); + let encryption_keys = init_res.managed_keys.encryption_keypair(); // the components are started in very specific order. Unless you know what you are doing, // do not change that. - let gateway_client = Self::start_gateway_client( - self.config, - init_res, - bandwidth_controller, - mixnet_messages_sender, - ack_sender, - task_manager.subscribe(), - ) - .await?; - - let reply_storage = - Self::setup_persistent_reply_storage(reply_storage_backend, task_manager.subscribe()) - .await?; + let bandwidth_controller = self + .dkg_query_client + .map(|client| BandwidthController::new(credential_store, client)); let topology_provider = Self::setup_topology_provider( self.custom_topology_provider.take(), @@ -568,11 +622,36 @@ where self.config.get_nym_api_endpoints(), ); + // needs to be started as the first thing to block if required waiting for the gateway Self::start_topology_refresher( topology_provider, self.config.debug.topology, shared_topology_accessor.clone(), - task_manager.subscribe(), + self_address.gateway(), + self.wait_for_gateway, + shutdown.fork("topology_refresher"), + ) + .await?; + + let gateway_packet_router = PacketRouter::new( + ack_sender, + mixnet_messages_sender, + shutdown.get_handle().named("gateway-packet-router"), + ); + + let gateway_transceiver = Self::setup_gateway_transceiver( + self.custom_gateway_transceiver, + self.config, + init_res, + bandwidth_controller, + gateway_packet_router, + shutdown.fork("gateway_transceiver"), + ) + .await?; + + let reply_storage = Self::setup_persistent_reply_storage( + reply_storage_backend, + shutdown.fork("persistent_reply_storage"), ) .await?; @@ -582,15 +661,17 @@ where mixnet_messages_receiver, reply_storage.key_storage(), reply_controller_sender.clone(), - task_manager.subscribe(), + shutdown.fork("received_messages_buffer"), ); // The message_sender is the transmitter for any component generating sphinx packets // that are to be sent to the mixnet. They are used by cover traffic stream and real // traffic stream. // The MixTrafficController then sends the actual traffic - let message_sender = - Self::start_mix_traffic_controller(gateway_client, task_manager.subscribe()); + let message_sender = Self::start_mix_traffic_controller( + gateway_transceiver, + shutdown.fork("mix_traffic_controller"), + ); // Channels that the websocket listener can use to signal downstream to the real traffic // controller that connections are closed. @@ -617,7 +698,7 @@ where reply_controller_receiver, shared_lane_queue_lengths.clone(), client_connection_rx, - task_manager.subscribe(), + shutdown.fork("real_traffic_controller"), self.config.debug.traffic.packet_type, ); @@ -633,7 +714,7 @@ where self_address, shared_topology_accessor.clone(), message_sender, - task_manager.subscribe(), + shutdown.fork("cover_traffic_stream"), ); } @@ -658,7 +739,7 @@ where reply_controller_sender, topology_accessor: shared_topology_accessor, }, - task_manager, + task_handle: shutdown, }) } } @@ -669,5 +750,5 @@ pub struct BaseClient { pub client_output: ClientOutputStatus, pub client_state: ClientState, - pub task_manager: TaskManager, + pub task_handle: TaskHandle, } diff --git a/common/client-core/src/client/base_client/storage/gateway_details.rs b/common/client-core/src/client/base_client/storage/gateway_details.rs index bcacdf731e..5493fb93a2 100644 --- a/common/client-core/src/client/base_client/storage/gateway_details.rs +++ b/common/client-core/src/client/base_client/storage/gateway_details.rs @@ -2,8 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::GatewayEndpointConfig; +use crate::error::ClientCoreError; +use crate::init::types::{EmptyCustomDetails, GatewayDetails}; use async_trait::async_trait; +use log::error; use nym_gateway_requests::registration::handshake::SharedKeys; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::error::Error; @@ -13,19 +17,61 @@ use zeroize::Zeroizing; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait GatewayDetailsStore { +pub trait GatewayDetailsStore { type StorageError: Error; - async fn load_gateway_details(&self) -> Result; + async fn load_gateway_details(&self) -> Result, Self::StorageError> + where + T: DeserializeOwned + Send + Sync; async fn store_gateway_details( &self, - details: &PersistedGatewayDetails, - ) -> Result<(), Self::StorageError>; + details: &PersistedGatewayDetails, + ) -> Result<(), Self::StorageError> + where + T: Serialize + Send + Sync; } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PersistedGatewayDetails { +#[serde(untagged)] +pub enum PersistedGatewayDetails { + /// Standard details of a remote gateway + Default(PersistedGatewayConfig), + + /// Custom gateway setup, such as for a client embedded inside gateway itself + Custom(PersistedCustomGatewayDetails), +} + +impl PersistedGatewayDetails { + // TODO: this should probably allow for custom verification over T + pub fn validate(&self, shared_key: Option<&SharedKeys>) -> Result<(), ClientCoreError> { + match self { + PersistedGatewayDetails::Default(details) => { + if !details.verify( + shared_key + .ok_or(ClientCoreError::UnavailableSharedKey)? + .deref(), + ) { + Err(ClientCoreError::MismatchedGatewayDetails { + gateway_id: details.details.gateway_id.clone(), + }) + } else { + Ok(()) + } + } + PersistedGatewayDetails::Custom(_) => { + if shared_key.is_some() { + error!("using custom persisted gateway setup with shared key present - are you sure that's what you want?"); + // but technically we could still continue. just ignore the key + } + Ok(()) + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PersistedGatewayConfig { // TODO: should we also verify correctness of the details themselves? // i.e. we could include a checksum or tag (via the shared keys) // counterargument: if we wanted to modify, say, the host information in the stored file on disk, @@ -38,13 +84,16 @@ pub struct PersistedGatewayDetails { pub(crate) details: GatewayEndpointConfig, } -impl From for GatewayEndpointConfig { - fn from(value: PersistedGatewayDetails) -> Self { - value.details - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PersistedCustomGatewayDetails { + // whatever custom method is used, gateway's identity must be known + pub gateway_id: String, + + #[serde(flatten)] + pub additional_data: T, } -impl PersistedGatewayDetails { +impl PersistedGatewayConfig { pub fn new(details: GatewayEndpointConfig, shared_key: &SharedKeys) -> Self { let key_bytes = Zeroizing::new(shared_key.to_bytes()); @@ -52,7 +101,7 @@ impl PersistedGatewayDetails { key_hasher.update(&key_bytes); let key_hash = key_hasher.finalize().to_vec(); - PersistedGatewayDetails { key_hash, details } + PersistedGatewayConfig { key_hash, details } } pub fn verify(&self, shared_key: &SharedKeys) -> bool { @@ -66,6 +115,50 @@ impl PersistedGatewayDetails { } } +impl PersistedGatewayDetails { + pub fn new( + details: GatewayDetails, + shared_key: Option<&SharedKeys>, + ) -> Result { + match details { + GatewayDetails::Configured(cfg) => { + let shared_key = shared_key.ok_or(ClientCoreError::UnavailableSharedKey)?; + Ok(PersistedGatewayDetails::Default( + PersistedGatewayConfig::new(cfg, shared_key), + )) + } + GatewayDetails::Custom(custom) => Ok(PersistedGatewayDetails::Custom(custom.into())), + } + } + + pub fn is_custom(&self) -> bool { + matches!(self, PersistedGatewayDetails::Custom(..)) + } + + pub fn matches(&self, other: &GatewayDetails) -> bool + where + T: PartialEq, + { + match self { + PersistedGatewayDetails::Default(default) => { + if let GatewayDetails::Configured(other_configured) = other { + &default.details == other_configured + } else { + false + } + } + PersistedGatewayDetails::Custom(custom) => { + if let GatewayDetails::Custom(other_custom) = other { + custom.gateway_id == other_custom.gateway_id + && custom.additional_data == other_custom.additional_data + } else { + false + } + } + } + } +} + // helper to make Vec serialization use base64 representation to make it human readable // so that it would be easier for users to copy contents from the disk if they wanted to use it elsewhere mod base64 { @@ -116,7 +209,10 @@ impl OnDiskGatewayDetails { } } - pub fn load_from_disk(&self) -> Result { + pub fn load_from_disk(&self) -> Result, OnDiskGatewayDetailsError> + where + T: DeserializeOwned, + { let file = std::fs::File::open(&self.file_location).map_err(|err| { OnDiskGatewayDetailsError::LoadFailure { path: self.file_location.display().to_string(), @@ -127,10 +223,13 @@ impl OnDiskGatewayDetails { Ok(serde_json::from_reader(file)?) } - pub fn store_to_disk( + pub fn store_to_disk( &self, - details: &PersistedGatewayDetails, - ) -> Result<(), OnDiskGatewayDetailsError> { + details: &PersistedGatewayDetails, + ) -> Result<(), OnDiskGatewayDetailsError> + where + T: Serialize, + { // ensure the whole directory structure exists if let Some(parent_dir) = &self.file_location.parent() { std::fs::create_dir_all(parent_dir).map_err(|err| { @@ -170,8 +269,8 @@ impl GatewayDetailsStore for OnDiskGatewayDetails { } #[derive(Default)] -pub struct InMemGatewayDetails { - details: Mutex>, +pub struct InMemGatewayDetails { + details: Mutex>>, } #[derive(Debug, thiserror::Error)] diff --git a/common/client-core/src/client/base_client/storage/mod.rs b/common/client-core/src/client/base_client/storage/mod.rs index 3d05044d4f..60e3ff9a73 100644 --- a/common/client-core/src/client/base_client/storage/mod.rs +++ b/common/client-core/src/client/base_client/storage/mod.rs @@ -38,9 +38,6 @@ pub trait MixnetClientStorage { type CredentialStore: CredentialStorage; type GatewayDetailsStore: GatewayDetailsStore; - // this is a TERRIBLE name... - // fn into_split(self) -> (Self::KeyStore, Self::ReplyStore, Self::CredentialStore, Self::GatewayDetailsStore); - fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore); fn key_store(&self) -> &Self::KeyStore; diff --git a/common/client-core/src/client/key_manager/mod.rs b/common/client-core/src/client/key_manager/mod.rs index 4c77f23632..6fb3858eb7 100644 --- a/common/client-core/src/client/key_manager/mod.rs +++ b/common/client-core/src/client/key_manager/mod.rs @@ -103,7 +103,7 @@ impl ManagedKeys { pub fn gateway_shared_key(&self) -> Option> { match self { ManagedKeys::Initial(_) => None, - ManagedKeys::FullyDerived(keys) => Some(keys.gateway_shared_key()), + ManagedKeys::FullyDerived(keys) => keys.gateway_shared_key(), ManagedKeys::Invalidated => unreachable!("the managed keys got invalidated"), } } @@ -124,10 +124,26 @@ impl ManagedKeys { } } - pub fn ensure_gateway_key(&self, gateway_shared_key: Arc) { + pub fn ensure_gateway_key(&self, gateway_shared_key: Option>) { if let ManagedKeys::FullyDerived(key_manager) = &self { - if !Arc::ptr_eq(&key_manager.gateway_shared_key, &gateway_shared_key) - || key_manager.gateway_shared_key != gateway_shared_key + if self.gateway_shared_key().is_none() && gateway_shared_key.is_none() { + // the key doesn't exist in either state + return; + } + + if gateway_shared_key.is_some() && self.gateway_shared_key().is_none() + || gateway_shared_key.is_none() && self.gateway_shared_key().is_some() + { + // if one is provided whilst the other is not... + // TODO: should this actually panic or return an error? would this branch be possible + // under normal operation? + panic!("inconsistent re-derived gateway key") + } + + // here we know both keys MUST exist + let provided = gateway_shared_key.unwrap(); + if !Arc::ptr_eq(key_manager.must_get_gateway_shared_key(), &provided) + || *key_manager.must_get_gateway_shared_key() != provided { // this should NEVER happen thus panic here panic!("derived fresh gateway shared key whilst already holding one!") @@ -137,12 +153,12 @@ impl ManagedKeys { pub async fn deal_with_gateway_key( &mut self, - gateway_shared_key: Arc, + gateway_shared_key: Option>, key_store: &S, ) -> Result<(), S::StorageError> { let key_manager = match std::mem::replace(self, ManagedKeys::Invalidated) { ManagedKeys::Initial(keys) => { - let key_manager = keys.insert_gateway_shared_key(gateway_shared_key); + let key_manager = keys.insert_maybe_gateway_shared_key(gateway_shared_key); key_manager.persist_keys(key_store).await?; key_manager } @@ -184,7 +200,10 @@ impl KeyManagerBuilder { } } - pub fn insert_gateway_shared_key(self, gateway_shared_key: Arc) -> KeyManager { + pub fn insert_maybe_gateway_shared_key( + self, + gateway_shared_key: Option>, + ) -> KeyManager { KeyManager { identity_keypair: self.identity_keypair, encryption_keypair: self.encryption_keypair, @@ -222,7 +241,11 @@ pub struct KeyManager { encryption_keypair: Arc, /// shared key derived with the gateway during "registration handshake" - gateway_shared_key: Arc, + // I'm not a fan of how we broke the nice transition of `KeyManagerBuilder` -> `KeyManager` + // by making this field optional. + // However, it has to be optional for when we use embedded NR inside a gateway, + // since it won't have a shared key (because why would it?) + gateway_shared_key: Option>, /// key used for producing and processing acknowledgement packets. ack_key: Arc, @@ -232,13 +255,13 @@ impl KeyManager { pub fn from_keys( id_keypair: identity::KeyPair, enc_keypair: encryption::KeyPair, - gateway_shared_key: SharedKeys, + gateway_shared_key: Option, ack_key: AckKey, ) -> Self { Self { identity_keypair: Arc::new(id_keypair), encryption_keypair: Arc::new(enc_keypair), - gateway_shared_key: Arc::new(gateway_shared_key), + gateway_shared_key: gateway_shared_key.map(Arc::new), ack_key: Arc::new(ack_key), } } @@ -265,13 +288,23 @@ impl KeyManager { Arc::clone(&self.ack_key) } + fn must_get_gateway_shared_key(&self) -> &Arc { + self.gateway_shared_key + .as_ref() + .expect("gateway shared key is unavailable") + } + + pub fn uses_custom_gateway(&self) -> bool { + self.gateway_shared_key.is_none() + } + /// Gets an atomically reference counted pointer to [`SharedKey`]. - pub fn gateway_shared_key(&self) -> Arc { - Arc::clone(&self.gateway_shared_key) + pub fn gateway_shared_key(&self) -> Option> { + self.gateway_shared_key.as_ref().map(Arc::clone) } pub fn remove_gateway_key(self) -> KeyManagerBuilder { - if Arc::strong_count(&self.gateway_shared_key) > 1 { + if Arc::strong_count(self.must_get_gateway_shared_key()) > 1 { panic!("attempted to remove gateway key whilst still holding multiple references!") } KeyManagerBuilder { diff --git a/common/client-core/src/client/key_manager/persistence.rs b/common/client-core/src/client/key_manager/persistence.rs index 0475de3990..984019eb93 100644 --- a/common/client-core/src/client/key_manager/persistence.rs +++ b/common/client-core/src/client/key_manager/persistence.rs @@ -88,20 +88,20 @@ impl OnDiskKeys { pub fn ephemeral_load_gateway_keys( &self, ) -> Result, OnDiskKeysError> { - self.load_key(self.paths.gateway_shared_key(), "gateway shared keys") + self.load_key(self.paths.gateway_shared_key(), "gateway shared") .map(zeroize::Zeroizing::new) } #[doc(hidden)] pub fn load_encryption_keypair(&self) -> Result { let encryption_paths = self.paths.encryption_key_pair_path(); - self.load_keypair(encryption_paths, "encryption keys") + self.load_keypair(encryption_paths, "encryption") } #[doc(hidden)] pub fn load_identity_keypair(&self) -> Result { let identity_paths = self.paths.identity_key_pair_path(); - self.load_keypair(identity_paths, "identity keys") + self.load_keypair(identity_paths, "identity") } fn load_key( @@ -161,8 +161,9 @@ impl OnDiskKeys { let encryption_keypair = self.load_encryption_keypair()?; let ack_key: AckKey = self.load_key(self.paths.ack_key(), "ack key")?; - let gateway_shared_key: SharedKeys = - self.load_key(self.paths.gateway_shared_key(), "gateway shared keys")?; + let gateway_shared_key: Option = self + .load_key(self.paths.gateway_shared_key(), "gateway shared keys") + .ok(); Ok(KeyManager::from_keys( identity_keypair, @@ -173,6 +174,8 @@ impl OnDiskKeys { } fn store_keys(&self, keys: &KeyManager) -> Result<(), OnDiskKeysError> { + use std::ops::Deref; + let identity_paths = self.paths.identity_key_pair_path(); let encryption_paths = self.paths.encryption_key_pair_path(); @@ -188,11 +191,14 @@ impl OnDiskKeys { )?; self.store_key(keys.ack_key.as_ref(), self.paths.ack_key(), "ack key")?; - self.store_key( - keys.gateway_shared_key.as_ref(), - self.paths.gateway_shared_key(), - "gateway shared keys", - )?; + + if let Some(shared_keys) = &keys.gateway_shared_key { + self.store_key( + shared_keys.deref(), + self.paths.gateway_shared_key(), + "gateway shared keys", + )?; + } Ok(()) } diff --git a/common/client-core/src/client/mix_traffic.rs b/common/client-core/src/client/mix_traffic/mod.rs similarity index 74% rename from common/client-core/src/client/mix_traffic.rs rename to common/client-core/src/client/mix_traffic/mod.rs index cb12a55d85..91c652efba 100644 --- a/common/client-core/src/client/mix_traffic.rs +++ b/common/client-core/src/client/mix_traffic/mod.rs @@ -1,24 +1,26 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::mix_traffic::transceiver::GatewayTransceiver; use crate::spawn_future; use log::*; -use nym_credential_storage::storage::Storage; -use nym_gateway_client::GatewayClient; use nym_sphinx::forwarding::packet::MixPacket; -use nym_validator_client::nyxd::contract_traits::DkgQueryClient; pub type BatchMixMessageSender = tokio::sync::mpsc::Sender>; pub type BatchMixMessageReceiver = tokio::sync::mpsc::Receiver>; +pub mod transceiver; + // We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer. pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32; const MAX_FAILURE_COUNT: usize = 100; -pub struct MixTrafficController { - // TODO: most likely to be replaced by some higher level construct as - // later on gateway_client will need to be accessible by other entities - gateway_client: GatewayClient, +// that's also disgusting. +pub struct Empty; + +pub struct MixTrafficController { + gateway_transceiver: Box, + mix_rx: BatchMixMessageReceiver, // TODO: this is temporary work-around. @@ -26,20 +28,31 @@ pub struct MixTrafficController { consecutive_gateway_failure_count: usize, } -impl MixTrafficController -where - C: DkgQueryClient + Sync + Send + 'static, - St: Storage + 'static, - ::StorageError: Send + Sync + 'static, -{ - pub fn new( - gateway_client: GatewayClient, - ) -> (MixTrafficController, BatchMixMessageSender) { +impl MixTrafficController { + pub fn new(gateway_transceiver: T) -> (MixTrafficController, BatchMixMessageSender) + where + T: GatewayTransceiver + Send + 'static, + { let (message_sender, message_receiver) = tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE); ( MixTrafficController { - gateway_client, + gateway_transceiver: Box::new(gateway_transceiver), + mix_rx: message_receiver, + consecutive_gateway_failure_count: 0, + }, + message_sender, + ) + } + + pub fn new_dynamic( + gateway_transceiver: Box, + ) -> (MixTrafficController, BatchMixMessageSender) { + let (message_sender, message_receiver) = + tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE); + ( + MixTrafficController { + gateway_transceiver, mix_rx: message_receiver, consecutive_gateway_failure_count: 0, }, @@ -52,16 +65,16 @@ where let result = if mix_packets.len() == 1 { let mix_packet = mix_packets.pop().unwrap(); - self.gateway_client.send_mix_packet(mix_packet).await + self.gateway_transceiver.send_mix_packet(mix_packet).await } else { - self.gateway_client + self.gateway_transceiver .batch_send_mix_packets(mix_packets) .await }; match result { Err(err) => { - error!("Failed to send sphinx packet(s) to the gateway! - {err}"); + error!("Failed to send sphinx packet(s) to the gateway: {err}"); self.consecutive_gateway_failure_count += 1; if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT { // todo: in the future this should initiate a 'graceful' shutdown or try diff --git a/common/client-core/src/client/mix_traffic/transceiver.rs b/common/client-core/src/client/mix_traffic/transceiver.rs new file mode 100644 index 0000000000..b078e02966 --- /dev/null +++ b/common/client-core/src/client/mix_traffic/transceiver.rs @@ -0,0 +1,262 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use log::{debug, error}; +use nym_crypto::asymmetric::identity; +use nym_gateway_client::GatewayClient; +pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter}; +use nym_sphinx::forwarding::packet::MixPacket; +use std::fmt::Debug; +use thiserror::Error; + +#[cfg(not(target_arch = "wasm32"))] +use futures::channel::{mpsc, oneshot}; + +// we need to type erase the error type since we can't have dynamic associated types alongside dynamic dispatch +#[derive(Debug, Error)] +#[error(transparent)] +pub struct ErasedGatewayError(Box); + +fn erase_err(err: E) -> ErasedGatewayError { + ErasedGatewayError(Box::new(err)) +} + +/// This combines combines the functionalities of being able to send and receive mix packets. +pub trait GatewayTransceiver: GatewaySender + GatewayReceiver { + fn gateway_identity(&self) -> identity::PublicKey; +} + +/// This trait defines the functionality of sending `MixPacket` into the mixnet, +/// usually through a gateway. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait GatewaySender { + async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError>; + + async fn batch_send_mix_packets( + &mut self, + packets: Vec, + ) -> Result<(), ErasedGatewayError> { + // allow for optimisation when sending multiple packets + for packet in packets { + self.send_mix_packet(packet).await?; + } + Ok(()) + } +} + +/// this trait defines the functionality of being able to correctly route +/// packets received from the mixnet, i.e. acks and 'proper' messages. +pub trait GatewayReceiver { + // ughhhh I really dislike this method, but couldn't come up wih anything better + // ideally this would have been an associated type, but heh. we can't. + fn set_packet_router( + &mut self, + _packet_router: PacketRouter, + ) -> Result<(), ErasedGatewayError> { + debug!("no-op packet router setup"); + Ok(()) + } +} + +// to allow for dynamic dispatch +impl GatewayTransceiver for Box { + #[inline] + fn gateway_identity(&self) -> identity::PublicKey { + (**self).gateway_identity() + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GatewaySender for Box { + #[inline] + async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> { + (**self).send_mix_packet(packet).await + } + + #[inline] + async fn batch_send_mix_packets( + &mut self, + packets: Vec, + ) -> Result<(), ErasedGatewayError> { + (**self).batch_send_mix_packets(packets).await + } +} + +impl GatewayReceiver for Box { + #[inline] + fn set_packet_router(&mut self, packet_router: PacketRouter) -> Result<(), ErasedGatewayError> { + (**self).set_packet_router(packet_router) + } +} + +/// Gateway to which the client is connected through a socket. +/// Most likely through a websocket. +pub struct RemoteGateway { + gateway_client: GatewayClient, +} + +impl RemoteGateway { + pub fn new(gateway_client: GatewayClient) -> Self { + Self { gateway_client } + } +} + +impl GatewayTransceiver for RemoteGateway +where + C: Send, + St: Send, +{ + fn gateway_identity(&self) -> identity::PublicKey { + self.gateway_client.gateway_identity() + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GatewaySender for RemoteGateway +where + C: Send, + St: Send, +{ + async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> { + self.gateway_client + .send_mix_packet(packet) + .await + .map_err(erase_err) + } + + async fn batch_send_mix_packets( + &mut self, + packets: Vec, + ) -> Result<(), ErasedGatewayError> { + self.gateway_client + .batch_send_mix_packets(packets) + .await + .map_err(erase_err) + } +} + +impl GatewayReceiver for RemoteGateway {} + +#[derive(Debug, Error)] +pub enum LocalGatewayError { + #[error("attempted to set the packet router for the second time")] + PacketRouterAlreadySet, + + #[error("failed to setup packet router - has the receiver been dropped?")] + FailedPacketRouterSetup, +} + +/// Gateway running within the same process. +#[cfg(not(target_arch = "wasm32"))] +pub struct LocalGateway { + /// Identity of the locally managed gateway + local_identity: identity::PublicKey, + + // 'sender' part + /// Channel responsible for taking mix packets and forwarding them further into the further mixnet layers. + packet_forwarder: mpsc::UnboundedSender, + + // 'receiver' part + packet_router_tx: Option>, +} + +#[cfg(not(target_arch = "wasm32"))] +impl LocalGateway { + pub fn new( + local_identity: identity::PublicKey, + packet_forwarder: mpsc::UnboundedSender, + packet_router_tx: oneshot::Sender, + ) -> Self { + LocalGateway { + local_identity, + packet_forwarder, + packet_router_tx: Some(packet_router_tx), + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +mod nonwasm_sealed { + use super::*; + + impl GatewayTransceiver for LocalGateway { + fn gateway_identity(&self) -> identity::PublicKey { + self.local_identity + } + } + + #[async_trait] + impl GatewaySender for LocalGateway { + async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> { + self.packet_forwarder + .unbounded_send(packet) + .map_err(|err| err.into_send_error()) + .map_err(erase_err) + } + } + + impl GatewayReceiver for LocalGateway { + fn set_packet_router( + &mut self, + packet_router: PacketRouter, + ) -> Result<(), ErasedGatewayError> { + let Some(packet_routex_tx) = self.packet_router_tx.take() else { + return Err(erase_err(LocalGatewayError::PacketRouterAlreadySet)); + }; + + packet_routex_tx + .send(packet_router) + .map_err(|_| erase_err(LocalGatewayError::FailedPacketRouterSetup)) + } + } +} + +// if we ever decided to start writing unit tests... : ) +pub struct MockGateway { + dummy_identity: identity::PublicKey, + packet_router: Option, + sent: Vec, +} + +impl Default for MockGateway { + fn default() -> Self { + MockGateway { + dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7" + .parse() + .unwrap(), + packet_router: None, + sent: vec![], + } + } +} + +#[derive(Debug, Error)] +#[error("mock gateway error")] +pub struct MockGatewayError; + +impl GatewayReceiver for MockGateway { + // TODO: that's frustrating. can't do anything about the behaviour here since all the routing is in the `PacketRouter`... + fn set_packet_router(&mut self, packet_router: PacketRouter) -> Result<(), ErasedGatewayError> { + self.packet_router = Some(packet_router); + Ok(()) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GatewaySender for MockGateway { + async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> { + self.sent.push(packet); + Ok(()) + } +} + +impl GatewayTransceiver for MockGateway { + fn gateway_identity(&self) -> identity::PublicKey { + self.dummy_identity + } +} diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index 18d2959181..0034e00567 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -71,7 +71,7 @@ impl AcknowledgementListener { while !shutdown.is_shutdown() { tokio::select! { acks = self.ack_receiver.next() => match acks { - Some(acks) => {self.handle_ack_receiver_item(acks).await} + Some(acks) => self.handle_ack_receiver_item(acks).await, None => { log::trace!("AcknowledgementListener: Stopping since channel closed"); break; diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index 5f2bf6f80d..acf0f54ca9 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -260,7 +260,7 @@ where let mut sent_notification_listener = self.sent_notification_listener; let mut action_controller = self.action_controller; - let shutdown_handle = shutdown.clone(); + let shutdown_handle = shutdown.fork("acknowledgement_listener"); spawn_future(async move { acknowledgement_listener .run_with_shutdown(shutdown_handle) @@ -268,7 +268,7 @@ where debug!("The acknowledgement listener has finished execution!"); }); - let shutdown_handle = shutdown.clone(); + let shutdown_handle = shutdown.fork("input_message_listener"); spawn_future(async move { input_message_listener .run_with_shutdown(shutdown_handle) @@ -276,7 +276,7 @@ where debug!("The input listener has finished execution!"); }); - let shutdown_handle = shutdown.clone(); + let shutdown_handle = shutdown.fork("retransmission_request_listener"); spawn_future(async move { retransmission_request_listener .run_with_shutdown(shutdown_handle, packet_type) @@ -284,7 +284,7 @@ where debug!("The retransmission request listener has finished execution!"); }); - let shutdown_handle = shutdown.clone(); + let shutdown_handle = shutdown.fork("sent_notification_listener"); spawn_future(async move { sent_notification_listener .run_with_shutdown(shutdown_handle) @@ -293,7 +293,9 @@ where }); spawn_future(async move { - action_controller.run_with_shutdown(shutdown).await; + action_controller + .run_with_shutdown(shutdown.with_suffix("action_controller")) + .await; debug!("The controller has finished execution!"); }); } diff --git a/common/client-core/src/client/real_messages_control/mod.rs b/common/client-core/src/client/real_messages_control/mod.rs index 32c913006d..9c57798fe7 100644 --- a/common/client-core/src/client/real_messages_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/mod.rs @@ -213,17 +213,17 @@ impl RealMessagesController { let ack_control = self.ack_control; let mut reply_control = self.reply_control; - let shutdown_handle = shutdown.clone(); + let shutdown_handle = shutdown.fork("out_queue_control"); spawn_future(async move { out_queue_control.run_with_shutdown(shutdown_handle).await; debug!("The out queue controller has finished execution!"); }); - let shutdown_handle = shutdown.clone(); + let shutdown_handle = shutdown.fork("reply_control"); spawn_future(async move { reply_control.run_with_shutdown(shutdown_handle).await; debug!("The reply controller has finished execution!"); }); - ack_control.start_with_shutdown(shutdown, packet_type); + ack_control.start_with_shutdown(shutdown.with_suffix("ack_control"), packet_type); } } diff --git a/common/client-core/src/client/received_buffer.rs b/common/client-core/src/client/received_buffer.rs index b9c93d8922..3421ed86ff 100644 --- a/common/client-core/src/client/received_buffer.rs +++ b/common/client-core/src/client/received_buffer.rs @@ -500,7 +500,7 @@ impl ReceivedMessagesBufferControll let mut fragmented_message_receiver = self.fragmented_message_receiver; let mut request_receiver = self.request_receiver; - let shutdown_handle = shutdown.clone(); + let shutdown_handle = shutdown.fork("fragmented_message_receiver"); spawn_future(async move { match fragmented_message_receiver .run_with_shutdown(shutdown_handle) @@ -511,7 +511,9 @@ impl ReceivedMessagesBufferControll } }); spawn_future(async move { - request_receiver.run_with_shutdown(shutdown).await; + request_receiver + .run_with_shutdown(shutdown.with_suffix("request_receiver")) + .await; }); } } diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index ed426dde50..ae50128942 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -5,10 +5,17 @@ use crate::spawn_future; pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit}; use futures::StreamExt; use log::*; +use nym_sphinx::addressing::nodes::NodeIdentity; use nym_topology::provider_trait::TopologyProvider; use nym_topology::NymTopologyError; use std::time::Duration; +#[cfg(not(target_arch = "wasm32"))] +use tokio::time::sleep; + +#[cfg(target_arch = "wasm32")] +use wasmtimer::tokio::sleep; + mod accessor; pub mod geo_aware_provider; pub(crate) mod nym_api_provider; @@ -86,6 +93,54 @@ impl TopologyRefresher { self.topology_accessor.ensure_is_routable().await } + pub async fn ensure_contains_gateway( + &self, + gateway: &NodeIdentity, + ) -> Result<(), NymTopologyError> { + let topology = self + .topology_accessor + .current_topology() + .await + .ok_or(NymTopologyError::EmptyNetworkTopology)?; + if !topology.gateway_exists(gateway) { + return Err(NymTopologyError::NonExistentGatewayError { + identity_key: gateway.to_base58_string(), + }); + } + + Ok(()) + } + + pub async fn wait_for_gateway( + &mut self, + gateway: &NodeIdentity, + timeout_duration: Duration, + ) -> Result<(), NymTopologyError> { + info!( + "going to wait for at most {timeout_duration:?} for gateway '{gateway}' to come online" + ); + + let deadline = sleep(timeout_duration); + tokio::pin!(deadline); + + loop { + tokio::select! { + _ = &mut deadline => { + return Err(NymTopologyError::TimedOutWaitingForGateway { + identity_key: gateway.to_base58_string() + }) + } + _ = self.try_refresh() => { + if self.ensure_contains_gateway(gateway).await.is_ok() { + return Ok(()) + } + info!("gateway '{gateway}' is still not online..."); + sleep(self.refresh_rate).await + } + } + } + } + pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) { spawn_future(async move { debug!("Started TopologyRefresher with graceful shutdown support"); diff --git a/common/client-core/src/config/disk_persistence/keys_paths.rs b/common/client-core/src/config/disk_persistence/keys_paths.rs index 3ae1400a19..aeab0359ea 100644 --- a/common/client-core/src/config/disk_persistence/keys_paths.rs +++ b/common/client-core/src/config/disk_persistence/keys_paths.rs @@ -35,7 +35,7 @@ pub struct ClientKeysPaths { } impl ClientKeysPaths { - pub fn new_default>(base_data_directory: P) -> Self { + pub fn new_base>(base_data_directory: P) -> Self { let base_dir = base_data_directory.as_ref(); ClientKeysPaths { diff --git a/common/client-core/src/config/disk_persistence/mod.rs b/common/client-core/src/config/disk_persistence/mod.rs index 6527283117..b323d059dc 100644 --- a/common/client-core/src/config/disk_persistence/mod.rs +++ b/common/client-core/src/config/disk_persistence/mod.rs @@ -29,14 +29,14 @@ pub struct CommonClientPaths { } impl CommonClientPaths { - pub fn new_default>(base_data_directory: P) -> Self { + pub fn new_base>(base_data_directory: P) -> Self { let base_dir = base_data_directory.as_ref(); CommonClientPaths { credentials_database: base_dir.join(DEFAULT_CREDENTIALS_DB_FILENAME), reply_surb_database: base_dir.join(DEFAULT_REPLY_SURB_DB_FILENAME), gateway_details: base_dir.join(DEFAULT_GATEWAY_DETAILS_FILENAME), - keys: ClientKeysPaths::new_default(base_data_directory), + keys: ClientKeysPaths::new_base(base_data_directory), } } } diff --git a/common/client-core/src/config/mod.rs b/common/client-core/src/config/mod.rs index d7779f0600..32d646e401 100644 --- a/common/client-core/src/config/mod.rs +++ b/common/client-core/src/config/mod.rs @@ -3,6 +3,7 @@ use nym_config::defaults::NymNetworkDetails; use nym_crypto::asymmetric::identity; +use nym_gateway_client::client::GatewayConfig; use nym_sphinx::{ addressing::clients::Recipient, params::{PacketSize, PacketType}, @@ -29,6 +30,8 @@ const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: Duration = Duration::from_millis(20) const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(50); const DEFAULT_TOPOLOGY_REFRESH_RATE: Duration = Duration::from_secs(5 * 60); // every 5min const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: Duration = Duration::from_millis(5_000); +const DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead + // Set this to a high value for now, so that we don't risk sporadic timeouts that might cause // bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the // bandwidth bridging protocol, we can come back to a smaller timeout value @@ -241,6 +244,19 @@ pub struct GatewayEndpointConfig { pub gateway_listener: String, } +impl TryFrom for GatewayConfig { + type Error = ClientCoreError; + + fn try_from(value: GatewayEndpointConfig) -> Result { + Ok(GatewayConfig { + gateway_identity: identity::PublicKey::from_base58_string(value.gateway_id) + .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?, + gateway_owner: Some(value.gateway_owner), + gateway_listener: value.gateway_listener, + }) + } +} + #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] impl GatewayEndpointConfig { #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] @@ -498,6 +514,11 @@ pub struct Topology { /// Supersedes `topology_refresh_rate_ms`. pub disable_refreshing: bool, + /// Defines how long the client is going to wait on startup for its gateway to come online, + /// before abandoning the procedure. + #[serde(with = "humantime_serde")] + pub max_startup_gateway_waiting_period: Duration, + /// Specifies the mixnode topology to be used for sending packets. pub topology_structure: TopologyStructure, } @@ -532,6 +553,7 @@ impl Default for Topology { topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, disable_refreshing: false, + max_startup_gateway_waiting_period: DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD, topology_structure: TopologyStructure::default(), } } diff --git a/common/client-core/src/config/old_config_v1_1_20_2.rs b/common/client-core/src/config/old_config_v1_1_20_2.rs index a097b55f2a..afb6536192 100644 --- a/common/client-core/src/config/old_config_v1_1_20_2.rs +++ b/common/client-core/src/config/old_config_v1_1_20_2.rs @@ -267,7 +267,7 @@ impl From for Topology { topology_refresh_rate: value.topology_refresh_rate, topology_resolution_timeout: value.topology_resolution_timeout, disable_refreshing: value.disable_refreshing, - topology_structure: Default::default(), + ..Default::default() } } } diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 0c30c149cb..15b6c1a1bf 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -1,6 +1,7 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::mix_traffic::transceiver::ErasedGatewayError; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; use nym_gateway_client::error::GatewayClientError; use nym_topology::gateway::GatewayConversionError; @@ -19,6 +20,12 @@ pub enum ClientCoreError { source: GatewayClientError, }, + #[error("Custom gateway client error: {source}")] + ErasedGatewayClientError { + #[from] + source: ErasedGatewayError, + }, + #[error("Ed25519 error: {0}")] Ed25519RecoveryError(#[from] Ed25519RecoveryError), @@ -31,15 +38,9 @@ pub enum ClientCoreError { #[error("No gateways on network")] NoGatewaysOnNetwork, - #[error("Failed to setup gateway")] - FailedToSetupGateway, - #[error("List of nym apis is empty")] ListOfNymApisIsEmpty, - #[error("Could not load existing gateway configuration: {0}")] - CouldNotLoadExistingGatewayConfiguration(std::io::Error), - #[error("The current network topology seem to be insufficient to route any packets through")] InsufficientNetworkTopology(#[from] NymTopologyError), @@ -61,15 +62,6 @@ pub enum ClientCoreError { #[error("The gateway id is invalid - {0}")] UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError), - #[error("The identity of the gateway is unknown - did you run init?")] - GatewayIdUnknown, - - #[error("The owner of the gateway is unknown - did you run init?")] - GatewayOwnerUnknown, - - #[error("The address of the gateway is unknown - did you run init?")] - GatewayAddressUnknown, - #[error("The gateway is malformed: {source}")] MalformedGateway { #[from] @@ -122,6 +114,18 @@ pub enum ClientCoreError { #[error("unable to upgrade config file to `{new_version}`")] UnableToUpgradeConfigFile { new_version: String }, + + #[error("the provided gateway details don't much the stored data")] + MismatchedStoredGatewayDetails, + + #[error("custom selection of gateway was expected")] + CustomGatewaySelectionExpected, + + #[error("the persisted gateway details were set for a custom setup")] + UnexpectedPersistedCustomGatewayDetails, + + #[error("this client has performed gateway initialisation in another session")] + NoInitClientPresent, } /// Set of messages that the client can send to listeners via the task manager diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 9d54130576..4d6685b4de 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -3,7 +3,7 @@ use crate::config::GatewayEndpointConfig; use crate::error::ClientCoreError; -use crate::init::RegistrationResult; +use crate::init::types::RegistrationResult; use futures::{SinkExt, StreamExt}; use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::identity; @@ -24,6 +24,7 @@ use tokio_tungstenite::connect_async; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; #[cfg(not(target_arch = "wasm32"))] type WsConn = WebSocketStream>; +use nym_validator_client::client::IdentityKeyRef; #[cfg(not(target_arch = "wasm32"))] use tokio::time::sleep; @@ -197,17 +198,27 @@ pub(super) fn uniformly_random_gateway( .cloned() } +pub(super) fn get_specified_gateway( + gateway_identity: IdentityKeyRef, + gateways: &[gateway::Node], +) -> Result { + let user_gateway = identity::PublicKey::from_base58_string(gateway_identity) + .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; + + gateways + .iter() + .find(|gateway| gateway.identity_key == user_gateway) + .ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_identity.to_string())) + .cloned() +} + pub(super) async fn register_with_gateway( gateway: &GatewayEndpointConfig, our_identity: Arc, ) -> Result { - let timeout = Duration::from_millis(1500); - let mut gateway_client = GatewayClient::new_init( - gateway.gateway_listener.clone(), - gateway.try_get_gateway_identity_key()?, - our_identity.clone(), - timeout, - ); + let mut gateway_client = + GatewayClient::new_init(gateway.to_owned().try_into()?, our_identity.clone()); + gateway_client.establish_connection().await.map_err(|err| { log::warn!("Failed to establish connection with gateway!"); ClientCoreError::GatewayClientError { @@ -230,6 +241,6 @@ pub(super) async fn register_with_gateway( })?; Ok(RegistrationResult { shared_keys, - authenticated_ephemeral_client: Some(gateway_client), + authenticated_ephemeral_client: gateway_client, }) } diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 1f8e10bcc9..df1afedc6c 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -8,246 +8,34 @@ use crate::client::base_client::storage::gateway_details::{ }; use crate::client::key_manager::persistence::KeyStore; use crate::client::key_manager::ManagedKeys; -use crate::init::helpers::{choose_gateway_by_latency, current_gateways, uniformly_random_gateway}; -use crate::{ - config::{Config, GatewayEndpointConfig}, - error::ClientCoreError, +use crate::config::GatewayEndpointConfig; +use crate::error::ClientCoreError; +use crate::init::helpers::{ + choose_gateway_by_latency, get_specified_gateway, uniformly_random_gateway, }; -use nym_crypto::asymmetric::identity; -use nym_gateway_client::client::InitOnly; -use nym_gateway_client::GatewayClient; -use nym_gateway_requests::registration::handshake::SharedKeys; -use nym_sphinx::addressing::{clients::Recipient, nodes::NodeIdentity}; +use crate::init::types::{ + CustomGatewayDetails, GatewayDetails, GatewaySelectionSpecification, GatewaySetup, + InitialisationResult, +}; +use nym_gateway_client::client::InitGatewayClient; use nym_topology::gateway; -use nym_validator_client::client::IdentityKey; use rand::rngs::OsRng; +use serde::de::DeserializeOwned; use serde::Serialize; -use std::fmt::{Debug, Display}; use std::sync::Arc; -use url::Url; pub mod helpers; - -pub struct RegistrationResult { - pub shared_keys: Arc, - pub authenticated_ephemeral_client: Option>, -} - -pub struct InitialisationResult { - pub details: InitialisationDetails, - pub authenticated_ephemeral_client: Option>, -} - -impl From for InitialisationResult { - fn from(details: InitialisationDetails) -> Self { - InitialisationResult { - details, - authenticated_ephemeral_client: None, - } - } -} - -// TODO: rename to something better... -#[derive(Debug)] -pub struct InitialisationDetails { - pub gateway_details: GatewayEndpointConfig, - pub managed_keys: ManagedKeys, -} - -impl InitialisationDetails { - pub fn new(gateway_details: GatewayEndpointConfig, managed_keys: ManagedKeys) -> Self { - InitialisationDetails { - gateway_details, - managed_keys, - } - } - - pub async fn try_load(key_store: &K, details_store: &D) -> Result - where - K: KeyStore, - D: GatewayDetailsStore, - K::StorageError: Send + Sync + 'static, - D::StorageError: Send + Sync + 'static, - { - let loaded_details = _load_gateway_details(details_store).await?; - let loaded_keys = _load_managed_keys(key_store).await?; - - if !loaded_details.verify(&loaded_keys.must_get_gateway_shared_key()) { - return Err(ClientCoreError::MismatchedGatewayDetails { - gateway_id: loaded_details.details.gateway_id, - }); - } - - Ok(InitialisationDetails { - gateway_details: loaded_details.into(), - managed_keys: loaded_keys, - }) - } - - pub fn client_address(&self) -> Result { - let client_recipient = Recipient::new( - *self.managed_keys.identity_public_key(), - *self.managed_keys.encryption_public_key(), - // TODO: below only works under assumption that gateway address == gateway id - // (which currently is true) - NodeIdentity::from_base58_string(&self.gateway_details.gateway_id)?, - ); - - Ok(client_recipient) - } -} - -pub enum GatewaySetup { - /// The gateway specification MUST BE loaded from the underlying storage. - MustLoad, - - /// Specifies usage of a new, random, gateway. - New { - /// Should the new gateway be selected based on latency. - by_latency: bool, - }, - - Specified { - /// Identity key of the gateway we want to try to use. - gateway_identity: IdentityKey, - }, - - Predefined { - /// Full gateway configuration - details: PersistedGatewayDetails, - }, - - ReuseConnection { - /// The authenticated ephemeral client that was created during `init` - authenticated_ephemeral_client: GatewayClient, - - /// Details of this pre-initialised client - details: InitialisationDetails, - }, -} - -impl From for GatewaySetup { - fn from(details: PersistedGatewayDetails) -> Self { - GatewaySetup::Predefined { details } - } -} - -impl From for GatewaySetup { - fn from(gateway_identity: IdentityKey) -> Self { - GatewaySetup::Specified { gateway_identity } - } -} - -impl Default for GatewaySetup { - fn default() -> Self { - GatewaySetup::New { by_latency: false } - } -} - -impl GatewaySetup { - pub fn new_fresh( - gateway_identity: Option, - latency_based_selection: Option, - ) -> Self { - if let Some(gateway_identity) = gateway_identity { - GatewaySetup::Specified { gateway_identity } - } else { - GatewaySetup::New { - by_latency: latency_based_selection.unwrap_or_default(), - } - } - } - - pub fn is_must_load(&self) -> bool { - matches!(self, GatewaySetup::MustLoad) - } - - pub fn has_full_details(&self) -> bool { - matches!(self, GatewaySetup::Predefined { .. }) || self.is_must_load() - } - - pub async fn choose_gateway( - &self, - gateways: &[gateway::Node], - ) -> Result { - match self { - GatewaySetup::New { by_latency } => { - let mut rng = OsRng; - if *by_latency { - choose_gateway_by_latency(&mut rng, gateways).await - } else { - uniformly_random_gateway(&mut rng, gateways) - } - } - .map(Into::into), - GatewaySetup::Specified { gateway_identity } => { - let user_gateway = identity::PublicKey::from_base58_string(gateway_identity) - .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; - - gateways - .iter() - .find(|gateway| gateway.identity_key == user_gateway) - .ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_identity.to_string())) - .cloned() - } - .map(Into::into), - _ => Err(ClientCoreError::UnexpectedGatewayDetails), - } - } - - pub async fn try_get_new_gateway_details( - &self, - validator_servers: &[Url], - ) -> Result { - let mut rng = OsRng; - let gateways = current_gateways(&mut rng, validator_servers).await?; - self.choose_gateway(&gateways).await - } -} - -/// Struct describing the results of the client initialization procedure. -#[derive(Debug, Serialize)] -pub struct InitResults { - version: String, - id: String, - identity_key: String, - encryption_key: String, - gateway_id: String, - gateway_listener: String, -} - -impl InitResults { - pub fn new(config: &Config, address: &Recipient, gateway: &GatewayEndpointConfig) -> Self { - Self { - version: config.client.version.clone(), - id: config.client.id.clone(), - identity_key: address.identity().to_base58_string(), - encryption_key: address.encryption_key().to_base58_string(), - gateway_id: gateway.gateway_id.clone(), - gateway_listener: gateway.gateway_listener.clone(), - } - } -} - -impl Display for InitResults { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Version: {}", self.version)?; - writeln!(f, "ID: {}", self.id)?; - writeln!(f, "Identity key: {}", self.identity_key)?; - writeln!(f, "Encryption: {}", self.encryption_key)?; - writeln!(f, "Gateway ID: {}", self.gateway_id)?; - write!(f, "Gateway: {}", self.gateway_listener) - } -} +pub mod types; // helpers for error wrapping -async fn _store_gateway_details( +async fn _store_gateway_details( details_store: &D, - details: &PersistedGatewayDetails, + details: &PersistedGatewayDetails, ) -> Result<(), ClientCoreError> where - D: GatewayDetailsStore, + D: GatewayDetailsStore, D::StorageError: Send + Sync + 'static, + T: Serialize + Send + Sync, { details_store .store_gateway_details(details) @@ -257,12 +45,13 @@ where }) } -async fn _load_gateway_details( +async fn _load_gateway_details( details_store: &D, -) -> Result +) -> Result, ClientCoreError> where - D: GatewayDetailsStore, + D: GatewayDetailsStore, D::StorageError: Send + Sync + 'static, + T: DeserializeOwned + Send + Sync, { details_store .load_gateway_details() @@ -284,190 +73,165 @@ where }) } -fn ensure_valid_details( - details: &PersistedGatewayDetails, +fn ensure_valid_details( + details: &PersistedGatewayDetails, loaded_keys: &ManagedKeys, ) -> Result<(), ClientCoreError> { - if !details.verify(&loaded_keys.must_get_gateway_shared_key()) { - Err(ClientCoreError::MismatchedGatewayDetails { - gateway_id: details.details.gateway_id.clone(), - }) - } else { - Ok(()) - } + details.validate(loaded_keys.gateway_shared_key().as_deref()) } -pub async fn setup_gateway_from( - setup: GatewaySetup, +async fn setup_new_gateway( key_store: &K, details_store: &D, overwrite_data: bool, - gateways: Option<&[gateway::Node]>, -) -> Result + selection_specification: GatewaySelectionSpecification, + available_gateways: Vec, +) -> Result, ClientCoreError> where K: KeyStore, - D: GatewayDetailsStore, + D: GatewayDetailsStore, K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync, { - // I don't like how we can't deal with this variant in the match below, but we need to take ownership of internal values. - if let GatewaySetup::ReuseConnection { - authenticated_ephemeral_client, - details, - } = setup - { - // if we have already performed the full setup, forward the details. - // it's up to the caller to ensure persistence - return Ok(InitialisationResult { - details, - authenticated_ephemeral_client: Some(authenticated_ephemeral_client), - }); + // if we're setting up new gateway, failing to load existing information is fine. + // as a matter of fact, it's only potentially a problem if we DO succeed + if _load_gateway_details(details_store).await.is_ok() && !overwrite_data { + return Err(ClientCoreError::ForbiddenKeyOverwrite); + } + if _load_managed_keys(key_store).await.is_ok() && !overwrite_data { + return Err(ClientCoreError::ForbiddenKeyOverwrite); } let mut rng = OsRng; + let mut new_keys = ManagedKeys::generate_new(&mut rng); - // try load gateway details - let loaded_details = _load_gateway_details(details_store).await; - - // try load keys and decide what to do based on the GatewaySetup - let mut managed_keys = match ManagedKeys::try_load(key_store).await { - Ok(loaded_keys) => { - match &setup { - GatewaySetup::MustLoad => { - // get EVERYTHING from the storage - let details = loaded_details?; - ensure_valid_details(&details, &loaded_keys)?; - - // no need to persist anything as we got everything from the storage - return Ok(InitialisationDetails::new(details.into(), loaded_keys).into()); - } - GatewaySetup::Predefined { details } => { - // we already have defined gateway details AND a shared key - ensure_valid_details(details, &loaded_keys)?; - - // if nothing was stored or we're allowed to overwrite what's there, just persist the passed data - if overwrite_data || loaded_details.is_err() { - _store_gateway_details(details_store, details).await?; - } - - return Ok( - InitialisationDetails::new(details.clone().into(), loaded_keys).into(), - ); - } - GatewaySetup::Specified { gateway_identity } => { - // if that data was already stored... - if let Ok(existing_gateway) = loaded_details { - ensure_valid_details(&existing_gateway, &loaded_keys)?; - if &existing_gateway.details.gateway_id != gateway_identity - && !overwrite_data - { - // if our loaded details don't match requested value and we CANT overwrite it... - return Err(ClientCoreError::UnexpectedGatewayDetails); - } else if &existing_gateway.details.gateway_id == gateway_identity { - // if they do match up, just return it - return Ok(InitialisationDetails::new( - existing_gateway.into(), - loaded_keys, - ) - .into()); - } - } - - // we didn't get full details from the store and we have loaded some keys - // so we can only continue if we're allowed to overwrite keys - if overwrite_data { - ManagedKeys::generate_new(&mut rng) - } else { - return Err(ClientCoreError::ForbiddenKeyOverwrite); - } - } - GatewaySetup::New { .. } => { - if let Ok(existing_gateway) = loaded_details { - ensure_valid_details(&existing_gateway, &loaded_keys)?; - return Ok(InitialisationDetails::new( - existing_gateway.into(), - loaded_keys, - ) - .into()); - } - - // we didn't get full details from the store and we have loaded some keys - // so we can only continue if we're allowed to overwrite keys - if overwrite_data { - ManagedKeys::generate_new(&mut rng) - } else { - return Err(ClientCoreError::ForbiddenKeyOverwrite); - } - } - GatewaySetup::ReuseConnection { .. } => { - unreachable!("the reuse connection variant was already manually covered") - } - } + let gateway_details = match selection_specification { + GatewaySelectionSpecification::UniformRemote => { + let gateway = uniformly_random_gateway(&mut rng, &available_gateways)?; + GatewayDetails::Configured(GatewayEndpointConfig::from(gateway)) } - Err(_) => { - // if we failed to load the keys, ensure we didn't provide gateway details in some form - // (in that case we CAN'T generate new keys - if setup.has_full_details() { - return Err(ClientCoreError::UnavailableSharedKey); - } - ManagedKeys::generate_new(&mut rng) + GatewaySelectionSpecification::RemoteByLatency => { + let gateway = choose_gateway_by_latency(&mut rng, &available_gateways).await?; + GatewayDetails::Configured(GatewayEndpointConfig::from(gateway)) } + GatewaySelectionSpecification::Specified { identity } => { + let gateway = get_specified_gateway(&identity, &available_gateways)?; + GatewayDetails::Configured(GatewayEndpointConfig::from(gateway)) + } + GatewaySelectionSpecification::Custom { + gateway_identity, + additional_data, + } => GatewayDetails::Custom(CustomGatewayDetails::new(gateway_identity, additional_data)), }; - // choose gateway - let gateway_details = setup.choose_gateway(gateways.unwrap_or_default()).await?; + let registration_result = if let GatewayDetails::Configured(gateway_cfg) = &gateway_details { + // if we're using a 'normal' gateway setup, do register + let our_identity = new_keys.identity_keypair(); + Some(helpers::register_with_gateway(gateway_cfg, our_identity).await?) + } else { + None + }; - // get our identity key - let our_identity = managed_keys.identity_keypair(); + let maybe_shared_keys = registration_result + .as_ref() + .map(|r| Arc::clone(&r.shared_keys)); - // Establish connection, authenticate and generate keys for talking with the gateway - let registration_result = - helpers::register_with_gateway(&gateway_details, our_identity).await?; - let shared_keys = registration_result.shared_keys; + let persisted_details = + PersistedGatewayDetails::new(gateway_details, maybe_shared_keys.as_deref())?; - let persisted_details = PersistedGatewayDetails::new(gateway_details, &shared_keys); - - // persist gateway keys - managed_keys - .deal_with_gateway_key(shared_keys, key_store) + // persist the keys + new_keys + .deal_with_gateway_key(maybe_shared_keys, key_store) .await .map_err(|source| ClientCoreError::KeyStoreError { source: Box::new(source), })?; - // persist gateway config + // persist gateway configs _store_gateway_details(details_store, &persisted_details).await?; Ok(InitialisationResult { - details: InitialisationDetails::new(persisted_details.into(), managed_keys), - authenticated_ephemeral_client: registration_result.authenticated_ephemeral_client, + gateway_details: persisted_details.into(), + managed_keys: new_keys, + authenticated_ephemeral_client: registration_result + .map(|r| r.authenticated_ephemeral_client), }) } -pub async fn setup_gateway( - setup: GatewaySetup, +async fn use_loaded_gateway_details( key_store: &K, details_store: &D, - overwrite_data: bool, - validator_servers: Option<&[Url]>, -) -> Result +) -> Result, ClientCoreError> where K: KeyStore, - D: GatewayDetailsStore, + D: GatewayDetailsStore, K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, + T: DeserializeOwned + Send + Sync, { - let mut rng = OsRng; - let gateways = current_gateways(&mut rng, validator_servers.unwrap_or_default()).await?; + let loaded_details = _load_gateway_details(details_store).await?; + let loaded_keys = _load_managed_keys(key_store).await?; - setup_gateway_from( - setup, - key_store, - details_store, - overwrite_data, - Some(&gateways), - ) - .await + ensure_valid_details(&loaded_details, &loaded_keys)?; + + // no need to persist anything as we got everything from the storage + Ok(InitialisationResult::new_loaded( + loaded_details.into(), + loaded_keys, + )) +} + +fn reuse_gateway_connection( + authenticated_ephemeral_client: InitGatewayClient, + gateway_details: GatewayDetails, + managed_keys: ManagedKeys, +) -> InitialisationResult { + InitialisationResult { + gateway_details, + managed_keys, + authenticated_ephemeral_client: Some(authenticated_ephemeral_client), + } +} + +pub async fn setup_gateway( + setup: GatewaySetup, + key_store: &K, + details_store: &D, +) -> Result, ClientCoreError> +where + K: KeyStore, + D: GatewayDetailsStore, + K::StorageError: Send + Sync + 'static, + D::StorageError: Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync, +{ + match setup { + GatewaySetup::MustLoad => use_loaded_gateway_details(key_store, details_store).await, + GatewaySetup::New { + specification, + available_gateways, + overwrite_data, + } => { + setup_new_gateway( + key_store, + details_store, + overwrite_data, + specification, + available_gateways, + ) + .await + } + GatewaySetup::ReuseConnection { + authenticated_ephemeral_client, + gateway_details, + managed_keys, + } => Ok(reuse_gateway_connection( + authenticated_ephemeral_client, + gateway_details, + managed_keys, + )), + } } pub fn output_to_json(init_results: &T, output_file: &str) { diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs new file mode 100644 index 0000000000..c1ff900001 --- /dev/null +++ b/common/client-core/src/init/types.rs @@ -0,0 +1,331 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::base_client::storage::gateway_details::{ + GatewayDetailsStore, PersistedCustomGatewayDetails, PersistedGatewayDetails, +}; +use crate::client::key_manager::persistence::KeyStore; +use crate::client::key_manager::ManagedKeys; +use crate::config::{Config, GatewayEndpointConfig}; +use crate::error::ClientCoreError; +use crate::init::{_load_gateway_details, _load_managed_keys, setup_gateway}; +use nym_gateway_client::client::InitGatewayClient; +use nym_gateway_requests::registration::handshake::SharedKeys; +use nym_sphinx::addressing::clients::Recipient; +use nym_sphinx::addressing::nodes::NodeIdentity; +use nym_topology::gateway; +use nym_validator_client::client::IdentityKey; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::sync::Arc; + +/// Result of registering with a gateway: +/// - shared keys derived between ourselves and the node +/// - an authenticated handle of an ephemeral handle created for the purposes of registration +pub struct RegistrationResult { + pub shared_keys: Arc, + pub authenticated_ephemeral_client: InitGatewayClient, +} + +/// Result of fully initialising a client: +/// - details of the associated gateway +/// - all loaded (or derived) keys +/// - an optional authenticated handle of an ephemeral gateway handle created for the purposes of registration, +/// if this was the first time this client registered +pub struct InitialisationResult { + pub gateway_details: GatewayDetails, + pub managed_keys: ManagedKeys, + pub authenticated_ephemeral_client: Option, +} + +impl InitialisationResult { + pub fn new_loaded(gateway_details: GatewayDetails, managed_keys: ManagedKeys) -> Self { + InitialisationResult { + gateway_details, + managed_keys, + authenticated_ephemeral_client: None, + } + } + + pub async fn try_load(key_store: &K, details_store: &D) -> Result + where + K: KeyStore, + D: GatewayDetailsStore, + K::StorageError: Send + Sync + 'static, + D::StorageError: Send + Sync + 'static, + T: DeserializeOwned + Send + Sync, + { + let loaded_details = _load_gateway_details(details_store).await?; + let loaded_keys = _load_managed_keys(key_store).await?; + + match &loaded_details { + PersistedGatewayDetails::Default(loaded_default) => { + if !loaded_default.verify(&loaded_keys.must_get_gateway_shared_key()) { + return Err(ClientCoreError::MismatchedGatewayDetails { + gateway_id: loaded_default.details.gateway_id.clone(), + }); + } + } + PersistedGatewayDetails::Custom(_) => {} + } + + Ok(InitialisationResult { + gateway_details: loaded_details.into(), + managed_keys: loaded_keys, + authenticated_ephemeral_client: None, + }) + } + + pub fn client_address(&self) -> Result { + let client_recipient = Recipient::new( + *self.managed_keys.identity_public_key(), + *self.managed_keys.encryption_public_key(), + // TODO: below only works under assumption that gateway address == gateway id + // (which currently is true) + NodeIdentity::from_base58_string(self.gateway_details.gateway_id())?, + ); + + Ok(client_recipient) + } +} + +/// Details of particular gateway client got registered with +#[derive(Debug, Clone)] +pub enum GatewayDetails { + /// Standard details of a remote gateway + Configured(GatewayEndpointConfig), + + /// Custom gateway setup, such as for a client embedded inside gateway itself + Custom(CustomGatewayDetails), +} + +#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct EmptyCustomDetails {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CustomGatewayDetails { + // whatever custom method is used, gateway's identity must be known + pub gateway_id: String, + + #[serde(flatten)] + pub additional_data: T, +} + +impl CustomGatewayDetails { + pub fn new(gateway_id: String, additional_data: T) -> Self { + Self { + gateway_id, + additional_data, + } + } +} + +impl GatewayDetails { + pub fn try_get_configured_endpoint(&self) -> Option<&GatewayEndpointConfig> { + if let GatewayDetails::Configured(endpoint) = &self { + Some(endpoint) + } else { + None + } + } + + pub fn is_custom(&self) -> bool { + matches!(self, GatewayDetails::Custom(_)) + } + + pub fn gateway_id(&self) -> &str { + match self { + GatewayDetails::Configured(cfg) => &cfg.gateway_id, + GatewayDetails::Custom(custom) => &custom.gateway_id, + } + } +} + +impl From for GatewayDetails { + fn from(value: GatewayEndpointConfig) -> Self { + GatewayDetails::Configured(value) + } +} + +impl From> for CustomGatewayDetails { + fn from(value: PersistedCustomGatewayDetails) -> Self { + CustomGatewayDetails { + gateway_id: value.gateway_id, + additional_data: value.additional_data, + } + } +} + +impl From> for PersistedCustomGatewayDetails { + fn from(value: CustomGatewayDetails) -> Self { + PersistedCustomGatewayDetails { + gateway_id: value.gateway_id, + additional_data: value.additional_data, + } + } +} + +impl From> for GatewayDetails { + fn from(value: PersistedGatewayDetails) -> Self { + match value { + PersistedGatewayDetails::Default(default) => { + GatewayDetails::Configured(default.details) + } + PersistedGatewayDetails::Custom(custom) => GatewayDetails::Custom(custom.into()), + } + } +} + +#[derive(Clone, Default)] +pub enum GatewaySelectionSpecification { + /// Uniformly choose a random remote gateway. + #[default] + UniformRemote, + + /// Should the new, remote, gateway be selected based on latency. + RemoteByLatency, + + /// Gateway with this specific identity should be chosen. + // JS: I don't really like the name of this enum variant but couldn't think of anything better at the time + Specified { identity: IdentityKey }, + + // TODO: this doesn't really fit in here..., but where else to put it? + /// This client has handled the selection by itself + Custom { + gateway_identity: String, + additional_data: T, + }, +} + +impl GatewaySelectionSpecification { + pub fn new(gateway_identity: Option, latency_based_selection: Option) -> Self { + if let Some(identity) = gateway_identity { + GatewaySelectionSpecification::Specified { identity } + } else if let Some(true) = latency_based_selection { + GatewaySelectionSpecification::RemoteByLatency + } else { + GatewaySelectionSpecification::UniformRemote + } + } +} + +pub enum GatewaySetup { + /// The gateway specification (details + keys) MUST BE loaded from the underlying storage. + MustLoad, + + /// Specifies usage of a new gateway + New { + specification: GatewaySelectionSpecification, + + // TODO: seems to be a bit inefficient to pass them by value + available_gateways: Vec, + + /// Specifies whether old data should be overwritten whilst setting up new gateway client. + overwrite_data: bool, + }, + + ReuseConnection { + /// The authenticated ephemeral client that was created during `init` + authenticated_ephemeral_client: InitGatewayClient, + + // Details of this pre-initialised client (i.e. gateway and keys) + gateway_details: GatewayDetails, + + managed_keys: ManagedKeys, + }, +} + +impl GatewaySetup { + // pub fn new_fresh( + // gateway_identity: Option, + // latency_based_selection: Option, + // gateways: Vec, + // can_overwrite: bool, + // ) -> Self { + // if let Some(gateway_identity) = gateway_identity { + // GatewaySetup::Specified { gateway_identity } + // } else { + // let specification = if let Some(true) = latency_based_selection { + // GatewaySelectionSpecification::RemoteByLatency + // } else { + // GatewaySelectionSpecification::UniformRemote + // }; + // + // GatewaySetup::New { specification } + // } + // } + + pub fn try_reuse_connection( + init_res: InitialisationResult, + ) -> Result { + if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client { + Ok(GatewaySetup::ReuseConnection { + authenticated_ephemeral_client, + gateway_details: init_res.gateway_details, + managed_keys: init_res.managed_keys, + }) + } else { + Err(ClientCoreError::NoInitClientPresent) + } + } + + pub async fn try_setup( + self, + key_store: &K, + details_store: &D, + ) -> Result, ClientCoreError> + where + K: KeyStore, + D: GatewayDetailsStore, + K::StorageError: Send + Sync + 'static, + D::StorageError: Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync, + { + setup_gateway(self, key_store, details_store).await + } + + pub fn is_must_load(&self) -> bool { + matches!(self, GatewaySetup::MustLoad) + } + + pub fn has_full_details(&self) -> bool { + self.is_must_load() + } +} + +/// Struct describing the results of the client initialization procedure. +#[derive(Debug, Serialize)] +pub struct InitResults { + version: String, + id: String, + identity_key: String, + encryption_key: String, + gateway_id: String, + gateway_listener: String, +} + +impl InitResults { + pub fn new(config: &Config, address: &Recipient, gateway: &GatewayEndpointConfig) -> Self { + Self { + version: config.client.version.clone(), + id: config.client.id.clone(), + identity_key: address.identity().to_base58_string(), + encryption_key: address.encryption_key().to_base58_string(), + gateway_id: gateway.gateway_id.clone(), + gateway_listener: gateway.gateway_listener.clone(), + } + } +} + +impl Display for InitResults { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Version: {}", self.version)?; + writeln!(f, "ID: {}", self.id)?; + writeln!(f, "Identity key: {}", self.identity_key)?; + writeln!(f, "Encryption: {}", self.encryption_key)?; + writeln!(f, "Gateway ID: {}", self.gateway_id)?; + write!(f, "Gateway: {}", self.gateway_listener) + } +} diff --git a/common/client-core/src/lib.rs b/common/client-core/src/lib.rs index 04e215daec..386e498e53 100644 --- a/common/client-core/src/lib.rs +++ b/common/client-core/src/lib.rs @@ -5,6 +5,11 @@ pub mod config; pub mod error; pub mod init; +pub use nym_topology::{ + HardcodedTopologyProvider, NymTopology, NymTopologyError, SerializableNymTopology, + SerializableTopologyError, TopologyProvider, +}; + #[cfg(target_arch = "wasm32")] pub(crate) fn spawn_future(future: F) where diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 3cd75b1b5f..a95fee0bf7 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -7,6 +7,7 @@ pub use crate::packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; use crate::socket_state::{PartiallyDelegated, SocketState}; +use crate::traits::GatewayPacketRouter; use crate::{cleanup_socket_message, try_decrypt_binary_message}; use futures::{SinkExt, StreamExt}; use log::*; @@ -39,9 +40,23 @@ use wasm_utils::websocket::JSWebsocket; #[cfg(target_arch = "wasm32")] use wasmtimer::tokio::sleep; +// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause +// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the +// bandwidth bridging protocol, we can come back to a smaller timeout value +const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); const DEFAULT_RECONNECTION_ATTEMPTS: usize = 10; const DEFAULT_RECONNECTION_BACKOFF: Duration = Duration::from_secs(5); +pub struct GatewayConfig { + pub gateway_identity: identity::PublicKey, + + // currently a dead field + pub gateway_owner: Option, + + pub gateway_listener: String, +} + +// TODO: this should be refactored into a state machine that keeps track of its authentication state pub struct GatewayClient { authenticated: bool, disabled_credentials_mode: bool, @@ -69,17 +84,12 @@ pub struct GatewayClient { } impl GatewayClient { - // TODO: put it all in a Config struct - #[allow(clippy::too_many_arguments)] pub fn new( - gateway_address: String, + config: GatewayConfig, local_identity: Arc, - gateway_identity: identity::PublicKey, // TODO: make it mandatory. if you don't want to pass it, use `new_init` shared_key: Option>, - mixnet_message_sender: MixnetMessageSender, - ack_sender: AcknowledgementSender, - response_timeout_duration: Duration, + packet_router: PacketRouter, bandwidth_controller: Option>, shutdown: TaskClient, ) -> Self { @@ -87,13 +97,13 @@ impl GatewayClient { authenticated: false, disabled_credentials_mode: true, bandwidth_remaining: 0, - gateway_address, - gateway_identity, + gateway_address: config.gateway_listener, + gateway_identity: config.gateway_identity, local_identity, shared_key, connection: SocketState::NotConnected, - packet_router: PacketRouter::new(ack_sender, mixnet_message_sender, shutdown.clone()), - response_timeout_duration, + packet_router, + response_timeout_duration: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, bandwidth_controller, should_reconnect_on_failure: true, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, @@ -102,21 +112,34 @@ impl GatewayClient { } } - pub fn set_disabled_credentials_mode(&mut self, disabled_credentials_mode: bool) { + #[must_use] + pub fn with_disabled_credentials_mode(mut self, disabled_credentials_mode: bool) -> Self { self.disabled_credentials_mode = disabled_credentials_mode; + self } - // TODO: later convert into proper builder methods - pub fn with_reconnection_on_failure(&mut self, should_reconnect_on_failure: bool) { - self.should_reconnect_on_failure = should_reconnect_on_failure + #[must_use] + pub fn with_reconnection_on_failure(mut self, should_reconnect_on_failure: bool) -> Self { + self.should_reconnect_on_failure = should_reconnect_on_failure; + self } - pub fn with_reconnection_attempts(&mut self, reconnection_attempts: usize) { - self.reconnection_attempts = reconnection_attempts + #[must_use] + pub fn with_response_timeout(mut self, response_timeout_duration: Duration) -> Self { + self.response_timeout_duration = response_timeout_duration; + self } - pub fn with_reconnection_backoff(&mut self, backoff: Duration) { - self.reconnection_backoff = backoff + #[must_use] + pub fn with_reconnection_attempts(mut self, reconnection_attempts: usize) -> Self { + self.reconnection_attempts = reconnection_attempts; + self + } + + #[must_use] + pub fn with_reconnection_backoff(mut self, backoff: Duration) -> Self { + self.reconnection_backoff = backoff; + self } pub fn gateway_identity(&self) -> identity::PublicKey { @@ -761,16 +784,14 @@ impl GatewayClient { } } +// type alias for an ease of use +pub type InitGatewayClient = GatewayClient; + pub struct InitOnly; impl GatewayClient { // for initialisation we do not need credential storage. Though it's still a bit weird we have to set the generic... - pub fn new_init( - gateway_address: String, - gateway_identity: identity::PublicKey, - local_identity: Arc, - response_timeout_duration: Duration, - ) -> Self { + pub fn new_init(config: GatewayConfig, local_identity: Arc) -> Self { use futures::channel::mpsc; // note: this packet_router is completely invalid in normal circumstances, but "works" @@ -784,13 +805,13 @@ impl GatewayClient { authenticated: false, disabled_credentials_mode: true, bandwidth_remaining: 0, - gateway_address, - gateway_identity, + gateway_address: config.gateway_listener, + gateway_identity: config.gateway_identity, local_identity, shared_key: None, connection: SocketState::NotConnected, packet_router, - response_timeout_duration, + response_timeout_duration: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, bandwidth_controller: None, should_reconnect_on_failure: false, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, @@ -801,9 +822,7 @@ impl GatewayClient { pub fn upgrade( self, - mixnet_message_sender: MixnetMessageSender, - ack_sender: AcknowledgementSender, - response_timeout_duration: Duration, + packet_router: PacketRouter, bandwidth_controller: Option>, shutdown: TaskClient, ) -> GatewayClient { @@ -822,8 +841,8 @@ impl GatewayClient { local_identity: self.local_identity, shared_key: self.shared_key, connection: self.connection, - packet_router: PacketRouter::new(ack_sender, mixnet_message_sender, shutdown.clone()), - response_timeout_duration, + packet_router, + response_timeout_duration: self.response_timeout_duration, bandwidth_controller, should_reconnect_on_failure: self.should_reconnect_on_failure, reconnection_attempts: self.reconnection_attempts, diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 8ac387b43f..65a6e6ca98 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -76,6 +76,16 @@ pub enum GatewayClientError { #[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway:?}")] IncompatibleProtocol { gateway: Option, current: u8 }, + + #[error( + "The packet router hasn't been set - are you sure you started up the client correctly?" + )] + PacketRouterUnavailable, + + #[error( + "this operation couldn't be completed as the program is in the process of shutting down" + )] + ShutdownInProgress, } impl GatewayClientError { diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index aea81dcda9..bc1b22b887 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -2,20 +2,23 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::GatewayClientError; -pub use client::GatewayClient; use log::warn; use nym_gateway_requests::BinaryResponse; -pub use packet_router::{ - AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, -}; use tungstenite::{protocol::Message, Error as WsError}; +pub use client::{GatewayClient, GatewayConfig}; pub use nym_gateway_requests::registration::handshake::SharedKeys; +pub use packet_router::{ + AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, + PacketRouter, +}; +pub use traits::GatewayPacketRouter; pub mod client; pub mod error; pub mod packet_router; pub mod socket_state; +pub mod traits; /// Helper method for reading from websocket stream. Helps to flatten the structure. pub(crate) fn cleanup_socket_message( diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 385d8bf333..bf73ecb8ca 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -5,10 +5,8 @@ // I will gladly take any suggestions on how to rename this. use crate::error::GatewayClientError; +use crate::GatewayPacketRouter; use futures::channel::mpsc; -use log::*; -use nym_sphinx::addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; -use nym_sphinx::params::packet_sizes::PacketSize; use nym_task::TaskClient; pub type MixnetMessageSender = mpsc::UnboundedSender>>; @@ -37,77 +35,52 @@ impl PacketRouter { } } - pub fn route_received( - &mut self, - unwrapped_packets: Vec>, + pub fn route_mixnet_messages( + &self, + received_messages: Vec>, ) -> Result<(), GatewayClientError> { - let mut received_messages = Vec::new(); - let mut received_acks = Vec::new(); - - // remember: gateway removes final layer of sphinx encryption and from the unwrapped - // data he takes the SURB-ACK and first hop address. - // currently SURB-ACKs are attached in EVERY packet, even cover, so this is always true - let ack_overhead = PacketSize::AckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN; - let outfox_ack_overhead = - PacketSize::OutfoxAckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN; - - for received_packet in unwrapped_packets { - if received_packet.len() == PacketSize::AckPacket.plaintext_size() - // we don't know the real size of the payload, it could be anything <= 48 bytes - || received_packet.len() <= PacketSize::OutfoxAckPacket.plaintext_size() - { - received_acks.push(received_packet); - } else if received_packet.len() - == PacketSize::RegularPacket.plaintext_size() - ack_overhead - || received_packet.len() - == PacketSize::OutfoxRegularPacket.plaintext_size() - outfox_ack_overhead - || received_packet.len() - == PacketSize::OutfoxRegularPacket.size() - outfox_ack_overhead - { - trace!("routing regular packet"); - received_messages.push(received_packet); - } else if received_packet.len() - == PacketSize::ExtendedPacket8.plaintext_size() - ack_overhead - { - trace!("routing extended8 packet"); - received_messages.push(received_packet); - } else if received_packet.len() - == PacketSize::ExtendedPacket16.plaintext_size() - ack_overhead - { - trace!("routing extended16 packet"); - received_messages.push(received_packet); - } else if received_packet.len() - == PacketSize::ExtendedPacket32.plaintext_size() - ack_overhead - { - trace!("routing extended32 packet"); - received_messages.push(received_packet); - } else { - // this can happen if other clients are not padding their messages - warn!("Received message of unexpected size. Probably from an outdated client... len: {}", received_packet.len()); - received_messages.push(received_packet); + if let Err(err) = self.mixnet_message_sender.unbounded_send(received_messages) { + // check if the failure is due to the shutdown being in progress and thus the receiver channel + // having already been dropped + if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { + // This should ideally not happen, but it's ok + log::warn!("Failed to send mixnet messages due to receiver task shutdown"); + return Err(GatewayClientError::ShutdownInProgress); } + // This should never happen during ordinary operation the way it's currently used. + // Abort to be on the safe side + panic!("Failed to send mixnet message: {err}"); } + Ok(()) + } - if !received_messages.is_empty() { - trace!("routing 'real'"); - if let Err(err) = self.mixnet_message_sender.unbounded_send(received_messages) { - if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { - // This should ideally not happen, but it's ok - log::warn!("Failed to send mixnet message due to receiver task shutdown"); - return Err(GatewayClientError::MixnetMsgSenderFailedToSend); - } - // This should never happen during ordinary operation the way it's currently used. - // Abort to be on the safe side - panic!("Failed to send mixnet message: {err}"); + pub fn route_acks(&self, received_acks: Vec>) -> Result<(), GatewayClientError> { + if let Err(err) = self.ack_sender.unbounded_send(received_acks) { + // check if the failure is due to the shutdown being in progress and thus the receiver channel + // having already been dropped + if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { + // This should ideally not happen, but it's ok + log::warn!("Failed to send acks due to receiver task shutdown"); + return Err(GatewayClientError::ShutdownInProgress); } - } - - if !received_acks.is_empty() { - trace!("routing acks"); - if let Err(err) = self.ack_sender.unbounded_send(received_acks) { - error!("failed to send ack: {err}"); - }; + // This should never happen during ordinary operation the way it's currently used. + // Abort to be on the safe side + panic!("Failed to send acks: {err}"); } Ok(()) } } + +impl GatewayPacketRouter for PacketRouter { + type Error = GatewayClientError; + + // note: this trait tries to decide whether a given message is an ack or a data message + + fn route_mixnet_messages(&self, received_messages: Vec>) -> Result<(), Self::Error> { + self.route_mixnet_messages(received_messages) + } + + fn route_acks(&self, received_acks: Vec>) -> Result<(), Self::Error> { + self.route_acks(received_acks) + } +} diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 0ffbcea034..6f82ef4939 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -3,6 +3,7 @@ use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; +use crate::traits::GatewayPacketRouter; use crate::{cleanup_socket_messages, try_decrypt_binary_message}; use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; diff --git a/common/client-libs/gateway-client/src/traits.rs b/common/client-libs/gateway-client/src/traits.rs new file mode 100644 index 0000000000..54b53ffc81 --- /dev/null +++ b/common/client-libs/gateway-client/src/traits.rs @@ -0,0 +1,96 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use log::{error, trace, warn}; +use nym_sphinx::addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; +use nym_sphinx::params::PacketSize; + +pub trait GatewayPacketRouter { + type Error: std::error::Error; + + fn route_received(&self, unwrapped_packets: Vec>) -> Result<(), Self::Error> { + let mut received_messages = Vec::new(); + let mut received_acks = Vec::new(); + + // remember: gateway removes final layer of sphinx encryption and from the unwrapped + // data he takes the SURB-ACK and first hop address. + // currently SURB-ACKs are attached in EVERY packet, even cover, so this is always true + let sphinx_ack_overhead = PacketSize::AckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN; + let outfox_ack_overhead = + PacketSize::OutfoxAckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN; + + for received_packet in unwrapped_packets { + // note: if we ever fail to route regular outfox, it might be because I've removed a match on + // `size == PacketSize::OutfoxRegularPacket.size() - outfox_ack_overhead` since it seemed + // redundant given we have `size == PacketSize::OutfoxRegularPacket.plaintext_size() - outfox_ack_overhead` + // and all the headers should have already be stripped at this point + match received_packet.len() { + n if n == PacketSize::AckPacket.plaintext_size() => { + trace!("received sphinx ack"); + received_acks.push(received_packet); + } + + n if n <= PacketSize::OutfoxAckPacket.plaintext_size() => { + // we don't know the real size of the payload, it could be anything <= 48 bytes + trace!("received outfox ack"); + received_acks.push(received_packet); + } + + n if n == PacketSize::RegularPacket.plaintext_size() - sphinx_ack_overhead => { + trace!("received regular sphinx packet"); + received_messages.push(received_packet); + } + + n if n + == PacketSize::OutfoxRegularPacket.plaintext_size() - outfox_ack_overhead => + { + trace!("received regular outfox packet"); + received_messages.push(received_packet); + } + + n if n == PacketSize::ExtendedPacket8.plaintext_size() - sphinx_ack_overhead => { + trace!("received extended8 packet"); + received_messages.push(received_packet); + } + + n if n == PacketSize::ExtendedPacket16.plaintext_size() - sphinx_ack_overhead => { + trace!("received extended16 packet"); + received_messages.push(received_packet); + } + + n if n == PacketSize::ExtendedPacket32.plaintext_size() - sphinx_ack_overhead => { + trace!("received extended32 packet"); + received_messages.push(received_packet); + } + + n => { + // this can happen if other clients are not padding their messages + warn!("Received message of unexpected size. Probably from an outdated client... len: {n}"); + received_messages.push(received_packet); + } + } + } + + if !received_messages.is_empty() { + trace!("routing {} received packets", received_messages.len()); + if let Err(err) = self.route_mixnet_messages(received_messages) { + error!("failed to route received messages: {err}"); + return Err(err); + } + } + + if !received_acks.is_empty() { + trace!("routing {} received acks", received_acks.len()); + if let Err(err) = self.route_acks(received_acks) { + error!("failed to route received acks: {err}"); + return Err(err); + } + } + + Ok(()) + } + + fn route_mixnet_messages(&self, received_messages: Vec>) -> Result<(), Self::Error>; + + fn route_acks(&self, received_acks: Vec>) -> Result<(), Self::Error>; +} diff --git a/common/client-libs/mixnet-client/src/forwarder.rs b/common/client-libs/mixnet-client/src/forwarder.rs index 8bbf5a47f6..630cc95663 100644 --- a/common/client-libs/mixnet-client/src/forwarder.rs +++ b/common/client-libs/mixnet-client/src/forwarder.rs @@ -56,7 +56,7 @@ impl PacketForwarder { log::trace!("PacketForwarder: Received shutdown"); } Some(mix_packet) = self.packet_receiver.next() => { - trace!("Going to forward packet to {:?}", mix_packet.next_hop()); + trace!("Going to forward packet to {}", mix_packet.next_hop()); let next_hop = mix_packet.next_hop(); let packet_type = mix_packet.packet_type(); diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index a751ba2414..d3e4dbf6b5 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -33,18 +33,17 @@ pub fn may_get_home() -> Option { } pub trait NymConfigTemplate: Serialize { - fn template() -> &'static str; + fn template(&self) -> &'static str; fn format_to_string(&self) -> String { // it is responsibility of whoever is implementing the trait to ensure the template is valid Handlebars::new() - .render_template(Self::template(), &self) + .render_template(self.template(), &self) .unwrap() } fn format_to_writer(&self, writer: W) -> io::Result<()> { - if let Err(err) = - Handlebars::new().render_template_to_write(Self::template(), &self, writer) + if let Err(err) = Handlebars::new().render_template_to_write(self.template(), &self, writer) { match err { TemplateRenderError::IOError(err, _) => return Err(err), @@ -64,7 +63,7 @@ where C: NymConfigTemplate, P: AsRef, { - log::trace!("trying to save config file to {}", path.as_ref().display()); + log::debug!("trying to save config file to {}", path.as_ref().display()); let file = File::create(path.as_ref())?; // TODO: check for whether any of our configs stores anything sensitive @@ -108,7 +107,7 @@ where // // // pub trait NymConfig: Default + Serialize + DeserializeOwned { -// fn template() -> &'static str; +// fn template(&self) -> &'static str; // // fn config_file_name() -> String { // "config.toml".to_string() diff --git a/common/credential-utils/src/utils.rs b/common/credential-utils/src/utils.rs index 2f92b53c90..b58211eba0 100644 --- a/common/credential-utils/src/utils.rs +++ b/common/credential-utils/src/utils.rs @@ -71,7 +71,7 @@ pub async fn setup_recovery_storage(recovery_dir: PathBuf) -> RecoveryStorage { pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> PersistentStorage { let data_dir = client_home_directory.join(DEFAULT_DATA_DIR); - let paths = CommonClientPaths::new_default(data_dir); + let paths = CommonClientPaths::new_base(data_dir); let db_path = paths.credentials_database; nym_credential_storage::initialise_persistent_storage(db_path).await diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index 9a17a64c68..ae001c2df6 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -150,6 +150,13 @@ impl PublicKey { } } +#[cfg(feature = "sphinx")] +impl From for DestinationAddressBytes { + fn from(value: PublicKey) -> Self { + value.derive_destination_address() + } +} + impl FromStr for PublicKey { type Err = Ed25519RecoveryError; diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index bfb3f42531..fd002d4db7 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -4,9 +4,9 @@ use crate::var_names; use crate::{DenomDetails, ValidatorDetails}; -pub(crate) const NETWORK_NAME: &str = "mainnet"; +pub const NETWORK_NAME: &str = "mainnet"; -pub(crate) const BECH32_PREFIX: &str = "n"; +pub const BECH32_PREFIX: &str = "n"; pub const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6); pub const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); @@ -15,13 +15,12 @@ pub const MIXNET_CONTRACT_ADDRESS: &str = "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr"; pub const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; -pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = - "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; -pub(crate) const GROUP_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; -pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; -pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; -pub(crate) const EPHEMERA_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; -pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; +pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub const GROUP_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub const EPHEMERA_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/"; pub const NYXD_URL: &str = "https://rpc.nymtech.net"; diff --git a/common/socks5-client-core/Cargo.toml b/common/socks5-client-core/Cargo.toml index 6a5221bba0..ca4c53023d 100644 --- a/common/socks5-client-core/Cargo.toml +++ b/common/socks5-client-core/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = { workspace = true } dirs = "4.0" futures = { workspace = true } log = { workspace = true } diff --git a/common/socks5-client-core/src/config/old_config_v1_1_13.rs b/common/socks5-client-core/src/config/old_config_v1_1_13.rs index 095b4b21d0..dc51308120 100644 --- a/common/socks5-client-core/src/config/old_config_v1_1_13.rs +++ b/common/socks5-client-core/src/config/old_config_v1_1_13.rs @@ -17,7 +17,7 @@ use std::path::PathBuf; // // // // // impl NymConfig for OldConfigV1_1_13 { -// // fn template() -> &'static str { +// // fn template(&self) -> &'static str { // // // not intended to be used // // unimplemented!() // // } diff --git a/common/socks5-client-core/src/lib.rs b/common/socks5-client-core/src/lib.rs index 44b7b42821..bbf9b7ed72 100644 --- a/common/socks5-client-core/src/lib.rs +++ b/common/socks5-client-core/src/lib.rs @@ -19,12 +19,13 @@ use nym_client_core::client::base_client::{ use nym_client_core::client::key_manager::persistence::KeyStore; use nym_client_core::client::replies::reply_storage::ReplyStorageBackend; use nym_client_core::config::DebugConfig; -use nym_client_core::init::GatewaySetup; +use nym_client_core::init::types::GatewaySetup; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::params::PacketType; -use nym_task::{TaskClient, TaskManager}; +use nym_task::{TaskClient, TaskHandle}; +use anyhow::anyhow; use std::error::Error; use std::path::PathBuf; @@ -44,7 +45,7 @@ pub enum Socks5ControlMessage { pub struct StartedSocks5Client { /// Handle for managing graceful shutdown of this client. If dropped, the client will be stopped. - pub shutdown_handle: TaskManager, + pub shutdown_handle: TaskHandle, /// Address of the started client pub address: Recipient, @@ -155,7 +156,7 @@ where pub async fn run_forever(self) -> Result<(), Box> { let started = self.start().await?; - let res = started.shutdown_handle.catch_interrupt().await; + let res = started.shutdown_handle.wait_for_shutdown().await; log::info!("Stopping nym-socks5-client"); res } @@ -168,7 +169,12 @@ where ) -> Result<(), Box> { // Start the main task let started = self.start().await?; - let mut shutdown = started.shutdown_handle; + let mut shutdown = started + .shutdown_handle + .try_into_task_manager() + .ok_or(anyhow!( + "attempted to use `run_and_listen` without owning shutdown handle" + ))?; // Listen to status messages from task, that we forward back to the caller shutdown.start_status_listener(sender).await; @@ -239,7 +245,7 @@ where client_output, client_state, self_address, - started_client.task_manager.subscribe(), + started_client.task_handle.get_handle(), packet_type, ); @@ -247,7 +253,7 @@ where info!("The address of this client is: {self_address}"); Ok(StartedSocks5Client { - shutdown_handle: started_client.task_manager, + shutdown_handle: started_client.task_handle, address: self_address, }) } diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index 416b88770f..71ee35dafd 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -132,7 +132,7 @@ impl Controller { } else { // check if there were any pending messages if let Some(pending) = self.pending_messages.remove(&conn_id) { - debug!("There were some pending messages for {}", conn_id); + debug!("There were some pending messages for {conn_id}"); for data in pending { self.send_to_connection(data) } @@ -141,12 +141,9 @@ impl Controller { } fn remove_connection(&mut self, conn_id: ConnectionId) { - debug!("Removing {} from controller", conn_id); + debug!("Removing {conn_id} from controller"); if self.active_connections.remove(&conn_id).is_none() { - error!( - "tried to remove non-existing connection with id: {:?}", - conn_id - ) + error!("tried to remove non-existing connection with id: {conn_id}",) } self.recently_closed.insert(conn_id); diff --git a/common/task/src/lib.rs b/common/task/src/lib.rs index 00bcb8b0bd..6615bc86e0 100644 --- a/common/task/src/lib.rs +++ b/common/task/src/lib.rs @@ -7,7 +7,7 @@ pub mod manager; pub mod signal; pub mod spawn; -pub use manager::{StatusReceiver, StatusSender, TaskClient, TaskManager}; +pub use manager::{StatusReceiver, StatusSender, TaskClient, TaskHandle, TaskManager}; #[cfg(not(target_arch = "wasm32"))] pub use signal::wait_for_signal_and_error; diff --git a/common/task/src/manager.rs b/common/task/src/manager.rs index e9147776b7..41dd3e7558 100644 --- a/common/task/src/manager.rs +++ b/common/task/src/manager.rs @@ -1,11 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::future::Future; -use std::{error::Error, time::Duration}; - use futures::{future::pending, FutureExt, SinkExt, StreamExt}; use log::{log, Level}; +use std::future::Future; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{error::Error, time::Duration}; use tokio::{ sync::{ mpsc, @@ -143,6 +143,17 @@ impl TaskManager { } } + pub fn subscribe_named>(&self, suffix: S) -> TaskClient { + let task_client = self.subscribe(); + let suffix = suffix.into(); + let child_name = if let Some(base) = &self.name { + format!("{base}-{suffix}") + } else { + format!("unknown-{suffix}") + }; + task_client.named(child_name) + } + pub fn signal_shutdown(&self) -> Result<(), SendError<()>> { self.notify_tx.send(()) } @@ -159,9 +170,9 @@ impl TaskManager { crate::spawn::spawn(async move { loop { if let Some(msg) = task_status_rx.next().await { - log::trace!("Got msg: {}", msg); + log::trace!("Got msg: {msg}"); if let Err(msg) = sender.send(msg).await { - log::error!("Error sending status message: {}", msg); + log::error!("Error sending status message: {msg}"); } } else { log::trace!("Stopping since channel closed"); @@ -238,7 +249,10 @@ pub struct TaskClient { name: Option, // If a shutdown notification has been registered - shutdown: bool, + // the reason for having an atomic here is to be able to cheat and modify that value whilst + // holding an immutable reference to the `TaskClient`. + // note: using `Relaxed` ordering everywhere is fine since it's not shared between threads + shutdown: AtomicBool, // Listen for shutdown notifications, as well as a mechanism to report back that we have // finished (the receiver is closed). @@ -272,7 +286,7 @@ impl Clone for TaskClient { TaskClient { name, - shutdown: self.shutdown, + shutdown: AtomicBool::new(self.shutdown.load(Ordering::Relaxed)), notify: self.notify.clone(), return_error: self.return_error.clone(), drop_error: self.drop_error.clone(), @@ -297,7 +311,7 @@ impl TaskClient { ) -> TaskClient { TaskClient { name: None, - shutdown: false, + shutdown: AtomicBool::new(false), notify, return_error, drop_error, @@ -341,6 +355,17 @@ impl TaskClient { self } + #[must_use] + pub fn with_suffix>(self, suffix: S) -> Self { + let suffix = suffix.into(); + let name = if let Some(base) = &self.name { + format!("{base}-{suffix}") + } else { + format!("unknown-{suffix}") + }; + self.named(name) + } + pub async fn run_future(&mut self, fut: Fut) -> Option where Fut: Future, @@ -360,7 +385,7 @@ impl TaskClient { let (task_status_tx, _task_status_rx) = futures::channel::mpsc::channel(128); TaskClient { name: None, - shutdown: false, + shutdown: AtomicBool::new(false), notify: notify_rx, return_error: task_halt_tx, drop_error: task_drop_tx, @@ -377,7 +402,7 @@ impl TaskClient { if self.mode.is_dummy() { false } else { - self.shutdown + self.shutdown.load(Ordering::Relaxed) } } @@ -385,11 +410,11 @@ impl TaskClient { if self.mode.is_dummy() { return pending().await; } - if self.shutdown { + if self.shutdown.load(Ordering::Relaxed) { return; } let _ = self.notify.changed().await; - self.shutdown = true; + self.shutdown.store(true, Ordering::Relaxed); } pub async fn recv_with_delay(&mut self) { @@ -406,26 +431,30 @@ impl TaskClient { #[cfg_attr(target_arch = "wasm32", allow(clippy::needless_return))] return pending().await; } + #[cfg(not(target_arch = "wasm32"))] - tokio::time::timeout( + if let Err(timeout) = tokio::time::timeout( Self::SHUTDOWN_TIMEOUT_WAITING_FOR_SIGNAL_ON_EXIT, self.recv(), ) .await - .expect("Task stopped without shutdown called"); + { + self.log(Level::Error, "Task stopped without shutdown called"); + panic!("{timeout}") + } } - pub fn is_shutdown_poll(&mut self) -> bool { + pub fn is_shutdown_poll(&self) -> bool { if self.mode.is_dummy() { return false; } - if self.shutdown { + if self.shutdown.load(Ordering::Relaxed) { return true; } match self.notify.has_changed() { Ok(has_changed) => { if has_changed { - self.shutdown = true; + self.shutdown.store(true, Ordering::Relaxed); } has_changed } @@ -520,6 +549,95 @@ impl ClientOperatingMode { } } +#[derive(Debug)] +pub enum TaskHandle { + /// Full [`TaskManager`] that was created by the underlying task. + Internal(TaskManager), + + /// `[TaskClient]` that was passed from an external task, that controls the shutdown process. + External(TaskClient), +} + +impl From for TaskHandle { + fn from(value: TaskManager) -> Self { + TaskHandle::Internal(value) + } +} + +impl From for TaskHandle { + fn from(value: TaskClient) -> Self { + TaskHandle::External(value) + } +} + +impl Default for TaskHandle { + fn default() -> Self { + TaskHandle::Internal(TaskManager::default()) + } +} + +impl TaskHandle { + #[must_use] + pub fn name_if_unnamed>(self, name: S) -> Self { + match self { + TaskHandle::Internal(task_manager) => { + if task_manager.name.is_none() { + TaskHandle::Internal(task_manager.named(name)) + } else { + TaskHandle::Internal(task_manager) + } + } + TaskHandle::External(task_client) => { + if task_client.name.is_none() { + TaskHandle::External(task_client.named(name)) + } else { + TaskHandle::External(task_client) + } + } + } + } + + #[must_use] + pub fn named>(self, name: S) -> Self { + match self { + TaskHandle::Internal(task_manager) => TaskHandle::Internal(task_manager.named(name)), + TaskHandle::External(task_client) => TaskHandle::External(task_client.named(name)), + } + } + + pub fn fork>(&self, child_suffix: S) -> TaskClient { + match self { + TaskHandle::External(shutdown) => shutdown.fork(child_suffix), + TaskHandle::Internal(shutdown) => shutdown.subscribe_named(child_suffix), + } + } + + pub fn get_handle(&self) -> TaskClient { + match self { + TaskHandle::External(shutdown) => shutdown.clone(), + TaskHandle::Internal(shutdown) => shutdown.subscribe(), + } + } + + pub fn try_into_task_manager(self) -> Option { + match self { + TaskHandle::External(_) => None, + TaskHandle::Internal(shutdown) => Some(shutdown), + } + } + + #[cfg(not(target_arch = "wasm32"))] + pub async fn wait_for_shutdown(self) -> Result<(), SentError> { + match self { + TaskHandle::Internal(task_manager) => task_manager.catch_interrupt().await, + TaskHandle::External(mut task_client) => { + task_client.recv().await; + Ok(()) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/topology/src/error.rs b/common/topology/src/error.rs index 3f2523576f..4c8714c0d6 100644 --- a/common/topology/src/error.rs +++ b/common/topology/src/error.rs @@ -21,6 +21,9 @@ pub enum NymTopologyError { #[error("Gateway with identity key {identity_key} doesn't exist")] NonExistentGatewayError { identity_key: String }, + #[error("timed out while waiting for gateway '{identity_key}' to come online")] + TimedOutWaitingForGateway { identity_key: String }, + #[error("Wanted to create a mix route with {requested} hops, while only {available} layers are available")] InvalidNumberOfHopsError { available: usize, requested: usize }, diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index b9b740997e..c23f8b8a23 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -402,8 +402,8 @@ pub fn nym_topology_from_detailed( let layer = bond.layer as MixLayer; if layer == 0 || layer > 3 { warn!( - "{} says it's on invalid layer {}!", - bond.mix_node.identity_key, layer + "{} says it's on invalid layer {layer}!", + bond.mix_node.identity_key ); continue; } @@ -414,7 +414,7 @@ pub fn nym_topology_from_detailed( match bond.try_into() { Ok(mix) => layer_entry.push(mix), Err(err) => { - warn!("Mix {} / {} is malformed - {err}", mix_id, mix_identity); + warn!("Mix {mix_id} / {mix_identity} is malformed: {err}"); continue; } } @@ -426,7 +426,7 @@ pub fn nym_topology_from_detailed( match bond.try_into() { Ok(gate) => gateways.push(gate), Err(err) => { - warn!("Gateway {} is malformed - {err}", gate_id); + warn!("Gateway {gate_id} is malformed: {err}"); continue; } } diff --git a/common/types/src/gateway.rs b/common/types/src/gateway.rs index 125cf4ba03..a26fea053f 100644 --- a/common/types/src/gateway.rs +++ b/common/types/src/gateway.rs @@ -82,23 +82,67 @@ pub struct GatewayNodeDetailsResponse { pub identity_key: String, pub sphinx_key: String, pub bind_address: String, - pub version: String, pub mix_port: u16, pub clients_port: u16, + pub config_path: String, pub data_store: String, + + pub network_requester: Option, } impl fmt::Display for GatewayNodeDetailsResponse { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Identity Key: {}", self.identity_key)?; - writeln!(f, "Sphinx Key: {}", self.sphinx_key)?; - writeln!(f, "Version: {}", self.version)?; + writeln!(f, "config path: {}", self.config_path)?; + writeln!(f, "identity key: {}", self.identity_key)?; + writeln!(f, "sphinx key: {}", self.sphinx_key)?; + writeln!(f, "bind address: {}", self.bind_address)?; writeln!( f, - "Mix Port: {}, Clients port: {}", + "mix Port: {}, clients port: {}", self.mix_port, self.clients_port )?; - writeln!(f, "Data store is at: {}", self.data_store) + writeln!(f, "data store is at: {}", self.data_store)?; + + if let Some(nr) = &self.network_requester { + nr.fmt(f)?; + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +pub struct GatewayNetworkRequesterDetails { + pub enabled: bool, + + pub identity_key: String, + pub encryption_key: String, + + pub open_proxy: bool, + pub enabled_statistics: bool, + + // just a convenience wrapper around all the keys + pub address: String, + + pub config_path: String, + pub allow_list_path: String, + pub unknown_list_path: String, +} + +impl fmt::Display for GatewayNetworkRequesterDetails { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Network requester:")?; + writeln!(f, "\tenabled: {}", self.enabled)?; + writeln!(f, "\tconfig path: {}", self.config_path)?; + + writeln!(f, "\tidentity key: {}", self.identity_key)?; + writeln!(f, "\tencryption key: {}", self.encryption_key)?; + writeln!(f, "\taddress: {}", self.address)?; + + writeln!(f, "\tuses open proxy: {}", self.open_proxy)?; + writeln!(f, "\tsends statistics: {}", self.enabled_statistics)?; + + writeln!(f, "\tallow list path: {}", self.allow_list_path)?; + writeln!(f, "\tunknown list path: {}", self.allow_list_path) } } diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index 429fae6400..77dad1da5c 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -356,6 +356,10 @@ pub struct TopologyWasm { /// did not reach its destination. pub topology_resolution_timeout_ms: u32, + /// Defines how long the client is going to wait on startup for its gateway to come online, + /// before abandoning the procedure. + pub max_startup_gateway_waiting_period_ms: u32, + /// Specifies whether the client should not refresh the network topology after obtaining /// the first valid instance. /// Supersedes `topology_refresh_rate_ms`. @@ -376,6 +380,9 @@ impl From for ConfigTopology { topology.topology_resolution_timeout_ms as u64, ), disable_refreshing: topology.disable_refreshing, + max_startup_gateway_waiting_period: Duration::from_millis( + topology.max_startup_gateway_waiting_period_ms as u64, + ), topology_structure: Default::default(), } } @@ -386,6 +393,9 @@ impl From for TopologyWasm { TopologyWasm { topology_refresh_rate_ms: topology.topology_refresh_rate.as_millis() as u32, topology_resolution_timeout_ms: topology.topology_resolution_timeout.as_millis() as u32, + max_startup_gateway_waiting_period_ms: topology + .max_startup_gateway_waiting_period + .as_millis() as u32, disable_refreshing: topology.disable_refreshing, } } diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index 20f3f0cf2a..147584f3d4 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -228,6 +228,11 @@ pub struct TopologyWasmOverride { #[tsify(optional)] pub topology_resolution_timeout_ms: Option, + /// Defines how long the client is going to wait on startup for its gateway to come online, + /// before abandoning the procedure. + #[tsify(optional)] + pub max_startup_gateway_waiting_period_ms: Option, + /// Specifies whether the client should not refresh the network topology after obtaining /// the first valid instance. /// Supersedes `topology_refresh_rate_ms`. @@ -246,6 +251,9 @@ impl From for TopologyWasm { topology_resolution_timeout_ms: value .topology_resolution_timeout_ms .unwrap_or(def.topology_resolution_timeout_ms), + max_startup_gateway_waiting_period_ms: value + .max_startup_gateway_waiting_period_ms + .unwrap_or(def.max_startup_gateway_waiting_period_ms), disable_refreshing: value.disable_refreshing.unwrap_or(def.disable_refreshing), } } diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 4c86cf0cfa..002f157c10 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -8,7 +8,11 @@ use js_sys::Promise; use nym_client_core::client::replies::reply_storage::browser_backend; use nym_client_core::config; use nym_client_core::init::helpers::current_gateways; -use nym_client_core::init::{setup_gateway_from, GatewaySetup, InitialisationResult}; +use nym_client_core::init::types::GatewaySelectionSpecification; +use nym_client_core::init::{ + self, + types::{GatewaySetup, InitialisationResult}, +}; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_topology::{gateway, NymTopology, SerializableNymTopology}; @@ -88,10 +92,16 @@ async fn setup_gateway( let setup = if client_store.has_full_gateway_info().await? { GatewaySetup::MustLoad } else { - GatewaySetup::new_fresh(chosen_gateway.clone(), None) + let selection_spec = GatewaySelectionSpecification::new(chosen_gateway.clone(), None); + + GatewaySetup::New { + specification: selection_spec, + available_gateways: gateways.to_vec(), + overwrite_data: false, + } }; - setup_gateway_from(setup, client_store, client_store, false, Some(gateways)) + init::setup_gateway(setup, client_store, client_store) .await .map_err(Into::into) } diff --git a/common/wasm/client-core/src/lib.rs b/common/wasm/client-core/src/lib.rs index 95ce12ad3c..568b2b459e 100644 --- a/common/wasm/client-core/src/lib.rs +++ b/common/wasm/client-core/src/lib.rs @@ -16,11 +16,9 @@ pub mod topology; pub use nym_bandwidth_controller::BandwidthController; pub use nym_client_core::*; pub use nym_client_core::{ - client::key_manager::ManagedKeys, - error::ClientCoreError, - init::{InitialisationDetails, InitialisationResult}, + client::key_manager::ManagedKeys, error::ClientCoreError, init::types::InitialisationResult, }; -pub use nym_gateway_client::{error::GatewayClientError, GatewayClient}; +pub use nym_gateway_client::{error::GatewayClientError, GatewayClient, GatewayConfig}; pub use nym_sphinx::{ addressing::{clients::Recipient, nodes::NodeIdentity}, params::PacketType, diff --git a/common/wasm/client-core/src/storage/core_client_traits.rs b/common/wasm/client-core/src/storage/core_client_traits.rs index 0a0114d55c..b017bb3914 100644 --- a/common/wasm/client-core/src/storage/core_client_traits.rs +++ b/common/wasm/client-core/src/storage/core_client_traits.rs @@ -80,7 +80,7 @@ impl KeyStore for ClientStorage { Ok(KeyManager::from_keys( identity_keypair, encryption_keypair, - gateway_shared_key, + Some(gateway_shared_key), ack_keypair, )) } @@ -93,8 +93,11 @@ impl KeyStore for ClientStorage { self.store_encryption_keypair(&keys.encryption_keypair()) .await?; self.store_ack_key(&keys.ack_key()).await?; - self.store_gateway_shared_key(&keys.gateway_shared_key()) - .await + + if let Some(shared_keys) = keys.gateway_shared_key() { + self.store_gateway_shared_key(&shared_keys).await?; + } + Ok(()) } } diff --git a/documentation/dev-portal/src/tutorials/cosmos-service/lib.md b/documentation/dev-portal/src/tutorials/cosmos-service/lib.md index 77236ea11e..9982403657 100644 --- a/documentation/dev-portal/src/tutorials/cosmos-service/lib.md +++ b/documentation/dev-portal/src/tutorials/cosmos-service/lib.md @@ -100,7 +100,6 @@ pub async fn create_client(config_path: PathBuf) -> MixnetClient { .await .unwrap() .build() - .await .unwrap(); client.connect_to_mixnet().await.unwrap() diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 20f96e08bf..2e4541e63b 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -15,14 +15,14 @@ rust-version = "1.56" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.53" +anyhow = { workspace = true } async-trait = { workspace = true } atty = "0.2" bip39 = { workspace = true } bs58 = "0.4.0" clap = { version = "4.0", features = ["cargo", "derive"] } colored = "2.0" -dashmap = "4.0" +dashmap = { workspace = true } dirs = "4.0" dotenvy = { workspace = true } futures = { workspace = true } @@ -55,6 +55,7 @@ nym-gateway-requests = { path = "gateway-requests" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } nym-mixnode-common = { path = "../common/mixnode-common" } nym-network-defaults = { path = "../common/network-defaults" } +nym-network-requester = { path = "../service-providers/network-requester" } nym-pemstore = { path = "../common/pemstore" } nym-sphinx = { path = "../common/nymsphinx" } nym-statistics-common = { path = "../common/statistics" } diff --git a/gateway/src/commands/helpers.rs b/gateway/src/commands/helpers.rs new file mode 100644 index 0000000000..1a83b76317 --- /dev/null +++ b/gateway/src/commands/helpers.rs @@ -0,0 +1,277 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::upgrade_helpers; +use crate::config::default_config_filepath; +use crate::config::persistence::paths::default_network_requester_data_dir; +use crate::config::Config; +use crate::error::GatewayError; +use log::{error, info}; +use nym_bin_common::version_checker; +use nym_config::{save_formatted_config_to_file, OptionalSet}; +use nym_crypto::asymmetric::identity; +use nym_network_defaults::mainnet; +use nym_network_defaults::var_names::NYXD; +use nym_network_defaults::var_names::{BECH32_PREFIX, NYM_API, STATISTICS_SERVICE_DOMAIN_ADDRESS}; +use nym_network_requester::config::BaseClientConfig; +use nym_network_requester::{ + setup_gateway, GatewaySelectionSpecification, GatewaySetup, OnDiskGatewayDetails, OnDiskKeys, +}; +use nym_types::gateway::GatewayNetworkRequesterDetails; +use nym_validator_client::nyxd::AccountId; +use std::net::IpAddr; +use std::path::PathBuf; + +// Configuration that can be overridden. +#[derive(Default)] +pub(crate) struct OverrideConfig { + pub(crate) host: Option, + pub(crate) mix_port: Option, + pub(crate) clients_port: Option, + pub(crate) datastore: Option, + pub(crate) enabled_statistics: Option, + pub(crate) statistics_service_url: Option, + pub(crate) nym_apis: Option>, + pub(crate) mnemonic: Option, + pub(crate) nyxd_urls: Option>, + pub(crate) only_coconut_credentials: Option, + pub(crate) with_network_requester: Option, +} + +impl OverrideConfig { + pub(crate) fn do_override(self, mut config: Config) -> Result { + config = config + .with_optional(Config::with_listening_address, self.host) + .with_optional(Config::with_mix_port, self.mix_port) + .with_optional(Config::with_clients_port, self.clients_port) + .with_optional_custom_env( + Config::with_custom_nym_apis, + self.nym_apis, + NYM_API, + nym_config::parse_urls, + ) + .with_optional(Config::with_enabled_statistics, self.enabled_statistics) + .with_optional_env( + Config::with_custom_statistics_service_url, + self.statistics_service_url, + STATISTICS_SERVICE_DOMAIN_ADDRESS, + ) + .with_optional(Config::with_custom_persistent_store, self.datastore) + .with_optional(Config::with_cosmos_mnemonic, self.mnemonic) + .with_optional_custom_env( + Config::with_custom_validator_nyxd, + self.nyxd_urls, + NYXD, + nym_config::parse_urls, + ) + .with_optional( + Config::with_only_coconut_credentials, + self.only_coconut_credentials, + ) + .with_optional( + Config::with_enabled_network_requester, + self.with_network_requester, + ); + + if config.network_requester.enabled + && config.storage_paths.network_requester_config.is_none() + { + Ok(config.with_default_network_requester_config_path()) + } else { + Ok(config) + } + } +} + +#[derive(Default)] +pub(crate) struct OverrideNetworkRequesterConfig { + pub(crate) fastmode: bool, + pub(crate) no_cover: bool, + pub(crate) medium_toggle: bool, + + pub(crate) open_proxy: Option, + pub(crate) enable_statistics: Option, + pub(crate) statistics_recipient: Option, +} + +/// Ensures that a given bech32 address is valid +pub(crate) fn ensure_correct_bech32_prefix(address: &AccountId) -> Result<(), GatewayError> { + let expected_prefix = + std::env::var(BECH32_PREFIX).unwrap_or(mainnet::BECH32_PREFIX.to_string()); + let actual_prefix = address.prefix(); + + if expected_prefix != actual_prefix { + return Err(GatewayError::InvalidBech32AccountPrefix { + account: address.to_owned(), + expected_prefix, + actual_prefix: actual_prefix.to_owned(), + }); + } + + Ok(()) +} + +// this only checks compatibility between config the binary. It does not take into consideration +// network version. It might do so in the future. +pub(crate) fn ensure_config_version_compatibility(cfg: &Config) -> Result<(), GatewayError> { + let binary_version = env!("CARGO_PKG_VERSION"); + let config_version = &cfg.gateway.version; + + if binary_version == config_version { + Ok(()) + } else if version_checker::is_minor_version_compatible(binary_version, config_version) { + log::warn!( + "The gateway binary has different version than what is specified in config file! {binary_version} and {config_version}. \ + But, they are still semver compatible. However, consider running the `upgrade` command."); + Ok(()) + } else { + log::error!( + "The gateway binary has different version than what is specified in config file! {binary_version} and {config_version}. \ + And they are semver incompatible! - please run the `upgrade` command before attempting `run` again"); + Err(GatewayError::LocalVersionCheckFailure { + binary_version: binary_version.to_owned(), + config_version: config_version.to_owned(), + }) + } +} + +pub(crate) fn try_load_current_config(id: &str) -> Result { + upgrade_helpers::try_upgrade_config(id)?; + + Config::read_from_default_path(id).map_err(|err| { + error!( + "Failed to load config for {id}. Are you sure you have run `init` before? (Error was: {err})", + ); + GatewayError::ConfigLoadFailure { + path: default_config_filepath(id), + id: id.to_string(), + source: err, + } + }) +} + +fn make_nr_id(gateway_id: &str) -> String { + format!("{gateway_id}-network-requester") +} + +// NOTE: make sure this is in sync with service-providers/network-requester/src/cli/mod.rs::override_config +pub(crate) fn override_network_requester_config( + mut cfg: nym_network_requester::Config, + opts: Option, +) -> nym_network_requester::Config { + let Some(opts) = opts else { return cfg }; + + // as of 12.09.23 the below is true (not sure how this comment will rot in the future) + // medium_toggle: + // - sets secondary packet size to 16kb + // - disables poisson distribution of the main traffic stream + // - sets the cover traffic stream to 1 packet / 5s (on average) + // - disables per hop delay + // + // fastmode (to be renamed to `fast-poisson`): + // - sets average per hop delay to 10ms + // - sets the cover traffic stream to 1 packet / 2000s (on average); for all intents and purposes it disables the stream + // - sets the poisson distribution of the main traffic stream to 4ms, i.e. 250 packets / s on average + // + // no_cover: + // - disables poisson distribution of the main traffic stream + // - disables the secondary cover traffic stream + + // disable poisson rate in the BASE client if the NR option is enabled + if cfg.network_requester.disable_poisson_rate { + cfg.set_no_poisson_process(); + } + + // those should be enforced by `clap` when parsing the arguments + if opts.medium_toggle { + assert!(!opts.fastmode); + assert!(!opts.no_cover); + + cfg.set_medium_toggle(); + } + + cfg.with_base( + BaseClientConfig::with_high_default_traffic_volume, + opts.fastmode, + ) + .with_base(BaseClientConfig::with_disabled_cover_traffic, opts.no_cover) + .with_optional( + nym_network_requester::Config::with_open_proxy, + opts.open_proxy, + ) + .with_optional( + nym_network_requester::Config::with_enabled_statistics, + opts.enable_statistics, + ) + .with_optional( + nym_network_requester::Config::with_statistics_recipient, + opts.statistics_recipient, + ) +} + +pub(crate) async fn initialise_local_network_requester( + gateway_config: &Config, + opts: OverrideNetworkRequesterConfig, + identity: identity::PublicKey, +) -> Result { + info!("initialising network requester..."); + let Some(nr_cfg_path) = gateway_config.storage_paths.network_requester_config() else { + return Err(GatewayError::UnspecifiedNetworkRequesterConfig); + }; + + let id = &gateway_config.gateway.id; + let nr_id = make_nr_id(id); + let nr_data_dir = default_network_requester_data_dir(id); + let mut nr_cfg = nym_network_requester::Config::new(&nr_id).with_data_directory(nr_data_dir); + nr_cfg = override_network_requester_config(nr_cfg, Some(opts)); + + let key_store = OnDiskKeys::new(nr_cfg.storage_paths.common_paths.keys.clone()); + let details_store = + OnDiskGatewayDetails::new(&nr_cfg.storage_paths.common_paths.gateway_details); + + // gateway setup here is way simpler as we're 'connecting' to ourselves + let init_res = setup_gateway( + GatewaySetup::New { + specification: GatewaySelectionSpecification::Custom { + gateway_identity: identity.to_base58_string(), + additional_data: Default::default(), + }, + available_gateways: vec![], + overwrite_data: false, + }, + &key_store, + &details_store, + ) + .await?; + + let address = init_res.client_address()?; + + if let Err(err) = save_formatted_config_to_file(&nr_cfg, nr_cfg_path) { + log::error!("Failed to save the network requester config file: {err}"); + return Err(GatewayError::ConfigSaveFailure { + id: nr_id, + path: nr_cfg_path.to_path_buf(), + source: err, + }); + }; + + Ok(GatewayNetworkRequesterDetails { + enabled: gateway_config.network_requester.enabled, + identity_key: address.identity().to_string(), + encryption_key: address.encryption_key().to_string(), + open_proxy: nr_cfg.network_requester.open_proxy, + enabled_statistics: nr_cfg.network_requester.enabled_statistics, + address: address.to_string(), + config_path: nr_cfg_path.display().to_string(), + allow_list_path: nr_cfg + .storage_paths + .allowed_list_location + .display() + .to_string(), + unknown_list_path: nr_cfg + .storage_paths + .unknown_list_location + .display() + .to_string(), + }) +} diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index 2901e116ef..c5123703ab 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -1,15 +1,15 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::{default_config_directory, default_config_filepath, default_data_directory}; -use crate::{ - commands::{override_config, OverrideConfig}, - config::Config, - OutputFormat, +use crate::commands::helpers::{ + initialise_local_network_requester, OverrideNetworkRequesterConfig, }; +use crate::config::{default_config_directory, default_config_filepath, default_data_directory}; +use crate::node::helpers::node_details; +use crate::{commands::helpers::OverrideConfig, config::Config, OutputFormat}; +use anyhow::bail; use clap::Args; use nym_crypto::asymmetric::{encryption, identity}; -use std::error::Error; use std::net::IpAddr; use std::path::PathBuf; use std::{fs, io}; @@ -17,32 +17,32 @@ use std::{fs, io}; #[derive(Args, Clone)] pub struct Init { /// Id of the gateway we want to create config for - #[clap(long)] + #[arg(long)] id: String, /// The custom host on which the gateway will be running for receiving sphinx packets - #[clap(long)] + #[arg(long)] host: IpAddr, /// The port on which the gateway will be listening for sphinx packets - #[clap(long)] + #[arg(long)] mix_port: Option, /// The port on which the gateway will be listening for clients gateway-requests - #[clap(long)] + #[arg(long)] clients_port: Option, /// Path to sqlite database containing all gateway persistent data - #[clap(long)] + #[arg(long)] datastore: Option, /// Comma separated list of endpoints of nym APIs - #[clap(long, alias = "validator_apis", value_delimiter = ',')] + #[arg(long, alias = "validator_apis", value_delimiter = ',')] // the alias here is included for backwards compatibility (1.1.4 and before) nym_apis: Option>, /// Comma separated list of endpoints of the validator - #[clap( + #[arg( long, alias = "validators", alias = "nyxd_validators", @@ -53,23 +53,71 @@ pub struct Init { nyxd_urls: Option>, /// Cosmos wallet mnemonic needed for double spending protection - #[clap(long)] + #[arg(long)] mnemonic: Option, /// Set this gateway to work only with coconut credentials; that would disallow clients to /// bypass bandwidth credential requirement - #[clap(long, hide = true)] + #[arg(long, hide = true)] only_coconut_credentials: Option, /// Enable/disable gateway anonymized statistics that get sent to a statistics aggregator server - #[clap(long)] + #[arg(long)] enabled_statistics: Option, /// URL where a statistics aggregator is running. The default value is a Nym aggregator server - #[clap(long)] + #[arg(long)] statistics_service_url: Option, - #[clap(short, long, default_value_t = OutputFormat::default())] + /// Allows this gateway to run an embedded network requester for minimal network overhead + #[arg(long)] + with_network_requester: bool, + + // ##### NETWORK REQUESTER FLAGS ##### + /// Specifies whether this network requester should run in 'open-proxy' mode + #[arg(long, requires = "with_network_requester")] + open_proxy: Option, + + /// Enable service anonymized statistics that get sent to a statistics aggregator server + #[arg(long, requires = "with_network_requester")] + enable_statistics: Option, + + /// Mixnet client address where a statistics aggregator is running. The default value is a Nym + /// aggregator client + #[arg(long, requires = "with_network_requester")] + statistics_recipient: Option, + + /// Mostly debug-related option to increase default traffic rate so that you would not need to + /// modify config post init + #[arg( + long, + hide = true, + conflicts_with = "medium_toggle", + requires = "with_network_requester" + )] + fastmode: bool, + + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[arg( + long, + hide = true, + conflicts_with = "medium_toggle", + requires = "with_network_requester" + )] + no_cover: bool, + + /// Enable medium mixnet traffic, for experiments only. + /// This includes things like disabling cover traffic, no per hop delays, etc. + #[arg( + long, + hide = true, + conflicts_with = "no_cover", + conflicts_with = "fastmode", + requires = "with_network_requester" + )] + medium_toggle: bool, + + #[arg(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -88,6 +136,20 @@ impl From for OverrideConfig { nyxd_urls: init_config.nyxd_urls, only_coconut_credentials: init_config.only_coconut_credentials, + with_network_requester: Some(init_config.with_network_requester), + } + } +} + +impl<'a> From<&'a Init> for OverrideNetworkRequesterConfig { + fn from(value: &'a Init) -> Self { + OverrideNetworkRequesterConfig { + fastmode: value.fastmode, + no_cover: value.no_cover, + medium_toggle: value.medium_toggle, + open_proxy: value.open_proxy, + enable_statistics: value.enable_statistics, + statistics_recipient: value.statistics_recipient.clone(), } } } @@ -97,8 +159,9 @@ fn init_paths(id: &str) -> io::Result<()> { fs::create_dir_all(default_config_directory(id)) } -pub async fn execute(args: Init) -> Result<(), Box> { +pub async fn execute(args: Init) -> anyhow::Result<()> { eprintln!("Initialising gateway {}...", args.id); + let output = args.output; let already_init = if default_config_filepath(&args.id).exists() { eprintln!( @@ -112,52 +175,60 @@ pub async fn execute(args: Init) -> Result<(), Box> { false }; - let override_config_fields = OverrideConfig::from(args.clone()); - // Initialising the config structure is just overriding a default constructed one - let config = override_config(Config::new(&args.id), override_config_fields)?; + let fresh_config = Config::new(&args.id); + let nr_opts = (&args).into(); + let mut config = OverrideConfig::from(args).do_override(fresh_config)?; - // if gateway was already initialised, don't generate new keys + // if gateway was already initialised, don't generate new keys, et al. if !already_init { let mut rng = rand::rngs::OsRng; let identity_keys = identity::KeyPair::new(&mut rng); let sphinx_keys = encryption::KeyPair::new(&mut rng); - nym_pemstore::store_keypair( + if let Err(err) = nym_pemstore::store_keypair( &identity_keys, &nym_pemstore::KeyPairPath::new( config.storage_paths.private_identity_key(), config.storage_paths.public_identity_key(), ), - ) - .expect("Failed to save identity keys"); + ) { + bail!("failed to save the identity keys: {err}") + } - nym_pemstore::store_keypair( + if let Err(err) = nym_pemstore::store_keypair( &sphinx_keys, &nym_pemstore::KeyPairPath::new( config.storage_paths.private_encryption_key(), config.storage_paths.public_encryption_key(), ), - ) - .expect("Failed to save sphinx keys"); + ) { + bail!("failed to save the sphinx keys: {err}") + } + + if config.network_requester.enabled { + initialise_local_network_requester(&config, nr_opts, *identity_keys.public_key()) + .await?; + } eprintln!("Saved identity and mixnet sphinx keypairs"); } let config_save_location = config.default_location(); - config - .save_to_default_location() - .expect("Failed to save the config file"); + if let Err(err) = config.save_to_default_location() { + bail!("failed to save the config file: {err}") + } + config.save_path = Some(config_save_location.clone()); + eprintln!( "Saved configuration file to {}", config_save_location.display() ); eprintln!("Gateway configuration completed.\n\n\n"); - crate::node::create_gateway(config) - .await - .print_node_details(args.output); + output.to_stdout(&node_details(&config)?); + Ok(()) } @@ -184,11 +255,20 @@ mod tests { nyxd_urls: None, only_coconut_credentials: None, output: Default::default(), + with_network_requester: false, + open_proxy: None, + enable_statistics: None, + statistics_recipient: None, + fastmode: false, + no_cover: false, + medium_toggle: false, }; std::env::set_var(BECH32_PREFIX, "n"); - let config = Config::new(&args.id); - let config = override_config(config, OverrideConfig::from(args.clone())).unwrap(); + let fresh_config = Config::new(&args.id); + let config = OverrideConfig::from(args) + .do_override(fresh_config) + .unwrap(); let (identity_keys, sphinx_keys) = { let mut rng = rand::rngs::OsRng; @@ -199,8 +279,13 @@ mod tests { }; // The test is really if this instantiates with InMemStorage without panics - let _gateway = - Gateway::new_from_keys_and_storage(config, identity_keys, sphinx_keys, InMemStorage) - .await; + let _gateway = Gateway::new_from_keys_and_storage( + config, + None, + identity_keys, + sphinx_keys, + InMemStorage, + ) + .await; } } diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index ed70c8b203..0df249edb1 100644 --- a/gateway/src/commands/mod.rs +++ b/gateway/src/commands/mod.rs @@ -1,28 +1,20 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::default_config_filepath; -use crate::config::old_config_v1_1_20::ConfigV1_1_20; -use crate::error::GatewayError; -use crate::{config::Config, Cli}; +use crate::Cli; use clap::CommandFactory; use clap::Subcommand; -use log::{error, info}; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_bin_common::version_checker; -use nym_config::OptionalSet; -use nym_network_defaults::var_names::NYXD; -use nym_network_defaults::var_names::{BECH32_PREFIX, NYM_API, STATISTICS_SERVICE_DOMAIN_ADDRESS}; -use nym_validator_client::nyxd::AccountId; use std::error::Error; -use std::net::IpAddr; -use std::path::PathBuf; pub(crate) mod build_info; +pub(crate) mod helpers; pub(crate) mod init; pub(crate) mod node_details; pub(crate) mod run; +pub(crate) mod setup_network_requester; pub(crate) mod sign; +mod upgrade_helpers; #[derive(Subcommand)] pub(crate) enum Commands { @@ -35,6 +27,10 @@ pub(crate) enum Commands { /// Starts the gateway Run(run::Run), + /// Add network requester support to this gateway + // essentially an option to include NR without having to setup fresh gateway + SetupNetworkRequester(setup_network_requester::CmdArgs), + /// Sign text to prove ownership of this mixnode Sign(sign::Sign), @@ -48,21 +44,6 @@ pub(crate) enum Commands { GenerateFigSpec, } -// Configuration that can be overridden. -#[derive(Default)] -pub(crate) struct OverrideConfig { - host: Option, - mix_port: Option, - clients_port: Option, - datastore: Option, - enabled_statistics: Option, - statistics_service_url: Option, - nym_apis: Option>, - mnemonic: Option, - nyxd_urls: Option>, - only_coconut_credentials: Option, -} - pub(crate) async fn execute(args: Cli) -> Result<(), Box> { let bin_name = "nym-gateway"; @@ -70,6 +51,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box init::execute(m).await?, Commands::NodeDetails(m) => node_details::execute(m).await?, Commands::Run(m) => run::execute(m).await?, + Commands::SetupNetworkRequester(m) => setup_network_requester::execute(m).await?, Commands::Sign(m) => sign::execute(m)?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut crate::Cli::command(), bin_name), @@ -78,118 +60,6 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box Result { - config = config - .with_optional(Config::with_listening_address, args.host) - .with_optional(Config::with_mix_port, args.mix_port) - .with_optional(Config::with_clients_port, args.clients_port) - .with_optional_custom_env( - Config::with_custom_nym_apis, - args.nym_apis, - NYM_API, - nym_config::parse_urls, - ) - .with_optional(Config::with_enabled_statistics, args.enabled_statistics) - .with_optional_env( - Config::with_custom_statistics_service_url, - args.statistics_service_url, - STATISTICS_SERVICE_DOMAIN_ADDRESS, - ) - .with_optional(Config::with_custom_persistent_store, args.datastore) - .with_optional(Config::with_cosmos_mnemonic, args.mnemonic) - .with_optional_custom_env( - Config::with_custom_validator_nyxd, - args.nyxd_urls, - NYXD, - nym_config::parse_urls, - ) - .with_optional( - Config::with_only_coconut_credentials, - args.only_coconut_credentials, - ); - - Ok(config) -} - -/// Ensures that a given bech32 address is valid -pub(crate) fn ensure_correct_bech32_prefix(address: &AccountId) -> Result<(), GatewayError> { - let expected_prefix = std::env::var(BECH32_PREFIX).expect("bech32 prefix not set"); - let actual_prefix = address.prefix(); - if expected_prefix != actual_prefix { - return Err(GatewayError::InvalidBech32AccountPrefix { - account: address.to_owned(), - expected_prefix, - actual_prefix: actual_prefix.to_owned(), - }); - } - - Ok(()) -} - -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -pub(crate) fn ensure_config_version_compatibility(cfg: &Config) -> Result<(), GatewayError> { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.gateway.version; - - if binary_version == config_version { - Ok(()) - } else if version_checker::is_minor_version_compatible(binary_version, config_version) { - log::warn!( - "The gateway binary has different version than what is specified in config file! {binary_version} and {config_version}. \ - But, they are still semver compatible. However, consider running the `upgrade` command."); - Ok(()) - } else { - log::error!( - "The gateway binary has different version than what is specified in config file! {binary_version} and {config_version}. \ - And they are semver incompatible! - please run the `upgrade` command before attempting `run` again"); - Err(GatewayError::LocalVersionCheckFailure { - binary_version: binary_version.to_owned(), - config_version: config_version.to_owned(), - }) - } -} - -fn try_upgrade_v1_1_20_config(id: &str) -> Result<(), GatewayError> { - use nym_config::legacy_helpers::nym_config::MigrationNymConfig; - - // explicitly load it as v1.1.20 (which is incompatible with the current, i.e. 1.1.21+) - let Ok(old_config) = ConfigV1_1_20::load_from_file(id) else { - // if we failed to load it, there might have been nothing to upgrade - // or maybe it was an even older file. in either way. just ignore it and carry on with our day - return Ok(()); - }; - info!("It seems the gateway is using <= v1.1.20 config template."); - info!("It is going to get updated to the current specification."); - - let updated: Config = old_config.into(); - updated - .save_to_default_location() - .map_err(|err| GatewayError::ConfigSaveFailure { - path: default_config_filepath(id), - id: id.to_string(), - source: err, - }) -} - -pub(crate) fn try_load_current_config(id: &str) -> Result { - try_upgrade_v1_1_20_config(id)?; - - Config::read_from_default_path(id).map_err(|err| { - error!( - "Failed to load config for {id}. Are you sure you have run `init` before? (Error was: {err})", - ); - GatewayError::ConfigLoadFailure { - path: default_config_filepath(id), - id: id.to_string(), - source: err, - } - }) -} - #[cfg(test)] mod tests { use super::*; diff --git a/gateway/src/commands/node_details.rs b/gateway/src/commands/node_details.rs index 11c855ace5..2d37617338 100644 --- a/gateway/src/commands/node_details.rs +++ b/gateway/src/commands/node_details.rs @@ -1,11 +1,10 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::commands::OverrideConfig; -use crate::support::config::build_config; +use crate::commands::helpers::try_load_current_config; +use crate::node::helpers::node_details; use clap::Args; use nym_bin_common::output_format::OutputFormat; -use std::error::Error; #[derive(Args, Clone)] pub struct NodeDetails { @@ -17,11 +16,9 @@ pub struct NodeDetails { output: OutputFormat, } -pub async fn execute(args: NodeDetails) -> Result<(), Box> { - let config = build_config(args.id.clone(), OverrideConfig::default())?; +pub async fn execute(args: NodeDetails) -> anyhow::Result<()> { + let config = try_load_current_config(&args.id)?; + args.output.to_stdout(&node_details(&config)?); - crate::node::create_gateway(config) - .await - .print_node_details(args.output); Ok(()) } diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index d60e66c7b5..6dd01e90bc 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -1,12 +1,14 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::commands::{ensure_config_version_compatibility, OverrideConfig}; +use crate::commands::helpers::{ + ensure_config_version_compatibility, OverrideConfig, OverrideNetworkRequesterConfig, +}; +use crate::node::helpers::node_details; use crate::support::config::build_config; use clap::Args; use nym_bin_common::output_format::OutputFormat; use nym_config::helpers::SPECIAL_ADDRESSES; -use std::error::Error; use std::net::IpAddr; use std::path::PathBuf; @@ -33,7 +35,12 @@ pub struct Run { datastore: Option, /// Comma separated list of endpoints of nym APIs - #[clap(long, alias = "validator_apis", value_delimiter = ',')] + #[clap( + long, + alias = "validator_apis", + value_delimiter = ',', + group = "network" + )] // the alias here is included for backwards compatibility (1.1.4 and before) nym_apis: Option>, @@ -65,6 +72,48 @@ pub struct Run { #[clap(long)] statistics_service_url: Option, + /// Allows this gateway to run an embedded network requester for minimal network overhead + #[clap(long)] + with_network_requester: Option, + + // ##### NETWORK REQUESTER FLAGS ##### + /// Specifies whether this network requester should run in 'open-proxy' mode + #[arg(long)] + open_proxy: Option, + + /// Enable service anonymized statistics that get sent to a statistics aggregator server + #[arg(long)] + enable_statistics: Option, + + /// Mixnet client address where a statistics aggregator is running. The default value is a Nym + /// aggregator client + #[arg(long)] + statistics_recipient: Option, + + /// Mostly debug-related option to increase default traffic rate so that you would not need to + /// modify config post init + #[arg(long, hide = true, conflicts_with = "medium_toggle")] + fastmode: bool, + + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[arg(long, hide = true, conflicts_with = "medium_toggle")] + no_cover: bool, + + /// Enable medium mixnet traffic, for experiments only. + /// This includes things like disabling cover traffic, no per hop delays, etc. + #[arg( + long, + hide = true, + conflicts_with = "no_cover", + conflicts_with = "fastmode" + )] + medium_toggle: bool, + + /// Path to .json file containing custom network specification. + /// Only usable when local network requester is enabled. + #[clap(long, group = "network", hide = true)] + custom_mixnet: Option, + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -83,6 +132,20 @@ impl From for OverrideConfig { statistics_service_url: run_config.statistics_service_url, nyxd_urls: run_config.nyxd_urls, only_coconut_credentials: run_config.only_coconut_credentials, + with_network_requester: run_config.with_network_requester, + } + } +} + +impl<'a> From<&'a Run> for OverrideNetworkRequesterConfig { + fn from(value: &'a Run) -> Self { + OverrideNetworkRequesterConfig { + fastmode: value.fastmode, + no_cover: value.no_cover, + medium_toggle: value.medium_toggle, + open_proxy: value.open_proxy, + enable_statistics: value.enable_statistics, + statistics_recipient: value.statistics_recipient.clone(), } } } @@ -97,11 +160,14 @@ fn show_binding_warning(address: &str) { eprintln!("\n\n"); } -pub async fn execute(args: Run) -> Result<(), Box> { +pub async fn execute(args: Run) -> anyhow::Result<()> { let id = args.id.clone(); eprintln!("Starting gateway {id}..."); let output = args.output; + let custom_mixnet = args.custom_mixnet.clone(); + let nr_opts = (&args).into(); + let config = build_config(id, args)?; ensure_config_version_compatibility(&config)?; @@ -109,11 +175,12 @@ pub async fn execute(args: Run) -> Result<(), Box> { show_binding_warning(&config.gateway.listening_address.to_string()); } - let mut gateway = crate::node::create_gateway(config).await; + let node_details = node_details(&config)?; + let gateway = crate::node::create_gateway(config, Some(nr_opts), custom_mixnet).await?; eprintln!( "\nTo bond your gateway you will need to install the Nym wallet, go to https://nymtech.net/get-involved and select the Download button.\n\ Select the correct version and install it to your machine. You will need to provide the following: \n "); - gateway.print_node_details(output); + output.to_stdout(&node_details); gateway.run().await } diff --git a/gateway/src/commands/setup_network_requester.rs b/gateway/src/commands/setup_network_requester.rs new file mode 100644 index 0000000000..f59f37dbd5 --- /dev/null +++ b/gateway/src/commands/setup_network_requester.rs @@ -0,0 +1,109 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::helpers::{ + initialise_local_network_requester, try_load_current_config, OverrideNetworkRequesterConfig, +}; +use crate::node::helpers::load_public_key; +use clap::Args; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(Args, Clone)] +pub struct CmdArgs { + /// The id of the gateway you want to initialise local network requester for. + #[arg(long)] + id: String, + + /// Path to custom location for network requester's config. + #[arg(long)] + custom_config_path: Option, + + /// Specify whether the network requester should be enabled. + // (you might want to create all the configs, generate keys, etc. but not actually run the NR just yet) + #[arg(long)] + enabled: Option, + + // note: those flags are set as bools as we want to explicitly override any settings values + // so say `open_proxy` was set to true in the config.toml. youd have to explicitly state `open-proxy=false` + // as an argument here to override it as opposed to not providing the value at all. + /// Specifies whether this network requester should run in 'open-proxy' mode + #[arg(long)] + open_proxy: Option, + + /// Enable service anonymized statistics that get sent to a statistics aggregator server + #[arg(long)] + enable_statistics: Option, + + /// Mixnet client address where a statistics aggregator is running. The default value is a Nym + /// aggregator client + #[arg(long)] + statistics_recipient: Option, + + /// Mostly debug-related option to increase default traffic rate so that you would not need to + /// modify config post init + #[arg(long, hide = true, conflicts_with = "medium_toggle")] + fastmode: bool, + + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[arg(long, hide = true, conflicts_with = "medium_toggle")] + no_cover: bool, + + /// Enable medium mixnet traffic, for experiments only. + /// This includes things like disabling cover traffic, no per hop delays, etc. + #[arg( + long, + hide = true, + conflicts_with = "no_cover", + conflicts_with = "fastmode" + )] + medium_toggle: bool, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl<'a> From<&'a CmdArgs> for OverrideNetworkRequesterConfig { + fn from(value: &'a CmdArgs) -> Self { + OverrideNetworkRequesterConfig { + fastmode: value.fastmode, + no_cover: value.no_cover, + medium_toggle: value.medium_toggle, + open_proxy: value.open_proxy, + enable_statistics: value.enable_statistics, + statistics_recipient: value.statistics_recipient.clone(), + } + } +} + +pub async fn execute(args: CmdArgs) -> anyhow::Result<()> { + let mut config = try_load_current_config(&args.id)?; + let opts = (&args).into(); + + // if somebody provided config file of a custom NR, that's fine + // but in 90% cases, I'd assume, it won't work due to invalid gateway configuration + // but it might be nice to be able to move files around. + if let Some(custom_config_path) = args.custom_config_path { + // if you specified anything as the argument, overwrite whatever was already in the config file + config.storage_paths.network_requester_config = Some(custom_config_path); + } + + if let Some(override_enabled) = args.enabled { + config.network_requester.enabled = override_enabled; + } + + if config.storage_paths.network_requester_config.is_none() { + config = config.with_default_network_requester_config_path() + } + + let identity_public_key = load_public_key( + &config.storage_paths.keys.public_identity_key_file, + "gateway identity", + )?; + let details = initialise_local_network_requester(&config, opts, identity_public_key).await?; + config.try_save()?; + + args.output.to_stdout(&details); + + Ok(()) +} diff --git a/gateway/src/commands/sign.rs b/gateway/src/commands/sign.rs index c8a7a1a728..ad8061e597 100644 --- a/gateway/src/commands/sign.rs +++ b/gateway/src/commands/sign.rs @@ -1,19 +1,18 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::commands::{ensure_correct_bech32_prefix, OverrideConfig}; -use crate::error::GatewayError; -use crate::support::config::build_config; -use crate::{ - commands::ensure_config_version_compatibility, config::persistence::paths::GatewayPaths, +use crate::commands::helpers::{ + ensure_config_version_compatibility, ensure_correct_bech32_prefix, OverrideConfig, }; +use crate::error::GatewayError; +use crate::node::helpers::load_identity_keys; +use crate::support::config::build_config; use anyhow::{bail, Result}; use clap::{ArgGroup, Args}; use nym_bin_common::output_format::OutputFormat; use nym_crypto::asymmetric::identity; use nym_types::helpers::ConsoleSigningOutput; use nym_validator_client::nyxd; -use std::error::Error; #[derive(Args, Clone)] #[clap(group(ArgGroup::new("sign").required(true).args(&["wallet_address", "text", "contract_msg"])))] @@ -64,16 +63,6 @@ impl TryFrom for SignedTarget { } } -pub fn load_identity_keys(paths: &GatewayPaths) -> identity::KeyPair { - let identity_keypair: identity::KeyPair = - nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new( - paths.private_identity_key().to_owned(), - paths.public_identity_key().to_owned(), - )) - .expect("Failed to read stored identity key files"); - identity_keypair -} - fn print_signed_address( private_key: &identity::PrivateKey, wallet_address: nyxd::AccountId, @@ -90,14 +79,11 @@ fn print_signed_text( text: &str, output: OutputFormat, ) -> Result<(), GatewayError> { - eprintln!( - "Signing the text {:?} using your mixnode's Ed25519 identity key...", - text - ); + eprintln!("Signing the text {text:?} using your mixnode's Ed25519 identity key...",); let signature = private_key.sign_text(text); let sign_output = ConsoleSigningOutput::new(text, signature); - println!("{}", output.format(&sign_output)); + output.to_stdout(&sign_output); Ok(()) } @@ -125,6 +111,7 @@ fn print_signed_contract_msg( }; // if this is a valid json, it MUST be a valid string + #[allow(clippy::unwrap_used)] let decoded_string = String::from_utf8(decoded.clone()).unwrap(); let signature = private_key.sign(&decoded).to_base58_string(); @@ -132,14 +119,14 @@ fn print_signed_contract_msg( println!("{}", output.format(&sign_output)); } -pub fn execute(args: Sign) -> Result<(), Box> { +pub fn execute(args: Sign) -> anyhow::Result<()> { let config = build_config(args.id.clone(), OverrideConfig::default())?; ensure_config_version_compatibility(&config)?; let output = args.output; let signed_target = SignedTarget::try_from(args)?; - let identity_keypair = load_identity_keys(&config.storage_paths); + let identity_keypair = load_identity_keys(&config)?; match signed_target { SignedTarget::Text(text) => { diff --git a/gateway/src/commands/upgrade_helpers.rs b/gateway/src/commands/upgrade_helpers.rs new file mode 100644 index 0000000000..d8555ca215 --- /dev/null +++ b/gateway/src/commands/upgrade_helpers.rs @@ -0,0 +1,66 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::old_config_v1_1_20::ConfigV1_1_20; +use crate::config::old_config_v1_1_28::ConfigV1_1_28; +use crate::config::{default_config_filepath, Config}; +use crate::error::GatewayError; +use log::info; + +fn try_upgrade_v1_1_20_config(id: &str) -> Result { + use nym_config::legacy_helpers::nym_config::MigrationNymConfig; + + // explicitly load it as v1.1.20 (which is incompatible with the current, i.e. 1.1.21+) + let Ok(old_config) = ConfigV1_1_20::load_from_file(id) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the gateway is using <= v1.1.20 config template."); + info!("It is going to get updated to the current specification."); + + let updated_step1: ConfigV1_1_28 = old_config.into(); + let updated: Config = updated_step1.into(); + updated + .save_to_default_location() + .map_err(|err| GatewayError::ConfigSaveFailure { + path: default_config_filepath(id), + id: id.to_string(), + source: err, + })?; + + Ok(true) +} + +fn try_upgrade_v1_1_28_config(id: &str) -> Result { + // explicitly load it as v1.1.28 (which is incompatible with the current, i.e. 1.1.29+) + let Ok(old_config) = ConfigV1_1_28::read_from_default_path(id) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the gateway is using <= v1.1.28 config template."); + info!("It is going to get updated to the current specification."); + + let updated: Config = old_config.into(); + updated + .save_to_default_location() + .map_err(|err| GatewayError::ConfigSaveFailure { + path: default_config_filepath(id), + id: id.to_string(), + source: err, + })?; + + Ok(true) +} + +pub(crate) fn try_upgrade_config(id: &str) -> Result<(), GatewayError> { + if try_upgrade_v1_1_20_config(id)? { + return Ok(()); + } + if try_upgrade_v1_1_28_config(id)? { + return Ok(()); + } + + Ok(()) +} diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 40dd27249e..23620953d6 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -3,6 +3,7 @@ use crate::config::persistence::paths::GatewayPaths; use crate::config::template::CONFIG_TEMPLATE; +use log::{debug, warn}; use nym_bin_common::logging::LoggingSettings; use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; use nym_config::helpers::inaddr_any; @@ -20,6 +21,7 @@ use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) mod old_config_v1_1_20; +pub(crate) mod old_config_v1_1_28; pub mod persistence; mod template; @@ -65,10 +67,16 @@ pub fn default_data_directory>(id: P) -> PathBuf { #[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + pub gateway: Gateway, pub storage_paths: GatewayPaths, + pub network_requester: NetworkRequester, + #[serde(default)] pub logging: LoggingSettings, @@ -77,7 +85,7 @@ pub struct Config { } impl NymConfigTemplate for Config { - fn template() -> &'static str { + fn template(&self) -> &'static str { CONFIG_TEMPLATE } } @@ -85,19 +93,33 @@ impl NymConfigTemplate for Config { impl Config { pub fn new>(id: S) -> Self { Config { + save_path: None, gateway: Gateway::new_default(id.as_ref()), storage_paths: GatewayPaths::new_default(id.as_ref()), + network_requester: Default::default(), logging: Default::default(), debug: Default::default(), } } + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> io::Result { + let path = path.as_ref(); + let mut loaded: Config = read_config_from_toml_file(path)?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } + + // currently this is dead code, but once we allow loading configs from custom paths + // well, we will have to be using it + #[allow(dead_code)] pub fn read_from_toml_file>(path: P) -> io::Result { - read_config_from_toml_file(path) + Self::read_from_path(path) } pub fn read_from_default_path>(id: P) -> io::Result { - Self::read_from_toml_file(default_config_filepath(id)) + Self::read_from_path(default_config_filepath(id)) } pub fn default_location(&self) -> PathBuf { @@ -109,6 +131,27 @@ impl Config { save_formatted_config_to_file(self, config_save_location) } + pub fn try_save(&self) -> io::Result<()> { + if let Some(save_location) = &self.save_path { + save_formatted_config_to_file(self, save_location) + } else { + warn!("config file save location is unknown. falling back to the default"); + self.save_to_default_location() + } + } + + pub fn with_enabled_network_requester(mut self, enabled_network_requester: bool) -> Self { + self.network_requester.enabled = enabled_network_requester; + self + } + + pub fn with_default_network_requester_config_path(mut self) -> Self { + self.storage_paths = self + .storage_paths + .with_default_network_requester_config(&self.gateway.id); + self + } + pub fn with_only_coconut_credentials(mut self, only_coconut_credentials: bool) -> Self { self.gateway.only_coconut_credentials = only_coconut_credentials; self @@ -227,6 +270,8 @@ pub struct Gateway { impl Gateway { pub fn new_default>(id: S) -> Self { + // allow usage of `expect` here as our default mainnet values should have been well-formed. + #[allow(clippy::expect_used)] Gateway { version: env!("CARGO_PKG_VERSION").to_string(), id: id.into(), @@ -240,11 +285,26 @@ impl Gateway { .expect("Invalid default statistics service URL"), nym_api_urls: vec![mainnet::NYM_API.parse().expect("Invalid default API URL")], nyxd_urls: vec![mainnet::NYXD_URL.parse().expect("Invalid default nyxd URL")], - cosmos_mnemonic: bip39::Mnemonic::generate(24).unwrap(), + cosmos_mnemonic: bip39::Mnemonic::generate(24) + .expect("failed to generate fresh mnemonic"), } } } +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct NetworkRequester { + /// Specifies whether network requester service is enabled in this process. + pub enabled: bool, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequester { + fn default() -> Self { + NetworkRequester { enabled: false } + } +} + #[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(default)] pub struct Debug { diff --git a/gateway/src/config/old_config_v1_1_20.rs b/gateway/src/config/old_config_v1_1_20.rs index 594043b8a7..f7a83fc2c0 100644 --- a/gateway/src/config/old_config_v1_1_20.rs +++ b/gateway/src/config/old_config_v1_1_20.rs @@ -1,13 +1,14 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::persistence::paths::{GatewayPaths, KeysPaths}; -use crate::config::{Config, Debug, Gateway}; -use nym_bin_common::logging::LoggingSettings; +use crate::config::old_config_v1_1_28::{ + ConfigV1_1_28, DebugV1_1_28, GatewayPathsV1_1_28, GatewayV1_1_28, KeysPathsV1_1_28, + LoggingSettingsV1_1_28, +}; use nym_config::legacy_helpers::nym_config::MigrationNymConfig; use nym_validator_client::nyxd; use serde::{Deserialize, Serialize}; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; @@ -28,8 +29,9 @@ const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; const DEFAULT_STORED_MESSAGE_FILENAME_LENGTH: u16 = 16; const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +/// returns a `0.0.0.0` / INADDR_ANY fn bind_all_address() -> IpAddr { - "0.0.0.0".parse().unwrap() + IpAddr::V4(Ipv4Addr::UNSPECIFIED) } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] @@ -42,10 +44,10 @@ pub struct ConfigV1_1_20 { debug: DebugV1_1_20, } -impl From for Config { +impl From for ConfigV1_1_28 { fn from(value: ConfigV1_1_20) -> Self { - Config { - gateway: Gateway { + ConfigV1_1_28 { + gateway: GatewayV1_1_28 { version: value.gateway.version, id: value.gateway.id, only_coconut_credentials: value.gateway.only_coconut_credentials, @@ -58,8 +60,8 @@ impl From for Config { statistics_service_url: value.gateway.statistics_service_url, cosmos_mnemonic: value.gateway.cosmos_mnemonic, }, - storage_paths: GatewayPaths { - keys: KeysPaths { + storage_paths: GatewayPathsV1_1_28 { + keys: KeysPathsV1_1_28 { private_identity_key_file: value.gateway.private_identity_key_file, public_identity_key_file: value.gateway.public_identity_key_file, private_sphinx_key_file: value.gateway.private_sphinx_key_file, @@ -75,6 +77,8 @@ impl From for Config { impl MigrationNymConfig for ConfigV1_1_20 { fn default_root_directory() -> PathBuf { + // unless this is run on some esoteric system, it should not fail thus the expect is fine + #[allow(clippy::expect_used)] dirs::home_dir() .expect("Failed to evaluate $HOME value") .join(".nym") @@ -112,6 +116,8 @@ pub struct GatewayV1_1_20 { impl Default for GatewayV1_1_20 { fn default() -> Self { + // allow usage of `expect` here as our default mainnet values should have been well-formed. + #[allow(clippy::expect_used)] GatewayV1_1_20 { version: env!("CARGO_PKG_VERSION").to_string(), id: "".to_string(), @@ -129,7 +135,8 @@ impl Default for GatewayV1_1_20 { .expect("Invalid default statistics service URL"), nym_api_urls: vec![Url::from_str(NYM_API).expect("Invalid default API URL")], nyxd_urls: vec![Url::from_str(NYXD_URL).expect("Invalid default nyxd URL")], - cosmos_mnemonic: bip39::Mnemonic::generate(24).unwrap(), + cosmos_mnemonic: bip39::Mnemonic::generate(24) + .expect("failed to generate fresh mnemonic"), nym_root_directory: ConfigV1_1_20::default_root_directory(), persistent_storage: Default::default(), wallet_address: None, @@ -141,9 +148,9 @@ impl Default for GatewayV1_1_20 { #[serde(deny_unknown_fields)] struct LoggingV1_1_20 {} -impl From for LoggingSettings { +impl From for LoggingSettingsV1_1_28 { fn from(_value: LoggingV1_1_20) -> Self { - LoggingSettings {} + LoggingSettingsV1_1_28 {} } } @@ -164,9 +171,9 @@ struct DebugV1_1_20 { use_legacy_framed_packet_version: bool, } -impl From for Debug { +impl From for DebugV1_1_28 { fn from(value: DebugV1_1_20) -> Self { - Debug { + DebugV1_1_28 { packet_forwarding_initial_backoff: value.packet_forwarding_initial_backoff, packet_forwarding_maximum_backoff: value.packet_forwarding_maximum_backoff, initial_connection_timeout: value.initial_connection_timeout, diff --git a/gateway/src/config/old_config_v1_1_28.rs b/gateway/src/config/old_config_v1_1_28.rs new file mode 100644 index 0000000000..7639d860f9 --- /dev/null +++ b/gateway/src/config/old_config_v1_1_28.rs @@ -0,0 +1,224 @@ +// Copyright 2020-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::persistence::paths::{GatewayPaths, KeysPaths}; +use crate::config::{Config, Debug, Gateway}; +use nym_bin_common::logging::LoggingSettings; +use nym_config::{ + must_get_home, read_config_from_toml_file, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR, +}; +use serde::{Deserialize, Serialize}; +use std::io; +use std::net::IpAddr; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use url::Url; + +const DEFAULT_GATEWAYS_DIR: &str = "gateways"; + +// 'DEBUG' +// where applicable, the below are defined in milliseconds +const DEFAULT_PRESENCE_SENDING_DELAY: Duration = Duration::from_millis(10_000); +const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000); +const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000); +const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); +const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; + +const DEFAULT_STORED_MESSAGE_FILENAME_LENGTH: u16 = 16; +const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + +/// Derive default path to gateway's config directory. +/// It should get resolved to `$HOME/.nym/gateways//config` +pub fn default_config_directory>(id: P) -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_GATEWAYS_DIR) + .join(id) + .join(DEFAULT_CONFIG_DIR) +} + +/// Derive default path to gateways's config file. +/// It should get resolved to `$HOME/.nym/gateways//config/config.toml` +pub fn default_config_filepath>(id: P) -> PathBuf { + default_config_directory(id).join(DEFAULT_CONFIG_FILENAME) +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct KeysPathsV1_1_28 { + pub private_identity_key_file: PathBuf, + pub public_identity_key_file: PathBuf, + pub private_sphinx_key_file: PathBuf, + pub public_sphinx_key_file: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayPathsV1_1_28 { + pub keys: KeysPathsV1_1_28, + #[serde(alias = "persistent_storage")] + pub clients_storage: PathBuf, +} + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LoggingSettingsV1_1_28 {} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_28 { + pub gateway: GatewayV1_1_28, + + pub storage_paths: GatewayPathsV1_1_28, + + #[serde(default)] + pub logging: LoggingSettingsV1_1_28, + + #[serde(default)] + pub debug: DebugV1_1_28, +} + +impl ConfigV1_1_28 { + pub fn read_from_default_path>(id: P) -> io::Result { + read_config_from_toml_file(default_config_filepath(id)) + } +} + +impl From for Config { + fn from(value: ConfigV1_1_28) -> Self { + Config { + save_path: None, + gateway: Gateway { + version: value.gateway.version, + id: value.gateway.id, + only_coconut_credentials: value.gateway.only_coconut_credentials, + listening_address: value.gateway.listening_address, + mix_port: value.gateway.mix_port, + clients_port: value.gateway.clients_port, + enabled_statistics: value.gateway.enabled_statistics, + nym_api_urls: value.gateway.nym_api_urls, + nyxd_urls: value.gateway.nyxd_urls, + statistics_service_url: value.gateway.statistics_service_url, + cosmos_mnemonic: value.gateway.cosmos_mnemonic, + }, + storage_paths: GatewayPaths { + keys: KeysPaths { + private_identity_key_file: value.storage_paths.keys.private_identity_key_file, + public_identity_key_file: value.storage_paths.keys.public_identity_key_file, + private_sphinx_key_file: value.storage_paths.keys.private_sphinx_key_file, + public_sphinx_key_file: value.storage_paths.keys.public_sphinx_key_file, + }, + clients_storage: value.storage_paths.clients_storage, + network_requester_config: None, + }, + network_requester: Default::default(), + logging: LoggingSettings {}, + debug: Debug { + packet_forwarding_initial_backoff: value.debug.packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: value.debug.packet_forwarding_maximum_backoff, + initial_connection_timeout: value.debug.initial_connection_timeout, + maximum_connection_buffer_size: value.debug.maximum_connection_buffer_size, + presence_sending_delay: value.debug.presence_sending_delay, + stored_messages_filename_length: value.debug.stored_messages_filename_length, + message_retrieval_limit: value.debug.message_retrieval_limit, + use_legacy_framed_packet_version: value.debug.use_legacy_framed_packet_version, + }, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct GatewayV1_1_28 { + /// Version of the gateway for which this configuration was created. + pub version: String, + + /// ID specifies the human readable ID of this particular gateway. + pub id: String, + + /// Indicates whether this gateway is accepting only coconut credentials for accessing the + /// the mixnet, or if it also accepts non-paying clients + #[serde(default)] + pub only_coconut_credentials: bool, + + /// Address to which this mixnode will bind to and will be listening for packets. + pub listening_address: IpAddr, + + /// Port used for listening for all mixnet traffic. + /// (default: 1789) + pub mix_port: u16, + + /// Port used for listening for all client-related traffic. + /// (default: 9000) + pub clients_port: u16, + + /// Whether gateway collects and sends anonymized statistics + pub enabled_statistics: bool, + + /// Domain address of the statistics service + pub statistics_service_url: Url, + + /// Addresses to APIs from which the node gets the view of the network. + #[serde(alias = "validator_api_urls")] + pub nym_api_urls: Vec, + + /// Addresses to validators which the node uses to check for double spending of ERC20 tokens. + #[serde(alias = "validator_nymd_urls")] + pub nyxd_urls: Vec, + + /// Mnemonic of a cosmos wallet used in checking for double spending. + // #[deprecated(note = "move to storage")] + // TODO: I don't think this should be stored directly in the config... + pub cosmos_mnemonic: bip39::Mnemonic, +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct DebugV1_1_28 { + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_initial_backoff: Duration, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_maximum_backoff: Duration, + + /// Timeout for establishing initial connection when trying to forward a sphinx packet. + #[serde(with = "humantime_serde")] + pub initial_connection_timeout: Duration, + + /// Maximum number of packets that can be stored waiting to get sent to a particular connection. + pub maximum_connection_buffer_size: usize, + + /// Delay between each subsequent presence data being sent. + #[serde(with = "humantime_serde")] + pub presence_sending_delay: Duration, + + /// Length of filenames for new client messages. + pub stored_messages_filename_length: u16, + + /// Number of messages from offline client that can be pulled at once from the storage. + pub message_retrieval_limit: i64, + + /// Specifies whether the mixnode should be using the legacy framing for the sphinx packets. + // it's set to true by default. The reason for that decision is to preserve compatibility with the + // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. + // It shall be disabled in the subsequent releases. + pub use_legacy_framed_packet_version: bool, +} + +impl Default for DebugV1_1_28 { + fn default() -> Self { + DebugV1_1_28 { + packet_forwarding_initial_backoff: DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + initial_connection_timeout: DEFAULT_INITIAL_CONNECTION_TIMEOUT, + presence_sending_delay: DEFAULT_PRESENCE_SENDING_DELAY, + maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH, + message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + use_legacy_framed_packet_version: false, + } + } +} diff --git a/gateway/src/config/persistence/paths.rs b/gateway/src/config/persistence/paths.rs index b5f44b6cd4..8d3be49335 100644 --- a/gateway/src/config/persistence/paths.rs +++ b/gateway/src/config/persistence/paths.rs @@ -1,8 +1,8 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::default_data_directory; -use serde::{Deserialize, Serialize}; +use crate::config::{default_config_directory, default_data_directory}; +use serde::{Deserialize, Deserializer, Serialize}; use std::path::{Path, PathBuf}; pub const DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME: &str = "private_identity.pem"; @@ -12,8 +12,28 @@ pub const DEFAULT_PUBLIC_SPHINX_KEY_FILENAME: &str = "public_sphinx.pem"; pub const DEFAULT_CLIENTS_STORAGE_FILENAME: &str = "db.sqlite"; +pub const DEFAULT_NETWORK_REQUESTER_CONFIG_FILENAME: &str = "network_requester_config.toml"; +pub const DEFAULT_NETWORK_REQUESTER_DATA_DIR: &str = "network-requester-data"; + // pub const DEFAULT_DESCRIPTION_FILENAME: &str = "description.toml"; +pub fn default_network_requester_data_dir>(id: P) -> PathBuf { + default_data_directory(id).join(DEFAULT_NETWORK_REQUESTER_DATA_DIR) +} + +/// makes sure that an empty path is converted into a `None` as opposed to `Some("")` +fn de_maybe_path<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let path = PathBuf::deserialize(deserializer)?; + if path.as_os_str().is_empty() { + Ok(None) + } else { + Ok(Some(path)) + } +} + #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct GatewayPaths { @@ -23,6 +43,10 @@ pub struct GatewayPaths { /// derived shared keys and available client bandwidths. #[serde(alias = "persistent_storage")] pub clients_storage: PathBuf, + + /// Path to the configuration of the embedded network requester. + #[serde(deserialize_with = "de_maybe_path")] + pub network_requester_config: Option, // pub node_description: PathBuf, // pub cosmos_bip39_mnemonic: PathBuf, @@ -34,9 +58,27 @@ impl GatewayPaths { keys: KeysPaths::new_default(id.as_ref()), clients_storage: default_data_directory(id).join(DEFAULT_CLIENTS_STORAGE_FILENAME), // node_description: default_config_filepath(id).join(DEFAULT_DESCRIPTION_FILENAME), + network_requester_config: None, } } + #[must_use] + pub fn with_network_requester_config>(mut self, path: P) -> Self { + self.network_requester_config = Some(path.as_ref().into()); + self + } + + #[must_use] + pub fn with_default_network_requester_config>(self, id: P) -> Self { + self.with_network_requester_config( + default_config_directory(id).join(DEFAULT_NETWORK_REQUESTER_CONFIG_FILENAME), + ) + } + + pub fn network_requester_config(&self) -> &Option { + &self.network_requester_config + } + pub fn private_identity_key(&self) -> &Path { self.keys.private_identity_key() } diff --git a/gateway/src/config/template.rs b/gateway/src/config/template.rs index e8543b8f44..7e7563edf9 100644 --- a/gateway/src/config/template.rs +++ b/gateway/src/config/template.rs @@ -53,7 +53,11 @@ nyxd_urls = [ {{/each}} ] -cosmos_mnemonic = "{{ gateway.cosmos_mnemonic }}" +cosmos_mnemonic = '{{ gateway.cosmos_mnemonic }}' + +[network_requester] +# Specifies whether network requester service is enabled in this process. +enabled = {{ network_requester.enabled }} [storage_paths] @@ -73,6 +77,9 @@ keys.public_sphinx_key_file = '{{ storage_paths.keys.public_sphinx_key_file }}' # derived shared keys and available client bandwidths. clients_storage = '{{ storage_paths.clients_storage }}' +# Path to the configuration of the embedded network requester. +network_requester_config = '{{ storage_paths.network_requester_config }}' + ##### logging configuration options ##### [logging] diff --git a/gateway/src/error.rs b/gateway/src/error.rs index 5786bd26fb..dde0690979 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -1,6 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::node::storage::error::StorageError; +use nym_network_requester::error::{ClientCoreError, NetworkRequesterError}; +use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; use nym_validator_client::ValidatorClientError; use std::io; @@ -9,8 +12,24 @@ use thiserror::Error; #[derive(Debug, Error)] pub(crate) enum GatewayError { + #[error("failed to load {keys} keys from '{}' (private key) and '{}' (public key): {err}", .paths.private_key_path.display(), .paths.public_key_path.display())] + KeyPairLoadFailure { + keys: String, + paths: nym_pemstore::KeyPairPath, + #[source] + err: io::Error, + }, + + #[error("failed to load {key} public key from '{}': {err}", .path.display())] + PublicKeyLoadFailure { + key: String, + path: PathBuf, + #[source] + err: io::Error, + }, + #[error( - "failed to load config file for id {id} using path {path}. detailed message: {source}" + "failed to load config file for id {id} using path '{}'. detailed message: {source}", path.display() )] ConfigLoadFailure { id: String, @@ -20,7 +39,17 @@ pub(crate) enum GatewayError { }, #[error( - "failed to save config file for id {id} using path {path}. detailed message: {source}" + "failed to load config file for network requester (gateway-id: '{id}') using path '{}'. detailed message: {source}", path.display() + )] + NetworkRequesterConfigLoadFailure { + id: String, + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error( + "failed to save config file for id {id} using path '{}'. detailed message: {source}", path.display() )] ConfigSaveFailure { id: String, @@ -47,4 +76,43 @@ pub(crate) enum GatewayError { expected_prefix: String, actual_prefix: String, }, + + #[error("storage failure: {source}")] + StorageError { + #[from] + source: StorageError, + }, + + #[error("Path to network requester configuration file hasn't been specified. Perhaps try to run `setup-network-requester`?")] + UnspecifiedNetworkRequesterConfig, + + #[error("there was an issue with the local network requester: {source}")] + NetworkRequesterFailure { + #[from] + source: NetworkRequesterError, + }, + + #[error("failed to startup local network requester")] + NetworkRequesterStartupFailure, + + #[error("there are no nym API endpoints available")] + NoNymApisAvailable, + + #[error("there are no nyxd endpoints available")] + NoNyxdAvailable, + + #[error("there was an issue attempting to use the validator [nyxd]: {source}")] + ValidatorFailure { + #[from] + source: NyxdError, + }, +} + +impl From for GatewayError { + fn from(value: ClientCoreError) -> Self { + // if we ever get a client core error, it must have come from the network requester + GatewayError::NetworkRequesterFailure { + source: value.into(), + } + } } diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 5290037266..1f3f54b398 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -1,6 +1,9 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] + use clap::{crate_name, crate_version, Parser}; use colored::Colorize; use lazy_static::lazy_static; diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index 280027dacd..a44de399ba 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -1,14 +1,41 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender}; +use crate::node::client_handling::embedded_network_requester::LocalNetworkRequesterHandle; use dashmap::DashMap; +use log::warn; use nym_sphinx::DestinationAddressBytes; use std::sync::Arc; -use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender}; +enum ActiveClient { + /// Handle to a remote client connected via a network socket. + Remote(ClientIncomingChannels), + + /// Handle to a locally (inside the same process) running network requester client. + Embedded(LocalNetworkRequesterHandle), +} + +impl ActiveClient { + fn get_sender_ref(&self) -> &MixMessageSender { + match self { + ActiveClient::Remote(remote) => &remote.mix_message_sender, + ActiveClient::Embedded(embedded) => &embedded.mix_message_sender, + } + } + + fn get_sender(&self) -> MixMessageSender { + match self { + ActiveClient::Remote(remote) => remote.mix_message_sender.clone(), + ActiveClient::Embedded(embedded) => embedded.mix_message_sender.clone(), + } + } +} #[derive(Clone)] -pub(crate) struct ActiveClientsStore(Arc>); +pub(crate) struct ActiveClientsStore { + inner: Arc>, +} #[derive(Clone)] pub(crate) struct ClientIncomingChannels { @@ -22,7 +49,9 @@ pub(crate) struct ClientIncomingChannels { impl ActiveClientsStore { /// Creates new instance of `ActiveClientsStore` to store in-memory handles to all currently connected clients. pub(crate) fn new() -> Self { - ActiveClientsStore(Arc::new(DashMap::new())) + ActiveClientsStore { + inner: Arc::new(DashMap::new()), + } } /// Tries to obtain sending channel to specified client. Note that if stale entry existed, it is @@ -31,29 +60,72 @@ impl ActiveClientsStore { /// # Arguments /// /// * `client`: address of the client for which to obtain the handle. - pub(crate) fn get(&self, client: DestinationAddressBytes) -> Option { - let entry = self.0.get(&client)?; - let handle = entry.value(); + pub(crate) fn get_sender(&self, client: DestinationAddressBytes) -> Option { + let entry = self.inner.get(&client)?; + let handle = entry.value().get_sender(); // if the entry is stale, remove it from the map // if handle.is_valid() { - if !handle.mix_message_sender.is_closed() { - Some(handle.clone()) + if !handle.is_closed() { + Some(handle) } else { // drop the reference to the map to prevent deadlocks drop(entry); - self.0.remove(&client); + self.inner.remove(&client); None } } + /// Attempts to get full handle to a remotely connected client + pub(crate) fn get_remote_client( + &self, + address: DestinationAddressBytes, + ) -> Option { + let entry = self.inner.get(&address)?; + let handle = entry.value(); + + let ActiveClient::Remote(channels) = handle else { + warn!("attempted to get a remote handle to a embedded network requester"); + return None; + }; + + // if the entry is stale, remove it from the map + if !channels.mix_message_sender.is_closed() { + Some(channels.clone()) + } else { + // drop the reference to the map to prevent deadlocks + drop(entry); + self.inner.remove(&address); + None + } + } + + /// Checks whether there's already an active connection to this client. + /// It will also remove the entry from the map if its stale. + pub(crate) fn is_active(&self, client: DestinationAddressBytes) -> bool { + let Some(entry) = self.inner.get(&client) else { + return false; + }; + let handle = entry.value().get_sender_ref(); + + // if the entry is stale, remove it from the map + if !handle.is_closed() { + true + } else { + // drop the reference to the map to prevent deadlocks + drop(entry); + self.inner.remove(&client); + false + } + } + /// Indicates particular client has disconnected from the gateway and its handle should get removed. /// /// # Arguments /// /// * `client`: address of the client for which to remove the handle. pub(crate) fn disconnect(&self, client: DestinationAddressBytes) { - self.0.remove(&client); + self.inner.remove(&client); } /// Insert new client handle into the store. @@ -62,23 +134,33 @@ impl ActiveClientsStore { /// /// * `client`: address of the client for which to insert the handle. /// * `handle`: the sender channel for all mix packets to be pushed back onto the websocket - pub(crate) fn insert( + pub(crate) fn insert_remote( &self, client: DestinationAddressBytes, handle: MixMessageSender, is_active_request_sender: IsActiveRequestSender, ) { - self.0.insert( - client, - ClientIncomingChannels { - mix_message_sender: handle, - is_active_request_sender, - }, - ); + let entry = ActiveClient::Remote(ClientIncomingChannels { + mix_message_sender: handle, + is_active_request_sender, + }); + if self.inner.insert(client, entry).is_some() { + panic!("inserted a duplicate remote client") + } + } + + /// Inserts a handle to the embedded network requester + pub(crate) fn insert_embedded(&self, local_nr_handle: LocalNetworkRequesterHandle) { + let key = local_nr_handle.client_destination(); + let entry = ActiveClient::Embedded(local_nr_handle); + if self.inner.insert(key, entry).is_some() { + // this is literally impossible since we're starting local NR before even spawning the websocket listener task + panic!("somehow we already had a client with the same address as our local NR!") + } } /// Get number of active clients in store pub(crate) fn size(&self) -> usize { - self.0.len() + self.inner.len() } } diff --git a/gateway/src/node/client_handling/embedded_network_requester/mod.rs b/gateway/src/node/client_handling/embedded_network_requester/mod.rs new file mode 100644 index 0000000000..7d2d612aa0 --- /dev/null +++ b/gateway/src/node/client_handling/embedded_network_requester/mod.rs @@ -0,0 +1,89 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::client_handling::websocket::message_receiver::{ + MixMessageReceiver, MixMessageSender, +}; +use futures::StreamExt; +use log::{debug, error}; +use nym_network_requester::core::OnStartData; +use nym_network_requester::{GatewayPacketRouter, PacketRouter}; +use nym_sphinx::addressing::clients::Recipient; +use nym_sphinx::DestinationAddressBytes; +use nym_task::TaskClient; + +#[derive(Debug)] +pub(crate) struct LocalNetworkRequesterHandle { + /// Nym address of the embedded network requester. + pub(crate) address: Recipient, + + /// Message channel used internally to forward any received mix packets to the network requester. + pub(crate) mix_message_sender: MixMessageSender, +} + +impl LocalNetworkRequesterHandle { + pub(crate) fn new(start_data: OnStartData, mix_message_sender: MixMessageSender) -> Self { + Self { + address: start_data.address, + mix_message_sender, + } + } + + pub(crate) fn client_destination(&self) -> DestinationAddressBytes { + self.address.identity().derive_destination_address() + } +} + +// we could have just passed a `PacketRouter` around instead of creating a dedicated task for +// calling the method. however, this would have caused slightly more complexity and more overhead +// (due to more data being copied to every [mix] connection) +// +/// task responsible for receiving messages for locally NR requester from multiple mix connections +/// and forwarding them via the router. kinda equivalent of a client socket handler +pub(crate) struct MessageRouter { + mix_receiver: MixMessageReceiver, + packet_router: PacketRouter, +} + +impl MessageRouter { + pub(crate) fn new(mix_receiver: MixMessageReceiver, packet_router: PacketRouter) -> Self { + Self { + mix_receiver, + packet_router, + } + } + + pub(crate) fn start_with_shutdown(self, shutdown: TaskClient) { + tokio::spawn(self.run_with_shutdown(shutdown)); + } + + fn handle_received_messages(&self, messages: Vec>) { + if let Err(err) = self.packet_router.route_received(messages) { + // TODO: what should we do here? I don't think this could/should ever fail. + // is panicking the appropriate thing to do then? + error!("failed to route packets to local NR: {err}") + } + } + + pub(crate) async fn run_with_shutdown(mut self, mut shutdown: TaskClient) { + debug!("Started embedded network requester message router with graceful shutdown support"); + while !shutdown.is_shutdown() { + tokio::select! { + messages = self.mix_receiver.next() => match messages { + Some(messages) => self.handle_received_messages(messages), + None => { + log::trace!("embedded_network_requester::MessageRouter: Stopping since channel closed"); + break; + } + }, + _ = shutdown.recv_with_delay() => { + log::trace!("embedded_network_requester::MessageRouter: Received shutdown"); + debug_assert!(shutdown.is_shutdown()); + break + } + } + } + + debug!("embedded_network_requester::MessageRouter: Exiting") + } +} diff --git a/gateway/src/node/client_handling/mod.rs b/gateway/src/node/client_handling/mod.rs index 671ac5a1c7..bc63605d58 100644 --- a/gateway/src/node/client_handling/mod.rs +++ b/gateway/src/node/client_handling/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod active_clients; mod bandwidth; +pub(crate) mod embedded_network_requester; pub(crate) mod websocket; pub(crate) const FREE_TESTNET_BANDWIDTH_VALUE: i64 = 64 * 1024 * 1024 * 1024; // 64GB diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 1bbd85b5f1..ff779cad36 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -375,21 +375,24 @@ where async fn handle_pong(&mut self, msg: Vec) { if let Ok(msg) = msg.try_into() { let msg = u64::from_be_bytes(msg); - trace!("Received pong from client: {}", msg); + trace!("Received pong from client: {msg}"); if let Some((tag, _)) = &self.is_active_ping_pending_reply { if tag == &msg { debug!("Reporting back to the handler that the client is still active"); + // safety: + // the unwrap here is fine as we can only enter this if branch if `self.is_active_ping_pending_reply` + // was a `Some` + #[allow(clippy::unwrap_used)] let tx = self.is_active_ping_pending_reply.take().unwrap().1; if let Err(err) = tx.send(IsActive::Active) { warn!("Failed to send pong reply back to the requesting handler: {err:?}"); } } else { - warn!( - "Received pong reply from the client with unexpected tag: {}", - msg - ); + warn!("Received pong reply from the client with unexpected tag: {msg}",); } } + } else { + warn!("the received pong message was not a valid u64") } } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 349cf4200b..f4389fb433 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use futures::{ @@ -10,6 +10,7 @@ use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; +use nym_gateway_requests::registration::handshake::shared_key::SharedKeyConversionError; use nym_gateway_requests::{ iv::{IVConversionError, IV}, registration::handshake::{error::HandshakeError, gateway_handshake, SharedKeys}, @@ -43,26 +44,35 @@ pub(crate) enum InitialAuthenticationError { #[error("Internal gateway storage error")] StorageError(#[from] StorageError), - #[error("Failed to perform registration handshake - {0}")] + #[error( + "our datastore is corrupted. the stored key for client {client_id} is malformed: {source}" + )] + MalformedStoredSharedKey { + client_id: String, + #[source] + source: SharedKeyConversionError, + }, + + #[error("Failed to perform registration handshake: {0}")] HandshakeError(#[from] HandshakeError), - #[error("Provided client address is malformed - {0}")] - // sphinx error is not used here directly as it's messaging might be confusing to people + #[error("Provided client address is malformed: {0}")] + // sphinx error is not used here directly as its messaging might be confusing to people MalformedClientAddress(String), - #[error("Provided encrypted client address is malformed - {0}")] + #[error("Provided encrypted client address is malformed: {0}")] MalformedEncryptedAddress(#[from] EncryptedAddressConversionError), #[error("There is already an open connection to this client")] DuplicateConnection, - #[error("Provided authentication IV is malformed - {0}")] + #[error("Provided authentication IV is malformed: {0}")] MalformedIV(#[from] IVConversionError), #[error("Only 'Register' or 'Authenticate' requests are allowed")] InvalidRequest, - #[error("Experienced connection error - {0}")] + #[error("Experienced connection error: {0}")] ConnectionError(#[from] WsError), #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")] @@ -316,13 +326,19 @@ where let shared_keys = self.storage.get_shared_keys(client_address).await?; if let Some(shared_keys) = shared_keys { - // the unwrap here is fine as we only ever construct persisted shared keys ourselves when inserting + // this should never fail as we only ever construct persisted shared keys ourselves when inserting // data to the storage. The only way it could fail is if we somehow changed implementation without // performing proper migration let keys = SharedKeys::try_from_base58_string( shared_keys.derived_aes128_ctr_blake3_hmac_keys_bs58, ) - .unwrap(); + .map_err(|source| { + InitialAuthenticationError::MalformedStoredSharedKey { + client_id: client_address.as_base58_string(), + source, + } + })?; + // TODO: SECURITY: // this is actually what we have been doing in the past, however, // after looking deeper into implementation it seems that only checks the encryption @@ -482,8 +498,8 @@ where let iv = IV::try_from_base58_string(iv)?; // Check for duplicate clients - if let Some(client_tx) = self.active_clients_store.get(address) { - log::warn!("Detected duplicate connection for client: {}", address); + if let Some(client_tx) = self.active_clients_store.get_remote_client(address) { + log::warn!("Detected duplicate connection for client: {address}"); self.handle_duplicate_client(address, client_tx.is_active_request_sender) .await?; } @@ -569,7 +585,7 @@ where let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); - if self.active_clients_store.get(remote_address).is_some() { + if self.active_clients_store.is_active(remote_address) { return Err(InitialAuthenticationError::DuplicateConnection); } @@ -675,7 +691,7 @@ where // Channel for handlers to ask other handlers if they are still active. let (is_active_request_sender, is_active_request_receiver) = mpsc::unbounded(); - self.active_clients_store.insert( + self.active_clients_store.insert_remote( client_details.address, mix_sender, is_active_request_sender, diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index 8811105b2e..01463e12c6 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -78,7 +78,7 @@ impl Listener { active_clients_store.clone(), Arc::clone(&self.coconut_verifier), ); - let shutdown = shutdown.clone(); + let shutdown = shutdown.clone().named(format!("ClientConnectionHandler_{remote_addr}")); tokio::spawn(async move { handle.start_handling(shutdown).await }); } Err(err) => warn!("failed to get client: {err}"), diff --git a/gateway/src/node/helpers.rs b/gateway/src/node/helpers.rs new file mode 100644 index 0000000000..9535519fce --- /dev/null +++ b/gateway/src/node/helpers.rs @@ -0,0 +1,148 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::Config; +use crate::error::GatewayError; +use crate::node::storage::PersistentStorage; +use nym_crypto::asymmetric::{encryption, identity}; +use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; +use nym_pemstore::KeyPairPath; +use nym_sphinx::addressing::clients::Recipient; +use nym_types::gateway::{GatewayNetworkRequesterDetails, GatewayNodeDetailsResponse}; +use std::path::Path; + +fn display_maybe_path>(path: Option

) -> String { + path.as_ref() + .map(|p| p.as_ref().display().to_string()) + .unwrap_or_default() +} + +fn display_path>(path: P) -> String { + path.as_ref().display().to_string() +} + +pub(crate) fn node_details(config: &Config) -> Result { + let gateway_identity_public_key: identity::PublicKey = load_public_key( + &config.storage_paths.keys.public_identity_key_file, + "gateway identity", + )?; + + let gateway_sphinx_public_key: encryption::PublicKey = load_public_key( + &config.storage_paths.keys.public_sphinx_key_file, + "gateway sphinx", + )?; + + let network_requester = + if let Some(nr_cfg_path) = &config.storage_paths.network_requester_config { + let cfg = load_network_requester_config(&config.gateway.id, nr_cfg_path)?; + + let nr_identity_public_key: identity::PublicKey = load_public_key( + &cfg.storage_paths.common_paths.keys.public_identity_key_file, + "network requester identity", + )?; + + let nr_encryption_key: encryption::PublicKey = load_public_key( + &cfg.storage_paths + .common_paths + .keys + .public_encryption_key_file, + "network requester encryption", + )?; + + let address = Recipient::new( + nr_identity_public_key, + nr_encryption_key, + gateway_identity_public_key, + ); + + Some(GatewayNetworkRequesterDetails { + enabled: config.network_requester.enabled, + identity_key: nr_identity_public_key.to_base58_string(), + encryption_key: nr_encryption_key.to_base58_string(), + open_proxy: cfg.network_requester.open_proxy, + enabled_statistics: cfg.network_requester.enabled_statistics, + address: address.to_string(), + config_path: display_path(nr_cfg_path), + allow_list_path: display_path(&cfg.storage_paths.allowed_list_location), + unknown_list_path: display_path(&cfg.storage_paths.unknown_list_location), + }) + } else { + None + }; + + Ok(GatewayNodeDetailsResponse { + identity_key: gateway_identity_public_key.to_base58_string(), + sphinx_key: gateway_sphinx_public_key.to_base58_string(), + bind_address: config.gateway.listening_address.to_string(), + mix_port: config.gateway.mix_port, + clients_port: config.gateway.clients_port, + config_path: display_maybe_path(config.save_path.as_ref()), + data_store: display_path(&config.storage_paths.clients_storage), + network_requester, + }) +} + +pub(crate) fn load_network_requester_config>( + id: &str, + path: P, +) -> Result { + let path = path.as_ref(); + nym_network_requester::Config::read_from_toml_file(path).map_err(|err| { + GatewayError::NetworkRequesterConfigLoadFailure { + id: id.to_string(), + path: path.to_path_buf(), + source: err, + } + }) +} + +pub(crate) async fn initialise_main_storage( + config: &Config, +) -> Result { + let path = &config.storage_paths.clients_storage; + let retrieval_limit = config.debug.message_retrieval_limit; + + Ok(PersistentStorage::init(path, retrieval_limit).await?) +} + +pub(crate) fn load_keypair( + paths: KeyPairPath, + name: impl Into, +) -> Result { + nym_pemstore::load_keypair(&paths).map_err(|err| GatewayError::KeyPairLoadFailure { + keys: name.into(), + paths, + err, + }) +} + +pub(crate) fn load_public_key(path: P, name: S) -> Result +where + T: PemStorableKey, + P: AsRef, + S: Into, +{ + nym_pemstore::load_key(path.as_ref()).map_err(|err| GatewayError::PublicKeyLoadFailure { + key: name.into(), + path: path.as_ref().to_path_buf(), + err, + }) +} + +/// Loads identity keys stored on disk +pub(crate) fn load_identity_keys(config: &Config) -> Result { + let identity_paths = KeyPairPath::new( + config.storage_paths.keys.private_identity_key(), + config.storage_paths.keys.public_identity_key(), + ); + load_keypair(identity_paths, "gateway identity") +} + +/// Loads Sphinx keys stored on disk +pub(crate) fn load_sphinx_keys(config: &Config) -> Result { + let sphinx_paths = KeyPairPath::new( + config.storage_paths.keys.private_encryption_key(), + config.storage_paths.keys.public_encryption_key(), + ); + load_keypair(sphinx_paths, "gateway sphinx") +} diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index e6071b8035..00fd98033b 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -6,6 +6,7 @@ use crate::node::client_handling::websocket::message_receiver::MixMessageSender; use crate::node::mixnet_handling::receiver::packet_processing::PacketProcessor; use crate::node::storage::error::StorageError; use crate::node::storage::Storage; +use futures::channel::mpsc::SendError; use futures::StreamExt; use log::*; use nym_mixnet_client::forwarder::MixForwardingSender; @@ -17,9 +18,17 @@ use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; use std::collections::HashMap; use std::net::SocketAddr; +use thiserror::Error; use tokio::net::TcpStream; use tokio_util::codec::Framed; +// defines errors that warrant a panic if not thrown in the context of a shutdown +#[derive(Debug, Error)] +enum CriticalPacketProcessingError { + #[error("failed to forward an ack")] + AckForwardingFailure { source: SendError }, +} + pub(crate) struct ConnectionHandler { packet_processor: PacketProcessor, @@ -70,9 +79,9 @@ impl ConnectionHandler { } fn update_clients_store_cache_entry(&mut self, client_address: DestinationAddressBytes) { - if let Some(client_senders) = self.active_clients_store.get(client_address) { + if let Some(client_senders) = self.active_clients_store.get_sender(client_address) { self.clients_store_cache - .insert(client_address, client_senders.mix_message_sender); + .insert(client_address, client_senders); } } @@ -97,11 +106,14 @@ impl ConnectionHandler { match self.clients_store_cache.get(&client_address) { None => Err(message), Some(sender_channel) => { - sender_channel - .unbounded_send(vec![message]) - // right now it's a "simpler" case here as we're only ever sending 1 message - // at the time, but the channel itself could accept arbitrary many messages at once - .map_err(|try_send_err| try_send_err.into_inner().pop().unwrap()) + if let Err(unsent) = sender_channel.unbounded_send(vec![message]) { + // the unwrap here is fine as the original message got returned; + // plus we're only ever sending 1 message at the time (for now) + #[allow(clippy::unwrap_used)] + return Err(unsent.into_inner().pop().unwrap()); + } else { + Ok(()) + } } } } @@ -111,27 +123,35 @@ impl ConnectionHandler { client_address: DestinationAddressBytes, message: Vec, ) -> Result<(), StorageError> { - debug!( - "Storing received message for {} on the disk...", - client_address - ); + debug!("Storing received message for {client_address} on the disk...",); self.storage.store_message(client_address, message).await } - fn forward_ack(&self, forward_ack: Option, client_address: DestinationAddressBytes) { + fn forward_ack( + &self, + forward_ack: Option, + client_address: DestinationAddressBytes, + ) -> Result<(), CriticalPacketProcessingError> { if let Some(forward_ack) = forward_ack { - trace!( - "Sending ack from packet for {} to {}", - client_address, - forward_ack.next_hop() - ); + let next_hop = forward_ack.next_hop(); + trace!("Sending ack from packet for {client_address} to {next_hop}",); - self.ack_sender.unbounded_send(forward_ack).unwrap(); + self.ack_sender + .unbounded_send(forward_ack) + .map_err( + |source| CriticalPacketProcessingError::AckForwardingFailure { + source: source.into_send_error(), + }, + )?; } + Ok(()) } - async fn handle_processed_packet(&mut self, processed_final_hop: ProcessedFinalHop) { + async fn handle_processed_packet( + &mut self, + processed_final_hop: ProcessedFinalHop, + ) -> Result<(), CriticalPacketProcessingError> { let client_address = processed_final_hop.destination; let message = processed_final_hop.message; let forward_ack = processed_final_hop.forward_ack; @@ -144,18 +164,21 @@ impl ConnectionHandler { .await { Err(err) => error!("Failed to store client data - {err}"), - Ok(_) => trace!("Stored packet for {}", client_address), + Ok(_) => trace!("Stored packet for {client_address}"), }, - Ok(_) => trace!("Pushed received packet to {}", client_address), + Ok(_) => trace!("Pushed received packet to {client_address}"), } // if we managed to either push message directly to the [online] client or store it at // its inbox, it means that it must exist at this gateway, hence we can send the // received ack back into the network - self.forward_ack(forward_ack, client_address); + self.forward_ack(forward_ack, client_address) } - async fn handle_received_packet(&mut self, framed_sphinx_packet: FramedNymPacket) { + async fn handle_received_packet( + &mut self, + framed_sphinx_packet: FramedNymPacket, + ) -> Result<(), CriticalPacketProcessingError> { // // TODO: here be replay attack detection - it will require similar key cache to the one in // packet processor for vpn packets, @@ -166,7 +189,7 @@ impl ConnectionHandler { { Err(err) => { debug!("We failed to process received sphinx packet - {err}"); - return; + return Ok(()); } Ok(processed_final_hop) => processed_final_hop, }; @@ -198,7 +221,11 @@ impl ConnectionHandler { // in theory we could process multiple sphinx packet from the same connection in parallel, // but we already handle multiple concurrent connections so if anything, making // that change would only slow things down - self.handle_received_packet(framed_sphinx_packet).await; + if let Err(critical_err) = self.handle_received_packet(framed_sphinx_packet).await { + if !shutdown.is_shutdown() { + panic!("experienced critical failure when processing received packet: {critical_err}") + } + } } Some(Err(err)) => { error!( @@ -212,9 +239,13 @@ impl ConnectionHandler { } } - info!( - "Closing connection from {:?}", - framed_conn.into_inner().peer_addr() - ); + match framed_conn.into_inner().peer_addr() { + Ok(peer_addr) => { + debug!("closing connection from {peer_addr}") + } + Err(err) => { + warn!("closing connection from an unknown peer: {err}") + } + } } } diff --git a/gateway/src/node/mixnet_handling/receiver/listener.rs b/gateway/src/node/mixnet_handling/receiver/listener.rs index 38fd8ef43b..6714906ed7 100644 --- a/gateway/src/node/mixnet_handling/receiver/listener.rs +++ b/gateway/src/node/mixnet_handling/receiver/listener.rs @@ -43,7 +43,7 @@ impl Listener { match connection { Ok((socket, remote_addr)) => { let handler = connection_handler.clone(); - tokio::spawn(handler.handle_connection(socket, remote_addr, self.shutdown.clone())); + tokio::spawn(handler.handle_connection(socket, remote_addr, self.shutdown.clone().named(format!("MixnetConnectionHandler_{remote_addr}")))); } Err(err) => warn!("failed to get client: {err}"), } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index e1d6b167af..80bd505ccf 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -2,19 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 use self::storage::PersistentStorage; +use crate::commands::helpers::{override_network_requester_config, OverrideNetworkRequesterConfig}; use crate::config::Config; use crate::error::GatewayError; use crate::node::client_handling::active_clients::ActiveClientsStore; +use crate::node::client_handling::embedded_network_requester::{ + LocalNetworkRequesterHandle, MessageRouter, +}; use crate::node::client_handling::websocket; use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; +use crate::node::helpers::{initialise_main_storage, load_network_requester_config}; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use crate::node::statistics::collector::GatewayStatisticsCollector; use crate::node::storage::Storage; +use anyhow::bail; +use futures::channel::{mpsc, oneshot}; use log::*; -use nym_bin_common::output_format::OutputFormat; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; +use nym_network_requester::{LocalGateway, NRServiceProviderBuilder}; use nym_statistics_common::collector::StatisticsSender; use nym_task::{TaskClient, TaskManager}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; @@ -22,32 +29,59 @@ use rand::seq::SliceRandom; use rand::thread_rng; use std::error::Error; use std::net::SocketAddr; +use std::path::PathBuf; use std::sync::Arc; pub(crate) mod client_handling; +pub(crate) mod helpers; pub(crate) mod mixnet_handling; pub(crate) mod statistics; pub(crate) mod storage; /// Wire up and create Gateway instance -pub(crate) async fn create_gateway(config: Config) -> Gateway { - let storage = initialise_storage(&config).await; - Gateway::new(config, storage).await -} - -async fn initialise_storage(config: &Config) -> PersistentStorage { - let path = &config.storage_paths.clients_storage; - let retrieval_limit = config.debug.message_retrieval_limit; - match PersistentStorage::init(path, retrieval_limit).await { - Err(err) => panic!("failed to initialise gateway storage: {err}"), - Ok(storage) => storage, - } -} - -pub(crate) struct Gateway { +pub(crate) async fn create_gateway( config: Config, + nr_config_override: Option, + custom_nr_mixnet: Option, +) -> Result { + // don't attempt to read config if NR is disabled + let network_requester_config = if config.network_requester.enabled { + if let Some(path) = &config.storage_paths.network_requester_config { + let cfg = load_network_requester_config(&config.gateway.id, path)?; + Some(override_network_requester_config(cfg, nr_config_override)) + } else { + // if NR is enabled, the config path must be specified + return Err(GatewayError::UnspecifiedNetworkRequesterConfig); + } + } else { + None + }; + + let storage = initialise_main_storage(&config).await?; + + let nr_opts = network_requester_config.map(|config| LocalNetworkRequesterOpts { + config, + custom_mixnet_path: custom_nr_mixnet, + }); + + Gateway::new(config, nr_opts, storage) +} + +#[derive(Debug, Clone)] +pub struct LocalNetworkRequesterOpts { + config: nym_network_requester::Config, + + custom_mixnet_path: Option, +} + +pub(crate) struct Gateway { + config: Config, + + network_requester_opts: Option, + /// ed25519 keypair used to assert one's identity. identity_keypair: Arc, + /// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation. sphinx_keypair: Arc, storage: St, @@ -55,71 +89,37 @@ pub(crate) struct Gateway { impl Gateway { /// Construct from the given `Config` instance. - pub async fn new(config: Config, storage: St) -> Self { - Gateway { + pub fn new( + config: Config, + network_requester_opts: Option, + storage: St, + ) -> Result { + Ok(Gateway { storage, - identity_keypair: Arc::new(Self::load_identity_keys(&config)), - sphinx_keypair: Arc::new(Self::load_sphinx_keys(&config)), + identity_keypair: Arc::new(helpers::load_identity_keys(&config)?), + sphinx_keypair: Arc::new(helpers::load_sphinx_keys(&config)?), config, - } + network_requester_opts, + }) } #[cfg(test)] pub async fn new_from_keys_and_storage( config: Config, + network_requester_opts: Option, identity_keypair: identity::KeyPair, sphinx_keypair: encryption::KeyPair, storage: St, ) -> Self { Gateway { config, + network_requester_opts, identity_keypair: Arc::new(identity_keypair), sphinx_keypair: Arc::new(sphinx_keypair), storage, } } - /// Loads identity keys stored on disk - pub(crate) fn load_identity_keys(config: &Config) -> identity::KeyPair { - let identity_keypair: identity::KeyPair = - nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new( - config.storage_paths.keys.private_identity_key(), - config.storage_paths.keys.public_identity_key(), - )) - .expect("Failed to read stored identity key files"); - identity_keypair - } - - /// Loads Sphinx keys stored on disk - fn load_sphinx_keys(config: &Config) -> encryption::KeyPair { - let sphinx_keypair: encryption::KeyPair = - nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new( - config.storage_paths.keys.private_encryption_key(), - config.storage_paths.keys.public_encryption_key(), - )) - .expect("Failed to read stored sphinx key files"); - sphinx_keypair - } - - pub(crate) fn print_node_details(&self, output: OutputFormat) { - let node_details = nym_types::gateway::GatewayNodeDetailsResponse { - identity_key: self.identity_keypair.public_key().to_base58_string(), - sphinx_key: self.sphinx_keypair.public_key().to_base58_string(), - bind_address: self.config.gateway.listening_address.to_string(), - version: self.config.gateway.version.clone(), - mix_port: self.config.gateway.mix_port, - clients_port: self.config.gateway.clients_port, - data_store: self - .config - .storage_paths - .clients_storage - .display() - .to_string(), - }; - - println!("{}", output.format(&node_details)); - } - fn start_mix_socket_listener( &self, ack_sender: MixForwardingSender, @@ -194,46 +194,106 @@ impl Gateway { packet_sender } - async fn wait_for_interrupt( + // TODO: rethink the logic in this function... + async fn start_network_requester( &self, - shutdown: TaskManager, - ) -> Result<(), Box> { + forwarding_channel: MixForwardingSender, + shutdown: TaskClient, + ) -> Result { + info!("Starting network requester..."); + + // if network requester is enabled, configuration file must be provided! + let Some(nr_opts) = &self.network_requester_opts else { + return Err(GatewayError::UnspecifiedNetworkRequesterConfig); + }; + + // this gateway, whenever it has anything to send to its local NR will use fake_client_tx + let (nr_mix_sender, nr_mix_receiver) = mpsc::unbounded(); + let router_shutdown = shutdown.fork("message_router"); + + let (router_tx, mut router_rx) = oneshot::channel(); + + let transceiver = LocalGateway::new( + *self.identity_keypair.public_key(), + forwarding_channel, + router_tx, + ); + + // TODO: well, wire it up internally to gateway traffic, shutdowns, etc. + let (on_start_tx, on_start_rx) = oneshot::channel(); + let mut nr_builder = NRServiceProviderBuilder::new(nr_opts.config.clone()) + .with_shutdown(shutdown) + .with_custom_gateway_transceiver(Box::new(transceiver)) + .with_wait_for_gateway(true) + .with_on_start(on_start_tx); + + if let Some(custom_mixnet) = &nr_opts.custom_mixnet_path { + nr_builder = nr_builder.with_stored_topology(custom_mixnet)? + } + + tokio::spawn(async move { + if let Err(err) = nr_builder.run_service_provider().await { + // no need to panic as we have passed a task client to the NR so we're most likely + // already in the process of shutting down + error!("network requester has failed: {err}") + } + }); + + let start_data = on_start_rx + .await + .map_err(|_| GatewayError::NetworkRequesterStartupFailure)?; + + // this should be instantaneous since the data is sent on this channel before the on start is called; + // the failure should be impossible + let Ok(Some(packet_router)) = router_rx.try_recv() else { + return Err(GatewayError::NetworkRequesterStartupFailure); + }; + + MessageRouter::new(nr_mix_receiver, packet_router).start_with_shutdown(router_shutdown); + info!( + "the local network requester is running on {}", + start_data.address + ); + + Ok(LocalNetworkRequesterHandle::new(start_data, nr_mix_sender)) + } + + async fn wait_for_interrupt(shutdown: TaskManager) -> Result<(), Box> { let res = shutdown.catch_interrupt().await; log::info!("Stopping nym gateway"); res } - fn random_api_client(&self) -> nym_validator_client::NymApiClient { + fn random_api_client(&self) -> Result { let endpoints = self.config.get_nym_api_endpoints(); let nym_api = endpoints .choose(&mut thread_rng()) - .expect("The list of validator apis is empty"); + .ok_or(GatewayError::NoNymApisAvailable)?; - nym_validator_client::NymApiClient::new(nym_api.clone()) + Ok(nym_validator_client::NymApiClient::new(nym_api.clone())) } - fn random_nyxd_client(&self) -> DirectSigningHttpRpcNyxdClient { + fn random_nyxd_client(&self) -> Result { let endpoints = self.config.get_nyxd_urls(); let validator_nyxd = endpoints .choose(&mut thread_rng()) - .expect("The list of validators is empty"); + .ok_or(GatewayError::NoNyxdAvailable)?; let network_details = NymNetworkDetails::new_from_env(); - let client_config = nyxd::Config::try_from_nym_network_details(&network_details) - .expect("failed to construct valid validator client config with the provided network"); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( client_config, validator_nyxd.as_ref(), self.config.get_cosmos_mnemonic(), ) - .expect("Could not connect with mnemonic") + .map_err(Into::into) } async fn check_if_bonded(&self) -> Result { // TODO: if anything, this should be getting data directly from the contract // as opposed to the validator API - let validator_client = self.random_api_client(); + let validator_client = self.random_api_client()?; let existing_nodes = match validator_client.get_cached_gateways().await { Ok(nodes) => nodes, Err(err) => { @@ -247,7 +307,7 @@ impl Gateway { })) } - pub async fn run(&mut self) -> Result<(), Box> + pub async fn run(self) -> anyhow::Result<()> where St: Storage + Clone + 'static, { @@ -260,17 +320,18 @@ impl Gateway { let shutdown = TaskManager::new(10); let coconut_verifier = { - let nyxd_client = self.random_nyxd_client(); + let nyxd_client = self.random_nyxd_client()?; CoconutVerifier::new(nyxd_client) }; - let mix_forwarding_channel = self.start_packet_forwarder(shutdown.subscribe()); + let mix_forwarding_channel = + self.start_packet_forwarder(shutdown.subscribe().named("PacketForwarder")); let active_clients_store = ActiveClientsStore::new(); self.start_mix_socket_listener( mix_forwarding_channel.clone(), active_clients_store.clone(), - shutdown.subscribe(), + shutdown.subscribe().named("mixnet_handling::Listener"), ); if self.config.gateway.enabled_statistics { @@ -286,20 +347,40 @@ impl Gateway { }); } + if self.config.network_requester.enabled { + let embedded_nr = self + .start_network_requester( + mix_forwarding_channel.clone(), + shutdown.subscribe().named("NetworkRequester"), + ) + .await?; + // insert information about embedded NR to the active clients store + active_clients_store.insert_embedded(embedded_nr) + } else { + info!("embedded network requester is disabled"); + } + self.start_client_websocket_listener( mix_forwarding_channel, active_clients_store, - shutdown.subscribe(), + shutdown.subscribe().named("websocket::Listener"), Arc::new(coconut_verifier), ); // Once this is a bit more mature, make this a commandline flag instead of a compile time // flag #[cfg(feature = "wireguard")] - nym_wireguard::start_wg_listener(shutdown.subscribe()).await?; + if let Err(err) = nym_wireguard::start_wg_listener(shutdown.subscribe()).await { + // that's a nasty workaround, but anyhow errors are generally nicer, especially on exit + bail!("{err}") + } info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!"); - self.wait_for_interrupt(shutdown).await + if let Err(err) = Self::wait_for_interrupt(shutdown).await { + // that's a nasty workaround, but anyhow errors are generally nicer, especially on exit + bail!("{err}") + } + Ok(()) } } diff --git a/gateway/src/node/storage/error.rs b/gateway/src/node/storage/error.rs index 7f6d023bab..306dafa557 100644 --- a/gateway/src/node/storage/error.rs +++ b/gateway/src/node/storage/error.rs @@ -5,9 +5,9 @@ use thiserror::Error; #[derive(Error, Debug)] pub(crate) enum StorageError { - #[error("Database experienced an internal error - {0}")] + #[error("Database experienced an internal error: {0}")] InternalDatabaseError(#[from] sqlx::Error), - #[error("Failed to perform database migration - {0}")] + #[error("Failed to perform database migration: {0}")] MigrationError(#[from] sqlx::migrate::MigrateError), } diff --git a/gateway/src/node/storage/inboxes.rs b/gateway/src/node/storage/inboxes.rs index d40ae5681f..f67fac94b4 100644 --- a/gateway/src/node/storage/inboxes.rs +++ b/gateway/src/node/storage/inboxes.rs @@ -18,7 +18,12 @@ impl InboxManager { /// # Arguments /// /// * `connection_pool`: database connection pool to use. - pub(crate) fn new(connection_pool: sqlx::SqlitePool, retrieval_limit: i64) -> Self { + pub(crate) fn new(connection_pool: sqlx::SqlitePool, mut retrieval_limit: i64) -> Self { + // TODO: make this into a hard error instead + if retrieval_limit == 0 { + retrieval_limit = 100; + } + InboxManager { connection_pool, retrieval_limit, @@ -99,7 +104,8 @@ impl InboxManager { if res.len() > self.retrieval_limit as usize { res.truncate(self.retrieval_limit as usize); - // assuming retrieval_limit > 0, unwrap will not fail + // given retrieval_limit > 0, unwrap will not fail + #[allow(clippy::unwrap_used)] let start_after = res.last().unwrap().id; Ok((res, Some(start_after))) // diff --git a/gateway/src/support/config.rs b/gateway/src/support/config.rs index cbbd4dd798..ff6cc91789 100644 --- a/gateway/src/support/config.rs +++ b/gateway/src/support/config.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::commands::{override_config, try_load_current_config, OverrideConfig}; +use crate::commands::helpers::{try_load_current_config, OverrideConfig}; use crate::config::Config; use crate::error::GatewayError; @@ -10,6 +10,5 @@ pub(crate) fn build_config>( override_args: O, ) -> Result { let config = try_load_current_config(&id)?; - - override_config(config, override_args.into()) + override_args.into().do_override(config) } diff --git a/mixnode/src/config/mod.rs b/mixnode/src/config/mod.rs index eeb8582ff4..7a6a73e06a 100644 --- a/mixnode/src/config/mod.rs +++ b/mixnode/src/config/mod.rs @@ -88,7 +88,7 @@ pub struct Config { } impl NymConfigTemplate for Config { - fn template() -> &'static str { + fn template(&self) -> &'static str { CONFIG_TEMPLATE } } diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index bbf40e6f72..ef3bda31af 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -16,8 +16,11 @@ use nym_bandwidth_controller::BandwidthController; use nym_config::defaults::REMAINING_BANDWIDTH_THRESHOLD; use nym_credential_storage::persistent_storage::PersistentStorage; use nym_crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH}; +use nym_gateway_client::client::GatewayConfig; use nym_gateway_client::error::GatewayClientError; -use nym_gateway_client::{AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver}; +use nym_gateway_client::{ + AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, +}; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; use pin_project::pin_project; @@ -55,6 +58,14 @@ impl GatewayPackets { } } + pub(crate) fn gateway_config(&self) -> GatewayConfig { + GatewayConfig { + gateway_identity: self.pub_key, + gateway_owner: None, + gateway_listener: self.clients_address.clone(), + } + } + pub(crate) fn empty(clients_address: String, pub_key: identity::PublicKey) -> Self { GatewayPackets { clients_address, @@ -169,13 +180,16 @@ impl PacketSender { } fn new_gateway_client_handle( - address: String, - identity: identity::PublicKey, + config: GatewayConfig, fresh_gateway_client_data: &FreshGatewayClientData, ) -> ( GatewayClientHandle, (MixnetMessageReceiver, AcknowledgementReceiver), ) { + // I think the proper one should be passed around instead... + let task_client = + nym_task::TaskClient::dummy().named(format!("gateway-{}", config.gateway_identity)); + // TODO: future optimization: if we're remaking client for a gateway to which we used to be connected in the past, // use old shared keys let (message_sender, message_receiver) = mpsc::unbounded(); @@ -184,20 +198,22 @@ impl PacketSender { // so that the gateway client would not crash let (ack_sender, ack_receiver) = mpsc::unbounded(); - let mut gateway_client = GatewayClient::new( - address, - Arc::clone(&fresh_gateway_client_data.local_identity), - identity, - None, - message_sender, + let gateway_packet_router = PacketRouter::new( ack_sender, - fresh_gateway_client_data.gateway_response_timeout, - Some(fresh_gateway_client_data.bandwidth_controller.clone()), - nym_task::TaskClient::dummy(), + message_sender, + task_client.fork("packet-router"), ); - gateway_client - .set_disabled_credentials_mode(fresh_gateway_client_data.disabled_credentials_mode); + let gateway_client = GatewayClient::new( + config, + Arc::clone(&fresh_gateway_client_data.local_identity), + None, + gateway_packet_router, + Some(fresh_gateway_client_data.bandwidth_controller.clone()), + task_client, + ) + .with_disabled_credentials_mode(fresh_gateway_client_data.disabled_credentials_mode) + .with_response_timeout(fresh_gateway_client_data.gateway_response_timeout); ( GatewayClientHandle::new(gateway_client), @@ -265,16 +281,16 @@ impl PacketSender { } async fn create_new_gateway_client_handle_and_authenticate( - address: String, - identity: identity::PublicKey, + config: GatewayConfig, fresh_gateway_client_data: &FreshGatewayClientData, gateway_connection_timeout: Duration, ) -> Option<( GatewayClientHandle, (MixnetMessageReceiver, AcknowledgementReceiver), )> { + let gateway_identity = config.gateway_identity; let (new_client, (message_receiver, ack_receiver)) = - Self::new_gateway_client_handle(address, identity, fresh_gateway_client_data); + Self::new_gateway_client_handle(config, fresh_gateway_client_data); // Put this in timeout in case the gateway has incorrectly set their ulimit and our connection // gets stuck in their TCP queue and just hangs on our end but does not terminate @@ -295,19 +311,12 @@ impl PacketSender { Some((new_client, (message_receiver, ack_receiver))) } Ok(Err(err)) => { - warn!( - "failed to authenticate with new gateway ({}) - {}", - identity.to_base58_string(), - err - ); + warn!("failed to authenticate with new gateway ({gateway_identity}): {err}",); // we failed to create a client, can't do much here None } Err(_) => { - warn!( - "timed out while trying to authenticate with new gateway ({})", - identity.to_base58_string() - ); + warn!("timed out while trying to authenticate with new gateway {gateway_identity}",); None } } @@ -349,8 +358,7 @@ impl PacketSender { } else { let (client, gateway_channels) = Self::create_new_gateway_client_handle_and_authenticate( - packets.clients_address, - packets.pub_key, + packets.gateway_config(), &fresh_gateway_client_data, gateway_connection_timeout, ) diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index a66c5ce8a7..9cf1404b38 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -110,7 +110,7 @@ impl<'a> From<&'a Config> for NymNetworkDetails { } impl NymConfigTemplate for Config { - fn template() -> &'static str { + fn template(&self) -> &'static str { CONFIG_TEMPLATE } } diff --git a/nym-browser-extension/storage/src/storage.rs b/nym-browser-extension/storage/src/storage.rs index 4a0874f47e..41389c8602 100644 --- a/nym-browser-extension/storage/src/storage.rs +++ b/nym-browser-extension/storage/src/storage.rs @@ -1,6 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +// I'm not convinced by the lint (on Arc). +// Sure. wasm is currently single threaded and does not require `Send` or `Sync` +// but this data is moved across futures, so imo we should leave the Arc as it is, +// because it might cause us headache in the future +#![allow(clippy::arc_with_non_send_sync)] + use crate::ExtensionStorageError; use js_sys::Promise; use std::sync::Arc; diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 2fd6ee3069..06e7e47d01 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -1513,9 +1513,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -4384,6 +4384,7 @@ dependencies = [ name = "nym-socks5-client-core" version = "0.1.0" dependencies = [ + "anyhow", "dirs 4.0.0", "futures", "log", diff --git a/nym-connect/desktop/src-tauri/src/config/mod.rs b/nym-connect/desktop/src-tauri/src/config/mod.rs index 267c970069..96ef14c6e8 100644 --- a/nym-connect/desktop/src-tauri/src/config/mod.rs +++ b/nym-connect/desktop/src-tauri/src/config/mod.rs @@ -9,7 +9,8 @@ use nym_client_core::client::base_client::storage::gateway_details::OnDiskGatewa use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::config::GatewayEndpointConfig; use nym_client_core::error::ClientCoreError; -use nym_client_core::init::GatewaySetup; +use nym_client_core::init::helpers::current_gateways; +use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; use nym_config::{ must_get_home, read_config_from_toml_file, save_formatted_config_to_file, NymConfigTemplate, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, @@ -70,7 +71,7 @@ pub struct Config { } impl NymConfigTemplate for Config { - fn template() -> &'static str { + fn template(&self) -> &'static str { CONFIG_TEMPLATE } } @@ -203,7 +204,15 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str } let gateway_setup = if register_gateway { - GatewaySetup::new_fresh(Some(chosen_gateway_id), None) + let selection_spec = GatewaySelectionSpecification::new(Some(chosen_gateway_id), None); + let mut rng = rand_07::thread_rng(); + let available_gateways = + current_gateways(&mut rng, &config.core.base.client.nym_api_urls).await?; + GatewaySetup::New { + specification: selection_spec, + available_gateways, + overwrite_data: register_gateway, + } } else { GatewaySetup::MustLoad }; @@ -212,21 +221,18 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys.clone()); let details_store = OnDiskGatewayDetails::new(&config.storage_paths.common_paths.gateway_details); - let init_details = nym_client_core::init::setup_gateway( - gateway_setup, - &key_store, - &details_store, - register_gateway, - Some(&config.core.base.client.nym_api_urls), - ) - .await? - .details; + let init_details = + nym_client_core::init::setup_gateway(gateway_setup, &key_store, &details_store).await?; + let gateway_endpoint = init_details + .gateway_details + .try_get_configured_endpoint() + .unwrap(); config.save_to_default_location().tap_err(|_| { log::error!("Failed to save the config file"); })?; - print_saved_config(&config, &init_details.gateway_details); + print_saved_config(&config, gateway_endpoint); let address = init_details.client_address()?; log::info!("The address of this client is: {address}"); diff --git a/nym-connect/desktop/src-tauri/src/config/persistence.rs b/nym-connect/desktop/src-tauri/src/config/persistence.rs index b5a67d1f54..69b311d474 100644 --- a/nym-connect/desktop/src-tauri/src/config/persistence.rs +++ b/nym-connect/desktop/src-tauri/src/config/persistence.rs @@ -14,7 +14,7 @@ pub struct NymConnectPaths { impl NymConnectPaths { pub fn new_default>(base_data_directory: P) -> Self { NymConnectPaths { - common_paths: CommonClientPaths::new_default(base_data_directory), + common_paths: CommonClientPaths::new_base(base_data_directory), } } } diff --git a/nym-connect/desktop/src-tauri/src/config/upgrade.rs b/nym-connect/desktop/src-tauri/src/config/upgrade.rs index 2f41a964f4..bdaf138a00 100644 --- a/nym-connect/desktop/src-tauri/src/config/upgrade.rs +++ b/nym-connect/desktop/src-tauri/src/config/upgrade.rs @@ -29,7 +29,7 @@ fn persist_gateway_details(config: &Config, details: GatewayEndpointConfig) -> R }, } })?; - let persisted_details = PersistedGatewayDetails::new(details, &shared_keys); + let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; details_store .store_to_disk(&persisted_details) .map_err(|source| BackendError::ClientCoreError { diff --git a/nym-connect/desktop/src-tauri/src/state.rs b/nym-connect/desktop/src-tauri/src/state.rs index f9abbe2b8f..20471e6171 100644 --- a/nym-connect/desktop/src-tauri/src/state.rs +++ b/nym-connect/desktop/src-tauri/src/state.rs @@ -257,7 +257,13 @@ impl State { let (control_tx, msg_rx, exit_status_rx, used_gateway) = tasks::start_nym_socks5_client(&id, &privacy_level).await?; self.socks5_client_sender = Some(control_tx); - self.gateway = Some(used_gateway.gateway_id); + self.gateway = Some( + used_gateway + .try_get_configured_endpoint() + .unwrap() + .gateway_id + .clone(), + ); Ok((msg_rx, exit_status_rx)) } diff --git a/nym-connect/desktop/src-tauri/src/tasks.rs b/nym-connect/desktop/src-tauri/src/tasks.rs index 454ecc55c8..d4264f2274 100644 --- a/nym-connect/desktop/src-tauri/src/tasks.rs +++ b/nym-connect/desktop/src-tauri/src/tasks.rs @@ -1,9 +1,10 @@ use futures::{channel::mpsc, StreamExt}; +use nym_client_core::init::types::GatewayDetails; use nym_client_core::{ client::base_client::storage::{ gateway_details::GatewayDetailsStore, MixnetClientStorage, OnDiskPersistent, }, - config::{GatewayEndpointConfig, GroupBy, TopologyStructure}, + config::{GroupBy, TopologyStructure}, error::ClientCoreStatusMessage, }; use nym_socks5_client_core::{NymClient as Socks5NymClient, Socks5ControlMessageSender}; @@ -73,7 +74,7 @@ pub async fn start_nym_socks5_client( Socks5ControlMessageSender, nym_task::StatusReceiver, ExitStatusReceiver, - GatewayEndpointConfig, + GatewayDetails, )> { log::info!("Loading config from file: {id}"); let mut config = Config::read_from_default_path(id) diff --git a/sdk/lib/socks5-listener/src/config/mod.rs b/sdk/lib/socks5-listener/src/config/mod.rs index c6a7060954..5ace48c88f 100644 --- a/sdk/lib/socks5-listener/src/config/mod.rs +++ b/sdk/lib/socks5-listener/src/config/mod.rs @@ -55,7 +55,7 @@ pub struct Config { } impl NymConfigTemplate for Config { - fn template() -> &'static str { + fn template(&self) -> &'static str { CONFIG_TEMPLATE } } diff --git a/sdk/lib/socks5-listener/src/config/persistence.rs b/sdk/lib/socks5-listener/src/config/persistence.rs index cdd46aad87..525ea7066f 100644 --- a/sdk/lib/socks5-listener/src/config/persistence.rs +++ b/sdk/lib/socks5-listener/src/config/persistence.rs @@ -15,12 +15,12 @@ pub struct MobileSocksClientPaths { impl MobileSocksClientPaths { pub fn new_default>(base_data_directory: P) -> Self { MobileSocksClientPaths { - common_paths: CommonClientPaths::new_default(base_data_directory), + common_paths: CommonClientPaths::new_base(base_data_directory), } } pub fn change_root, R: AsRef>(&mut self, new_root: P, id: R) { let new_data_dir = data_directory_from_root(new_root, id); - self.common_paths = CommonClientPaths::new_default(new_data_dir) + self.common_paths = CommonClientPaths::new_base(new_data_dir) } } diff --git a/sdk/lib/socks5-listener/src/lib.rs b/sdk/lib/socks5-listener/src/lib.rs index 74b785d9a0..79f4f4c2e9 100644 --- a/sdk/lib/socks5-listener/src/lib.rs +++ b/sdk/lib/socks5-listener/src/lib.rs @@ -8,9 +8,11 @@ use anyhow::{anyhow, Result}; use lazy_static::lazy_static; use log::{debug, info, warn}; use nym_bin_common::logging::setup_logging; -use nym_client_core::init::GatewaySetup; +use nym_client_core::init::helpers::current_gateways; +use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; use nym_config_common::defaults::setup_env; use nym_socks5_client_core::NymClient as Socks5NymClient; +use rand::rngs::OsRng; use safer_ffi::char_p::char_p_boxed; use std::marker::Send; use std::path::PathBuf; @@ -258,17 +260,25 @@ where F: FnMut(String), S: FnMut(), { + let mut rng = OsRng; + set_default_env(); let stop_handle = Arc::new(Notify::new()); set_shutdown_handle(stop_handle.clone()).await; let config = load_or_generate_base_config(storage_dir, client_id, service_provider).await?; + let nym_apis = config.core.base.client.nym_api_urls.clone(); + let storage = MobileClientStorage::new(&config); - let socks5_client = Socks5NymClient::new(config.core, storage, None) - .with_gateway_setup(GatewaySetup::New { by_latency: false }); + let socks5_client = + Socks5NymClient::new(config.core, storage, None).with_gateway_setup(GatewaySetup::New { + specification: GatewaySelectionSpecification::UniformRemote, + available_gateways: current_gateways(&mut rng, &nym_apis).await?, + overwrite_data: false, + }); eprintln!("starting the socks5 client"); - let mut started_client = socks5_client.start().await?; + let started_client = socks5_client.start().await?; eprintln!("the client has started!"); // invoke the callback since we've started! @@ -278,8 +288,12 @@ where stop_handle.notified().await; // and then do graceful shutdown of all tasks - started_client.shutdown_handle.signal_shutdown().ok(); - started_client.shutdown_handle.wait_for_shutdown().await; + let mut task_manager = started_client + .shutdown_handle + .try_into_task_manager() + .unwrap(); + task_manager.signal_shutdown().ok(); + task_manager.wait_for_shutdown().await; // and the corresponding one for shutdown! on_shutdown_callback(); diff --git a/sdk/rust/nym-sdk/examples/bandwidth.rs b/sdk/rust/nym-sdk/examples/bandwidth.rs index 377dd48527..96a064cf1d 100644 --- a/sdk/rust/nym-sdk/examples/bandwidth.rs +++ b/sdk/rust/nym-sdk/examples/bandwidth.rs @@ -16,8 +16,7 @@ async fn main() -> anyhow::Result<()> { let mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral() .network_details(sandbox_network) .enable_credentials_mode() - .build() - .await?; + .build()?; let bandwidth_client = mixnet_client.create_bandwidth_client(mnemonic)?; diff --git a/sdk/rust/nym-sdk/examples/builder.rs b/sdk/rust/nym-sdk/examples/builder.rs index 6ab50bc92c..c06fc55913 100644 --- a/sdk/rust/nym-sdk/examples/builder.rs +++ b/sdk/rust/nym-sdk/examples/builder.rs @@ -9,7 +9,6 @@ async fn main() { // where you don't want to connect just yet. let client = mixnet::MixnetClientBuilder::new_ephemeral() .build() - .await .unwrap(); // Now we connect to the mixnet, using ephemeral keys already created diff --git a/sdk/rust/nym-sdk/examples/builder_with_storage.rs b/sdk/rust/nym-sdk/examples/builder_with_storage.rs index 900b2d323f..b733239f5f 100644 --- a/sdk/rust/nym-sdk/examples/builder_with_storage.rs +++ b/sdk/rust/nym-sdk/examples/builder_with_storage.rs @@ -16,7 +16,6 @@ async fn main() { .await .unwrap() .build() - .await .unwrap(); // Now we connect to the mixnet, using keys now stored in the paths provided. diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index 7232f5ac58..d21916e6c9 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -60,7 +60,6 @@ async fn main() { let mut client = mixnet::MixnetClientBuilder::new_ephemeral() .custom_topology_provider(Box::new(my_topology_provider)) .build() - .await .unwrap() .connect_to_mixnet() .await diff --git a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index 6130af5a25..dd650b1e2a 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -16,7 +16,6 @@ async fn main() { let mock_storage = MockClientStorage::empty(); let mut client = mixnet::MixnetClientBuilder::new_with_storage(mock_storage) .build() - .await .unwrap() .connect_to_mixnet() .await diff --git a/sdk/rust/nym-sdk/examples/socks5.rs b/sdk/rust/nym-sdk/examples/socks5.rs index bf028a7e76..05732db53b 100644 --- a/sdk/rust/nym-sdk/examples/socks5.rs +++ b/sdk/rust/nym-sdk/examples/socks5.rs @@ -11,11 +11,10 @@ async fn main() { let sending_client = mixnet::MixnetClientBuilder::new_ephemeral() .socks5_config(socks5_config) .build() - .await .unwrap(); println!("Connecting sender"); - let mut sending_client = sending_client.connect_to_mixnet_via_socks5().await.unwrap(); + let sending_client = sending_client.connect_to_mixnet_via_socks5().await.unwrap(); let proxy = reqwest::Proxy::all(sending_client.socks5_url()).unwrap(); let reqwest_client = reqwest::Client::builder().proxy(proxy).build().unwrap(); diff --git a/sdk/rust/nym-sdk/examples/surb-reply.rs b/sdk/rust/nym-sdk/examples/surb-reply.rs index abe955b2b9..926cac0314 100644 --- a/sdk/rust/nym-sdk/examples/surb-reply.rs +++ b/sdk/rust/nym-sdk/examples/surb-reply.rs @@ -18,7 +18,6 @@ async fn main() { .await .unwrap() .build() - .await .unwrap(); // Now we connect to the mixnet, using keys now stored in the paths provided. diff --git a/sdk/rust/nym-sdk/src/bandwidth.rs b/sdk/rust/nym-sdk/src/bandwidth.rs index e9cfb0f8fb..b1548dcb96 100644 --- a/sdk/rust/nym-sdk/src/bandwidth.rs +++ b/sdk/rust/nym-sdk/src/bandwidth.rs @@ -13,7 +13,6 @@ //! let mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral() //! .enable_credentials_mode() //! .build() -//! .await //! .unwrap(); //! //! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic")).unwrap(); diff --git a/sdk/rust/nym-sdk/src/error.rs b/sdk/rust/nym-sdk/src/error.rs index e88368b19a..e47b8b784f 100644 --- a/sdk/rust/nym-sdk/src/error.rs +++ b/sdk/rust/nym-sdk/src/error.rs @@ -93,6 +93,17 @@ pub enum Error { #[error("failed to send the provided message")] MessageSendingFailure, + + #[error("this operation is currently unsupported: {details}")] + Unsupported { details: String }, +} + +impl Error { + pub fn new_unsupported>(details: S) -> Self { + Error::Unsupported { + details: details.into(), + } + } } pub type Result = std::result::Result; diff --git a/sdk/rust/nym-sdk/src/lib.rs b/sdk/rust/nym-sdk/src/lib.rs index fee4a2f323..7834aca487 100644 --- a/sdk/rust/nym-sdk/src/lib.rs +++ b/sdk/rust/nym-sdk/src/lib.rs @@ -8,3 +8,11 @@ pub mod bandwidth; pub mod mixnet; pub use error::{Error, Result}; +pub use nym_client_core::client::mix_traffic::transceiver::*; +pub use nym_network_defaults::{ + ChainDetails, DenomDetails, DenomDetailsOwned, NymContracts, NymNetworkDetails, + ValidatorDetails, +}; +// we have to re-expose TaskClient since we're allowing custom shutdown in public API +// (which is quite a shame if you ask me...) +pub use nym_task::TaskClient; diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 215305f408..06e878fbbc 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -5,9 +5,12 @@ use super::{connection_state::BuilderState, Config, StoragePaths}; use crate::bandwidth::BandwidthAcquireClient; use crate::mixnet::socks5_client::Socks5MixnetClient; use crate::mixnet::{CredentialStorage, MixnetClient, Recipient}; +use crate::GatewayTransceiver; +use crate::NymNetworkDetails; use crate::{Error, Result}; use futures::channel::mpsc; use futures::StreamExt; +use log::warn; use nym_client_core::client::base_client::storage::gateway_details::GatewayDetailsStore; use nym_client_core::client::base_client::storage::{ Ephemeral, MixnetClientStorage, OnDiskPersistent, @@ -15,16 +18,18 @@ use nym_client_core::client::base_client::storage::{ use nym_client_core::client::base_client::BaseClient; use nym_client_core::client::key_manager::persistence::KeyStore; use nym_client_core::config::DebugConfig; -use nym_client_core::init::GatewaySetup; +use nym_client_core::init::helpers::current_gateways; +use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; use nym_client_core::{ client::{base_client::BaseClientBuilder, replies::reply_storage::ReplyStorageBackend}, config::GatewayEndpointConfig, }; -use nym_network_defaults::NymNetworkDetails; use nym_socks5_client_core::config::Socks5; use nym_task::manager::TaskStatus; +use nym_task::{TaskClient, TaskHandle}; use nym_topology::provider_trait::TopologyProvider; use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient}; +use rand::rngs::OsRng; use std::path::Path; use std::path::PathBuf; use url::Url; @@ -38,7 +43,11 @@ pub struct MixnetClientBuilder { storage_paths: Option, gateway_config: Option, socks5_config: Option, + + wait_for_gateway: bool, custom_topology_provider: Option>, + custom_gateway_transceiver: Option>, + custom_shutdown: Option, // TODO: incorporate it properly into `MixnetClientStorage` (I will need it in wasm anyway) gateway_endpoint_config_path: Option, @@ -69,11 +78,14 @@ impl MixnetClientBuilder { storage_paths: None, gateway_config: None, socks5_config: None, + wait_for_gateway: false, custom_topology_provider: None, storage: storage_paths .initialise_default_persistent_storage() .await?, gateway_endpoint_config_path: None, + custom_shutdown: None, + custom_gateway_transceiver: None, }) } } @@ -95,7 +107,10 @@ where storage_paths: None, gateway_config: None, socks5_config: None, + wait_for_gateway: false, custom_topology_provider: None, + custom_gateway_transceiver: None, + custom_shutdown: None, gateway_endpoint_config_path: None, storage, } @@ -109,7 +124,10 @@ where storage_paths: self.storage_paths, gateway_config: self.gateway_config, socks5_config: self.socks5_config, + wait_for_gateway: self.wait_for_gateway, custom_topology_provider: self.custom_topology_provider, + custom_gateway_transceiver: self.custom_gateway_transceiver, + custom_shutdown: self.custom_shutdown, gateway_endpoint_config_path: self.gateway_endpoint_config_path, storage, } @@ -169,6 +187,31 @@ where self } + /// Use an externally managed shutdown mechanism. + #[must_use] + pub fn custom_shutdown(mut self, shutdown: TaskClient) -> Self { + self.custom_shutdown = Some(shutdown); + self + } + + /// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded. + #[must_use] + pub fn with_wait_for_gateway(mut self, wait_for_gateway: bool) -> Self { + self.wait_for_gateway = wait_for_gateway; + self + } + + /// Use custom mixnet sender that might not be the default websocket gateway connection. + /// only for advanced use + #[must_use] + pub fn custom_gateway_transceiver( + mut self, + gateway_transceiver: Box, + ) -> Self { + self.custom_gateway_transceiver = Some(gateway_transceiver); + self + } + /// Use specified file for storing gateway configuration. pub fn gateway_endpoint_config_path>(mut self, path: P) -> Self { self.gateway_endpoint_config_path = Some(path.as_ref().to_owned()); @@ -176,14 +219,12 @@ where } /// Construct a [`DisconnectedMixnetClient`] from the setup specified. - pub async fn build(self) -> Result> { - let client = DisconnectedMixnetClient::new( - self.config, - self.socks5_config, - self.storage, - self.custom_topology_provider, - ) - .await?; + pub fn build(self) -> Result> { + let client = DisconnectedMixnetClient::new(self.config, self.socks5_config, self.storage)? + .custom_gateway_transceiver(self.custom_gateway_transceiver) + .custom_topology_provider(self.custom_topology_provider) + .custom_shutdown(self.custom_shutdown) + .wait_for_gateway(self.wait_for_gateway); Ok(client) } @@ -217,6 +258,15 @@ where /// Alternative provider of network topology used for constructing sphinx packets. custom_topology_provider: Option>, + + /// advanced usage of custom gateways + custom_gateway_transceiver: Option>, + + /// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded. + wait_for_gateway: bool, + + /// Allows passing an externally controlled shutdown handle. + custom_shutdown: Option, } impl DisconnectedMixnetClient @@ -235,11 +285,10 @@ where /// Callers have the option of supplying further parameters to: /// - store persistent identities at a location on-disk, if desired; /// - use SOCKS5 mode - async fn new( + fn new( config: Config, socks5_config: Option, storage: S, - custom_topology_provider: Option>, ) -> Result> { // don't create dkg client for the bandwidth controller if credentials are disabled let dkg_query_client = if config.enabled_credentials_mode { @@ -260,10 +309,43 @@ where state: BuilderState::New, dkg_query_client, storage, - custom_topology_provider, + custom_topology_provider: None, + custom_gateway_transceiver: None, + wait_for_gateway: false, + custom_shutdown: None, }) } + #[must_use] + pub fn custom_shutdown(mut self, shutdown: Option) -> Self { + self.custom_shutdown = shutdown; + self + } + + #[must_use] + pub fn custom_topology_provider( + mut self, + provider: Option>, + ) -> Self { + self.custom_topology_provider = provider; + self + } + + #[must_use] + pub fn custom_gateway_transceiver( + mut self, + gateway_transceiver: Option>, + ) -> Self { + self.custom_gateway_transceiver = gateway_transceiver; + self + } + + #[must_use] + pub fn wait_for_gateway(mut self, wait_for_gateway: bool) -> Self { + self.wait_for_gateway = wait_for_gateway; + self + } + fn get_api_endpoints(&self) -> Vec { self.config .network_details @@ -288,16 +370,34 @@ where /// key, however, is created during the gateway registration handshake so it might not /// necessarily be available. /// Furthermore, it has to be coupled with particular gateway's config. - async fn has_gateway_info(&self) -> bool { - let has_keys = self.storage.key_store().load_keys().await.is_ok(); - let has_gateway_details = self + async fn has_valid_gateway_info(&self) -> bool { + let keys = match self.storage.key_store().load_keys().await { + Ok(keys) => keys, + Err(err) => { + warn!("failed to load stored keys: {err}"); + return false; + } + }; + + let gateway_details = match self .storage .gateway_details_store() .load_gateway_details() .await - .is_ok(); + { + Ok(details) => details, + Err(err) => { + warn!("failed to load stored gateway details: {err}"); + return false; + } + }; - has_keys && has_gateway_details + if let Err(err) = gateway_details.validate(keys.gateway_shared_key().as_deref()) { + warn!("stored key verification failure: {err}"); + return false; + } + + true } /// Register with a gateway. If a gateway is provided in the config then that will try to be @@ -316,10 +416,19 @@ where let api_endpoints = self.get_api_endpoints(); - let gateway_setup = if self.has_gateway_info().await { + let gateway_setup = if self.has_valid_gateway_info().await { GatewaySetup::MustLoad } else { - GatewaySetup::new_fresh(self.config.user_chosen_gateway.clone(), None) + let selection_spec = + GatewaySelectionSpecification::new(self.config.user_chosen_gateway.clone(), None); + + let mut rng = OsRng; + + GatewaySetup::New { + specification: selection_spec, + available_gateways: current_gateways(&mut rng, &api_endpoints).await?, + overwrite_data: !self.config.key_mode.is_keep(), + } }; // this will perform necessary key and details load and optional store @@ -327,8 +436,6 @@ where gateway_setup, self.storage.key_store(), self.storage.gateway_details_store(), - !self.config.key_mode.is_keep(), - Some(&api_endpoints), ) .await?; @@ -367,24 +474,40 @@ where // a temporary workaround let base_config = self .config - .as_base_client_config(nyxd_endpoints, nym_api_endpoints); + .as_base_client_config(nyxd_endpoints, nym_api_endpoints.clone()); - let known_gateway = self.has_gateway_info().await; + let known_gateway = self.has_valid_gateway_info().await; let mut base_builder: BaseClientBuilder<_, _> = - BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client); + BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client) + .with_wait_for_gateway(self.wait_for_gateway); if !known_gateway { - base_builder = base_builder.with_gateway_setup(GatewaySetup::new_fresh( - self.config.user_chosen_gateway, - None, - )) + let selection_spec = + GatewaySelectionSpecification::new(self.config.user_chosen_gateway, None); + + let mut rng = OsRng; + let setup = GatewaySetup::New { + specification: selection_spec, + available_gateways: current_gateways(&mut rng, &nym_api_endpoints).await?, + overwrite_data: !self.config.key_mode.is_keep(), + }; + + base_builder = base_builder.with_gateway_setup(setup) } if let Some(topology_provider) = self.custom_topology_provider { base_builder = base_builder.with_topology_provider(topology_provider); } + if let Some(custom_shutdown) = self.custom_shutdown { + base_builder = base_builder.with_shutdown(custom_shutdown) + } + + if let Some(gateway_transceiver) = self.custom_gateway_transceiver { + base_builder = base_builder.with_gateway_transceiver(gateway_transceiver); + } + let started_client = base_builder.start_base().await?; self.state = BuilderState::Registered {}; let nym_address = started_client.address; @@ -412,7 +535,6 @@ where /// let client = mixnet::MixnetClientBuilder::new_ephemeral() /// .socks5_config(socks5_config) /// .build() - /// .await /// .unwrap(); /// let client = client.connect_to_mixnet_via_socks5().await.unwrap(); /// } @@ -438,29 +560,34 @@ where client_output, client_state.clone(), nym_address, - started_client.task_manager.subscribe(), + started_client.task_handle.get_handle(), packet_type, ); - started_client - .task_manager - .start_status_listener(socks5_status_tx) - .await; - match socks5_status_rx - .next() - .await - .ok_or(Error::Socks5NotStarted)? - .downcast_ref::() - .ok_or(Error::Socks5NotStarted)? - { - TaskStatus::Ready => { - log::debug!("Socks5 connected"); + + // TODO: more graceful handling here, surely both variants should work... I think? + if let TaskHandle::Internal(task_manager) = &mut started_client.task_handle { + task_manager.start_status_listener(socks5_status_tx).await; + match socks5_status_rx + .next() + .await + .ok_or(Error::Socks5NotStarted)? + .downcast_ref::() + .ok_or(Error::Socks5NotStarted)? + { + TaskStatus::Ready => { + log::debug!("Socks5 connected"); + } } + } else { + return Err(Error::new_unsupported( + "connecting with socks5 is currently unsupported with custom shutdown", + )); } Ok(Socks5MixnetClient { nym_address, client_state, - task_manager: started_client.task_manager, + task_handle: started_client.task_handle, socks5_config, }) } @@ -481,7 +608,6 @@ where /// async fn main() { /// let client = mixnet::MixnetClientBuilder::new_ephemeral() /// .build() - /// .await /// .unwrap(); /// let client = client.connect_to_mixnet().await.unwrap(); /// } @@ -503,7 +629,7 @@ where client_output, client_state, reconstructed_receiver, - started_client.task_manager, + started_client.task_handle, None, )) } diff --git a/sdk/rust/nym-sdk/src/mixnet/native_client.rs b/sdk/rust/nym-sdk/src/mixnet/native_client.rs index 3266b37506..519961d6fd 100644 --- a/sdk/rust/nym-sdk/src/mixnet/native_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/native_client.rs @@ -13,7 +13,7 @@ use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::{params::PacketType, receiver::ReconstructedMessage}; use nym_task::{ connections::{ConnectionCommandSender, LaneQueueLengths}, - TaskManager, + TaskHandle, }; use nym_topology::NymTopology; use std::pin::Pin; @@ -24,7 +24,7 @@ pub struct MixnetClient { /// The nym address of this connected client. pub(crate) nym_address: Recipient, - /// Input to the client from the users perspective. This can be either data to send or controll + /// Input to the client from the users perspective. This can be either data to send or control /// messages. pub(crate) client_input: ClientInput, @@ -40,8 +40,8 @@ pub struct MixnetClient { /// A channel for messages arriving from the mixnet after they have been reconstructed. pub(crate) reconstructed_receiver: ReconstructedMessagesReceiver, - /// The task manager that controlls all the spawned tasks that the clients uses to do it's job. - pub(crate) task_manager: TaskManager, + /// The task manager that controls all the spawned tasks that the clients uses to do it's job. + pub(crate) task_handle: TaskHandle, pub(crate) packet_type: Option, // internal state used for the `Stream` implementation @@ -55,7 +55,7 @@ impl MixnetClient { client_output: ClientOutput, client_state: ClientState, reconstructed_receiver: ReconstructedMessagesReceiver, - task_manager: TaskManager, + task_handle: TaskHandle, packet_type: Option, ) -> Self { Self { @@ -64,7 +64,7 @@ impl MixnetClient { client_output, client_state, reconstructed_receiver, - task_manager, + task_handle, packet_type, _buffered: Vec::new(), } @@ -86,8 +86,7 @@ impl MixnetClient { /// ``` pub async fn connect_new() -> Result { MixnetClientBuilder::new_ephemeral() - .build() - .await? + .build()? .connect_to_mixnet() .await } @@ -158,9 +157,14 @@ impl MixnetClient { /// Disconnect from the mixnet. Currently it is not supported to reconnect a disconnected /// client. - pub async fn disconnect(&mut self) { - self.task_manager.signal_shutdown().ok(); - self.task_manager.wait_for_shutdown().await; + pub async fn disconnect(mut self) { + if let TaskHandle::Internal(task_manager) = &mut self.task_handle { + task_manager.signal_shutdown().ok(); + task_manager.wait_for_shutdown().await; + } + + // note: it's important to take ownership of the struct as if the shutdown is `TaskHandle::External`, + // it must be dropped to finalize the shutdown } } diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index add4294f4e..776fd8b9b3 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -1,7 +1,7 @@ use nym_client_core::client::base_client::ClientState; use nym_socks5_client_core::config::Socks5; use nym_sphinx::addressing::clients::Recipient; -use nym_task::{connections::LaneQueueLengths, TaskManager}; +use nym_task::{connections::LaneQueueLengths, TaskHandle}; use nym_topology::NymTopology; @@ -17,8 +17,8 @@ pub struct Socks5MixnetClient { /// current message send queue length. pub(crate) client_state: ClientState, - /// The task manager that controlls all the spawned tasks that the clients uses to do it's job. - pub(crate) task_manager: TaskManager, + /// The task manager that controls all the spawned tasks that the clients uses to do it's job. + pub(crate) task_handle: TaskHandle, /// SOCKS5 configuration parameters. pub(crate) socks5_config: Socks5, @@ -43,8 +43,7 @@ impl Socks5MixnetClient { pub async fn connect_new>(provider_mix_address: S) -> Result { MixnetClientBuilder::new_ephemeral() .socks5_config(Socks5::new(provider_mix_address)) - .build() - .await? + .build()? .connect_to_mixnet_via_socks5() .await } @@ -87,8 +86,13 @@ impl Socks5MixnetClient { /// Disconnect from the mixnet. Currently it is not supported to reconnect a disconnected /// client. - pub async fn disconnect(&mut self) { - self.task_manager.signal_shutdown().ok(); - self.task_manager.wait_for_shutdown().await; + pub async fn disconnect(mut self) { + if let TaskHandle::Internal(task_manager) = &mut self.task_handle { + task_manager.signal_shutdown().ok(); + task_manager.wait_for_shutdown().await; + } + + // note: it's important to take ownership of the struct as if the shutdown is `TaskHandle::External`, + // it must be dropped to finalize the shutdown } } diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 88c1446563..28fe7a0212 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -10,6 +10,10 @@ rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "nym_network_requester" +path = "src/lib.rs" + [dependencies] async-trait = { workspace = true } bs58 = "0.4.0" diff --git a/service-providers/network-requester/src/allowed_hosts/filter.rs b/service-providers/network-requester/src/allowed_hosts/filter.rs index 77dbc6c94b..bb33bb931d 100644 --- a/service-providers/network-requester/src/allowed_hosts/filter.rs +++ b/service-providers/network-requester/src/allowed_hosts/filter.rs @@ -60,6 +60,14 @@ impl OutboundRequestFilter { } } + pub(crate) fn standard_list(&self) -> StandardList { + self.standard_list.clone() + } + + pub(crate) fn allowed_hosts(&self) -> StoredAllowedHosts { + self.allowed_hosts.clone() + } + async fn check_allowed_hosts(&self, host: &RequestHost) -> bool { let guard = self.allowed_hosts.get().await; self.check_group(&guard.data, host) diff --git a/service-providers/network-requester/src/cli/init.rs b/service-providers/network-requester/src/cli/init.rs index 24b887c039..76820d0d5e 100644 --- a/service-providers/network-requester/src/cli/init.rs +++ b/service-providers/network-requester/src/cli/init.rs @@ -13,7 +13,10 @@ use nym_bin_common::output_format::OutputFormat; use nym_client_core::client::base_client::storage::gateway_details::OnDiskGatewayDetails; use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::config::GatewayEndpointConfig; -use nym_client_core::init::GatewaySetup; +use nym_client_core::error::ClientCoreError; +use nym_client_core::init::helpers::current_gateways; +use nym_client_core::init::types::GatewaySetup; +use nym_client_core::init::types::{GatewayDetails, GatewaySelectionSpecification}; use nym_crypto::asymmetric::identity; use nym_sphinx::addressing::clients::Recipient; use serde::Serialize; @@ -91,14 +94,18 @@ impl From for OverrideConfig { #[derive(Debug, Serialize)] pub struct InitResults { #[serde(flatten)] - client_core: nym_client_core::init::InitResults, + client_core: nym_client_core::init::types::InitResults, client_address: String, } impl InitResults { fn new(config: &Config, address: &Recipient, gateway: &GatewayEndpointConfig) -> Self { Self { - client_core: nym_client_core::init::InitResults::new(&config.base, address, gateway), + client_core: nym_client_core::init::types::InitResults::new( + &config.base, + address, + gateway, + ), client_address: address.to_string(), } } @@ -150,7 +157,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> { // Attempt to use a user-provided gateway, if possible let user_chosen_gateway_id = args.gateway; - let gateway_setup = GatewaySetup::new_fresh( + let selection_spec = GatewaySelectionSpecification::new( user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(args.latency_based_selection), ); @@ -164,16 +171,22 @@ pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> { let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys.clone()); let details_store = OnDiskGatewayDetails::new(&config.storage_paths.common_paths.gateway_details); - let init_details = nym_client_core::init::setup_gateway( - gateway_setup, - &key_store, - &details_store, - register_gateway, - Some(&config.base.client.nym_api_urls), - ) - .await - .tap_err(|err| log::error!("Failed to setup gateway\nError: {err}"))? - .details; + + let available_gateways = { + let mut rng = rand::thread_rng(); + current_gateways(&mut rng, &config.base.client.nym_api_urls).await? + }; + + let gateway_setup = GatewaySetup::New { + specification: selection_spec, + available_gateways, + overwrite_data: register_gateway, + }; + + let init_details = + nym_client_core::init::setup_gateway(gateway_setup, &key_store, &details_store) + .await + .tap_err(|err| log::error!("Failed to setup gateway\nError: {err}"))?; let config_save_location = config.default_location(); config.save_to_default_location().tap_err(|_| { @@ -188,7 +201,10 @@ pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> { log::info!("Client configuration completed.\n"); - let init_results = InitResults::new(&config, &address, &init_details.gateway_details); + let GatewayDetails::Configured(gateway_details) = init_details.gateway_details else { + return Err(ClientCoreError::UnexpectedPersistedCustomGatewayDetails)?; + }; + let init_results = InitResults::new(&config, &address, &gateway_details); println!("{}", args.output.format(&init_results)); Ok(()) diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index 927980377e..5566de0e93 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -20,7 +20,6 @@ use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::config::GatewayEndpointConfig; use nym_client_core::error::ClientCoreError; use nym_config::OptionalSet; -use nym_sphinx::params::PacketSize; mod build_info; mod init; @@ -88,40 +87,42 @@ pub(crate) struct OverrideConfig { statistics_recipient: Option, } -pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { - // These flags have overlapping effects, meaning the order matters here. Making it a bit messy. - // Since a big chunk of these are hidden experimental flags there is hope we can remove them - // soonish and clean this up. +// NOTE: make sure this is in sync with `gateway/src/commands/helpers.rs::override_network_requester_config` +pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { + // as of 12.09.23 the below is true (not sure how this comment will rot in the future) + // medium_toggle: + // - sets secondary packet size to 16kb + // - disables poisson distribution of the main traffic stream + // - sets the cover traffic stream to 1 packet / 5s (on average) + // - disables per hop delay + // + // fastmode (to be renamed to `fast-poisson`): + // - sets average per hop delay to 10ms + // - sets the cover traffic stream to 1 packet / 2000s (on average); for all intents and purposes it disables the stream + // - sets the poisson distribution of the main traffic stream to 4ms, i.e. 250 packets / s on average + // + // no_cover: + // - disables poisson distribution of the main traffic stream + // - disables the secondary cover traffic stream - // This is the default - let disable_poisson_rate = config.network_requester.disable_poisson_rate; + // disable poisson rate in the BASE client if the NR option is enabled + if config.network_requester.disable_poisson_rate { + config.set_no_poisson_process(); + } - // This is with the medium toggle - let disable_cover_traffic_with_keepalive = args.medium_toggle; - let secondary_packet_size = args.medium_toggle.then_some(PacketSize::ExtendedPacket16); - let no_per_hop_delays = args.medium_toggle; + // those should be enforced by `clap` when parsing the arguments + if args.medium_toggle { + assert!(!args.fastmode); + assert!(!args.no_cover); + + config.set_medium_toggle(); + } config .with_base( BaseClientConfig::with_high_default_traffic_volume, args.fastmode, ) - .with_base( - // NOTE: This interacts with disabling cover traffic fully, so we want to this to be set before - BaseClientConfig::with_disabled_poisson_process, - disable_poisson_rate, - ) - .with_base( - // NOTE: This interacts with disabling cover traffic fully, so we want to this to be set before - BaseClientConfig::with_disabled_cover_traffic_with_keepalive, - disable_cover_traffic_with_keepalive, - ) - .with_base( - BaseClientConfig::with_secondary_packet_size, - secondary_packet_size, - ) - .with_base(BaseClientConfig::with_no_per_hop_delays, no_per_hop_delays) - // NOTE: see comment above about the order of the other disble cover traffic config .with_base(BaseClientConfig::with_disabled_cover_traffic, args.no_cover) .with_optional_base_custom_env( BaseClientConfig::with_custom_nym_apis, @@ -170,7 +171,7 @@ fn persist_gateway_details( source: Box::new(source), }) })?; - let persisted_details = PersistedGatewayDetails::new(details, &shared_keys); + let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; details_store .store_to_disk(&persisted_details) .map_err(|source| { diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index a2ca949675..577e14f8e0 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -38,16 +38,21 @@ pub(crate) struct Run { /// Mostly debug-related option to increase default traffic rate so that you would not need to /// modify config post init - #[arg(long, hide = true)] + #[arg(long, hide = true, conflicts_with = "medium_toggle")] fastmode: bool, /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) - #[arg(long, hide = true)] + #[arg(long, hide = true, conflicts_with = "medium_toggle")] no_cover: bool, /// Enable medium mixnet traffic, for experiments only. /// This includes things like disabling cover traffic, no per hop delays, etc. - #[arg(long, hide = true)] + #[arg( + long, + hide = true, + conflicts_with = "no_cover", + conflicts_with = "fastmode" + )] medium_toggle: bool, } @@ -94,6 +99,6 @@ pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { } log::info!("Starting socks5 service provider"); - let server = crate::core::NRServiceProviderBuilder::new(config).await; + let server = crate::core::NRServiceProviderBuilder::new(config); server.run_service_provider().await } diff --git a/service-providers/network-requester/src/config/mod.rs b/service-providers/network-requester/src/config/mod.rs index 29b373ed9f..b26ec1ff20 100644 --- a/service-providers/network-requester/src/config/mod.rs +++ b/service-providers/network-requester/src/config/mod.rs @@ -17,6 +17,7 @@ use std::time::Duration; pub use nym_client_core::config::Config as BaseClientConfig; pub use nym_client_core::config::{DebugConfig, GatewayEndpointConfig}; +use nym_sphinx::params::PacketSize; pub mod old_config_v1_1_13; pub mod old_config_v1_1_20; @@ -56,7 +57,7 @@ pub fn default_data_directory>(id: P) -> PathBuf { .join(DEFAULT_DATA_DIR) } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { #[serde(flatten)] @@ -74,7 +75,7 @@ pub struct Config { } impl NymConfigTemplate for Config { - fn template() -> &'static str { + fn template(&self) -> &'static str { CONFIG_TEMPLATE } } @@ -84,12 +85,20 @@ impl Config { Config { base: BaseClientConfig::new(id.as_ref(), env!("CARGO_PKG_VERSION")), network_requester: Default::default(), - storage_paths: NetworkRequesterPaths::new_default(default_data_directory(id.as_ref())), + storage_paths: NetworkRequesterPaths::new_base(default_data_directory(id.as_ref())), network_requester_debug: Default::default(), logging: Default::default(), } } + // this is a false positive, this method is actually called when used as a library + // but clippy complains about it when building the binary + #[allow(unused)] + pub fn with_data_directory>(mut self, data_directory: P) -> Self { + self.storage_paths = NetworkRequesterPaths::new_base(data_directory); + self + } + pub fn read_from_toml_file>(path: P) -> io::Result { read_config_from_toml_file(path) } @@ -112,6 +121,20 @@ impl Config { self.base.validate() } + /// Enable medium mixnet traffic, for experiments only. + /// This includes things like disabling cover traffic, no per hop delays, etc. + #[doc(hidden)] + pub fn set_medium_toggle(&mut self) { + self.base.set_no_cover_traffic_with_keepalive(); + self.base.set_no_per_hop_delays(); + self.base.debug.traffic.secondary_packet_size = Some(PacketSize::ExtendedPacket16); + } + + #[doc(hidden)] + pub fn set_no_poisson_process(&mut self) { + self.base.set_no_poisson_process() + } + #[must_use] pub fn with_open_proxy(mut self, open_proxy: bool) -> Self { self.network_requester.open_proxy = open_proxy; diff --git a/service-providers/network-requester/src/config/persistence.rs b/service-providers/network-requester/src/config/persistence.rs index a8aa09f18e..563116933d 100644 --- a/service-providers/network-requester/src/config/persistence.rs +++ b/service-providers/network-requester/src/config/persistence.rs @@ -27,11 +27,11 @@ pub struct NetworkRequesterPaths { } impl NetworkRequesterPaths { - pub fn new_default>(base_data_directory: P) -> Self { + pub fn new_base>(base_data_directory: P) -> Self { let base_dir = base_data_directory.as_ref(); NetworkRequesterPaths { - common_paths: CommonClientPaths::new_default(base_dir), + common_paths: CommonClientPaths::new_base(base_dir), allowed_list_location: base_dir.join(DEFAULT_ALLOWED_LIST_FILENAME), unknown_list_location: base_dir.join(DEFAULT_UNKNOWN_LIST_FILENAME), nr_description: base_dir.join(DEFAULT_DESCRIPTION_FILENAME), diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 8d743e3c03..0bad4c36e7 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -1,7 +1,6 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::allowed_hosts; use crate::allowed_hosts::standard_list::StandardListUpdater; use crate::allowed_hosts::stored_allowed_hosts::{start_allowed_list_reloader, StoredAllowedHosts}; use crate::allowed_hosts::{OutboundRequestFilter, StandardList}; @@ -9,14 +8,17 @@ use crate::config::{BaseClientConfig, Config}; use crate::error::NetworkRequesterError; use crate::reply::MixnetMessage; use crate::statistics::ServiceStatisticsCollector; -use crate::{reply, socks5}; +use crate::{allowed_hosts, reply, socks5}; use async_trait::async_trait; -use futures::channel::mpsc; -use log::warn; +use futures::channel::{mpsc, oneshot}; +use futures::stream::StreamExt; +use log::{debug, warn}; use nym_bin_common::bin_info_owned; +use nym_client_core::client::mix_traffic::transceiver::GatewayTransceiver; use nym_client_core::config::disk_persistence::CommonClientPaths; +use nym_client_core::HardcodedTopologyProvider; use nym_network_defaults::NymNetworkDetails; -use nym_sdk::mixnet::MixnetMessageSender; +use nym_sdk::mixnet::{MixnetMessageSender, TopologyProvider}; use nym_service_providers_common::interface::{ BinaryInformation, ProviderInterfaceVersion, Request, RequestVersion, }; @@ -33,9 +35,12 @@ use nym_socks5_requests::{ use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::params::PacketSize; +use nym_sphinx::receiver::ReconstructedMessage; use nym_statistics_common::collector::StatisticsSender; use nym_task::connections::LaneQueueLengths; -use nym_task::{TaskClient, TaskManager}; +use nym_task::manager::TaskHandle; +use nym_task::TaskClient; +use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; // Since it's an atomic, it's safe to be kept static and shared across threads @@ -48,25 +53,40 @@ pub(crate) fn new_legacy_request_version() -> RequestVersion { } } +pub struct OnStartData { + // to add more fields as required + pub address: Recipient, +} + +impl OnStartData { + fn new(address: Recipient) -> Self { + Self { address } + } +} + // TODO: 'Builder' is not the most appropriate name here, but I needed // ... something ... pub struct NRServiceProviderBuilder { config: Config, outbound_request_filter: OutboundRequestFilter, - standard_list: StandardList, - allowed_hosts: StoredAllowedHosts, + + wait_for_gateway: bool, + custom_topology_provider: Option>, + custom_gateway_transceiver: Option>, + shutdown: Option, + on_start: Option>, } -struct NRServiceProvider { +pub struct NRServiceProvider { config: Config, - outbound_request_filter: OutboundRequestFilter, - mixnet_client: nym_sdk::mixnet::MixnetClient, + mixnet_client: nym_sdk::mixnet::MixnetClient, controller_sender: ControllerSender, + mix_input_sender: MixProxySender, stats_collector: Option, - shutdown: TaskManager, + shutdown: TaskHandle, } #[async_trait] @@ -78,6 +98,7 @@ impl ServiceProvider for NRServiceProvider { sender: Option, request: Request, ) -> Result<(), Self::ServiceProviderError> { + // TODO: this should perhaps be parallelised log::debug!("on_request {:?}", request); if let Some(response) = self.handle_request(sender, request).await? { // TODO: this (i.e. `reply::MixnetAddress`) should be incorporated into the actual interface @@ -157,7 +178,7 @@ impl ServiceProvider for NRServiceProvider { } impl NRServiceProviderBuilder { - pub async fn new(config: Config) -> NRServiceProviderBuilder { + pub fn new(config: Config) -> NRServiceProviderBuilder { let standard_list = StandardList::new(); let allowed_hosts = StoredAllowedHosts::new(&config.storage_paths.allowed_list_location); @@ -170,11 +191,77 @@ impl NRServiceProviderBuilder { NRServiceProviderBuilder { config, outbound_request_filter, - standard_list, - allowed_hosts, + wait_for_gateway: false, + custom_topology_provider: None, + custom_gateway_transceiver: None, + shutdown: None, + on_start: None, } } + #[must_use] + // this is a false positive, this method is actually called when used as a library + // but clippy complains about it when building the binary + #[allow(unused)] + pub fn with_shutdown(mut self, shutdown: TaskClient) -> Self { + self.shutdown = Some(shutdown); + self + } + + #[must_use] + // this is a false positive, this method is actually called when used as a library + // but clippy complains about it when building the binary + #[allow(unused)] + pub fn with_custom_gateway_transceiver( + mut self, + gateway_transceiver: Box, + ) -> Self { + self.custom_gateway_transceiver = Some(gateway_transceiver); + self + } + + #[must_use] + // this is a false positive, this method is actually called when used as a library + // but clippy complains about it when building the binary + #[allow(unused)] + pub fn with_wait_for_gateway(mut self, wait_for_gateway: bool) -> Self { + self.wait_for_gateway = wait_for_gateway; + self + } + + #[must_use] + // this is a false positive, this method is actually called when used as a library + // but clippy complains about it when building the binary + #[allow(unused)] + pub fn with_on_start(mut self, on_start: oneshot::Sender) -> Self { + self.on_start = Some(on_start); + self + } + + #[must_use] + // this is a false positive, this method is actually called when used as a library + // but clippy complains about it when building the binary + #[allow(unused)] + pub fn with_custom_topology_provider( + mut self, + topology_provider: Box, + ) -> Self { + self.custom_topology_provider = Some(topology_provider); + self + } + + // this is a false positive, this method is actually called when used as a library + // but clippy complains about it when building the binary + #[allow(unused)] + pub fn with_stored_topology>( + mut self, + file: P, + ) -> Result { + self.custom_topology_provider = + Some(Box::new(HardcodedTopologyProvider::new_from_file(file)?)); + Ok(self) + } + /// Start all subsystems pub async fn run_service_provider(self) -> Result<(), NetworkRequesterError> { let stats_provider_addr = self @@ -186,22 +273,30 @@ impl NRServiceProviderBuilder { .transpose() .unwrap_or(None); + // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). + let shutdown: TaskHandle = self.shutdown.map(Into::into).unwrap_or_default(); + // Connect to the mixnet - let mixnet_client = - create_mixnet_client(&self.config.base, &self.config.storage_paths.common_paths) - .await?; + let mixnet_client = create_mixnet_client( + &self.config.base, + shutdown.get_handle().named("nym_sdk::MixnetClient"), + self.custom_gateway_transceiver, + self.custom_topology_provider, + self.wait_for_gateway, + &self.config.storage_paths.common_paths, + ) + .await?; // channels responsible for managing messages that are to be sent to the mix network. The receiver is // going to be used by `mixnet_response_listener` let (mix_input_sender, mix_input_receiver) = tokio::sync::mpsc::channel::(1); - // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). - let shutdown = nym_task::TaskManager::default(); - // Controller for managing all active connections. let (mut active_connections_controller, controller_sender) = Controller::new( mixnet_client.connection_command_sender(), - shutdown.subscribe(), + shutdown + .get_handle() + .named("nym_socks5_proxy_helpers::connection_controller::Controller"), ); tokio::spawn(async move { @@ -242,15 +337,19 @@ impl NRServiceProviderBuilder { self.config .network_requester_debug .standard_list_update_interval, - self.standard_list, - shutdown.subscribe(), + self.outbound_request_filter.standard_list(), + shutdown.get_handle().named("StandardListUpdater"), ) .start(); // start the allowed.list watcher and updater - start_allowed_list_reloader(self.allowed_hosts, shutdown.subscribe()).await; + start_allowed_list_reloader( + self.outbound_request_filter.allowed_hosts(), + shutdown.get_handle().named("stored_allowed_hosts_reloader"), + ) + .await; - let service_provider = NRServiceProvider { + let mut service_provider = NRServiceProvider { config: self.config, outbound_request_filter: self.outbound_request_filter, mixnet_client, @@ -260,40 +359,61 @@ impl NRServiceProviderBuilder { shutdown, }; - log::info!("The address of this client is: {}", self_address); + log::info!("The address of this client is: {self_address}"); log::info!("All systems go. Press CTRL-C to stop the server."); + + if let Some(on_start) = self.on_start { + if on_start.send(OnStartData::new(self_address)).is_err() { + // the parent has dropped the channel before receiving the response + return Err(NetworkRequesterError::DisconnectedParent); + } + } + service_provider.run().await } } impl NRServiceProvider { - async fn run(mut self) -> Result<(), NetworkRequesterError> { - // TODO: incorporate graceful shutdowns - while let Some(reconstructed_messages) = self.mixnet_client.wait_for_messages().await { - for reconstructed in reconstructed_messages { - let sender = reconstructed.sender_tag; - let request = match Socks5ProviderRequest::try_from_bytes(&reconstructed.message) { - Ok(req) => req, - Err(err) => { - // TODO: or should it even be further lowered to debug/trace? - log::warn!("Failed to deserialize received message: {err}"); - continue; + async fn run(&mut self) -> Result<(), NetworkRequesterError> { + let mut shutdown = self.shutdown.fork("main_loop"); + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + debug!("NRServiceProvider [main loop]: received shutdown") + }, + msg = self.mixnet_client.next() => match msg { + Some(msg) => self.on_message(msg).await, + None => { + log::trace!("NRServiceProvider::run: Stopping since channel closed"); + break; } - }; - - if let Err(err) = self.on_request(sender, request).await { - // TODO: again, should it be a warning? - // we should also probably log some information regarding the origin of the request - // so that it would be easier to debug it - log::warn!("failed to resolve the received request: {err}"); - } + }, } } - log::error!("Network requester exited unexpectedly"); Ok(()) } + async fn on_message(&mut self, reconstructed: ReconstructedMessage) { + let sender = reconstructed.sender_tag; + let request = match Socks5ProviderRequest::try_from_bytes(&reconstructed.message) { + Ok(req) => req, + Err(err) => { + // TODO: or should it even be further lowered to debug/trace? + log::warn!("Failed to deserialize received message: {err}"); + return; + } + }; + + if let Err(err) = self.on_request(sender, request).await { + // TODO: again, should it be a warning? + // we should also probably log some information regarding the origin of the request + // so that it would be easier to debug it + log::warn!("failed to resolve the received request: {err}"); + } + } + /// Listens for any messages from `mix_reader` that should be written back to the mix network /// via the `websocket_writer`. async fn mixnet_response_listener( @@ -352,11 +472,7 @@ impl NRServiceProvider { { Ok(conn) => conn, Err(err) => { - log::error!( - "error while connecting to {:?} ! - {:?}", - remote_addr.clone(), - err - ); + log::error!("error while connecting to {remote_addr}: {err}",); // inform the remote that the connection is closed before it even was established let mixnet_message = MixnetMessage::new_network_data_response( @@ -386,8 +502,7 @@ impl NRServiceProvider { let old_count = ACTIVE_PROXIES.fetch_add(1, Ordering::SeqCst); log::info!( - "Starting proxy for {} (currently there are {} proxies being handled)", - remote_addr, + "Starting proxy for {remote_addr} (currently there are {} proxies being handled)", old_count + 1 ); @@ -409,8 +524,7 @@ impl NRServiceProvider { let old_count = ACTIVE_PROXIES.fetch_sub(1, Ordering::SeqCst); log::info!( - "Proxy for {} is finished (currently there are {} proxies being handled)", - remote_addr, + "Proxy for {remote_addr} is finished (currently there are {} proxies being handled)", old_count - 1 ); } @@ -436,7 +550,7 @@ impl NRServiceProvider { let open_proxy = self.config.network_requester.open_proxy; if !open_proxy && !self.outbound_request_filter.check(&remote_addr).await { let log_msg = format!("Domain {remote_addr:?} failed filter check"); - log::info!("{}", log_msg); + log::info!("{log_msg}"); let msg = MixnetMessage::new_connection_error( return_address, remote_version, @@ -458,7 +572,7 @@ impl NRServiceProvider { let controller_sender_clone = self.controller_sender.clone(); let mix_input_sender_clone = self.mix_input_sender.clone(); let lane_queue_lengths_clone = self.mixnet_client.shared_lane_queue_lengths(); - let shutdown = self.shutdown.subscribe(); + let shutdown = self.shutdown.get_handle(); // and start the proxy for this connection tokio::spawn(async move { @@ -506,8 +620,13 @@ impl NRServiceProvider { // Helper function to create the mixnet client. // This is NOT in the SDK since we don't want to expose any of the client-core config types. // We could however consider moving it to a crate in common in the future. +// TODO: refactor this function and its arguments async fn create_mixnet_client( config: &BaseClientConfig, + shutdown: TaskClient, + custom_transceiver: Option>, + custom_topology_provider: Option>, + wait_for_gateway: bool, paths: &CommonClientPaths, ) -> Result { let debug_config = config.debug; @@ -519,14 +638,21 @@ async fn create_mixnet_client( .await .map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { source: err })? .network_details(NymNetworkDetails::new_from_env()) - .debug_config(debug_config); + .debug_config(debug_config) + .custom_shutdown(shutdown) + .with_wait_for_gateway(wait_for_gateway); if !config.get_disabled_credentials_mode() { client_builder = client_builder.enable_credentials_mode(); } + if let Some(gateway_transceiver) = custom_transceiver { + client_builder = client_builder.custom_gateway_transceiver(gateway_transceiver); + } + if let Some(topology_provider) = custom_topology_provider { + client_builder = client_builder.custom_topology_provider(topology_provider); + } let mixnet_client = client_builder .build() - .await .map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { source: err })?; mixnet_client diff --git a/service-providers/network-requester/src/error.rs b/service-providers/network-requester/src/error.rs index 9248273d4b..cb71cd715a 100644 --- a/service-providers/network-requester/src/error.rs +++ b/service-providers/network-requester/src/error.rs @@ -1,4 +1,4 @@ -use nym_client_core::error::ClientCoreError; +pub use nym_client_core::error::ClientCoreError; use nym_socks5_requests::Socks5RequestError; #[derive(thiserror::Error, Debug)] @@ -30,4 +30,7 @@ pub enum NetworkRequesterError { #[error("failed to connect to mixnet: {source}")] FailedToConnectToMixnet { source: nym_sdk::Error }, + + #[error("the entity wrapping the network requester has disconnected")] + DisconnectedParent, } diff --git a/service-providers/network-requester/src/lib.rs b/service-providers/network-requester/src/lib.rs new file mode 100644 index 0000000000..cc3915c481 --- /dev/null +++ b/service-providers/network-requester/src/lib.rs @@ -0,0 +1,27 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod allowed_hosts; +pub mod config; +pub mod core; +pub mod error; +mod reply; +mod socks5; +mod statistics; + +pub use crate::core::{NRServiceProvider, NRServiceProviderBuilder}; +pub use config::Config; +pub use nym_client_core::{ + client::{ + base_client::storage::{gateway_details::OnDiskGatewayDetails, OnDiskPersistent}, + key_manager::persistence::OnDiskKeys, + mix_traffic::transceiver::*, + }, + init::{ + setup_gateway, + types::{ + CustomGatewayDetails, GatewayDetails, GatewaySelectionSpecification, GatewaySetup, + InitResults, InitialisationResult, + }, + }, +}; diff --git a/service-providers/network-requester/src/main.rs b/service-providers/network-requester/src/main.rs index 89ac717d37..76e9349a7e 100644 --- a/service-providers/network-requester/src/main.rs +++ b/service-providers/network-requester/src/main.rs @@ -2,11 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use clap::{crate_name, crate_version, Parser}; +use error::NetworkRequesterError; use nym_bin_common::logging::{maybe_print_banner, setup_logging}; use nym_network_defaults::setup_env; -use error::NetworkRequesterError; - mod allowed_hosts; mod cli; mod config; diff --git a/tools/nym-nr-query/src/main.rs b/tools/nym-nr-query/src/main.rs index 2de1c08468..362e1312ff 100644 --- a/tools/nym-nr-query/src/main.rs +++ b/tools/nym-nr-query/src/main.rs @@ -1,5 +1,3 @@ -use std::fmt; - use clap::{Parser, ValueEnum}; use nym_bin_common::output_format::OutputFormat; use nym_sdk::mixnet::{self, IncludedSurbs, MixnetMessageSender}; @@ -10,6 +8,7 @@ use nym_socks5_requests::{ QueryRequest, QueryResponse, Socks5ProtocolVersion, Socks5Request, Socks5Response, }; use serde::Serialize; +use std::fmt; use tokio::time::{timeout, Duration}; const RESPONSE_TIMEOUT: Duration = Duration::from_secs(10); @@ -109,7 +108,6 @@ async fn connect_to_mixnet(gateway: Option) -> mixnet::Mix Some(gateway) => mixnet::MixnetClientBuilder::new_ephemeral() .request_gateway(gateway.to_base58_string()) .build() - .await .expect("Failed to create mixnet client") .connect_to_mixnet() .await diff --git a/wasm/client/src/client.rs b/wasm/client/src/client.rs index 6488903e96..7649571a04 100644 --- a/wasm/client/src/client.rs +++ b/wasm/client/src/client.rs @@ -19,7 +19,7 @@ use wasm_client_core::config::r#override::DebugWasmOverride; use wasm_client_core::helpers::{ parse_recipient, parse_sender_tag, setup_from_topology, setup_gateway_from_api, }; -use wasm_client_core::init::GatewaySetup; +use wasm_client_core::init::types::GatewaySetup; use wasm_client_core::nym_task::connections::TransmissionLane; use wasm_client_core::nym_task::TaskManager; use wasm_client_core::storage::core_client_traits::FullWasmClientStorage; @@ -166,11 +166,9 @@ impl NymClientBuilder { if let Some(topology_provider) = maybe_topology_provider { base_builder = base_builder.with_topology_provider(topology_provider); } - if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client { - base_builder = base_builder.with_gateway_setup(GatewaySetup::ReuseConnection { - authenticated_ephemeral_client, - details: init_res.details, - }); + + if let Ok(reuse_setup) = GatewaySetup::try_reuse_connection(init_res) { + base_builder = base_builder.with_gateway_setup(reuse_setup); } let mut started_client = base_builder.start_base().await?; @@ -186,7 +184,8 @@ impl NymClientBuilder { client_input: Arc::new(client_input), client_state: Arc::new(started_client.client_state), _full_topology: None, - _task_manager: started_client.task_manager, + // this cannot failed as we haven't passed an external task manager + _task_manager: started_client.task_handle.try_into_task_manager().unwrap(), packet_type, }) } diff --git a/wasm/mix-fetch/src/client.rs b/wasm/mix-fetch/src/client.rs index 27f871feda..42ad946758 100644 --- a/wasm/mix-fetch/src/client.rs +++ b/wasm/mix-fetch/src/client.rs @@ -15,7 +15,7 @@ use wasm_bindgen_futures::future_to_promise; use wasm_client_core::client::base_client::{BaseClientBuilder, ClientInput, ClientOutput}; use wasm_client_core::client::inbound_messages::InputMessage; use wasm_client_core::helpers::setup_gateway_from_api; -use wasm_client_core::init::GatewaySetup; +use wasm_client_core::init::types::GatewaySetup; use wasm_client_core::nym_task::connections::TransmissionLane; use wasm_client_core::nym_task::TaskManager; use wasm_client_core::storage::core_client_traits::FullWasmClientStorage; @@ -100,12 +100,11 @@ impl MixFetchClientBuilder { storage, None, ); - if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client { - base_builder = base_builder.with_gateway_setup(GatewaySetup::ReuseConnection { - authenticated_ephemeral_client, - details: init_res.details, - }); + + if let Ok(reuse_setup) = GatewaySetup::try_reuse_connection(init_res) { + base_builder = base_builder.with_gateway_setup(reuse_setup); } + let mut started_client = base_builder.start_base().await?; let self_address = started_client.address; @@ -122,7 +121,8 @@ impl MixFetchClientBuilder { self_address, client_input, requests: active_requests, - _task_manager: started_client.task_manager, + // this cannot failed as we haven't passed an external task manager + _task_manager: started_client.task_handle.try_into_task_manager().unwrap(), }) } } diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index b7fb1aa714..5e7cc5b1f9 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -20,15 +20,16 @@ use tokio::sync::Mutex as AsyncMutex; use tsify::Tsify; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; +use wasm_client_core::client::mix_traffic::transceiver::PacketRouter; use wasm_client_core::helpers::{ current_network_topology_async, setup_from_topology, EphemeralCredentialStorage, }; +use wasm_client_core::init::types::GatewayDetails; use wasm_client_core::storage::ClientStorage; use wasm_client_core::topology::SerializableNymTopology; use wasm_client_core::{ - nym_task, BandwidthController, GatewayClient, IdentityKey, InitialisationDetails, - InitialisationResult, ManagedKeys, NodeIdentity, NymTopology, QueryReqwestRpcNyxdClient, - Recipient, + nym_task, BandwidthController, GatewayClient, GatewayConfig, IdentityKey, InitialisationResult, + ManagedKeys, NodeIdentity, NymTopology, QueryReqwestRpcNyxdClient, Recipient, }; use wasm_utils::check_promise_result; use wasm_utils::error::PromisableResult; @@ -136,8 +137,8 @@ impl NymNodeTesterBuilder { &self, client_store: &ClientStorage, ) -> Result { - if let Ok(loaded) = InitialisationDetails::try_load(client_store, client_store).await { - Ok(loaded.into()) + if let Ok(loaded) = InitialisationResult::try_load(client_store, client_store).await { + Ok(loaded) } else { Ok( setup_from_topology(self.gateway.clone(), &self.base_topology, client_store) @@ -157,38 +158,46 @@ impl NymNodeTesterBuilder { let client_store = ClientStorage::new_async(&storage_id, None).await?; let initialisation_result = self.gateway_info(&client_store).await?; - let init_details = initialisation_result.details; - let managed_keys = init_details.managed_keys; - let gateway_endpoint = init_details.gateway_details; - let gateway_identity = gateway_endpoint.try_get_gateway_identity_key()?; + let GatewayDetails::Configured(gateway_endpoint) = initialisation_result.gateway_details + else { + // don't bother supporting it + panic!("unsupported custom gateway configuration in wasm node tester") + }; + + let managed_keys = initialisation_result.managed_keys; let (mixnet_message_sender, mixnet_message_receiver) = mpsc::unbounded(); let (ack_sender, ack_receiver) = mpsc::unbounded(); + let gateway_task = task_manager.subscribe().named("gateway_client"); + let packet_router = PacketRouter::new( + ack_sender, + mixnet_message_sender, + gateway_task.fork("packet_router"), + ); + + let gateway_config: GatewayConfig = gateway_endpoint.try_into()?; + let gateway_identity = gateway_config.gateway_identity; + let mut gateway_client = if let Some(existing_client) = initialisation_result.authenticated_ephemeral_client { existing_client.upgrade( - mixnet_message_sender, - ack_sender, - Duration::from_secs(10), + packet_router, self.bandwidth_controller.take(), - task_manager.subscribe(), + gateway_task, ) } else { GatewayClient::new( - gateway_endpoint.gateway_listener, + gateway_config, managed_keys.identity_keypair(), - gateway_identity, Some(managed_keys.must_get_gateway_shared_key()), - mixnet_message_sender, - ack_sender, - Duration::from_secs(10), + packet_router, self.bandwidth_controller.take(), - task_manager.subscribe(), + gateway_task, ) - }; + } + .with_disabled_credentials_mode(true); - gateway_client.set_disabled_credentials_mode(true); gateway_client.authenticate_and_start().await?; // TODO: make those values configurable later