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
This commit is contained in:
Jędrzej Stuczyński
2023-09-20 15:47:05 +01:00
committed by GitHub
parent 6dc8148372
commit 114d92f93f
130 changed files with 4219 additions and 1620 deletions
Generated
+7 -15
View File
@@ -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",
+1
View File
@@ -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"
+1 -1
View File
@@ -67,7 +67,7 @@ pub struct Config {
}
impl NymConfigTemplate for Config {
fn template() -> &'static str {
fn template(&self) -> &'static str {
CONFIG_TEMPLATE
}
}
@@ -14,7 +14,7 @@ pub struct ClientPaths {
impl ClientPaths {
pub fn new_default<P: AsRef<Path>>(base_data_directory: P) -> Self {
ClientPaths {
common_paths: CommonClientPaths::new_default(base_data_directory),
common_paths: CommonClientPaths::new_base(base_data_directory),
}
}
}
+5 -142
View File
@@ -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<dyn Error + Send + Sync>> {
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<TaskManager, ClientError> {
pub async fn start_socket(self) -> Result<TaskHandle, ClientError> {
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<DirectClient, ClientError> {
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<u8>) {
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<u8>,
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<u8>) {
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<Item=ReconstructedMessage>`
pub async fn wait_for_messages(&mut self) -> Vec<ReconstructedMessage> {
use futures::StreamExt;
self.reconstructed_receiver
.next()
.await
.expect("buffer controller seems to have somehow died!")
Ok(started_client.task_handle)
}
}
+25 -17
View File
@@ -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<Init> 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(())
+1 -1
View File
@@ -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| {
+20 -16
View File
@@ -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<Init> 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(())
+1 -1
View File
@@ -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| {
+1 -1
View File
@@ -62,7 +62,7 @@ pub struct Config {
}
impl NymConfigTemplate for Config {
fn template() -> &'static str {
fn template(&self) -> &'static str {
CONFIG_TEMPLATE
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ pub struct SocksClientPaths {
impl SocksClientPaths {
pub fn new_default<P: AsRef<Path>>(base_data_directory: P) -> Self {
SocksClientPaths {
common_paths: CommonClientPaths::new_default(base_data_directory),
common_paths: CommonClientPaths::new_base(base_data_directory),
}
}
}
@@ -32,4 +32,14 @@ impl OutputFormat {
OutputFormat::Json => serde_json::to_string(data).unwrap(),
}
}
#[cfg(feature = "output_format")]
pub fn to_stdout<T: serde::Serialize + ToString>(&self, data: &T) {
println!("{}", self.format(data))
}
#[cfg(feature = "output_format")]
pub fn to_stderr<T: serde::Serialize + ToString>(&self, data: &T) {
eprintln!("{}", self.format(data))
}
}
+1 -2
View File
@@ -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" }
+166 -85
View File
@@ -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<C>,
wait_for_gateway: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
shutdown: Option<TaskClient>,
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<dyn GatewayTransceiver + Send>) -> 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<P: AsRef<Path>>(
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<BandwidthController<C, S::CredentialStore>>,
mixnet_message_sender: MixnetMessageSender,
ack_sender: AcknowledgementSender,
packet_router: PacketRouter,
shutdown: TaskClient,
) -> Result<GatewayClient<C, S::CredentialStore>, ClientCoreError>
where
<S::KeyStore as KeyStore>::StorageError: Send + Sync + 'static,
<S::CredentialStore as CredentialStorage>::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<Box<dyn GatewayTransceiver + Send>>,
config: &Config,
initialisation_result: InitialisationResult,
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
packet_router: PacketRouter,
mut shutdown: TaskClient,
) -> Result<Box<dyn GatewayTransceiver + Send>, ClientCoreError>
where
<S::KeyStore as KeyStore>::StorageError: Send + Sync + 'static,
<S::CredentialStore as CredentialStorage>::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<Box<dyn TopologyProvider + Send + Sync>>,
provider_from_config: config::TopologyStructure,
@@ -387,6 +441,8 @@ where
topology_provider: Box<dyn TopologyProvider + Send + Sync>,
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<C, S::CredentialStore>,
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
shutdown: TaskClient,
) -> BatchMixMessageSender
where
<S::CredentialStore as CredentialStorage>::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<InitialisationResult, ClientCoreError>
where
<S::KeyStore as KeyStore>::StorageError: Sync + Send,
<S::GatewayDetailsStore as GatewayDetailsStore>::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<BaseClient, ClientCoreError>
@@ -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::<TaskHandle>::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,
}
@@ -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<T = EmptyCustomDetails> {
type StorageError: Error;
async fn load_gateway_details(&self) -> Result<PersistedGatewayDetails, Self::StorageError>;
async fn load_gateway_details(&self) -> Result<PersistedGatewayDetails<T>, Self::StorageError>
where
T: DeserializeOwned + Send + Sync;
async fn store_gateway_details(
&self,
details: &PersistedGatewayDetails,
) -> Result<(), Self::StorageError>;
details: &PersistedGatewayDetails<T>,
) -> Result<(), Self::StorageError>
where
T: Serialize + Send + Sync;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedGatewayDetails {
#[serde(untagged)]
pub enum PersistedGatewayDetails<T = EmptyCustomDetails> {
/// Standard details of a remote gateway
Default(PersistedGatewayConfig),
/// Custom gateway setup, such as for a client embedded inside gateway itself
Custom(PersistedCustomGatewayDetails<T>),
}
impl<T> PersistedGatewayDetails<T> {
// 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<PersistedGatewayDetails> for GatewayEndpointConfig {
fn from(value: PersistedGatewayDetails) -> Self {
value.details
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedCustomGatewayDetails<T> {
// 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<T> PersistedGatewayDetails<T> {
pub fn new(
details: GatewayDetails<T>,
shared_key: Option<&SharedKeys>,
) -> Result<Self, ClientCoreError> {
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<T>) -> 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<u8> 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<PersistedGatewayDetails, OnDiskGatewayDetailsError> {
pub fn load_from_disk<T>(&self) -> Result<PersistedGatewayDetails<T>, 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<T>(
&self,
details: &PersistedGatewayDetails,
) -> Result<(), OnDiskGatewayDetailsError> {
details: &PersistedGatewayDetails<T>,
) -> 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<Option<PersistedGatewayDetails>>,
pub struct InMemGatewayDetails<T = EmptyCustomDetails> {
details: Mutex<Option<PersistedGatewayDetails<T>>>,
}
#[derive(Debug, thiserror::Error)]
@@ -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;
@@ -103,7 +103,7 @@ impl ManagedKeys {
pub fn gateway_shared_key(&self) -> Option<Arc<SharedKeys>> {
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<SharedKeys>) {
pub fn ensure_gateway_key(&self, gateway_shared_key: Option<Arc<SharedKeys>>) {
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<S: KeyStore>(
&mut self,
gateway_shared_key: Arc<SharedKeys>,
gateway_shared_key: Option<Arc<SharedKeys>>,
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<SharedKeys>) -> KeyManager {
pub fn insert_maybe_gateway_shared_key(
self,
gateway_shared_key: Option<Arc<SharedKeys>>,
) -> KeyManager {
KeyManager {
identity_keypair: self.identity_keypair,
encryption_keypair: self.encryption_keypair,
@@ -222,7 +241,11 @@ pub struct KeyManager {
encryption_keypair: Arc<encryption::KeyPair>,
/// shared key derived with the gateway during "registration handshake"
gateway_shared_key: Arc<SharedKeys>,
// 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<Arc<SharedKeys>>,
/// key used for producing and processing acknowledgement packets.
ack_key: Arc<AckKey>,
@@ -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<SharedKeys>,
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<SharedKeys> {
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<SharedKeys> {
Arc::clone(&self.gateway_shared_key)
pub fn gateway_shared_key(&self) -> Option<Arc<SharedKeys>> {
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 {
@@ -88,20 +88,20 @@ impl OnDiskKeys {
pub fn ephemeral_load_gateway_keys(
&self,
) -> Result<zeroize::Zeroizing<SharedKeys>, 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<encryption::KeyPair, OnDiskKeysError> {
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<identity::KeyPair, OnDiskKeysError> {
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<T: PemStorableKey>(
@@ -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<SharedKeys> = 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(())
}
@@ -1,24 +1,26 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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<Vec<MixPacket>>;
pub type BatchMixMessageReceiver = tokio::sync::mpsc::Receiver<Vec<MixPacket>>;
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<C, St: Storage> {
// 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<C, St>,
// that's also disgusting.
pub struct Empty;
pub struct MixTrafficController {
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
mix_rx: BatchMixMessageReceiver,
// TODO: this is temporary work-around.
@@ -26,20 +28,31 @@ pub struct MixTrafficController<C, St: Storage> {
consecutive_gateway_failure_count: usize,
}
impl<C, St> MixTrafficController<C, St>
where
C: DkgQueryClient + Sync + Send + 'static,
St: Storage + 'static,
<St as Storage>::StorageError: Send + Sync + 'static,
{
pub fn new(
gateway_client: GatewayClient<C, St>,
) -> (MixTrafficController<C, St>, BatchMixMessageSender) {
impl MixTrafficController {
pub fn new<T>(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<dyn GatewayTransceiver + Send>,
) -> (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
@@ -0,0 +1,262 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<dyn std::error::Error + Send + Sync>);
fn erase_err<E: std::error::Error + Send + Sync + 'static>(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<MixPacket>,
) -> 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<G: GatewayTransceiver + ?Sized + Send> GatewayTransceiver for Box<G> {
#[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<G: GatewaySender + ?Sized + Send> GatewaySender for Box<G> {
#[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<MixPacket>,
) -> Result<(), ErasedGatewayError> {
(**self).batch_send_mix_packets(packets).await
}
}
impl<G: GatewayReceiver + ?Sized> GatewayReceiver for Box<G> {
#[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<C, St> {
gateway_client: GatewayClient<C, St>,
}
impl<C, St> RemoteGateway<C, St> {
pub fn new(gateway_client: GatewayClient<C, St>) -> Self {
Self { gateway_client }
}
}
impl<C, St> GatewayTransceiver for RemoteGateway<C, St>
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<C, St> GatewaySender for RemoteGateway<C, St>
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<MixPacket>,
) -> Result<(), ErasedGatewayError> {
self.gateway_client
.batch_send_mix_packets(packets)
.await
.map_err(erase_err)
}
}
impl<C, St> GatewayReceiver for RemoteGateway<C, St> {}
#[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<MixPacket>,
// 'receiver' part
packet_router_tx: Option<oneshot::Sender<PacketRouter>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl LocalGateway {
pub fn new(
local_identity: identity::PublicKey,
packet_forwarder: mpsc::UnboundedSender<MixPacket>,
packet_router_tx: oneshot::Sender<PacketRouter>,
) -> 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<PacketRouter>,
sent: Vec<MixPacket>,
}
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
}
}
@@ -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;
@@ -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!");
});
}
@@ -213,17 +213,17 @@ impl RealMessagesController<OsRng> {
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);
}
}
@@ -500,7 +500,7 @@ impl<R: MessageReceiver + Clone + Send + 'static> 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<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
}
});
spawn_future(async move {
request_receiver.run_with_shutdown(shutdown).await;
request_receiver
.run_with_shutdown(shutdown.with_suffix("request_receiver"))
.await;
});
}
}
@@ -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");
@@ -35,7 +35,7 @@ pub struct ClientKeysPaths {
}
impl ClientKeysPaths {
pub fn new_default<P: AsRef<Path>>(base_data_directory: P) -> Self {
pub fn new_base<P: AsRef<Path>>(base_data_directory: P) -> Self {
let base_dir = base_data_directory.as_ref();
ClientKeysPaths {
@@ -29,14 +29,14 @@ pub struct CommonClientPaths {
}
impl CommonClientPaths {
pub fn new_default<P: AsRef<Path>>(base_data_directory: P) -> Self {
pub fn new_base<P: AsRef<Path>>(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),
}
}
}
+22
View File
@@ -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<GatewayEndpointConfig> for GatewayConfig {
type Error = ClientCoreError;
fn try_from(value: GatewayEndpointConfig) -> Result<Self, Self::Error> {
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(),
}
}
@@ -267,7 +267,7 @@ impl From<TopologyV1_1_20_2> 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()
}
}
}
+20 -16
View File
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// 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
+20 -9
View File
@@ -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<MaybeTlsStream<TcpStream>>;
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<R: Rng>(
.cloned()
}
pub(super) fn get_specified_gateway(
gateway_identity: IdentityKeyRef,
gateways: &[gateway::Node],
) -> Result<gateway::Node, ClientCoreError> {
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<identity::KeyPair>,
) -> Result<RegistrationResult, ClientCoreError> {
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,
})
}
+136 -372
View File
@@ -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<SharedKeys>,
pub authenticated_ephemeral_client: Option<GatewayClient<InitOnly>>,
}
pub struct InitialisationResult {
pub details: InitialisationDetails,
pub authenticated_ephemeral_client: Option<GatewayClient<InitOnly>>,
}
impl From<InitialisationDetails> 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<K, D>(key_store: &K, details_store: &D) -> Result<Self, ClientCoreError>
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<Recipient, ClientCoreError> {
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<InitOnly>,
/// Details of this pre-initialised client
details: InitialisationDetails,
},
}
impl From<PersistedGatewayDetails> for GatewaySetup {
fn from(details: PersistedGatewayDetails) -> Self {
GatewaySetup::Predefined { details }
}
}
impl From<IdentityKey> 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<String>,
latency_based_selection: Option<bool>,
) -> 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<GatewayEndpointConfig, ClientCoreError> {
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<GatewayEndpointConfig, ClientCoreError> {
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<D>(
async fn _store_gateway_details<T, D>(
details_store: &D,
details: &PersistedGatewayDetails,
details: &PersistedGatewayDetails<T>,
) -> Result<(), ClientCoreError>
where
D: GatewayDetailsStore,
D: GatewayDetailsStore<T>,
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<D>(
async fn _load_gateway_details<T, D>(
details_store: &D,
) -> Result<PersistedGatewayDetails, ClientCoreError>
) -> Result<PersistedGatewayDetails<T>, ClientCoreError>
where
D: GatewayDetailsStore,
D: GatewayDetailsStore<T>,
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<T>(
details: &PersistedGatewayDetails<T>,
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<K, D>(
setup: GatewaySetup,
async fn setup_new_gateway<T, K, D>(
key_store: &K,
details_store: &D,
overwrite_data: bool,
gateways: Option<&[gateway::Node]>,
) -> Result<InitialisationResult, ClientCoreError>
selection_specification: GatewaySelectionSpecification<T>,
available_gateways: Vec<gateway::Node>,
) -> Result<InitialisationResult<T>, ClientCoreError>
where
K: KeyStore,
D: GatewayDetailsStore,
D: GatewayDetailsStore<T>,
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<K, D>(
setup: GatewaySetup,
async fn use_loaded_gateway_details<T, K, D>(
key_store: &K,
details_store: &D,
overwrite_data: bool,
validator_servers: Option<&[Url]>,
) -> Result<InitialisationResult, ClientCoreError>
) -> Result<InitialisationResult<T>, ClientCoreError>
where
K: KeyStore,
D: GatewayDetailsStore,
D: GatewayDetailsStore<T>,
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<T>(
authenticated_ephemeral_client: InitGatewayClient,
gateway_details: GatewayDetails<T>,
managed_keys: ManagedKeys,
) -> InitialisationResult<T> {
InitialisationResult {
gateway_details,
managed_keys,
authenticated_ephemeral_client: Some(authenticated_ephemeral_client),
}
}
pub async fn setup_gateway<T, K, D>(
setup: GatewaySetup<T>,
key_store: &K,
details_store: &D,
) -> Result<InitialisationResult<T>, ClientCoreError>
where
K: KeyStore,
D: GatewayDetailsStore<T>,
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<T: Serialize>(init_results: &T, output_file: &str) {
+331
View File
@@ -0,0 +1,331 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<SharedKeys>,
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<T = EmptyCustomDetails> {
pub gateway_details: GatewayDetails<T>,
pub managed_keys: ManagedKeys,
pub authenticated_ephemeral_client: Option<InitGatewayClient>,
}
impl<T> InitialisationResult<T> {
pub fn new_loaded(gateway_details: GatewayDetails<T>, managed_keys: ManagedKeys) -> Self {
InitialisationResult {
gateway_details,
managed_keys,
authenticated_ephemeral_client: None,
}
}
pub async fn try_load<K, D>(key_store: &K, details_store: &D) -> Result<Self, ClientCoreError>
where
K: KeyStore,
D: GatewayDetailsStore<T>,
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<Recipient, ClientCoreError> {
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<T = EmptyCustomDetails> {
/// Standard details of a remote gateway
Configured(GatewayEndpointConfig),
/// Custom gateway setup, such as for a client embedded inside gateway itself
Custom(CustomGatewayDetails<T>),
}
#[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<T = EmptyCustomDetails> {
// whatever custom method is used, gateway's identity must be known
pub gateway_id: String,
#[serde(flatten)]
pub additional_data: T,
}
impl<T> CustomGatewayDetails<T> {
pub fn new(gateway_id: String, additional_data: T) -> Self {
Self {
gateway_id,
additional_data,
}
}
}
impl<T> GatewayDetails<T> {
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<GatewayEndpointConfig> for GatewayDetails {
fn from(value: GatewayEndpointConfig) -> Self {
GatewayDetails::Configured(value)
}
}
impl<T> From<PersistedCustomGatewayDetails<T>> for CustomGatewayDetails<T> {
fn from(value: PersistedCustomGatewayDetails<T>) -> Self {
CustomGatewayDetails {
gateway_id: value.gateway_id,
additional_data: value.additional_data,
}
}
}
impl<T> From<CustomGatewayDetails<T>> for PersistedCustomGatewayDetails<T> {
fn from(value: CustomGatewayDetails<T>) -> Self {
PersistedCustomGatewayDetails {
gateway_id: value.gateway_id,
additional_data: value.additional_data,
}
}
}
impl<T> From<PersistedGatewayDetails<T>> for GatewayDetails<T> {
fn from(value: PersistedGatewayDetails<T>) -> Self {
match value {
PersistedGatewayDetails::Default(default) => {
GatewayDetails::Configured(default.details)
}
PersistedGatewayDetails::Custom(custom) => GatewayDetails::Custom(custom.into()),
}
}
}
#[derive(Clone, Default)]
pub enum GatewaySelectionSpecification<T = EmptyCustomDetails> {
/// 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<T> GatewaySelectionSpecification<T> {
pub fn new(gateway_identity: Option<String>, latency_based_selection: Option<bool>) -> 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<T = EmptyCustomDetails> {
/// The gateway specification (details + keys) MUST BE loaded from the underlying storage.
MustLoad,
/// Specifies usage of a new gateway
New {
specification: GatewaySelectionSpecification<T>,
// TODO: seems to be a bit inefficient to pass them by value
available_gateways: Vec<gateway::Node>,
/// 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<T>,
managed_keys: ManagedKeys,
},
}
impl<T> GatewaySetup<T> {
// pub fn new_fresh(
// gateway_identity: Option<String>,
// latency_based_selection: Option<bool>,
// gateways: Vec<gateway::Node>,
// 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<T>,
) -> Result<Self, ClientCoreError> {
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<K, D>(
self,
key_store: &K,
details_store: &D,
) -> Result<InitialisationResult<T>, ClientCoreError>
where
K: KeyStore,
D: GatewayDetailsStore<T>,
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)
}
}
+5
View File
@@ -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<F>(future: F)
where
+52 -33
View File
@@ -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<String>,
pub gateway_listener: String,
}
// TODO: this should be refactored into a state machine that keeps track of its authentication state
pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
authenticated: bool,
disabled_credentials_mode: bool,
@@ -69,17 +84,12 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
}
impl<C, St> GatewayClient<C, St> {
// TODO: put it all in a Config struct
#[allow(clippy::too_many_arguments)]
pub fn new(
gateway_address: String,
config: GatewayConfig,
local_identity: Arc<identity::KeyPair>,
gateway_identity: identity::PublicKey,
// TODO: make it mandatory. if you don't want to pass it, use `new_init`
shared_key: Option<Arc<SharedKeys>>,
mixnet_message_sender: MixnetMessageSender,
ack_sender: AcknowledgementSender,
response_timeout_duration: Duration,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
shutdown: TaskClient,
) -> Self {
@@ -87,13 +97,13 @@ impl<C, St> GatewayClient<C, St> {
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<C, St> GatewayClient<C, St> {
}
}
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<C, St> GatewayClient<C, St> {
}
}
// type alias for an ease of use
pub type InitGatewayClient = GatewayClient<InitOnly>;
pub struct InitOnly;
impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
// 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<identity::KeyPair>,
response_timeout_duration: Duration,
) -> Self {
pub fn new_init(config: GatewayConfig, local_identity: Arc<identity::KeyPair>) -> Self {
use futures::channel::mpsc;
// note: this packet_router is completely invalid in normal circumstances, but "works"
@@ -784,13 +805,13 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
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<InitOnly, EphemeralCredentialStorage> {
pub fn upgrade<C, St>(
self,
mixnet_message_sender: MixnetMessageSender,
ack_sender: AcknowledgementSender,
response_timeout_duration: Duration,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
shutdown: TaskClient,
) -> GatewayClient<C, St> {
@@ -822,8 +841,8 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
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,
@@ -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<u8>, 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 {
+7 -4
View File
@@ -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(
@@ -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<Vec<Vec<u8>>>;
@@ -37,77 +35,52 @@ impl PacketRouter {
}
}
pub fn route_received(
&mut self,
unwrapped_packets: Vec<Vec<u8>>,
pub fn route_mixnet_messages(
&self,
received_messages: Vec<Vec<u8>>,
) -> 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<Vec<u8>>) -> 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<Vec<u8>>) -> Result<(), Self::Error> {
self.route_mixnet_messages(received_messages)
}
fn route_acks(&self, received_acks: Vec<Vec<u8>>) -> Result<(), Self::Error> {
self.route_acks(received_acks)
}
}
@@ -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};
@@ -0,0 +1,96 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<Vec<u8>>) -> 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<Vec<u8>>) -> Result<(), Self::Error>;
fn route_acks(&self, received_acks: Vec<Vec<u8>>) -> Result<(), Self::Error>;
}
@@ -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();
+5 -6
View File
@@ -33,18 +33,17 @@ pub fn may_get_home() -> Option<PathBuf> {
}
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<W: Write>(&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<Path>,
{
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()
+1 -1
View File
@@ -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
@@ -150,6 +150,13 @@ impl PublicKey {
}
}
#[cfg(feature = "sphinx")]
impl From<PublicKey> for DestinationAddressBytes {
fn from(value: PublicKey) -> Self {
value.derive_destination_address()
}
}
impl FromStr for PublicKey {
type Err = Ed25519RecoveryError;
+8 -9
View File
@@ -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";
+1
View File
@@ -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 }
@@ -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!()
// // }
+13 -7
View File
@@ -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<dyn Error + Send + Sync>> {
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<dyn Error + Send + Sync>> {
// 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,
})
}
@@ -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);
+1 -1
View File
@@ -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;
+135 -17
View File
@@ -1,11 +1,11 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<S: Into<String>>(&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<String>,
// 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<S: Into<String>>(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<Fut, T>(&mut self, fut: Fut) -> Option<T>
where
Fut: Future<Output = T>,
@@ -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<TaskManager> for TaskHandle {
fn from(value: TaskManager) -> Self {
TaskHandle::Internal(value)
}
}
impl From<TaskClient> 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<S: Into<String>>(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<S: Into<String>>(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<S: Into<String>>(&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<TaskManager> {
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::*;
+3
View File
@@ -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 },
+4 -4
View File
@@ -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;
}
}
+50 -6
View File
@@ -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<GatewayNetworkRequesterDetails>,
}
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)
}
}
+10
View File
@@ -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<TopologyWasm> 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<ConfigTopology> 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,
}
}
@@ -228,6 +228,11 @@ pub struct TopologyWasmOverride {
#[tsify(optional)]
pub topology_resolution_timeout_ms: Option<u32>,
/// 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<u32>,
/// 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<TopologyWasmOverride> 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),
}
}
+13 -3
View File
@@ -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)
}
+2 -4
View File
@@ -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,
@@ -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(())
}
}
@@ -100,7 +100,6 @@ pub async fn create_client(config_path: PathBuf) -> MixnetClient {
.await
.unwrap()
.build()
.await
.unwrap();
client.connect_to_mixnet().await.unwrap()
+3 -2
View File
@@ -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" }
+277
View File
@@ -0,0 +1,277 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<IpAddr>,
pub(crate) mix_port: Option<u16>,
pub(crate) clients_port: Option<u16>,
pub(crate) datastore: Option<PathBuf>,
pub(crate) enabled_statistics: Option<bool>,
pub(crate) statistics_service_url: Option<url::Url>,
pub(crate) nym_apis: Option<Vec<url::Url>>,
pub(crate) mnemonic: Option<bip39::Mnemonic>,
pub(crate) nyxd_urls: Option<Vec<url::Url>>,
pub(crate) only_coconut_credentials: Option<bool>,
pub(crate) with_network_requester: Option<bool>,
}
impl OverrideConfig {
pub(crate) fn do_override(self, mut config: Config) -> Result<Config, GatewayError> {
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<bool>,
pub(crate) enable_statistics: Option<bool>,
pub(crate) statistics_recipient: Option<String>,
}
/// 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<Config, GatewayError> {
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<OverrideNetworkRequesterConfig>,
) -> 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<GatewayNetworkRequesterDetails, GatewayError> {
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(),
})
}
+125 -40
View File
@@ -1,15 +1,15 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// 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<u16>,
/// The port on which the gateway will be listening for clients gateway-requests
#[clap(long)]
#[arg(long)]
clients_port: Option<u16>,
/// Path to sqlite database containing all gateway persistent data
#[clap(long)]
#[arg(long)]
datastore: Option<PathBuf>,
/// 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<Vec<url::Url>>,
/// 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<Vec<url::Url>>,
/// Cosmos wallet mnemonic needed for double spending protection
#[clap(long)]
#[arg(long)]
mnemonic: Option<bip39::Mnemonic>,
/// 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<bool>,
/// Enable/disable gateway anonymized statistics that get sent to a statistics aggregator server
#[clap(long)]
#[arg(long)]
enabled_statistics: Option<bool>,
/// URL where a statistics aggregator is running. The default value is a Nym aggregator server
#[clap(long)]
#[arg(long)]
statistics_service_url: Option<url::Url>,
#[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<bool>,
/// Enable service anonymized statistics that get sent to a statistics aggregator server
#[arg(long, requires = "with_network_requester")]
enable_statistics: Option<bool>,
/// 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<String>,
/// 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<Init> 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<dyn Error + Send + Sync>> {
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<dyn Error + Send + Sync>> {
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;
}
}
+9 -139
View File
@@ -1,28 +1,20 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// 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<IpAddr>,
mix_port: Option<u16>,
clients_port: Option<u16>,
datastore: Option<PathBuf>,
enabled_statistics: Option<bool>,
statistics_service_url: Option<url::Url>,
nym_apis: Option<Vec<url::Url>>,
mnemonic: Option<bip39::Mnemonic>,
nyxd_urls: Option<Vec<url::Url>>,
only_coconut_credentials: Option<bool>,
}
pub(crate) async fn execute(args: Cli) -> Result<(), Box<dyn Error + Send + Sync>> {
let bin_name = "nym-gateway";
@@ -70,6 +51,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box<dyn Error + Send + Sync
Commands::Init(m) => 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<dyn Error + Send + Sync
Ok(())
}
pub(crate) fn override_config(
mut config: Config,
args: OverrideConfig,
) -> Result<Config, GatewayError> {
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<Config, GatewayError> {
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::*;
+5 -8
View File
@@ -1,11 +1,10 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// 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<dyn Error + Send + Sync>> {
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(())
}
+73 -6
View File
@@ -1,12 +1,14 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// 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<PathBuf>,
/// 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<Vec<url::Url>>,
@@ -65,6 +72,48 @@ pub struct Run {
#[clap(long)]
statistics_service_url: Option<url::Url>,
/// Allows this gateway to run an embedded network requester for minimal network overhead
#[clap(long)]
with_network_requester: Option<bool>,
// ##### NETWORK REQUESTER FLAGS #####
/// Specifies whether this network requester should run in 'open-proxy' mode
#[arg(long)]
open_proxy: Option<bool>,
/// Enable service anonymized statistics that get sent to a statistics aggregator server
#[arg(long)]
enable_statistics: Option<bool>,
/// Mixnet client address where a statistics aggregator is running. The default value is a Nym
/// aggregator client
#[arg(long)]
statistics_recipient: Option<String>,
/// 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<PathBuf>,
#[clap(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
@@ -83,6 +132,20 @@ impl From<Run> 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<dyn Error + Send + Sync>> {
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<dyn Error + Send + Sync>> {
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
}
@@ -0,0 +1,109 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<PathBuf>,
/// 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<bool>,
// 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<bool>,
/// Enable service anonymized statistics that get sent to a statistics aggregator server
#[arg(long)]
enable_statistics: Option<bool>,
/// Mixnet client address where a statistics aggregator is running. The default value is a Nym
/// aggregator client
#[arg(long)]
statistics_recipient: Option<String>,
/// 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(())
}
+10 -23
View File
@@ -1,19 +1,18 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// 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<Sign> 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<dyn Error + Send + Sync>> {
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) => {
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<bool, 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(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<bool, GatewayError> {
// 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(())
}
+64 -4
View File
@@ -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<P: AsRef<Path>>(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<PathBuf>,
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<S: AsRef<str>>(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<P: AsRef<Path>>(path: P) -> io::Result<Self> {
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<P: AsRef<Path>>(path: P) -> io::Result<Self> {
read_config_from_toml_file(path)
Self::read_from_path(path)
}
pub fn read_from_default_path<P: AsRef<Path>>(id: P) -> io::Result<Self> {
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<S: Into<String>>(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 {
+22 -15
View File
@@ -1,13 +1,14 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<ConfigV1_1_20> for Config {
impl From<ConfigV1_1_20> 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<ConfigV1_1_20> 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<ConfigV1_1_20> 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<LoggingV1_1_20> for LoggingSettings {
impl From<LoggingV1_1_20> 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<DebugV1_1_20> for Debug {
impl From<DebugV1_1_20> 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,
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// 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/<id>/config`
pub fn default_config_directory<P: AsRef<Path>>(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/<id>/config/config.toml`
pub fn default_config_filepath<P: AsRef<Path>>(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<P: AsRef<Path>>(id: P) -> io::Result<Self> {
read_config_from_toml_file(default_config_filepath(id))
}
}
impl From<ConfigV1_1_28> 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<Url>,
/// Addresses to validators which the node uses to check for double spending of ERC20 tokens.
#[serde(alias = "validator_nymd_urls")]
pub nyxd_urls: Vec<Url>,
/// 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,
}
}
}
+44 -2
View File
@@ -1,8 +1,8 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// 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<P: AsRef<Path>>(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<Option<PathBuf>, 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<PathBuf>,
// 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<P: AsRef<Path>>(mut self, path: P) -> Self {
self.network_requester_config = Some(path.as_ref().into());
self
}
#[must_use]
pub fn with_default_network_requester_config<P: AsRef<Path>>(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<PathBuf> {
&self.network_requester_config
}
pub fn private_identity_key(&self) -> &Path {
self.keys.private_identity_key()
}
+8 -1
View File
@@ -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]
+70 -2
View File
@@ -1,6 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<ClientCoreError> 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(),
}
}
}
+3
View File
@@ -1,6 +1,9 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// 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;
@@ -1,14 +1,41 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// 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<DashMap<DestinationAddressBytes, ClientIncomingChannels>>);
pub(crate) struct ActiveClientsStore {
inner: Arc<DashMap<DestinationAddressBytes, ActiveClient>>,
}
#[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<ClientIncomingChannels> {
let entry = self.0.get(&client)?;
let handle = entry.value();
pub(crate) fn get_sender(&self, client: DestinationAddressBytes) -> Option<MixMessageSender> {
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<ClientIncomingChannels> {
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()
}
}
@@ -0,0 +1,89 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<Vec<u8>>) {
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")
}
}
+1
View File
@@ -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
@@ -375,21 +375,24 @@ where
async fn handle_pong(&mut self, msg: Vec<u8>) {
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")
}
}
@@ -1,4 +1,4 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// 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,
@@ -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}"),
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<P: AsRef<Path>>(path: Option<P>) -> String {
path.as_ref()
.map(|p| p.as_ref().display().to_string())
.unwrap_or_default()
}
fn display_path<P: AsRef<Path>>(path: P) -> String {
path.as_ref().display().to_string()
}
pub(crate) fn node_details(config: &Config) -> Result<GatewayNodeDetailsResponse, GatewayError> {
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<P: AsRef<Path>>(
id: &str,
path: P,
) -> Result<nym_network_requester::Config, GatewayError> {
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<PersistentStorage, GatewayError> {
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<T: PemStorableKeyPair>(
paths: KeyPairPath,
name: impl Into<String>,
) -> Result<T, GatewayError> {
nym_pemstore::load_keypair(&paths).map_err(|err| GatewayError::KeyPairLoadFailure {
keys: name.into(),
paths,
err,
})
}
pub(crate) fn load_public_key<T, P, S>(path: P, name: S) -> Result<T, GatewayError>
where
T: PemStorableKey,
P: AsRef<Path>,
S: Into<String>,
{
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<identity::KeyPair, GatewayError> {
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<encryption::KeyPair, GatewayError> {
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")
}
@@ -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<St: Storage> {
packet_processor: PacketProcessor,
@@ -70,9 +79,9 @@ impl<St: Storage> ConnectionHandler<St> {
}
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<St: Storage> ConnectionHandler<St> {
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<St: Storage> ConnectionHandler<St> {
client_address: DestinationAddressBytes,
message: Vec<u8>,
) -> 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<MixPacket>, client_address: DestinationAddressBytes) {
fn forward_ack(
&self,
forward_ack: Option<MixPacket>,
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<St: Storage> ConnectionHandler<St> {
.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<St: Storage> ConnectionHandler<St> {
{
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<St: Storage> ConnectionHandler<St> {
// 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<St: Storage> ConnectionHandler<St> {
}
}
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}")
}
}
}
}
@@ -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}"),
}
+162 -81
View File
@@ -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<PersistentStorage> {
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<St> {
pub(crate) async fn create_gateway(
config: Config,
nr_config_override: Option<OverrideNetworkRequesterConfig>,
custom_nr_mixnet: Option<PathBuf>,
) -> Result<Gateway, GatewayError> {
// 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<PathBuf>,
}
pub(crate) struct Gateway<St = PersistentStorage> {
config: Config,
network_requester_opts: Option<LocalNetworkRequesterOpts>,
/// ed25519 keypair used to assert one's identity.
identity_keypair: Arc<identity::KeyPair>,
/// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation.
sphinx_keypair: Arc<encryption::KeyPair>,
storage: St,
@@ -55,71 +89,37 @@ pub(crate) struct Gateway<St> {
impl<St> Gateway<St> {
/// 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<LocalNetworkRequesterOpts>,
storage: St,
) -> Result<Self, GatewayError> {
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<LocalNetworkRequesterOpts>,
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<St> Gateway<St> {
packet_sender
}
async fn wait_for_interrupt(
// TODO: rethink the logic in this function...
async fn start_network_requester(
&self,
shutdown: TaskManager,
) -> Result<(), Box<dyn Error + Send + Sync>> {
forwarding_channel: MixForwardingSender,
shutdown: TaskClient,
) -> Result<LocalNetworkRequesterHandle, GatewayError> {
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<dyn Error + Send + Sync>> {
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<nym_validator_client::NymApiClient, GatewayError> {
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<DirectSigningHttpRpcNyxdClient, GatewayError> {
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<bool, GatewayError> {
// 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<St> Gateway<St> {
}))
}
pub async fn run(&mut self) -> Result<(), Box<dyn Error + Send + Sync>>
pub async fn run(self) -> anyhow::Result<()>
where
St: Storage + Clone + 'static,
{
@@ -260,17 +320,18 @@ impl<St> Gateway<St> {
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<St> Gateway<St> {
});
}
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(())
}
}
+2 -2
View File
@@ -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),
}
+8 -2
View File
@@ -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)))
//
+2 -3
View File
@@ -1,7 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<O: Into<OverrideConfig>>(
override_args: O,
) -> Result<Config, GatewayError> {
let config = try_load_current_config(&id)?;
override_config(config, override_args.into())
override_args.into().do_override(config)
}
+1 -1
View File
@@ -88,7 +88,7 @@ pub struct Config {
}
impl NymConfigTemplate for Config {
fn template() -> &'static str {
fn template(&self) -> &'static str {
CONFIG_TEMPLATE
}
}
+36 -28
View File
@@ -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,
)
+1 -1
View File
@@ -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
}
}
@@ -1,6 +1,12 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// I'm not convinced by the lint (on Arc<WasmStorage>).
// 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;
+3 -2
View File
@@ -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",
+19 -13
View File
@@ -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}");
@@ -14,7 +14,7 @@ pub struct NymConnectPaths {
impl NymConnectPaths {
pub fn new_default<P: AsRef<Path>>(base_data_directory: P) -> Self {
NymConnectPaths {
common_paths: CommonClientPaths::new_default(base_data_directory),
common_paths: CommonClientPaths::new_base(base_data_directory),
}
}
}
@@ -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 {
+7 -1
View File
@@ -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))
}
+3 -2
View File
@@ -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)
+1 -1
View File
@@ -55,7 +55,7 @@ pub struct Config {
}
impl NymConfigTemplate for Config {
fn template() -> &'static str {
fn template(&self) -> &'static str {
CONFIG_TEMPLATE
}
}

Some files were not shown because too many files have changed in this diff Show More