From abbf7041a48d1fe5d99e4219b0458c7d73cc24a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 30 Apr 2020 10:14:21 +0100 Subject: [PATCH] Feature/gateway provider merge (#208) * Initial draft for ClientsHandler * Created listener struct * typo * Stateful websocket connection handler * Exposing modules * Depdendencies updates * Moved listener to correct file + made start consume listener * Main starting new listener * Catching sigint * Copied client storage from provider into gateway * Exposed websocket listener type for nicer import path * Defined websocket message receiver concrete type * Client ledger struct without implementation * ClientsHandler using more concrete types * Mixnet sender + receiver and exposed listener type * Handling mix packets * Ability to forward mix packets * "starting" both listeners at main * Depedencies updates * Initial type definitions for client messages * Initial "gateway-requests" with AuthToken * ibid. * Restored most of ledger's functionalities * Ability to retrieve all messages regardless of rate limit * ClientsHandler request handling logic * Required 'new' dependencies * Main changes required for compilation * PacketProcessor getting private key pointer * "moved" types into gateway requests crate * Moved and renamed types * ibid. * Added required serde_json dependency * Skeleton for websocket request handling * helper methods on ServerResponse * WebSocket Handler pushing received mix messages directly to client * PacketForwarder returning JoinHandle alongside the channel * ClientsHandler following the same pattern * Made websocket listener start method consistent with mix listener * Syntax error + formatting * Websocket handler having access to mix forwarder * Minimal binary request parsing * Implicitly derived std::error::Error on GatewayRequestsError * Handling of all websocket requests * Types import cleanup * Updated placeholder fields * Everything put into main * Missing license notices * Cleaned up unused code * Copied and did initial minor changes to commands and config * It's actually gateway * Gateway sending its regular presence to directory server * Re-organisation of gateway code + Gateway struct * Updated provider argument description * Restored duplicate ip check without importing NymTopology trait * Moved hardcoded values into config * Cargo fmt * Compilation errors in other crates due to topology adjustments * Test fixes * Initial gateway-client * Seemingly working version with bunch of hardcoded and temporary values * cargo fmt * Removed provider poller * Updated config with gateway values instead of provider * Gateway address including ws * Removed hardcoded gateway address * Properly skipping loop cover messages * Updated log filter with tokio tungstenite related modules * Updated sphinx version used * Very minor cleanup * unused import --- Cargo.lock | 60 +- Cargo.toml | 2 + clients/desktop/Cargo.toml | 4 +- clients/desktop/src/client/mix_traffic.rs | 41 +- clients/desktop/src/client/mod.rs | 151 +++-- clients/desktop/src/client/provider_poller.rs | 122 ---- clients/desktop/src/client/received_buffer.rs | 23 +- .../desktop/src/client/topology_control.rs | 73 ++- clients/desktop/src/commands/init.rs | 87 +-- clients/desktop/src/commands/mod.rs | 4 +- clients/desktop/src/commands/run.rs | 6 +- clients/desktop/src/config/mod.rs | 108 +--- clients/desktop/src/config/template.rs | 11 +- clients/desktop/src/main.rs | 2 + .../src/sockets/websocket/connection.rs | 12 +- clients/webassembly/src/lib.rs | 5 +- .../client-libs/directory-client/src/lib.rs | 6 + .../directory-client/src/presence/gateways.rs | 86 +++ .../directory-client/src/presence/mod.rs | 17 +- .../directory-client/src/requests/mod.rs | 1 + .../src/requests/presence_gateways_post.rs | 103 +++ .../src/requests/presence_topology_get.rs | 415 +++++-------- common/client-libs/gateway-client/Cargo.toml | 22 + common/client-libs/gateway-client/src/lib.rs | 449 ++++++++++++++ common/client-libs/mix-client/src/packet.rs | 19 +- common/healthcheck/src/path_check.rs | 1 + common/healthcheck/src/result.rs | 16 + common/nymsphinx/Cargo.toml | 2 +- common/nymsphinx/src/lib.rs | 6 +- common/topology/src/gateway.rs | 61 ++ common/topology/src/lib.rs | 22 +- gateway/Cargo.toml | 34 +- .../websocket/types.rs => build.rs} | 19 +- gateway/gateway-requests/Cargo.toml | 14 + gateway/gateway-requests/src/auth_token.rs | 125 ++++ gateway/gateway-requests/src/lib.rs | 22 + gateway/gateway-requests/src/types.rs | 194 ++++++ gateway/src/built_info.rs | 16 + .../src/client_handling/clients_handler.rs | 65 -- gateway/src/client_handling/ledger.rs | 162 ----- gateway/src/client_handling/mod.rs | 3 - .../websocket/connection_handler.rs | 74 --- .../websocket/message_receiver.rs | 4 - gateway/src/client_handling/websocket/mod.rs | 6 - gateway/src/commands/init.rs | 131 ++++ gateway/src/commands/mod.rs | 105 ++++ gateway/src/commands/run.rs | 233 +++++++ gateway/src/config/mod.rs | 586 ++++++++++++++++++ gateway/src/config/persistence/mod.rs | 15 + gateway/src/config/persistence/pathfinder.rs | 58 ++ gateway/src/config/template.rs | 92 +++ gateway/src/main.rs | 74 ++- gateway/src/mixnet_client.rs | 43 -- gateway/src/mixnet_handling/mod.rs | 4 - .../node/client_handling/clients_handler.rs | 313 ++++++++++ gateway/src/node/client_handling/mod.rs | 16 + .../websocket/connection_handler.rs | 379 +++++++++++ .../client_handling/websocket/listener.rs | 38 +- .../websocket/message_receiver.rs | 18 + .../src/node/client_handling/websocket/mod.rs | 19 + gateway/src/node/mixnet_handling/mod.rs | 19 + .../receiver/connection_handler.rs | 23 +- .../mixnet_handling/receiver/listener.rs | 5 +- .../mixnet_handling/receiver/mod.rs | 0 .../receiver/packet_processing.rs | 32 +- .../{ => node}/mixnet_handling/sender/mod.rs | 32 +- gateway/src/node/mod.rs | 161 +++++ gateway/src/node/presence/mod.rs | 120 ++++ .../mod.rs => node/storage/inboxes.rs} | 31 +- gateway/src/node/storage/ledger.rs | 169 +++++ gateway/src/node/storage/mod.rs | 4 + mixnode/src/node/packet_processing.rs | 17 +- sfw-provider/src/commands/init.rs | 2 +- sfw-provider/src/commands/run.rs | 4 +- .../mix_handling/packet_processing.rs | 17 +- .../services/mixmining/health_check_runner.rs | 7 +- 76 files changed, 4246 insertions(+), 1196 deletions(-) delete mode 100644 clients/desktop/src/client/provider_poller.rs create mode 100644 common/client-libs/directory-client/src/presence/gateways.rs create mode 100644 common/client-libs/directory-client/src/requests/presence_gateways_post.rs create mode 100644 common/client-libs/gateway-client/Cargo.toml create mode 100644 common/client-libs/gateway-client/src/lib.rs create mode 100644 common/topology/src/gateway.rs rename gateway/{src/client_handling/websocket/types.rs => build.rs} (57%) create mode 100644 gateway/gateway-requests/Cargo.toml create mode 100644 gateway/gateway-requests/src/auth_token.rs create mode 100644 gateway/gateway-requests/src/lib.rs create mode 100644 gateway/gateway-requests/src/types.rs create mode 100644 gateway/src/built_info.rs delete mode 100644 gateway/src/client_handling/clients_handler.rs delete mode 100644 gateway/src/client_handling/ledger.rs delete mode 100644 gateway/src/client_handling/mod.rs delete mode 100644 gateway/src/client_handling/websocket/connection_handler.rs delete mode 100644 gateway/src/client_handling/websocket/message_receiver.rs delete mode 100644 gateway/src/client_handling/websocket/mod.rs create mode 100644 gateway/src/commands/init.rs create mode 100644 gateway/src/commands/mod.rs create mode 100644 gateway/src/commands/run.rs create mode 100644 gateway/src/config/mod.rs create mode 100644 gateway/src/config/persistence/mod.rs create mode 100644 gateway/src/config/persistence/pathfinder.rs create mode 100644 gateway/src/config/template.rs delete mode 100644 gateway/src/mixnet_client.rs delete mode 100644 gateway/src/mixnet_handling/mod.rs create mode 100644 gateway/src/node/client_handling/clients_handler.rs create mode 100644 gateway/src/node/client_handling/mod.rs create mode 100644 gateway/src/node/client_handling/websocket/connection_handler.rs rename gateway/src/{ => node}/client_handling/websocket/listener.rs (56%) create mode 100644 gateway/src/node/client_handling/websocket/message_receiver.rs create mode 100644 gateway/src/node/client_handling/websocket/mod.rs create mode 100644 gateway/src/node/mixnet_handling/mod.rs rename gateway/src/{ => node}/mixnet_handling/receiver/connection_handler.rs (81%) rename gateway/src/{ => node}/mixnet_handling/receiver/listener.rs (92%) rename gateway/src/{ => node}/mixnet_handling/receiver/mod.rs (100%) rename gateway/src/{ => node}/mixnet_handling/receiver/packet_processing.rs (90%) rename gateway/src/{ => node}/mixnet_handling/sender/mod.rs (62%) create mode 100644 gateway/src/node/mod.rs create mode 100644 gateway/src/node/presence/mod.rs rename gateway/src/{storage/mod.rs => node/storage/inboxes.rs} (84%) create mode 100644 gateway/src/node/storage/ledger.rs create mode 100644 gateway/src/node/storage/mod.rs diff --git a/Cargo.lock b/Cargo.lock index a20f860f25..a91f02ff92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -896,23 +896,25 @@ dependencies = [ ] [[package]] -name = "gateway" +name = "gateway-client" version = "0.1.0" dependencies = [ - "crypto", - "dotenv", "futures 0.3.4", - "log 0.4.8", - "mix-client", - "multi-tcp-client", + "gateway-requests", "nymsphinx", - "pretty_env_logger", - "rand 0.7.3", - "serde", - "sled", "tokio 0.2.16", "tokio-tungstenite", - "tungstenite", +] + +[[package]] +name = "gateway-requests" +version = "0.1.0" +dependencies = [ + "bs58", + "nymsphinx", + "serde", + "serde_json", + "tokio-tungstenite", ] [[package]] @@ -1621,6 +1623,8 @@ dependencies = [ "dirs", "dotenv", "futures 0.3.4", + "gateway-client", + "gateway-requests", "healthcheck", "log 0.4.8", "mix-client", @@ -1633,11 +1637,41 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sfw-provider-requests", "tempfile", "tokio 0.2.16", "tokio-tungstenite", "topology", + "url 2.1.1", +] + +[[package]] +name = "nym-gateway" +version = "0.7.0-dev" +dependencies = [ + "built", + "clap", + "config", + "crypto", + "directory-client", + "dirs", + "dotenv", + "futures 0.3.4", + "gateway-requests", + "hmac", + "log 0.4.8", + "mix-client", + "multi-tcp-client", + "nymsphinx", + "pemstore", + "pretty_env_logger", + "rand 0.7.3", + "serde", + "sha2", + "sled", + "tempfile", + "tokio 0.2.16", + "tokio-tungstenite", + "tungstenite", ] [[package]] @@ -2653,7 +2687,7 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=298c7fda6a577daf6d9bb955fa52c2bffecf6926#298c7fda6a577daf6d9bb955fa52c2bffecf6926" +source = "git+https://github.com/nymtech/sphinx?rev=72368ea048472823f7f9e78eed77a2c3d30a471f#72368ea048472823f7f9e78eed77a2c3d30a471f" dependencies = [ "aes-ctr", "arrayref", diff --git a/Cargo.toml b/Cargo.toml index 3c73502e56..d780fecd75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "clients/desktop", "clients/webassembly", "common/client-libs/directory-client", + "common/client-libs/gateway-client", "common/client-libs/mix-client", "common/client-libs/multi-tcp-client", "common/client-libs/provider-client", @@ -19,6 +20,7 @@ members = [ "common/pemstore", "common/topology", "gateway", + "gateway/gateway-requests", "mixnode", "sfw-provider", "sfw-provider/sfw-provider-requests", diff --git a/clients/desktop/Cargo.toml b/clients/desktop/Cargo.toml index e9009eefab..56d05ef070 100644 --- a/clients/desktop/Cargo.toml +++ b/clients/desktop/Cargo.toml @@ -26,18 +26,20 @@ serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" tokio = { version = "0.2", features = ["full"] } tokio-tungstenite = "0.10.1" +url = "2.1" ## internal config = {path = "../../common/config"} crypto = {path = "../../common/crypto"} directory-client = { path = "../../common/client-libs/directory-client" } +gateway-client = { path = "../../common/client-libs/gateway-client" } +gateway-requests = { path = "../../gateway/gateway-requests" } healthcheck = { path = "../../common/healthcheck" } mix-client = { path = "../../common/client-libs/mix-client" } multi-tcp-client = { path = "../../common/client-libs/multi-tcp-client" } nymsphinx = { path = "../../common/nymsphinx" } pemstore = {path = "../../common/pemstore"} provider-client = { path = "../../common/client-libs/provider-client" } -sfw-provider-requests = { path = "../../sfw-provider/sfw-provider-requests" } topology = {path = "../../common/topology" } [build-dependencies] diff --git a/clients/desktop/src/client/mix_traffic.rs b/clients/desktop/src/client/mix_traffic.rs index 67937b6cad..4886cbfe19 100644 --- a/clients/desktop/src/client/mix_traffic.rs +++ b/clients/desktop/src/client/mix_traffic.rs @@ -14,10 +14,10 @@ use futures::channel::mpsc; use futures::StreamExt; +use gateway_client::GatewayClient; use log::*; use nymsphinx::SphinxPacket; use std::net::SocketAddr; -use std::time::Duration; use tokio::runtime::Handle; use tokio::task::JoinHandle; @@ -31,37 +31,40 @@ impl MixMessage { } } -pub(crate) struct MixTrafficController { - tcp_client: multi_tcp_client::Client, +pub(crate) struct MixTrafficController<'a> { + // 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<'a, url::Url>, mix_rx: MixMessageReceiver, } -impl MixTrafficController { +impl<'a> MixTrafficController<'static> { pub(crate) fn new( - initial_reconnection_backoff: Duration, - maximum_reconnection_backoff: Duration, - initial_connection_timeout: Duration, mix_rx: MixMessageReceiver, - ) -> Self { - let tcp_client_config = multi_tcp_client::Config::new( - initial_reconnection_backoff, - maximum_reconnection_backoff, - initial_connection_timeout, - ); - + gateway_client: GatewayClient<'a, url::Url>, + ) -> MixTrafficController<'a> { MixTrafficController { - tcp_client: multi_tcp_client::Client::new(tcp_client_config), + gateway_client, mix_rx, } } async fn on_message(&mut self, mix_message: MixMessage) { debug!("Got a mix_message for {:?}", mix_message.0); - self.tcp_client - // TODO: possibly we might want to get an actual result here at some point - .send(mix_message.0, mix_message.1.to_bytes(), false) + match self + .gateway_client + .send_sphinx_packet(mix_message.0, mix_message.1.to_bytes()) .await - .unwrap(); // if we're not waiting for response, we MUST get an Ok + { + Err(e) => error!("Failed to send sphinx packet to the gateway! - {:?}", e), + Ok(was_successful) if !was_successful => { + warn!("Sent sphinx packet to the gateway but it failed to get processed!") + } + Ok(was_successful) if was_successful => { + trace!("Successfully forwarded sphinx packet to the gateway!") + } + Ok(_) => unreachable!("to shut up the compiler because all patterns ARE covered"), + } } pub(crate) async fn run(&mut self) { diff --git a/clients/desktop/src/client/mod.rs b/clients/desktop/src/client/mod.rs index 37ba5478a5..05b86772ed 100644 --- a/clients/desktop/src/client/mod.rs +++ b/clients/desktop/src/client/mod.rs @@ -14,7 +14,6 @@ use crate::client::cover_traffic_stream::LoopCoverTrafficStream; use crate::client::mix_traffic::{MixMessageReceiver, MixMessageSender, MixTrafficController}; -use crate::client::provider_poller::{PolledMessagesReceiver, PolledMessagesSender}; use crate::client::received_buffer::{ ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, }; @@ -27,17 +26,17 @@ use crate::sockets::{tcp, websocket}; use crypto::identity::MixIdentityKeyPair; use directory_client::presence; use futures::channel::{mpsc, oneshot}; +use gateway_client::{GatewayClient, SphinxPacketReceiver, SphinxPacketSender}; +use gateway_requests::auth_token::AuthToken; use log::*; use nymsphinx::chunking::split_and_prepare_payloads; -use nymsphinx::Destination; +use nymsphinx::{Destination, DestinationAddressBytes}; use pemstore::pemstore::PemStore; -use sfw_provider_requests::auth_token::AuthToken; use tokio::runtime::Runtime; use topology::NymTopology; mod cover_traffic_stream; mod mix_traffic; -mod provider_poller; mod real_traffic_stream; pub(crate) mod received_buffer; pub(crate) mod topology_control; @@ -85,6 +84,9 @@ impl NymClient { } } + #[deprecated( + note = "SURB_IDs are irrelevant in this system design and this method alongside everything using it, should be updated accordingly" + )] pub fn as_mix_destination(&self) -> Destination { Destination::new( self.identity_keypair.public_key().derive_address(), @@ -93,6 +95,10 @@ impl NymClient { ) } + pub fn as_mix_destination_address(&self) -> DestinationAddressBytes { + self.identity_keypair.public_key.derive_address() + } + // future constantly pumping loop cover traffic at some specified average rate // the pumped traffic goes to the MixTrafficController fn start_cover_traffic_stream( @@ -144,57 +150,72 @@ impl NymClient { fn start_received_messages_buffer_controller( &self, query_receiver: ReceivedBufferRequestReceiver, - poller_receiver: PolledMessagesReceiver, + sphinx_receiver: SphinxPacketReceiver, ) { info!("Starting 'received messages buffer controller'..."); - ReceivedMessagesBufferController::new(query_receiver, poller_receiver) + ReceivedMessagesBufferController::new(query_receiver, sphinx_receiver) .start(self.runtime.handle()) } - // future constantly trying to fetch any received messages from the provider - // the received messages are sent to ReceivedMessagesBuffer to be available to rest of the system - fn start_provider_poller( + fn start_gateway_client( &mut self, - mut topology_accessor: TopologyAccessor, - poller_input_tx: PolledMessagesSender, - ) { - info!("Starting provider poller..."); - // we already have our provider written in the config - let provider_id = self.config.get_provider_id(); + sphinx_packet_sender: SphinxPacketSender, + gateway_address: url::Url, + ) -> GatewayClient<'static, url::Url> { + let auth_token = self + .config + .get_gateway_auth_token() + .map(|str_token| AuthToken::try_from_base58_string(str_token).ok()) + .unwrap_or(None); - // TODO: a slightly more graceful termination here - if !self.runtime.block_on(topology_accessor.is_routable()) { - panic!( - "The current network topology seem to be insufficient to route any packets through\ - - check if enough nodes and a sfw-provider are online" - ); - } - - // TODO: a slightly more graceful termination here - let provider_client_listener_address = self.runtime.block_on( - topology_accessor.get_provider_socket_addr(&provider_id) - ).unwrap_or_else(|| panic!("Could not find provider with id {:?}. It does not seem to be present in the current network topology.\ - Are you sure it is still online? Perhaps try to run `nym-client init` again to obtain a new provider", provider_id)); - - let mut provider_poller = provider_poller::ProviderPoller::new( - poller_input_tx, - provider_client_listener_address, - self.identity_keypair.public_key().derive_address(), - self.config - .get_provider_auth_token() - .map(|str_token| AuthToken::try_from_base58_string(str_token).ok()) - .unwrap_or(None), - self.config.get_fetch_message_delay(), - self.config.get_max_response_size(), + let mut gateway_client = GatewayClient::new( + gateway_address, + self.as_mix_destination_address(), + auth_token, + sphinx_packet_sender, + self.config.get_gateway_response_timeout(), ); - if !provider_poller.is_registered() { - info!("Trying to perform initial provider registration..."); - self.runtime - .block_on(provider_poller.perform_initial_registration()) - .expect("Failed to perform initial provider registration"); - } - provider_poller.start(self.runtime.handle()); + let auth_token = self.runtime.block_on(async { + gateway_client + .establish_connection() + .await + .expect("could not establish initial connection with the gateway"); + gateway_client + .perform_initial_authentication() + .await + .expect("could not perform initial authentication with the gateway") + }); + + // TODO: if we didn't have an auth_token initially, save it to config or something? + info!( + "Performed initial authentication and our auth token is {:?}", + auth_token.to_base58_string() + ); + + gateway_client + } + + async fn get_gateway_address( + gateway_id: String, + mut topology_accessor: TopologyAccessor, + ) -> url::Url { + // we already have our gateway written in the config + let gateway_address = topology_accessor + .get_gateway_socket_url(&gateway_id) + .await + .expect( + format!( + "Could not find gateway with id {:?}.\ + It does not seem to be present in the current network topology.\ + Are you sure it is still online?\ + Perhaps try to run `nym-client init` again to obtain a new gateway", + gateway_id + ) + .as_ref(), + ); + + url::Url::parse(&gateway_address).expect("provided gateway address is invalid!") } // future responsible for periodically polling directory server and updating @@ -228,18 +249,16 @@ impl NymClient { } // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) - fn start_mix_traffic_controller(&mut self, mix_rx: MixMessageReceiver) { + // 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( + &mut self, + mix_rx: MixMessageReceiver, + gateway_client: GatewayClient<'static, url::Url>, + ) { info!("Starting mix traffic controller..."); - self.runtime - .enter(|| { - MixTrafficController::new( - self.config.get_packet_forwarding_initial_backoff(), - self.config.get_packet_forwarding_maximum_backoff(), - self.config.get_initial_connection_timeout(), - mix_rx, - ) - }) - .start(self.runtime.handle()); + MixTrafficController::new(mix_rx, gateway_client).start(self.runtime.handle()); } fn start_socket_listener( @@ -330,9 +349,9 @@ impl NymClient { // mix_rx is the receiver used by MixTrafficController that sends the actual traffic let (mix_tx, mix_rx) = mpsc::unbounded(); - // poller_input_tx is the transmitter of messages fetched from the provider - used by ProviderPoller - // poller_input_rx is the receiver for said messages - used by ReceivedMessagesBuffer - let (poller_input_tx, poller_input_rx) = mpsc::unbounded(); + // sphinx_packet_tx is the transmitter of sphinx messages received from the gateway + // sphinx_packet_rx is the receiver for said messages - used by ReceivedMessagesBuffer + let (sphinx_packet_tx, sphinx_packet_rx) = mpsc::unbounded(); // received_messages_buffer_output_tx is the transmitter for *REQUESTS* for messages contained in ReceivedMessagesBuffer - used by sockets // the requests contain a oneshot channel to send a reply on @@ -350,10 +369,16 @@ impl NymClient { self.start_topology_refresher(shared_topology_accessor.clone()); self.start_received_messages_buffer_controller( received_messages_buffer_output_rx, - poller_input_rx, + sphinx_packet_rx, ); - self.start_provider_poller(shared_topology_accessor.clone(), poller_input_tx); - self.start_mix_traffic_controller(mix_rx); + + let gateway_url = self.runtime.block_on(Self::get_gateway_address( + self.config.get_gateway_id(), + shared_topology_accessor.clone(), + )); + let gateway_client = self.start_gateway_client(sphinx_packet_tx, gateway_url); + + self.start_mix_traffic_controller(mix_rx, gateway_client); self.start_cover_traffic_stream(shared_topology_accessor.clone(), mix_tx.clone()); self.start_real_traffic_stream(shared_topology_accessor.clone(), mix_tx, input_rx); self.start_socket_listener( diff --git a/clients/desktop/src/client/provider_poller.rs b/clients/desktop/src/client/provider_poller.rs deleted file mode 100644 index da128fa08c..0000000000 --- a/clients/desktop/src/client/provider_poller.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use futures::channel::mpsc; -use log::*; -use nymsphinx::DestinationAddressBytes; -use provider_client::ProviderClientError; -use sfw_provider_requests::auth_token::AuthToken; -use std::net::SocketAddr; -use std::time; -use tokio::runtime::Handle; -use tokio::task::JoinHandle; - -pub(crate) type PolledMessagesSender = mpsc::UnboundedSender>>; -pub(crate) type PolledMessagesReceiver = mpsc::UnboundedReceiver>>; - -pub(crate) struct ProviderPoller { - polling_rate: time::Duration, - provider_client: provider_client::ProviderClient, - poller_tx: mpsc::UnboundedSender>>, -} - -impl ProviderPoller { - pub(crate) fn new( - poller_tx: mpsc::UnboundedSender>>, - provider_client_listener_address: SocketAddr, - client_address: DestinationAddressBytes, - auth_token: Option, - polling_rate: time::Duration, - max_response_size: usize, - ) -> Self { - ProviderPoller { - provider_client: provider_client::ProviderClient::new( - provider_client_listener_address, - client_address, - auth_token, - max_response_size, - ), - poller_tx, - polling_rate, - } - } - - pub(crate) fn is_registered(&self) -> bool { - self.provider_client.is_registered() - } - - // This method is only temporary until registration is moved to `client init` - pub(crate) async fn perform_initial_registration(&mut self) -> Result<(), ProviderClientError> { - debug!("performing initial provider registration"); - - if !self.is_registered() { - let auth_token = match self.provider_client.register().await { - // in this particular case we can ignore this error - Err(ProviderClientError::ClientAlreadyRegisteredError) => return Ok(()), - Err(err) => return Err(err), - Ok(token) => token, - }; - - self.provider_client.update_token(auth_token) - } else { - warn!("did not perform provider registration - we were already registered") - } - - Ok(()) - } - - pub(crate) async fn start_provider_polling(&mut self) { - let loop_message = &mix_client::packet::LOOP_COVER_MESSAGE_PAYLOAD.to_vec(); - let dummy_message = &sfw_provider_requests::DUMMY_MESSAGE_CONTENT.to_vec(); - - let extended_delay_duration = self.polling_rate * 10; - - loop { - debug!("Polling provider..."); - - let messages = match self.provider_client.retrieve_messages().await { - Err(err) => { - error!("Failed to query the provider for messages: {:?}, ... Going to wait {:?} before retrying", err, extended_delay_duration); - tokio::time::delay_for(extended_delay_duration).await; - continue; - } - Ok(messages) => messages, - }; - - // todo: this will probably need to be updated at some point due to changed structure - // of nym-sphinx messages sent. However, for time being this is still compatible. - // Basically it is more of a personal note of if client keeps getting errors and is - // not getting messages, look at this filter here. - let good_messages: Vec<_> = messages - .into_iter() - .filter(|message| message != loop_message && message != dummy_message) - .collect(); - trace!("Obtained the following messages: {:?}", good_messages); - - // if this one fails, there's no retrying because it means that either: - // - we run out of memory - // - the receiver channel is closed - // in either case there's no recovery and we can only panic - if !good_messages.is_empty() { - self.poller_tx.unbounded_send(good_messages).unwrap(); - } - - tokio::time::delay_for(self.polling_rate).await; - } - } - - pub(crate) fn start(mut self, handle: &Handle) -> JoinHandle<()> { - handle.spawn(async move { self.start_provider_polling().await }) - } -} diff --git a/clients/desktop/src/client/received_buffer.rs b/clients/desktop/src/client/received_buffer.rs index f50f46d7b7..4781ba4613 100644 --- a/clients/desktop/src/client/received_buffer.rs +++ b/clients/desktop/src/client/received_buffer.rs @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::client::provider_poller::PolledMessagesReceiver; use futures::channel::{mpsc, oneshot}; use futures::lock::Mutex; use futures::StreamExt; +use gateway_client::SphinxPacketReceiver; use log::*; +use mix_client::packet::LOOP_COVER_MESSAGE_PAYLOAD; use nymsphinx::chunking::reconstruction::MessageReconstructor; use std::sync::Arc; use tokio::runtime::Handle; @@ -64,6 +65,11 @@ impl ReceivedMessagesBuffer { let mut completed_messages = Vec::new(); let mut inner_guard = self.inner.lock().await; for msg_fragment in msgs { + if msg_fragment == LOOP_COVER_MESSAGE_PAYLOAD { + trace!("The message was a loop cover message! Skipping it"); + continue; + } + if let Some(reconstructed_message) = inner_guard.message_reconstructor.new_fragment(msg_fragment) { @@ -117,22 +123,22 @@ impl RequestReceiver { struct MessageReceiver { received_buffer: ReceivedMessagesBuffer, - poller_receiver: PolledMessagesReceiver, + sphinx_packet_receiver: SphinxPacketReceiver, } impl MessageReceiver { fn new( received_buffer: ReceivedMessagesBuffer, - poller_receiver: PolledMessagesReceiver, + sphinx_packet_receiver: SphinxPacketReceiver, ) -> Self { MessageReceiver { received_buffer, - poller_receiver, + sphinx_packet_receiver, } } fn start(mut self, handle: &Handle) -> JoinHandle<()> { handle.spawn(async move { - while let Some(new_messages) = self.poller_receiver.next().await { + while let Some(new_messages) = self.sphinx_packet_receiver.next().await { self.received_buffer .add_new_message_fragments(new_messages) .await; @@ -149,12 +155,15 @@ pub(crate) struct ReceivedMessagesBufferController { impl ReceivedMessagesBufferController { pub(crate) fn new( query_receiver: ReceivedBufferRequestReceiver, - poller_receiver: PolledMessagesReceiver, + sphinx_packet_receiver: SphinxPacketReceiver, ) -> Self { let received_buffer = ReceivedMessagesBuffer::new(); ReceivedMessagesBufferController { - messsage_receiver: MessageReceiver::new(received_buffer.clone(), poller_receiver), + messsage_receiver: MessageReceiver::new( + received_buffer.clone(), + sphinx_packet_receiver, + ), request_receiver: RequestReceiver::new(received_buffer, query_receiver), } } diff --git a/clients/desktop/src/client/topology_control.rs b/clients/desktop/src/client/topology_control.rs index 98bc254dea..21919edb33 100644 --- a/clients/desktop/src/client/topology_control.rs +++ b/clients/desktop/src/client/topology_control.rs @@ -56,6 +56,7 @@ impl TopologyAccessor { self.inner.lock().await.update(new_topology); } + // not removed until healtchecker is not fully changed to use gateways instead of providers pub(crate) async fn get_provider_socket_addr(&mut self, id: &str) -> Option { match &self.inner.lock().await.0 { None => None, @@ -67,7 +68,19 @@ impl TopologyAccessor { } } + pub(crate) async fn get_gateway_socket_url(&mut self, id: &str) -> Option { + match &self.inner.lock().await.0 { + None => None, + Some(ref topology) => topology + .gateways() + .iter() + .find(|gateway| gateway.pub_key == id) + .map(|gateway| gateway.client_listener.clone()), + } + } + // only used by the client at startup to get a slightly more reasonable error message + // (currently displays as unused because healthchecker is disabled due to required changes) pub(crate) async fn is_routable(&self) -> bool { match &self.inner.lock().await.0 { None => false, @@ -101,12 +114,12 @@ impl TopologyAccessor { match &self.inner.lock().await.0 { None => None, Some(ref topology) => { - let mut providers = topology.providers(); - if providers.is_empty() { + let mut gateways = topology.gateways(); + if gateways.is_empty() { return None; } // unwrap is fine here as we asserted there is at least single provider - let provider = providers.pop().unwrap().into(); + let provider = gateways.pop().unwrap().into(); topology.route_to(provider).ok() } } @@ -183,35 +196,35 @@ impl TopologyRefresher { async fn get_current_compatible_topology(&self) -> Result { let full_topology = T::new(self.directory_server.clone()); - let version_filtered_topology = full_topology.filter_node_versions( - built_info::PKG_VERSION, - built_info::PKG_VERSION, - built_info::PKG_VERSION, - ); + let version_filtered_topology = + full_topology.filter_system_version(built_info::PKG_VERSION); - let healthcheck_result = self - .health_checker - .do_check(&version_filtered_topology) - .await; - let healthcheck_scores = match healthcheck_result { - Err(err) => { - error!("Error while performing the healthcheck: {:?}", err); - return Err(TopologyError::HealthCheckError); - } - Ok(scores) => scores, - }; + // healthcheck needs some adjustments to work with gateways so for time being just dont run it + return Ok(version_filtered_topology); - debug!("{}", healthcheck_scores); - - let healthy_topology = healthcheck_scores - .filter_topology_by_score(&version_filtered_topology, self.node_score_threshold); - - // make sure you can still send a packet through the network: - if !healthy_topology.can_construct_path_through() { - return Err(TopologyError::NoValidPathsError); - } - - Ok(healthy_topology) + // let healthcheck_result = self + // .health_checker + // .do_check(&version_filtered_topology) + // .await; + // let healthcheck_scores = match healthcheck_result { + // Err(err) => { + // error!("Error while performing the healthcheck: {:?}", err); + // return Err(TopologyError::HealthCheckError); + // } + // Ok(scores) => scores, + // }; + // + // debug!("{}", healthcheck_scores); + // + // let healthy_topology = healthcheck_scores + // .filter_topology_by_score(&version_filtered_topology, self.node_score_threshold); + // + // // make sure you can still send a packet through the network: + // if !healthy_topology.can_construct_path_through() { + // return Err(TopologyError::NoValidPathsError); + // } + // + // Ok(healthy_topology) } pub(crate) async fn refresh(&mut self) { diff --git a/clients/desktop/src/commands/init.rs b/clients/desktop/src/commands/init.rs index bae272ced5..2e57b4e8ea 100644 --- a/clients/desktop/src/commands/init.rs +++ b/clients/desktop/src/commands/init.rs @@ -19,10 +19,13 @@ use clap::{App, Arg, ArgMatches}; use config::NymConfig; use crypto::identity::MixIdentityKeyPair; use directory_client::presence::Topology; +use futures::channel::mpsc; +use gateway_client::GatewayClient; +use gateway_requests::AuthToken; use nymsphinx::DestinationAddressBytes; use pemstore::pemstore::PemStore; -use sfw_provider_requests::auth_token::AuthToken; -use topology::provider::Node; +use std::time::Duration; +use topology::gateway::Node; use topology::NymTopology; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -34,9 +37,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .takes_value(true) .required(true) ) - .arg(Arg::with_name("provider") - .long("provider") - .help("Id of the provider we have preference to connect to. If left empty, a random provider will be chosen.") + .arg(Arg::with_name("gateway") + .long("gateway") + .help("Id of the gateway we have preference to connect to. If left empty, a random gateway will be chosen.") .takes_value(true) ) .arg(Arg::with_name("directory") @@ -57,61 +60,57 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { ) } -async fn try_provider_registrations( - providers: Vec, +async fn try_gateway_registration( + gateways: Vec, our_address: DestinationAddressBytes, ) -> Option<(String, AuthToken)> { - // we don't expect the response to be longer than AUTH_TOKEN_SIZE, but allow for more bytes - // in case there was an error message - let max_response_len = 16 * 1024; - // since the order of providers is non-deterministic we can just try to get a first 'working' provider - for provider in providers { - let mut provider_client = provider_client::ProviderClient::new( - provider.client_listener, + // TODO: having to do something like this suggests that perhaps GatewayClient's constructor + // could be improved + let (sphinx_tx, _) = mpsc::unbounded(); + let timeout = Duration::from_millis(1500); + for gateway in gateways { + let mut gateway_client = GatewayClient::new( + url::Url::parse(&gateway.client_listener).unwrap(), our_address.clone(), None, - max_response_len, + sphinx_tx.clone(), + timeout, ); - let auth_token = provider_client.register().await; - if let Ok(token) = auth_token { - return Some((provider.pub_key, token)); + if let Ok(_) = gateway_client.establish_connection().await { + if let Ok(token) = gateway_client.register().await { + return Some((gateway.pub_key, token)); + } } } None } -// in the long run this will be provider specific and only really applicable to a -// relatively small subset of all providers -async fn choose_provider( +async fn choose_gateway( directory_server: String, our_address: DestinationAddressBytes, ) -> (String, AuthToken) { // TODO: once we change to graph topology this here will need to be updated! let topology = Topology::new(directory_server.clone()); - let version_filtered_topology = topology.filter_node_versions( - built_info::PKG_VERSION, - built_info::PKG_VERSION, - built_info::PKG_VERSION, - ); + let version_filtered_topology = topology.filter_system_version(built_info::PKG_VERSION); // don't care about health of the networks as mixes can go up and down any time, - // but DO care about providers - let providers = version_filtered_topology.providers(); + // but DO care about gateways + let gateways = version_filtered_topology.gateways(); // try to perform registration so that we wouldn't need to do it at startup - // + at the same time we'll know if we can actually talk with that provider - let registration_result = try_provider_registrations(providers, our_address).await; + // + at the same time we'll know if we can actually talk with that gateway + let registration_result = try_gateway_registration(gateways, our_address).await; match registration_result { None => { // while technically there's no issue client-side, it will be impossible to execute - // `nym-client run` as no provider is available so it might be best to not finalize + // `nym-client run` as no gateway is available so it might be best to not finalize // the init and rely on users trying to init another time? panic!( - "Currently there are no valid providers available on the network ({}). \ + "Currently there are no valid gateways available on the network ({}). \ Please try to run `init` again at later time or change your directory server", directory_server ) } - Some((provider_id, auth_token)) => (provider_id, auth_token), + Some((gateway_id, auth_token)) => (gateway_id, auth_token), } } @@ -125,16 +124,18 @@ pub fn execute(matches: &ArgMatches) { let mix_identity_keys = MixIdentityKeyPair::new(); - // if there is no provider chosen, get a random-ish one from the topology - if config.get_provider_id().is_empty() { + // if there is no gateway chosen, get a random-ish one from the topology + if config.get_gateway_id().is_empty() { let our_address = mix_identity_keys.public_key().derive_address(); // TODO: is there perhaps a way to make it work without having to spawn entire runtime? let mut rt = tokio::runtime::Runtime::new().unwrap(); - let (provider_id, auth_token) = - rt.block_on(choose_provider(config.get_directory_server(), our_address)); + let (gateway_id, auth_token) = + rt.block_on(choose_gateway(config.get_directory_server(), our_address)); + + // TODO: this isn't really a gateway, but gateway, yet another change to make config = config - .with_provider_id(provider_id) - .with_provider_auth_token(auth_token); + .with_gateway_id(gateway_id) + .with_gateway_auth_token(auth_token); } let pathfinder = ClientPathfinder::new_from_config(&config); @@ -151,13 +152,13 @@ pub fn execute(matches: &ArgMatches) { println!("Saved configuration file to {:?}", config_save_location); println!( - "Unless overridden in all `nym-client run` we will be talking to the following provider: {}...", - config.get_provider_id(), + "Unless overridden in all `nym-client run` we will be talking to the following gateway: {}...", + config.get_gateway_id(), ); - if config.get_provider_auth_token().is_some() { + if config.get_gateway_auth_token().is_some() { println!( "using optional AuthToken: {:?}", - config.get_provider_auth_token().unwrap() + config.get_gateway_auth_token().unwrap() ) } println!("Client configuration completed.\n\n\n") diff --git a/clients/desktop/src/commands/mod.rs b/clients/desktop/src/commands/mod.rs index 3104104595..fc1329caa5 100644 --- a/clients/desktop/src/commands/mod.rs +++ b/clients/desktop/src/commands/mod.rs @@ -23,8 +23,8 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Confi config = config.with_custom_directory(directory); } - if let Some(provider_id) = matches.value_of("provider") { - config = config.with_provider_id(provider_id); + if let Some(gateway_id) = matches.value_of("gateway") { + config = config.with_gateway_id(gateway_id); } if let Some(socket_type) = matches.value_of("socket-type") { diff --git a/clients/desktop/src/commands/run.rs b/clients/desktop/src/commands/run.rs index 9a6ee572a9..07048a071a 100644 --- a/clients/desktop/src/commands/run.rs +++ b/clients/desktop/src/commands/run.rs @@ -38,9 +38,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .help("Address of the directory server the client is getting topology from") .takes_value(true), ) - .arg(Arg::with_name("provider") - .long("provider") - .help("Id of the provider we want to connect to. If overridden, it is user's responsibility to ensure prior registration happened") + .arg(Arg::with_name("gateway") + .long("gateway") + .help("Id of the gateway we want to connect to. If overridden, it is user's responsibility to ensure prior registration happened") .takes_value(true) ) .arg(Arg::with_name("socket-type") diff --git a/clients/desktop/src/config/mod.rs b/clients/desktop/src/config/mod.rs index 7fc1cd08d0..68e5d01b7d 100644 --- a/clients/desktop/src/config/mod.rs +++ b/clients/desktop/src/config/mod.rs @@ -29,20 +29,15 @@ const DEFAULT_LISTENING_PORT: u16 = 9001; const DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY: u64 = 1000; // 1s const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: u64 = 500; // 0.5s const DEFAULT_AVERAGE_PACKET_DELAY: u64 = 200; // 0.2s -const DEFAULT_FETCH_MESSAGES_DELAY: u64 = 1000; // 1s const DEFAULT_TOPOLOGY_REFRESH_RATE: u64 = 30_000; // 30s const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: u64 = 5_000; // 5s -const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: u64 = 10_000; // 10s -const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: u64 = 300_000; // 5min -const DEFAULT_INITIAL_CONNECTION_TIMEOUT: u64 = 1_500; // 1.5s -const DEFAULT_HEALTHCHECK_CONNECTION_TIMEOUT: u64 = DEFAULT_INITIAL_CONNECTION_TIMEOUT; + +const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: u64 = 1_500; // 1.5s +const DEFAULT_HEALTHCHECK_CONNECTION_TIMEOUT: u64 = 1_500; // 1.5s const DEFAULT_NUMBER_OF_HEALTHCHECK_TEST_PACKETS: u64 = 2; const DEFAULT_NODE_SCORE_THRESHOLD: f64 = 0.0; -// for time being treat it as if there was no limit -const DEFAULT_MAX_RESPONSE_SIZE: u32 = u32::max_value(); - #[derive(Debug, Deserialize, PartialEq, Serialize, Clone, Copy)] #[serde(deny_unknown_fields)] pub enum SocketType { @@ -130,13 +125,13 @@ impl Config { self } - pub fn with_provider_id>(mut self, id: S) -> Self { - self.client.provider_id = id.into(); + pub fn with_gateway_id>(mut self, id: S) -> Self { + self.client.gateway_id = id.into(); self } - pub fn with_provider_auth_token>(mut self, token: S) -> Self { - self.client.provider_authtoken = Some(token.into()); + pub fn with_gateway_auth_token>(mut self, token: S) -> Self { + self.client.gateway_authtoken = Some(token.into()); self } @@ -172,12 +167,12 @@ impl Config { self.client.directory_server.clone() } - pub fn get_provider_id(&self) -> String { - self.client.provider_id.clone() + pub fn get_gateway_id(&self) -> String { + self.client.gateway_id.clone() } - pub fn get_provider_auth_token(&self) -> Option { - self.client.provider_authtoken.clone() + pub fn get_gateway_auth_token(&self) -> Option { + self.client.gateway_authtoken.clone() } pub fn get_socket_type(&self) -> SocketType { @@ -197,16 +192,12 @@ impl Config { time::Duration::from_millis(self.debug.loop_cover_traffic_average_delay) } - pub fn get_fetch_message_delay(&self) -> time::Duration { - time::Duration::from_millis(self.debug.fetch_message_delay) - } - pub fn get_message_sending_average_delay(&self) -> time::Duration { time::Duration::from_millis(self.debug.message_sending_average_delay) } - pub fn get_rate_compliant_cover_messages_disabled(&self) -> bool { - self.debug.rate_compliant_cover_messages_disabled + pub fn get_gateway_response_timeout(&self) -> time::Duration { + time::Duration::from_millis(self.debug.gateway_response_timeout) } pub fn get_topology_refresh_rate(&self) -> time::Duration { @@ -225,25 +216,9 @@ impl Config { self.debug.node_score_threshold } - pub fn get_packet_forwarding_initial_backoff(&self) -> time::Duration { - time::Duration::from_millis(self.debug.packet_forwarding_initial_backoff) - } - - pub fn get_packet_forwarding_maximum_backoff(&self) -> time::Duration { - time::Duration::from_millis(self.debug.packet_forwarding_maximum_backoff) - } - - pub fn get_initial_connection_timeout(&self) -> time::Duration { - time::Duration::from_millis(self.debug.initial_connection_timeout) - } - pub fn get_healthcheck_connection_timeout(&self) -> time::Duration { time::Duration::from_millis(self.debug.healthcheck_connection_timeout) } - - pub fn get_max_response_size(&self) -> usize { - self.debug.max_response_size as usize - } } fn de_option_string<'de, D>(deserializer: D) -> Result, D::Error> @@ -273,14 +248,14 @@ pub struct Client { /// Path to file containing public identity key. public_identity_key_file: PathBuf, - /// provider_id specifies ID of the provider to which the client should send messages. - /// If initially omitted, a random provider will be chosen from the available topology. - provider_id: String, + /// gateway_id specifies ID of the gateway to which the client should send messages. + /// If initially omitted, a random gateway will be chosen from the available topology. + gateway_id: String, - /// A provider specific, optional, base58 stringified authentication token used for - /// communication with particular provider. + /// A gateway specific, optional, base58 stringified authentication token used for + /// communication with particular gateway. #[serde(deserialize_with = "de_option_string")] - provider_authtoken: Option, + gateway_authtoken: Option, /// nym_home_directory specifies absolute path to the home nym Clients directory. /// It is expected to use default value and hence .toml file should not redefine this field. @@ -295,8 +270,8 @@ impl Default for Client { directory_server: Self::default_directory_server(), private_identity_key_file: Default::default(), public_identity_key_file: Default::default(), - provider_id: "".to_string(), - provider_authtoken: None, + gateway_id: "".to_string(), + gateway_authtoken: None, nym_root_directory: Config::default_root_directory(), } } @@ -362,10 +337,6 @@ pub struct Debug { /// The provided value is interpreted as milliseconds. loop_cover_traffic_average_delay: u64, - /// The uniform delay every which clients are querying the providers for received packets. - /// The provided value is interpreted as milliseconds. - fetch_message_delay: u64, - /// The parameter of Poisson distribution determining how long, on average, /// it is going to take another 'real traffic stream' message to be sent. /// If no real packets are available and cover traffic is enabled, @@ -373,11 +344,10 @@ pub struct Debug { /// The provided value is interpreted as milliseconds. message_sending_average_delay: u64, - /// Whether loop cover messages should be sent to respect message_sending_rate. - /// In the case of it being disabled and not having enough real traffic - /// waiting to be sent the actual sending rate is going be lower than the desired value - /// thus decreasing the anonymity. - rate_compliant_cover_messages_disabled: bool, + /// How long we're willing to wait for a response to a message sent to the gateway, + /// before giving up on it. + /// The provided value is interpreted as milliseconds. + gateway_response_timeout: u64, /// The uniform delay every which clients are querying the directory server /// to try to obtain a compatible network topology to send sphinx packets through. @@ -398,29 +368,10 @@ pub struct Debug { /// considered healthy. node_score_threshold: f64, - /// Initial value of an exponential backoff to reconnect to dropped TCP connection when - /// forwarding sphinx packets. - /// The provided value is interpreted as milliseconds. - packet_forwarding_initial_backoff: u64, - - /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when - /// forwarding sphinx packets. - /// The provided value is interpreted as milliseconds. - packet_forwarding_maximum_backoff: u64, - - /// Timeout for establishing initial connection when trying to forward a sphinx packet. - /// The provider value is interpreted as milliseconds. - initial_connection_timeout: u64, - /// Timeout for establishing initial connection when trying to forward a sphinx packet /// during healthcheck. - /// The provider value is interpreted as milliseconds. + /// The provided value is interpreted as milliseconds. healthcheck_connection_timeout: u64, - - /// Maximum allowed length for sfw-provider responses received. - /// Anything declaring bigger size than that will be regarded as an error and - /// is going to be rejected. - max_response_size: u32, } impl Default for Debug { @@ -428,18 +379,13 @@ impl Default for Debug { Debug { average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, loop_cover_traffic_average_delay: DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY, - fetch_message_delay: DEFAULT_FETCH_MESSAGES_DELAY, message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, - rate_compliant_cover_messages_disabled: false, + gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, number_of_healthcheck_test_packets: DEFAULT_NUMBER_OF_HEALTHCHECK_TEST_PACKETS, node_score_threshold: DEFAULT_NODE_SCORE_THRESHOLD, - 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, healthcheck_connection_timeout: DEFAULT_HEALTHCHECK_CONNECTION_TIMEOUT, - max_response_size: DEFAULT_MAX_RESPONSE_SIZE, } } } diff --git a/clients/desktop/src/config/template.rs b/clients/desktop/src/config/template.rs index 2b2e4c9a79..f06acab0e1 100644 --- a/clients/desktop/src/config/template.rs +++ b/clients/desktop/src/config/template.rs @@ -38,12 +38,12 @@ public_identity_key_file = '{{ client.public_identity_key_file }}' ##### additional client config options ##### -# ID of the provider from which the client should be fetching messages. -provider_id = '{{ client.provider_id }}' +# ID of the gateway from which the client should be fetching messages. +gateway_id = '{{ client.gateway_id }}' -# A provider specific, optional, base58 stringified authentication token used for -# communication with particular provider. -provider_authtoken = '{{ client.provider_authtoken }}' +# A gateway specific, optional, base58 stringified authentication token used for +# communication with particular gateway. +gateway_authtoken = '{{ client.gateway_authtoken }}' ##### advanced configuration options ##### @@ -78,7 +78,6 @@ listening_port = {{ socket.listening_port }} average_packet_delay = {{ debug.average_packet_delay }} loop_cover_traffic_average_delay = {{ debug.loop_cover_traffic_average_delay }} -fetch_message_delay = {{ debug.fetch_message_delay }} message_sending_average_delay = {{ debug.message_sending_average_delay }} "# diff --git a/clients/desktop/src/main.rs b/clients/desktop/src/main.rs index cb68443b0e..7890cbeb48 100644 --- a/clients/desktop/src/main.rs +++ b/clients/desktop/src/main.rs @@ -80,5 +80,7 @@ fn setup_logging() { .filter_module("reqwest", log::LevelFilter::Warn) .filter_module("mio", log::LevelFilter::Warn) .filter_module("want", log::LevelFilter::Warn) + .filter_module("tungstenite", log::LevelFilter::Warn) + .filter_module("tokio_tungstenite", log::LevelFilter::Warn) .init(); } diff --git a/clients/desktop/src/sockets/websocket/connection.rs b/clients/desktop/src/sockets/websocket/connection.rs index 46950c29b0..43f4be2125 100644 --- a/clients/desktop/src/sockets/websocket/connection.rs +++ b/clients/desktop/src/sockets/websocket/connection.rs @@ -87,9 +87,15 @@ impl Connection { fn handle_text_send(&self, msg: String, recipient_address: String) -> ServerResponse { let message_bytes = msg.into_bytes(); - // TODO: the below can panic if recipient_address is malformed, but it should be - // resolved when refactoring sphinx code to make `from_base58_string` return a Result - let address = DestinationAddressBytes::from_base58_string(recipient_address); + let address = match DestinationAddressBytes::try_from_base58_string(recipient_address) { + Ok(address) => address, + Err(e) => { + trace!("failed to parse received DestinationAddress: {:?}", e); + return ServerResponse::Error { + message: "malformed destination address".to_string(), + }; + } + }; let dummy_surb = [0; 16]; // in case the message is too long and needs to be split into multiple packets: diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index 779c32c8b7..11a026fc0e 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -62,10 +62,11 @@ pub fn create_sphinx_packet(topology_json: &str, msg: &str, destination: &str) - let average_delay = Duration::from_secs_f64(0.1); let delays = delays::generate_from_average_duration(route.len(), average_delay); - let dest_bytes = DestinationAddressBytes::from_base58_string(destination.to_owned()); + let dest_bytes = + DestinationAddressBytes::try_from_base58_string(destination.to_owned()).unwrap(); let dest = Destination::new(dest_bytes, [4u8; IDENTIFIER_LENGTH]); let message = split_and_prepare_payloads(&msg.as_bytes()).pop().unwrap(); - let sphinx_packet = match SphinxPacket::new(message, &route, &dest, &delays).unwrap() { + let sphinx_packet = match SphinxPacket::new(message, &route, &dest, &delays, None).unwrap() { SphinxPacket { header, payload } => SphinxPacket { header, payload }, }; diff --git a/common/client-libs/directory-client/src/lib.rs b/common/client-libs/directory-client/src/lib.rs index be0f49c85a..8a4160ab48 100644 --- a/common/client-libs/directory-client/src/lib.rs +++ b/common/client-libs/directory-client/src/lib.rs @@ -18,6 +18,9 @@ use crate::requests::metrics_mixes_post::{MetricsMixPoster, Request as MetricsMi use crate::requests::presence_coconodes_post::{ PresenceCocoNodesPoster, Request as PresenceCocoNodesPost, }; +use crate::requests::presence_gateways_post::{ + PresenceGatewayPoster, Request as PresenceGatewayPost, +}; use crate::requests::presence_mixnodes_post::{ PresenceMixNodesPoster, Request as PresenceMixNodesPost, }; @@ -51,6 +54,7 @@ pub struct Client { pub metrics_mixes: MetricsMixRequest, pub metrics_post: MetricsMixPost, pub presence_coconodes_post: PresenceCocoNodesPost, + pub presence_gateway_post: PresenceGatewayPost, pub presence_mix_nodes_post: PresenceMixNodesPost, pub presence_providers_post: PresenceProvidersPost, pub presence_topology: PresenceTopologyRequest, @@ -63,6 +67,7 @@ impl DirectoryClient for Client { let metrics_post = MetricsMixPost::new(config.base_url.clone()); let presence_topology = PresenceTopologyRequest::new(config.base_url.clone()); let presence_coconodes_post = PresenceCocoNodesPost::new(config.base_url.clone()); + let presence_gateway_post = PresenceGatewayPost::new(config.base_url.clone()); let presence_mix_nodes_post = PresenceMixNodesPost::new(config.base_url.clone()); let presence_providers_post = PresenceProvidersPost::new(config.base_url); Client { @@ -70,6 +75,7 @@ impl DirectoryClient for Client { metrics_mixes, metrics_post, presence_coconodes_post, + presence_gateway_post, presence_mix_nodes_post, presence_providers_post, presence_topology, diff --git a/common/client-libs/directory-client/src/presence/gateways.rs b/common/client-libs/directory-client/src/presence/gateways.rs new file mode 100644 index 0000000000..78ffa78f44 --- /dev/null +++ b/common/client-libs/directory-client/src/presence/gateways.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde::{Deserialize, Serialize}; +use topology::gateway; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GatewayPresence { + pub location: String, + pub client_listener: String, + pub mixnet_listener: String, + pub pub_key: String, + pub registered_clients: Vec, + pub last_seen: u64, + pub version: String, +} + +impl Into for GatewayPresence { + fn into(self) -> topology::gateway::Node { + topology::gateway::Node { + location: self.location, + client_listener: self.client_listener.parse().unwrap(), + mixnet_listener: self.mixnet_listener.parse().unwrap(), + pub_key: self.pub_key, + registered_clients: self + .registered_clients + .into_iter() + .map(|c| c.into()) + .collect(), + last_seen: self.last_seen, + version: self.version, + } + } +} + +impl From for GatewayPresence { + fn from(mpn: gateway::Node) -> Self { + GatewayPresence { + location: mpn.location, + client_listener: mpn.client_listener.to_string(), + mixnet_listener: mpn.mixnet_listener.to_string(), + pub_key: mpn.pub_key, + registered_clients: mpn + .registered_clients + .into_iter() + .map(|c| c.into()) + .collect(), + last_seen: mpn.last_seen, + version: mpn.version, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GatewayClient { + pub pub_key: String, +} + +impl Into for GatewayClient { + fn into(self) -> topology::gateway::Client { + topology::gateway::Client { + pub_key: self.pub_key, + } + } +} + +impl From for GatewayClient { + fn from(mpc: topology::gateway::Client) -> Self { + GatewayClient { + pub_key: mpc.pub_key, + } + } +} diff --git a/common/client-libs/directory-client/src/presence/mod.rs b/common/client-libs/directory-client/src/presence/mod.rs index fa7bb93df7..cea08e9efc 100644 --- a/common/client-libs/directory-client/src/presence/mod.rs +++ b/common/client-libs/directory-client/src/presence/mod.rs @@ -17,12 +17,10 @@ use crate::{Client, Config, DirectoryClient}; use log::*; use serde::{Deserialize, Serialize}; use std::convert::TryInto; -use topology::coco; -use topology::mix; -use topology::provider; -use topology::NymTopology; +use topology::{coco, gateway, mix, provider, NymTopology}; pub mod coconodes; +pub mod gateways; pub mod mixnodes; pub mod providers; @@ -33,9 +31,11 @@ pub struct Topology { pub coco_nodes: Vec, pub mix_nodes: Vec, pub mix_provider_nodes: Vec, + pub gateway_nodes: Vec, } impl NymTopology for Topology { + // TODO: this will need some changes to not imply having to make an HTTP request in constructor fn new(directory_server: String) -> Self { debug!("Using directory server: {:?}", directory_server); let directory_config = Config { @@ -53,6 +53,7 @@ impl NymTopology for Topology { mix_nodes: Vec, mix_provider_nodes: Vec, coco_nodes: Vec, + gateway_nodes: Vec, ) -> Self { Topology { coco_nodes: coco_nodes.into_iter().map(|node| node.into()).collect(), @@ -61,6 +62,7 @@ impl NymTopology for Topology { .into_iter() .map(|node| node.into()) .collect(), + gateway_nodes: gateway_nodes.into_iter().map(|node| node.into()).collect(), } } @@ -78,6 +80,13 @@ impl NymTopology for Topology { .collect() } + fn gateways(&self) -> Vec { + self.gateway_nodes + .iter() + .map(|x| x.clone().into()) + .collect() + } + fn coco_nodes(&self) -> Vec { self.coco_nodes.iter().map(|x| x.clone().into()).collect() } diff --git a/common/client-libs/directory-client/src/requests/mod.rs b/common/client-libs/directory-client/src/requests/mod.rs index 6490770e73..3ac0f648fa 100644 --- a/common/client-libs/directory-client/src/requests/mod.rs +++ b/common/client-libs/directory-client/src/requests/mod.rs @@ -16,6 +16,7 @@ pub mod health_check_get; pub mod metrics_mixes_get; pub mod metrics_mixes_post; pub mod presence_coconodes_post; +pub mod presence_gateways_post; pub mod presence_mixnodes_post; pub mod presence_providers_post; pub mod presence_topology_get; diff --git a/common/client-libs/directory-client/src/requests/presence_gateways_post.rs b/common/client-libs/directory-client/src/requests/presence_gateways_post.rs new file mode 100644 index 0000000000..c15779f2a5 --- /dev/null +++ b/common/client-libs/directory-client/src/requests/presence_gateways_post.rs @@ -0,0 +1,103 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::presence::gateways::GatewayPresence; +use reqwest::Response; + +pub struct Request { + base_url: String, + path: String, +} + +pub trait PresenceGatewayPoster { + fn new(base_url: String) -> Self; + fn post(&self, presence: &GatewayPresence) -> Result; +} + +impl PresenceGatewayPoster for Request { + fn new(base_url: String) -> Self { + Request { + base_url, + path: "/api/presence/gateways".to_string(), + } + } + + fn post(&self, presence: &GatewayPresence) -> Result { + let url = format!("{}{}", self.base_url, self.path); + let client = reqwest::Client::new(); + let p = client.post(&url).json(&presence).send()?; + Ok(p) + } +} + +#[cfg(test)] +mod metrics_get_request { + use super::*; + + #[cfg(test)] + use mockito::mock; + + #[cfg(test)] + mod on_a_400_status { + use super::*; + + #[test] + fn it_returns_an_error() { + let _m = mock("POST", "/api/presence/gateways") + .with_status(400) + .create(); + let req = Request::new(mockito::server_url()); + let presence = fixtures::new_presence(); + let result = req.post(&presence); + assert_eq!(400, result.unwrap().status()); + _m.assert(); + } + } + + #[cfg(test)] + mod on_a_200 { + use super::*; + #[test] + fn it_returns_a_response_with_201() { + let json = r#"{ + "ok": true + }"#; + let _m = mock("POST", "/api/presence/gateways") + .with_status(201) + .with_body(json) + .create(); + let req = Request::new(mockito::server_url()); + let presence = fixtures::new_presence(); + let result = req.post(&presence); + assert_eq!(true, result.is_ok()); + _m.assert(); + } + } + #[cfg(test)] + mod fixtures { + use crate::presence::gateways::GatewayPresence; + + pub fn new_presence() -> GatewayPresence { + GatewayPresence { + location: "foomp".to_string(), + client_listener: "foo.com".to_string(), + mixnet_listener: "foo.com".to_string(), + pub_key: "abc".to_string(), + registered_clients: vec![], + last_seen: 0, + version: "0.1.0".to_string(), + } + } + } +} diff --git a/common/client-libs/directory-client/src/requests/presence_topology_get.rs b/common/client-libs/directory-client/src/requests/presence_topology_get.rs index deffca83ab..21b714ff4a 100644 --- a/common/client-libs/directory-client/src/requests/presence_topology_get.rs +++ b/common/client-libs/directory-client/src/requests/presence_topology_get.rs @@ -71,7 +71,7 @@ mod topology_requests { .create(); let req = Request::new(mockito::server_url()); let result = req.get(); - assert_eq!(true, result.is_ok()); + assert!(result.is_ok()); assert_eq!( 1575915097085539300, result.unwrap().coco_nodes.first().unwrap().last_seen @@ -84,263 +84,162 @@ mod topology_requests { #[cfg(test)] pub fn topology_response_json() -> String { r#"{ - "cocoNodes": [ - { - "location": "unknown", - "host": "3.8.244.109:4000", - "pubKey": "AAAAAAAAAAEKwAECSqKy8I8KkSYIBSctxRBRxuR61PpAOwK0UQtkeuPRdwusAyaoBbvv1IBWyMEhvbgT4CtgUnGfYH2s06CIJ09lWWvQ0Jkgthq12mG73H9QSTNM8RITlF1X5ax9BV0EK34M5dUncn1uEYzJzcbaLjUarf2bqoy906dtQpppUWDRLJI6ycw7rKKJ4ZNUhgi4KAEGBsSgLqc0zDKs0rArwouZyz4ofoWnY68mdJKrVy6Zqz83DSdc7B2hqqkHX_Bfeb4SwAEFuhRpy4HfcuxwcRI9sIWMo_LVmbk19g1gfMRlBrmZqoEQL6rDApVLZ9eMp-5IQK8WLlZpWf4Zjy7kZolARAyp_rHUQkH4PrDjgoPrKbm6qK_iejYpL7qx28Q3VeInMpwMIMaSbbW9y36sEVtGc2I0Iu5vS0sp8ESiVlQ5NaBz72deZ8oKJJ4IEPPHP99-b0UQX80fVIrNM88mMzKy0bHri9NFlmIG-e0G1cqmw_ry3XWGQkcr1M5RuNa6oX50w5QawAEVxd5FP5bE8bS4x54Csof11sQWUTwMp6Q7_3H7ZCTSlKSqujlOhmfqSHfGPO2sDIYPHDhDzjakZpKAZWWhn_hiR6DfPpomQ01ZYUhVKKSMxz7_VPjsQplP0bZXA2gfnkADUN8UQ0N9g_usIw73r4aZsOviMsRM8oByvsjVfUWc4_HTLSdnQyImFkHz9CiCmrIYL2dYQRePRatWggvBAyeRzntxI4jDqLKiBdi54ZlAKgV6MCRaJ7Bu7BtmLXrtK4sawAED3QYxuvOSZrbZdUr4yG-U9yVvJ9Klkf-5Mo4EYp3qTL2KBB6_LrZepjAQqp486YkZ03mTIezcsZ48EboXVTWKBZ3QnTI5tX-j4gGxQb7klOJc97qJkDxsvpz4F0ChgCUIZhpIItWHia7_R3Gi-b5siLIdQdUho9isn3kiDGm6t0NED2Bgy3ZxxQwzqsBZm4kPr2_fPX4YyvIoP9895YcGjZyE5iiRC_TE41RJmB1GZYdxegTMq3lNDllKgiqaiPgawAEJASDkmZHTwlg9YOev5OWpQD-FnhPkqVNo_QcDyRu9eoGcWSGFp2sYqjG2SpmiXq0VNnAO7AcKxRzDFu7TjfhlU3Kt0uTKIcrWVU1zFNbJNMjYEq90pp50nowwx8INz20IXET2ZNX6kIXYFCsEvPLZFlG2OoL6xg3uQS1qMl3lIS_VxdO_JfVe0rT65WsJ_P4Nkc1jYiuNPHY6d_iFO0BVYqX0sOCX73GC_TT13BR0jnPwDAVw0rGtYHsXBb8TKOsawAEZIClauuT1V3qOZnb7uRZhFXO-PKTxgc1LCzJt2ChOrMZaBpjlkf3IPpJ2UF4JH4kGaDeBf2k_S-FLAs3drK21efbi5P6_a4QTxAiiRimXGoQIyvOg462s6kP_ZRFufo8YYQHS4olaOeqU4564dNskg_uBPsFMz_2GNOhmn_15cJqP1jfkyD49Z16GTS5YLHgVl9bJKqyvLuypsToLbt1BJzipEP0L2OohuRm-_MvqvwwWKyjNQsubgee1K728d9AawAEBkGggcNVCtXyhoSqi3_w0tVxtkAYeud8sBeAtZHGs06me_QL8co0MFLlO-zdkUb4ZBq08rFEbgLOma8_3whleM8NIPaHNISp1q3IsIhB5zdXcZoGsqLixODBFHtID3YEHAlr4f9T_yh11yJ95xGCl_6Y37hpwLQVGyrfSfccM24mVFqnV3TT5Wdq3ile-jesUx1Q2G1yK_xVqc6itmk-kDuBjyZgzYi1-jsIXAjnhM9G7t8J_Bv5yGGZhLK2dCzM=", - "type": "validator", - "lastSeen": 1575915097085539300, - "version": "0.1.0" - }, - { - "location": "unknown", - "host": "3.9.129.61:4000", - "pubKey": "AAAAAAAAAAMKwAECSqKy8I8KkSYIBSctxRBRxuR61PpAOwK0UQtkeuPRdwusAyaoBbvv1IBWyMEhvbgT4CtgUnGfYH2s06CIJ09lWWvQ0Jkgthq12mG73H9QSTNM8RITlF1X5ax9BV0EK34M5dUncn1uEYzJzcbaLjUarf2bqoy906dtQpppUWDRLJI6ycw7rKKJ4ZNUhgi4KAEGBsSgLqc0zDKs0rArwouZyz4ofoWnY68mdJKrVy6Zqz83DSdc7B2hqqkHX_Bfeb4SwAEEv6RMevAQmLGkeK0uJKnMPPAtm8GgXjWSQijYdnxlPh5SJSNeJUbPZKWFFWdk8yIFXKa8jnzETtdGFKgUUt5AVUDpTBmEdwaHCzlFhXrttshy0V5OhPUlV8cGABmxbagMYm0bFPg0r-snSkrB9YG6wqJYQVeIMOCGYCPbHmDA8R_0-h8VkRKWs1d9KvQOK4kShqgZtYN71KJW8uDE4q2jsGDVvxFt1AgmU9b93xsXF17KrpZy5WxlLZ73HtnTD_oawAED4vd_rK-Kx_n8x_OdDiiEOPUlYDlDCQUqenU9XHKH3B6ijfkJ368wd3LDDVStjDwNORrAyUSw_VlSNUpd1XLC8d17gTaIq5ZI2fWuwwZaoN1JCsYU8fQ6USgtIehQX7IPP8EkFuNmuCBCmpr4schtYniGe9J8Q4dsV-TYPr2uLJkdx1r7luzF--I22k7NfQQM14QDci_0kgrgmZ54CJGkjXyOhCppBXg3fqLC6aFvT3ZocfiiXBJt0huGgPMDtYsawAECLh8KUdNsDolERwJ8v04bS5jI_KKf7uUnCHWuCELwbJSUI3OK1ufS1qSpauvSzVQSbrhEzrEfwQn4VtxQxJlX4UdDU-R-hafiZvVC6DLLAbuORBAC3FScn9W58CnezH4DvCp_w7nftDfdxeuungbZT9XaxS3iNC6PnFsWF6WM3DxMwrzOrFe6wEEoTSPe1mcUDrtwM5UksIvJr6MBRAXrdl0IdBTQr7cLwKe_KYi4siwdjfJEJtOh7oxQBxBg2UkawAEJAPZK2Gg2MQwpxdDT24lNQHF7FVfkO_LuhJwn0RbwNDSVeA4P6-tWL5TkCpqr8xYHfwQ6Z3ILfpGCZr8PspwIoRzqZHQ16f8Pq9xnr0hLEI9BOQU0FS2EtuyPgju5iwsAJAfehUzu6kNLphuLGsXoIZdXDG5mbylwh9JzAVXTwgaR0hNqyXVJxgbt7jcYaSEBFcMGV-hjXyVVNzBleE-G9o_noI_KWU4Ce7K-qOMcewMKfy_VEw-gVaD6dHz6AMoawAEE9XuOLwRttvKybAssZ9gsK-_YRUwuFOeRDIr3NX___9bx6pCc18adCIlH_8EJWFwXZ05ZpNNE88mYx7ZQ3aqaArZJRoWeZeKhqH_s05V10xbzkYX71G5cqz--8vr9ZlQRb2BeETF_Tdq_PLk7qbT8WTGIoq7ZwyDRQTgzvkCgyzj_hBLh2o7sSVNgUo38SFUTMn7YtvVFYlSrTDE3WKE-T-nh5SWdDBxgDTc3Bw8JpzNH-WkoJ4Lim7sB4Op1gEUawAEW4-kenlffwsNr_3b3aV0YuusLpxB03sxPzQ5B0CWNiVtbja1Z4tWhKGUUrdq_eUgMV0y5Of-BqNi5FspAQnhJBFSSxtOzRGV1h3qyUTksfZyed9z8zPI-ZPP9XXm7hYgJgDz_kxte-NfS9UG9q5AZetHUN4kGxXutjjzfUQZ9yTvhBKgKgTI2Dp_R_jZrWQ8F1BoWzIJzjddT1K2MvCQEkARYw08isbOeFmCwgVUcjxYZO45WyOmLQA7QJRL9WvA=", - "type": "validator", - "lastSeen": 1575915097388409000, - "version": "0.1.0" - }, - { - "location": "unknown", - "host": "3.9.222.1:4000", - "pubKey": "AAAAAAAAAAQKwAECSqKy8I8KkSYIBSctxRBRxuR61PpAOwK0UQtkeuPRdwusAyaoBbvv1IBWyMEhvbgT4CtgUnGfYH2s06CIJ09lWWvQ0Jkgthq12mG73H9QSTNM8RITlF1X5ax9BV0EK34M5dUncn1uEYzJzcbaLjUarf2bqoy906dtQpppUWDRLJI6ycw7rKKJ4ZNUhgi4KAEGBsSgLqc0zDKs0rArwouZyz4ofoWnY68mdJKrVy6Zqz83DSdc7B2hqqkHX_Bfeb4SwAECh9xcxpjOp1r7kiNIgrI9GgAlvXwgHkTchOxUiyOzTq6FDWdGN64KiC3NDeyGTg8FmzvGzS3jREeJqOdr4G9ZGtWkauAITgLFiH62t-YntRslhr8_1shxlmzKiNKJN_QFflEq79pZIlWtp3N8LIHMvXRtl-zt2DMze4s02XDmEkviyVE4CkQUDtCc-2MfPT4JcmEFqtFIxjrXn18SbYg3c6XUQHsGIkuDrKuCTRlpC8kvmM0uVoIeWdmwDlZk4jUawAEJhRwK5ozjqIWRP1bFzBPS9VhaJnfKU9PeFYtN5beiAHrYr2ylIB3yDfmAQUdKDowDUm5nfJATejEjEnrTGxh70QtfoNV391rSns3F71tBwY62KLaNr8qnVfeSFHV3FcQTMHHF_8mDb5_11Rj6aiMvW0y6eetHo7CDPMdEyDPmok_U2ZM5BzOUnwjT21HtnvcKxKKwHJ_QGfnAHPyDIhNOMgxJCrVazOidLCHeYGpyCLw1ipeTyKOQX0_ByB8dH6AawAEGV1GuF5SSlT67B1ityPJK2ZwXjeeKB4gGdCG3qRtWxLTZfGhVm7YAYm2f5tw_wrsJAZ9FubVhateGg0ZN67NxZtsvOOejXz6743f7ijnQopPgd_8pH-iVf6BEcSO8ZdcHxNRUTayzjVLs99bwMo2zaPevW4X4G_bN4mh---aPkdGYHwaiklzUhqJ-eqycrYAFyjyEXaPBXLQm1rpczqluNvnKbd8Q9LZWukgm7_uWv_HxufIvdWgoq8bAt78UU3oawAEP9VDehhqrQG5-WHMB66XVxo1TgMM8aVV0SwAq3lCRkpiFBz_9kw8T1F9Hx2AiNrEGT1QLbdMkpms1cG_5gBBahQofdt_NmUs1jfTFXY9iyMy1Q7A6ZYaLP8Z6q-orc1cKqySY-BJZQ_CpGFfXS0OVniFDQ6v78ytPK7K-yRgT1PxFgm3rZqrG0Tjbrpsg2PUL5S5fuXfMhUosP0uoLj0D1guWAR9Y7kfFBIXaTSFMoa8fghVBUTRNhK9f72a8SxQawAEOiv71taLjKqaaWQ_QjcDhWbvjG1EnsCyI0toNjGkcF19x4Vk-5NC96_4ioUGz404IC0XN03roRnibRT_78D9vZFVCWCqve9EjdF5TcApx03zIP4JT2g2q0MKIGgGrwt4Pz6LO6yOfMm7B8Yraps8IV-nP1w7K1m9XKP_FvH8egl5GHJe-_omlC2YyL_b28jMLENbxDFD-3KPjZFBhSLrRukX2PlayYTwEiTtokA2R9_11vQvJgP8KFEjGHg6zsAMawAEBn2H_hz2knb8ltnpEA5YSKVcV3nUtojkCNi_WUz7xUKd7efw1oI_lbnKrS7HkyC0JkQUZ1pCWUlSXNmgjMEhsn823a1LFzpV7rOv4vayYvvFX61hB9R78VjpyxJiYpDwRZLiUY3AK4WY8NqFDbjXR7rT4CkFHEf-VhSQQ8ZNvlpod1nmeVQVizHH9e7Tq7wsWz-LWEk3Hx6LmcrgDsL79LZYG9JXU5IdvG8RvLNx9cSwEI8yxcchpISAaot7UoYQ=", - "type": "validator", - "lastSeen": 1575915094734973000, - "version": "0.1.0" - }, - { - "location": "unknown", - "host": "3.9.102.214:4000", - "pubKey": "AAAAAAAAAAIKwAECSqKy8I8KkSYIBSctxRBRxuR61PpAOwK0UQtkeuPRdwusAyaoBbvv1IBWyMEhvbgT4CtgUnGfYH2s06CIJ09lWWvQ0Jkgthq12mG73H9QSTNM8RITlF1X5ax9BV0EK34M5dUncn1uEYzJzcbaLjUarf2bqoy906dtQpppUWDRLJI6ycw7rKKJ4ZNUhgi4KAEGBsSgLqc0zDKs0rArwouZyz4ofoWnY68mdJKrVy6Zqz83DSdc7B2hqqkHX_Bfeb4SwAEOVCUN3EwiVroS5-TOq2o7hYSxphK9X0G23N-IBZ0Tr1Rl8XEiJ-OEy0rqnAKwmhAZJWnx3u8oXqbZtOWIZmzQSpcoxhgwfhdmTZJCqT2RVzZyeFItX4sVeilEP3z2xdsJs8-a1kg6UZnx1s1BNLBo7eZrreZygWojPCIDBn03fSAflXoVc5PpY2CGy5MA_IgWgSYBHDdoZEtigp_amjqK7Us44Db20XpLxMXfbahiqa7WKNnMgi6Ca2H67VtaaD8awAEF3zbE1nZRAa7a8vbU25c80YBYJBaW8P6FwXQI-K0Xk5MakwYeMMnIrm6w6IS_0XAO5YlD453GLqnxY8H1BEnRpfOnT7PE4el9mJ8MuYQMo6R2up0lGCmYM0YA9FORjroM3ng69SEPfJPCReG7LfJkERl_m2U403ertDRBYrlqCDagDfyI500srBcMrjSvV3oNouyyx3yZUrjLQfbHhDteQFsYdmakJs8Y-Q9-5MXCcrz6Qa4xwv522Euv0CCxkHcawAEYjfsU_zDhUZA1ey1aquWXlFOnx-iEALqxW1slDYHwQ1M2SILc-v_E6i1doa5e_bAZHVezBHFAlaNAVedNyHFFJxYAqAK3hbzbvl2glw3Q6h_rTXElymloqtaqVFIJ-oUWWOHsZBmu8EDA-HzvGCiBa_GbRaVfh2lE4ObeMXoJrEm_5dbxxeEic2l3IYeIz40N9ooQQOkQcOZdY4AXWYCavIAwWEJBjLtptJgCLu9a_zM1S5GsiyJHpdDs46WbP0EawAEWZ-95Sf0YAHujxRNLdXgpqe0ZF8loVwzZfvyMvqaxF1Ug274BqHuY_c5NdPAzuqoTwjfEn8NKEoaNqlumM75FUYbaTd7mXvk4WVYWjVnkO40dfQjRB7DYhvj0LBlbndAJ4wJIA2ilPYgjZsXVbNNh3e2j3u9eABd0VaFMbSb8Sz5_31r8HzoWmPJs3HiyuyANGFUA6CvAnMN6K3b-D8BhFZU_nPUTgu80o8_n6LQt-XWbaC_mTHzsnOjzBiPJxlYawAEW3bmOEtStH2T8q7vMkhchImp2-hg9MFYGBmEe9sSByTn3NUf8eksqXOC1dUjHkXoZm298FgUYLkNdnlxWpf993j5mEDoFxjcTB7scBD7k6nu6Nrs_wK0-seS8gsHrx9UK7GwAsi10q82Cm4PFyAtrWjmy_d9WLHuZt6VIOKunTs8cf0FwNUiMcvZsruqIFJcP7iWxdiFdUkh65P_iCz1ZEjJcj2GEZoq4v3a3by1aizGPaaiKc1jd_T-XJg_YpncawAEWnstu5b9WiZv0x8xfsiMk6YRlU0Cnj5svxLLXz_8drvwAa--GBY5yH0ke2EM6udMEi2EPeFcGTe6Sjs0YEhSbY7Uad_8suD2J4tIWJSWBbiyvh7rSqzv57m7BlsVcHfQJn_wNH-UlC9xkx8vg-LwfN8_FlxvHNPTc7XZG3lKYbwpUWlZxAziOYT1VQ-2K2bQQBBMdix-ht_SjccL1Dc2dP5kDazQ8yZV_8xnyeheazEedWe63uutfkHlZRg9YwP8=", - "type": "validator", - "lastSeen": 1575915094967382800, - "version": "0.1.0" - } - ], - "mixNodes": [ - { - "location": "unknown", - "host": "35.176.155.107:1789", - "pubKey": "zSob16499jT7C3S3ky4GihNOjlU6aLfSRkf1xAxOwV0=", - "layer": 3, - "lastSeen": 1575915096805374500, - "version": "0.1.0" - }, - { - "location": "unknown", - "host": "18.130.86.190:1789", - "pubKey": "vCdpFc0NvW0NSqsuTxtjFtiSY35aXesgT3JNA8sSIXk=", - "layer": 1, - "lastSeen": 1575915097370376000, - "version": "0.1.0" - }, - { - "location": "unknown", - "host": "3.10.22.152:1789", - "pubKey": "OwOqwWjh_IlnaWS2PxO6odnhNahOYpRCkju50beQCTA=", - "layer": 1, - "lastSeen": 1575915097639423500, - "version": "0.1.0" - }, - { - "location": "unknown", - "host": "35.178.213.77:1789", - "pubKey": "nkkrUjgL8UJk05QydvWvFSvtRB6nmeV8RMvH5540J3s=", - "layer": 2, - "lastSeen": 1575915097895166500, - "version": "0.1.0" - }, - { - "location": "unknown", - "host": "52.56.99.196:1789", - "pubKey": "whHuBuEc6zyOZOquKbuATaH4Crml61V_3Y-MztpWhF4=", - "layer": 2, - "lastSeen": 1575915096255174700, - "version": "0.1.0" - }, - { - "location": "unknown", - "host": "3.9.12.238:1789", - "pubKey": "vk5Sr-Xyi0cTbugACv8U42ZJ6hs6cGDox0rpmXY94Fc=", - "layer": 3, - "lastSeen": 1575915096497827600, - "version": "0.1.0" - } - ], - "mixProviderNodes": [ - { - "location": "unknown", - "clientListener": "3.8.176.11:8888", - "mixnetListener": "3.8.176.11:9999", - "pubKey": "54U6krAr-j9nQXFlsHk3io04_p0tctuqH71t7w_usgI=", - "registeredClients": [ - { - "pubKey": "zOqdJFH49HcgGSCRnmbXGzovnwRLEPN0YGN1SCafTyo=" - }, - { - "pubKey": "fy9xo69hZ2UJ9uxhIS1YzKHZsH8saV-02AiyCNXPNUc=" - }, - { - "pubKey": "6hFCz42d5AODAoMXqcBWtoOoZh-7hPMDXbLKKhS7x3I=" - }, - { - "pubKey": "pPZMDzw0FyZ-LBAhwCvjlPGj1p2bO_vaWbRBc7Ojq3Q=" - }, - { - "pubKey": "kjl5poXt2GdIrHLuMBjmTBSChV-zMqsXFBhKZoIREVI=" - }, - { - "pubKey": "T1jMGuk7-rcIUWnVopAsdGzhAJ7ZVymwON2LzjuOfnY=" - }, - { - "pubKey": "3X0eKPkflx5Vzok0Gk0jm5-YEfvPAuWP55ovyBUOtXA=" - }, - { - "pubKey": "5wC3i8rCZolLKbWJ9U6eLieXNGLKM21dtL6lR30u_hE=" - }, - { - "pubKey": "1P6p7fgjwjlEepD9JgbN1V-rk9n36-hCmPN5P6y62n4=" - }, - { - "pubKey": "aBjlLQKKFroqhX_kvYLnMm1uq3FJdQWqVy9Q35zzERA=" - }, - { - "pubKey": "I0gVOPj6lv9ha60xPYKeAgbeUU8pdyMD-Y7Nb1nS9EQ=" - }, - { - "pubKey": "WgdvQ74QH1uFDWDL2YeApvv7oniNGh9BQJ_HZam20QA=" - }, - { - "pubKey": "Mlw23KaSL2hyrIjEZM76bZStt2iMzxVAqXwO5clJfxg=" - }, - { - "pubKey": "F9xzbjnMQVN4ZidcqN2ip9kVnI9wbS39aVayZGiMihY=" - }, - { - "pubKey": "s6pfVkZrUG--RNjfzS55N2oPvFkMdvgb1LUut6gqRy4=" - }, - { - "pubKey": "bSi-9k0jJNKc8PGx8M3SWFaNpORFjYw-NkWXRZVRWGU=" - }, - { - "pubKey": "pz6ahQcGOQcZBFx1tGmzRngqk6BecXB_wFd4WVdOQDU=" - }, - { - "pubKey": "5sfwIMcG2zRCxhDh4D9Evw1WPI5bfKZAShM_6o9Pu1I=" - }, - { - "pubKey": "9fZnxXy9onGPpZ3Ygckqw0okqCw3di02sLr-NTBr4SE=" - }, - { - "pubKey": "Q0TbbggOwzZjalUdi5eEHVFi9VMv-rMm5mJPUZZs12A=" - }, - { - "pubKey": "aPNyox_qAIGFB2-wZ0lc9iAWDN6jzLojApSiWVFjCks=" - }, - { - "pubKey": "DUKLEsIGMw-ucs3DjS7Ag9qCb5-C_A84DuIsZuLkdwI=" - }, - { - "pubKey": "YV84vPoSrLf1p9Sw6FnnrcCpS3kvpJUfKyKpnwk8z0Q=" - }, - { - "pubKey": "_IYEzZQoBAYeTxqzpEe_ez1-7pn7aId8AKliazy0qlE=" - }, - { - "pubKey": "srkOAoVU-a02lnEsoH_wOLLw7HFx_xHIZUSbnhjFwDw=" - }, - { - "pubKey": "LxSCBn_OEQq-hI2xDi6bfGoioRO_lSTIq6AQ9l1k5jg=" - }, - { - "pubKey": "OC0OOqtGfAytgZjthpjoKeYNa0VrtzfgZ0iO5Fag5y8=" - }, - { - "pubKey": "ImVEch2focRhm1ial1gA6YJPr6WDyW3oh-OgYcO5Ll4=" - }, - { - "pubKey": "yTDnzLvEaSq0mC9xNrtyjpAKtsIU6yRuBepCuWQMBm4=" - }, - { - "pubKey": "-LCUUc46HuL7iUEOMkrlVAkvvulRiQ7dR1QYh7bkKxw=" - }, - { - "pubKey": "Bx1MGpISig25rqe7mhoX68EROUPPzmF7yGLYah9DPgM=" - }, - { - "pubKey": "Z4Nu6iwLmgJ93yoIFTbTEBeDAHRwS-vo1T_K2Kv1FQo=" - }, - { - "pubKey": "cKFAGxllwAmEXCtDxG_T1iEm3-lKWUVQxxpDBje6mQU=" - }, - { - "pubKey": "pQV40whlQWUSXtrNTTePzO6sdq3zr1JUIWZWvD443nY=" - }, - { - "pubKey": "6Bb5HwnVqJPy5wcNsaHY-0y__coZCE7XC80kUkesnRU=" - }, - { - "pubKey": "COGdpfhmzNGR6YX820GqJIkjOihL8mr6-h-d3JlTDFA=" - } - ], - "lastSeen": 1575915097358694100, - "version": "0.1.0" - }, - { - "location": "unknown", - "clientListener": "3.8.176.12:8888", - "mixnetListener": "3.8.176.12:9999", - "pubKey": "sA-sxi038pEbGy4lgZWG-RdHHDkA6kZzu44G0LUxFSc=", - "registeredClients": [ - { - "pubKey": "UE-7r6-bpw0b4T3GxOBVxlg02psx23DF2p5Tuf-OBSE=" - }, - { - "pubKey": "UnZuLpzq64_EPtIcr1Fd-5AESBCBLFnDMDsjUaOqrUA=" - }, - { - "pubKey": "4ExXPrW5w0nZIQ5ravBRT6H9r0RH0MXuOcGIF8HzUhg=" - }, - { - "pubKey": "wPsRpJPi1e2sjItyRKDkFACbxwu3Cw5GlYVPmdYxk2U=" - }, - { - "pubKey": "UfK5UvT3HUkT1SGbv1QGafy3in3uQ9a6NSy5EOT6k0k=" - }, - { - "pubKey": "5Giu-tJpGXU0S9Av75iAv5qDO0k6l8v_k9-UCcRUCl4=" - }, - { - "pubKey": "MnQxlmmKDybku4CfxsQQxfftilsaphF9Gq1w3MB1ZCE=" - }, - { - "pubKey": "GdP1fHVs2R65EkuWVWKZSz6WPDh0MgThyuBOv6_xsmQ=" - }, - { - "pubKey": "4JEtSrKsonmBuDvxJ9nITSu7iC4f8reutXRAVugPgS4=" - }, - { - "pubKey": "q4XyuUJbSGJaoRb3SmWzeX88V2dKB7sPTf72BAtQp3k=" - }, - { - "pubKey": "gTzKd1Ph5bpUw-JxTZiCe8RBfO-FsZiVYDYioQ-6dVg=" - }, - { - "pubKey": "NX4oaDLYEOmMUP_9pcEaZv5MJHJ4ZYAoxPQDxov7tRs=" - }, - { - "pubKey": "7fbk4oGQNlTW-tnWjVz8rWtKrtAicTsiNWgO98sqMyk=" - }, - { - "pubKey": "w1bfLpnd3rWu5JczB0nQfnE2S6nUCbx2AA7HDE48DQo=" - } - ], - "lastSeen": 1575915097869025000, - "version": "0.1.0" - } - ] - }"#.to_string() + "cocoNodes": [ + { + "location": "unknown", + "host": "3.8.244.109:4000", + "pubKey": "AAAAAAAAAAEKwAECSqKy8I8KkSYIBSctxRBRxuR61PpAOwK0UQtkeuPRdwusAyaoBbvv1IBWyMEhvbgT4CtgUnGfYH2s06CIJ09lWWvQ0Jkgthq12mG73H9QSTNM8RITlF1X5ax9BV0EK34M5dUncn1uEYzJzcbaLjUarf2bqoy906dtQpppUWDRLJI6ycw7rKKJ4ZNUhgi4KAEGBsSgLqc0zDKs0rArwouZyz4ofoWnY68mdJKrVy6Zqz83DSdc7B2hqqkHX_Bfeb4SwAEFuhRpy4HfcuxwcRI9sIWMo_LVmbk19g1gfMRlBrmZqoEQL6rDApVLZ9eMp-5IQK8WLlZpWf4Zjy7kZolARAyp_rHUQkH4PrDjgoPrKbm6qK_iejYpL7qx28Q3VeInMpwMIMaSbbW9y36sEVtGc2I0Iu5vS0sp8ESiVlQ5NaBz72deZ8oKJJ4IEPPHP99-b0UQX80fVIrNM88mMzKy0bHri9NFlmIG-e0G1cqmw_ry3XWGQkcr1M5RuNa6oX50w5QawAEVxd5FP5bE8bS4x54Csof11sQWUTwMp6Q7_3H7ZCTSlKSqujlOhmfqSHfGPO2sDIYPHDhDzjakZpKAZWWhn_hiR6DfPpomQ01ZYUhVKKSMxz7_VPjsQplP0bZXA2gfnkADUN8UQ0N9g_usIw73r4aZsOviMsRM8oByvsjVfUWc4_HTLSdnQyImFkHz9CiCmrIYL2dYQRePRatWggvBAyeRzntxI4jDqLKiBdi54ZlAKgV6MCRaJ7Bu7BtmLXrtK4sawAED3QYxuvOSZrbZdUr4yG-U9yVvJ9Klkf-5Mo4EYp3qTL2KBB6_LrZepjAQqp486YkZ03mTIezcsZ48EboXVTWKBZ3QnTI5tX-j4gGxQb7klOJc97qJkDxsvpz4F0ChgCUIZhpIItWHia7_R3Gi-b5siLIdQdUho9isn3kiDGm6t0NED2Bgy3ZxxQwzqsBZm4kPr2_fPX4YyvIoP9895YcGjZyE5iiRC_TE41RJmB1GZYdxegTMq3lNDllKgiqaiPgawAEJASDkmZHTwlg9YOev5OWpQD-FnhPkqVNo_QcDyRu9eoGcWSGFp2sYqjG2SpmiXq0VNnAO7AcKxRzDFu7TjfhlU3Kt0uTKIcrWVU1zFNbJNMjYEq90pp50nowwx8INz20IXET2ZNX6kIXYFCsEvPLZFlG2OoL6xg3uQS1qMl3lIS_VxdO_JfVe0rT65WsJ_P4Nkc1jYiuNPHY6d_iFO0BVYqX0sOCX73GC_TT13BR0jnPwDAVw0rGtYHsXBb8TKOsawAEZIClauuT1V3qOZnb7uRZhFXO-PKTxgc1LCzJt2ChOrMZaBpjlkf3IPpJ2UF4JH4kGaDeBf2k_S-FLAs3drK21efbi5P6_a4QTxAiiRimXGoQIyvOg462s6kP_ZRFufo8YYQHS4olaOeqU4564dNskg_uBPsFMz_2GNOhmn_15cJqP1jfkyD49Z16GTS5YLHgVl9bJKqyvLuypsToLbt1BJzipEP0L2OohuRm-_MvqvwwWKyjNQsubgee1K728d9AawAEBkGggcNVCtXyhoSqi3_w0tVxtkAYeud8sBeAtZHGs06me_QL8co0MFLlO-zdkUb4ZBq08rFEbgLOma8_3whleM8NIPaHNISp1q3IsIhB5zdXcZoGsqLixODBFHtID3YEHAlr4f9T_yh11yJ95xGCl_6Y37hpwLQVGyrfSfccM24mVFqnV3TT5Wdq3ile-jesUx1Q2G1yK_xVqc6itmk-kDuBjyZgzYi1-jsIXAjnhM9G7t8J_Bv5yGGZhLK2dCzM=", + "type": "validator", + "lastSeen": 1575915097085539300, + "version": "0.1.0" + }, + { + "location": "unknown", + "host": "3.9.129.61:4000", + "pubKey": "AAAAAAAAAAMKwAECSqKy8I8KkSYIBSctxRBRxuR61PpAOwK0UQtkeuPRdwusAyaoBbvv1IBWyMEhvbgT4CtgUnGfYH2s06CIJ09lWWvQ0Jkgthq12mG73H9QSTNM8RITlF1X5ax9BV0EK34M5dUncn1uEYzJzcbaLjUarf2bqoy906dtQpppUWDRLJI6ycw7rKKJ4ZNUhgi4KAEGBsSgLqc0zDKs0rArwouZyz4ofoWnY68mdJKrVy6Zqz83DSdc7B2hqqkHX_Bfeb4SwAEEv6RMevAQmLGkeK0uJKnMPPAtm8GgXjWSQijYdnxlPh5SJSNeJUbPZKWFFWdk8yIFXKa8jnzETtdGFKgUUt5AVUDpTBmEdwaHCzlFhXrttshy0V5OhPUlV8cGABmxbagMYm0bFPg0r-snSkrB9YG6wqJYQVeIMOCGYCPbHmDA8R_0-h8VkRKWs1d9KvQOK4kShqgZtYN71KJW8uDE4q2jsGDVvxFt1AgmU9b93xsXF17KrpZy5WxlLZ73HtnTD_oawAED4vd_rK-Kx_n8x_OdDiiEOPUlYDlDCQUqenU9XHKH3B6ijfkJ368wd3LDDVStjDwNORrAyUSw_VlSNUpd1XLC8d17gTaIq5ZI2fWuwwZaoN1JCsYU8fQ6USgtIehQX7IPP8EkFuNmuCBCmpr4schtYniGe9J8Q4dsV-TYPr2uLJkdx1r7luzF--I22k7NfQQM14QDci_0kgrgmZ54CJGkjXyOhCppBXg3fqLC6aFvT3ZocfiiXBJt0huGgPMDtYsawAECLh8KUdNsDolERwJ8v04bS5jI_KKf7uUnCHWuCELwbJSUI3OK1ufS1qSpauvSzVQSbrhEzrEfwQn4VtxQxJlX4UdDU-R-hafiZvVC6DLLAbuORBAC3FScn9W58CnezH4DvCp_w7nftDfdxeuungbZT9XaxS3iNC6PnFsWF6WM3DxMwrzOrFe6wEEoTSPe1mcUDrtwM5UksIvJr6MBRAXrdl0IdBTQr7cLwKe_KYi4siwdjfJEJtOh7oxQBxBg2UkawAEJAPZK2Gg2MQwpxdDT24lNQHF7FVfkO_LuhJwn0RbwNDSVeA4P6-tWL5TkCpqr8xYHfwQ6Z3ILfpGCZr8PspwIoRzqZHQ16f8Pq9xnr0hLEI9BOQU0FS2EtuyPgju5iwsAJAfehUzu6kNLphuLGsXoIZdXDG5mbylwh9JzAVXTwgaR0hNqyXVJxgbt7jcYaSEBFcMGV-hjXyVVNzBleE-G9o_noI_KWU4Ce7K-qOMcewMKfy_VEw-gVaD6dHz6AMoawAEE9XuOLwRttvKybAssZ9gsK-_YRUwuFOeRDIr3NX___9bx6pCc18adCIlH_8EJWFwXZ05ZpNNE88mYx7ZQ3aqaArZJRoWeZeKhqH_s05V10xbzkYX71G5cqz--8vr9ZlQRb2BeETF_Tdq_PLk7qbT8WTGIoq7ZwyDRQTgzvkCgyzj_hBLh2o7sSVNgUo38SFUTMn7YtvVFYlSrTDE3WKE-T-nh5SWdDBxgDTc3Bw8JpzNH-WkoJ4Lim7sB4Op1gEUawAEW4-kenlffwsNr_3b3aV0YuusLpxB03sxPzQ5B0CWNiVtbja1Z4tWhKGUUrdq_eUgMV0y5Of-BqNi5FspAQnhJBFSSxtOzRGV1h3qyUTksfZyed9z8zPI-ZPP9XXm7hYgJgDz_kxte-NfS9UG9q5AZetHUN4kGxXutjjzfUQZ9yTvhBKgKgTI2Dp_R_jZrWQ8F1BoWzIJzjddT1K2MvCQEkARYw08isbOeFmCwgVUcjxYZO45WyOmLQA7QJRL9WvA=", + "type": "validator", + "lastSeen": 1575915097388409000, + "version": "0.1.0" + }, + { + "location": "unknown", + "host": "3.9.222.1:4000", + "pubKey": "AAAAAAAAAAQKwAECSqKy8I8KkSYIBSctxRBRxuR61PpAOwK0UQtkeuPRdwusAyaoBbvv1IBWyMEhvbgT4CtgUnGfYH2s06CIJ09lWWvQ0Jkgthq12mG73H9QSTNM8RITlF1X5ax9BV0EK34M5dUncn1uEYzJzcbaLjUarf2bqoy906dtQpppUWDRLJI6ycw7rKKJ4ZNUhgi4KAEGBsSgLqc0zDKs0rArwouZyz4ofoWnY68mdJKrVy6Zqz83DSdc7B2hqqkHX_Bfeb4SwAECh9xcxpjOp1r7kiNIgrI9GgAlvXwgHkTchOxUiyOzTq6FDWdGN64KiC3NDeyGTg8FmzvGzS3jREeJqOdr4G9ZGtWkauAITgLFiH62t-YntRslhr8_1shxlmzKiNKJN_QFflEq79pZIlWtp3N8LIHMvXRtl-zt2DMze4s02XDmEkviyVE4CkQUDtCc-2MfPT4JcmEFqtFIxjrXn18SbYg3c6XUQHsGIkuDrKuCTRlpC8kvmM0uVoIeWdmwDlZk4jUawAEJhRwK5ozjqIWRP1bFzBPS9VhaJnfKU9PeFYtN5beiAHrYr2ylIB3yDfmAQUdKDowDUm5nfJATejEjEnrTGxh70QtfoNV391rSns3F71tBwY62KLaNr8qnVfeSFHV3FcQTMHHF_8mDb5_11Rj6aiMvW0y6eetHo7CDPMdEyDPmok_U2ZM5BzOUnwjT21HtnvcKxKKwHJ_QGfnAHPyDIhNOMgxJCrVazOidLCHeYGpyCLw1ipeTyKOQX0_ByB8dH6AawAEGV1GuF5SSlT67B1ityPJK2ZwXjeeKB4gGdCG3qRtWxLTZfGhVm7YAYm2f5tw_wrsJAZ9FubVhateGg0ZN67NxZtsvOOejXz6743f7ijnQopPgd_8pH-iVf6BEcSO8ZdcHxNRUTayzjVLs99bwMo2zaPevW4X4G_bN4mh---aPkdGYHwaiklzUhqJ-eqycrYAFyjyEXaPBXLQm1rpczqluNvnKbd8Q9LZWukgm7_uWv_HxufIvdWgoq8bAt78UU3oawAEP9VDehhqrQG5-WHMB66XVxo1TgMM8aVV0SwAq3lCRkpiFBz_9kw8T1F9Hx2AiNrEGT1QLbdMkpms1cG_5gBBahQofdt_NmUs1jfTFXY9iyMy1Q7A6ZYaLP8Z6q-orc1cKqySY-BJZQ_CpGFfXS0OVniFDQ6v78ytPK7K-yRgT1PxFgm3rZqrG0Tjbrpsg2PUL5S5fuXfMhUosP0uoLj0D1guWAR9Y7kfFBIXaTSFMoa8fghVBUTRNhK9f72a8SxQawAEOiv71taLjKqaaWQ_QjcDhWbvjG1EnsCyI0toNjGkcF19x4Vk-5NC96_4ioUGz404IC0XN03roRnibRT_78D9vZFVCWCqve9EjdF5TcApx03zIP4JT2g2q0MKIGgGrwt4Pz6LO6yOfMm7B8Yraps8IV-nP1w7K1m9XKP_FvH8egl5GHJe-_omlC2YyL_b28jMLENbxDFD-3KPjZFBhSLrRukX2PlayYTwEiTtokA2R9_11vQvJgP8KFEjGHg6zsAMawAEBn2H_hz2knb8ltnpEA5YSKVcV3nUtojkCNi_WUz7xUKd7efw1oI_lbnKrS7HkyC0JkQUZ1pCWUlSXNmgjMEhsn823a1LFzpV7rOv4vayYvvFX61hB9R78VjpyxJiYpDwRZLiUY3AK4WY8NqFDbjXR7rT4CkFHEf-VhSQQ8ZNvlpod1nmeVQVizHH9e7Tq7wsWz-LWEk3Hx6LmcrgDsL79LZYG9JXU5IdvG8RvLNx9cSwEI8yxcchpISAaot7UoYQ=", + "type": "validator", + "lastSeen": 1575915094734973000, + "version": "0.1.0" + }, + { + "location": "unknown", + "host": "3.9.102.214:4000", + "pubKey": "AAAAAAAAAAIKwAECSqKy8I8KkSYIBSctxRBRxuR61PpAOwK0UQtkeuPRdwusAyaoBbvv1IBWyMEhvbgT4CtgUnGfYH2s06CIJ09lWWvQ0Jkgthq12mG73H9QSTNM8RITlF1X5ax9BV0EK34M5dUncn1uEYzJzcbaLjUarf2bqoy906dtQpppUWDRLJI6ycw7rKKJ4ZNUhgi4KAEGBsSgLqc0zDKs0rArwouZyz4ofoWnY68mdJKrVy6Zqz83DSdc7B2hqqkHX_Bfeb4SwAEOVCUN3EwiVroS5-TOq2o7hYSxphK9X0G23N-IBZ0Tr1Rl8XEiJ-OEy0rqnAKwmhAZJWnx3u8oXqbZtOWIZmzQSpcoxhgwfhdmTZJCqT2RVzZyeFItX4sVeilEP3z2xdsJs8-a1kg6UZnx1s1BNLBo7eZrreZygWojPCIDBn03fSAflXoVc5PpY2CGy5MA_IgWgSYBHDdoZEtigp_amjqK7Us44Db20XpLxMXfbahiqa7WKNnMgi6Ca2H67VtaaD8awAEF3zbE1nZRAa7a8vbU25c80YBYJBaW8P6FwXQI-K0Xk5MakwYeMMnIrm6w6IS_0XAO5YlD453GLqnxY8H1BEnRpfOnT7PE4el9mJ8MuYQMo6R2up0lGCmYM0YA9FORjroM3ng69SEPfJPCReG7LfJkERl_m2U403ertDRBYrlqCDagDfyI500srBcMrjSvV3oNouyyx3yZUrjLQfbHhDteQFsYdmakJs8Y-Q9-5MXCcrz6Qa4xwv522Euv0CCxkHcawAEYjfsU_zDhUZA1ey1aquWXlFOnx-iEALqxW1slDYHwQ1M2SILc-v_E6i1doa5e_bAZHVezBHFAlaNAVedNyHFFJxYAqAK3hbzbvl2glw3Q6h_rTXElymloqtaqVFIJ-oUWWOHsZBmu8EDA-HzvGCiBa_GbRaVfh2lE4ObeMXoJrEm_5dbxxeEic2l3IYeIz40N9ooQQOkQcOZdY4AXWYCavIAwWEJBjLtptJgCLu9a_zM1S5GsiyJHpdDs46WbP0EawAEWZ-95Sf0YAHujxRNLdXgpqe0ZF8loVwzZfvyMvqaxF1Ug274BqHuY_c5NdPAzuqoTwjfEn8NKEoaNqlumM75FUYbaTd7mXvk4WVYWjVnkO40dfQjRB7DYhvj0LBlbndAJ4wJIA2ilPYgjZsXVbNNh3e2j3u9eABd0VaFMbSb8Sz5_31r8HzoWmPJs3HiyuyANGFUA6CvAnMN6K3b-D8BhFZU_nPUTgu80o8_n6LQt-XWbaC_mTHzsnOjzBiPJxlYawAEW3bmOEtStH2T8q7vMkhchImp2-hg9MFYGBmEe9sSByTn3NUf8eksqXOC1dUjHkXoZm298FgUYLkNdnlxWpf993j5mEDoFxjcTB7scBD7k6nu6Nrs_wK0-seS8gsHrx9UK7GwAsi10q82Cm4PFyAtrWjmy_d9WLHuZt6VIOKunTs8cf0FwNUiMcvZsruqIFJcP7iWxdiFdUkh65P_iCz1ZEjJcj2GEZoq4v3a3by1aizGPaaiKc1jd_T-XJg_YpncawAEWnstu5b9WiZv0x8xfsiMk6YRlU0Cnj5svxLLXz_8drvwAa--GBY5yH0ke2EM6udMEi2EPeFcGTe6Sjs0YEhSbY7Uad_8suD2J4tIWJSWBbiyvh7rSqzv57m7BlsVcHfQJn_wNH-UlC9xkx8vg-LwfN8_FlxvHNPTc7XZG3lKYbwpUWlZxAziOYT1VQ-2K2bQQBBMdix-ht_SjccL1Dc2dP5kDazQ8yZV_8xnyeheazEedWe63uutfkHlZRg9YwP8=", + "type": "validator", + "lastSeen": 1575915094967382800, + "version": "0.1.0" + } + ], + "mixNodes": [ + { + "location": "unknown", + "host": "35.176.155.107:1789", + "pubKey": "zSob16499jT7C3S3ky4GihNOjlU6aLfSRkf1xAxOwV0=", + "layer": 3, + "lastSeen": 1575915096805374500, + "version": "0.1.0" + }, + { + "location": "unknown", + "host": "18.130.86.190:1789", + "pubKey": "vCdpFc0NvW0NSqsuTxtjFtiSY35aXesgT3JNA8sSIXk=", + "layer": 1, + "lastSeen": 1575915097370376000, + "version": "0.1.0" + }, + { + "location": "unknown", + "host": "3.10.22.152:1789", + "pubKey": "OwOqwWjh_IlnaWS2PxO6odnhNahOYpRCkju50beQCTA=", + "layer": 1, + "lastSeen": 1575915097639423500, + "version": "0.1.0" + }, + { + "location": "unknown", + "host": "35.178.213.77:1789", + "pubKey": "nkkrUjgL8UJk05QydvWvFSvtRB6nmeV8RMvH5540J3s=", + "layer": 2, + "lastSeen": 1575915097895166500, + "version": "0.1.0" + }, + { + "location": "unknown", + "host": "52.56.99.196:1789", + "pubKey": "whHuBuEc6zyOZOquKbuATaH4Crml61V_3Y-MztpWhF4=", + "layer": 2, + "lastSeen": 1575915096255174700, + "version": "0.1.0" + }, + { + "location": "unknown", + "host": "3.9.12.238:1789", + "pubKey": "vk5Sr-Xyi0cTbugACv8U42ZJ6hs6cGDox0rpmXY94Fc=", + "layer": 3, + "lastSeen": 1575915096497827600, + "version": "0.1.0" + } + ], + "mixProviderNodes": [ + { + "location": "unknown", + "clientListener": "3.8.176.11:8888", + "mixnetListener": "3.8.176.11:9999", + "pubKey": "54U6krAr-j9nQXFlsHk3io04_p0tctuqH71t7w_usgI=", + "registeredClients": [ + { + "pubKey": "zOqdJFH49HcgGSCRnmbXGzovnwRLEPN0YGN1SCafTyo=" + }, + { + "pubKey": "fy9xo69hZ2UJ9uxhIS1YzKHZsH8saV-02AiyCNXPNUc=" + }, + { + "pubKey": "COGdpfhmzNGR6YX820GqJIkjOihL8mr6-h-d3JlTDFA=" + } + ], + "lastSeen": 1575915097358694100, + "version": "0.1.0" + }, + { + "location": "unknown", + "clientListener": "3.8.176.12:8888", + "mixnetListener": "3.8.176.12:9999", + "pubKey": "sA-sxi038pEbGy4lgZWG-RdHHDkA6kZzu44G0LUxFSc=", + "registeredClients": [ + { + "pubKey": "7fbk4oGQNlTW-tnWjVz8rWtKrtAicTsiNWgO98sqMyk=" + }, + { + "pubKey": "w1bfLpnd3rWu5JczB0nQfnE2S6nUCbx2AA7HDE48DQo=" + } + ], + "lastSeen": 1575915097869025000, + "version": "0.1.0" + } + ], + "gatewayNodes": [ + { + "location": "unknown", + "clientListener": "3.8.176.11:8888", + "mixnetListener": "3.8.176.11:9999", + "pubKey": "54U6krAr-j9nQXFlsHk3io04_p0tctuqH71t7w_usgI=", + "registeredClients": [ + { + "pubKey": "zOqdJFH49HcgGSCRnmbXGzovnwRLEPN0YGN1SCafTyo=" + }, + { + "pubKey": "fy9xo69hZ2UJ9uxhIS1YzKHZsH8saV-02AiyCNXPNUc=" + } + ], + "lastSeen": 1575915097358694100, + "version": "0.1.0" + }, + { + "location": "unknown", + "clientListener": "3.8.176.12:8888", + "mixnetListener": "3.8.176.12:9999", + "pubKey": "sA-sxi038pEbGy4lgZWG-RdHHDkA6kZzu44G0LUxFSc=", + "registeredClients": [ + { + "pubKey": "UE-7r6-bpw0b4T3GxOBVxlg02psx23DF2p5Tuf-OBSE=" + }, + { + "pubKey": "UnZuLpzq64_EPtIcr1Fd-5AESBCBLFnDMDsjUaOqrUA=" + } + ], + "lastSeen": 1575915097869025000, + "version": "0.1.0" + } + ] + }"#.to_string() } } } diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml new file mode 100644 index 0000000000..3077250306 --- /dev/null +++ b/common/client-libs/gateway-client/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "gateway-client" +version = "0.1.0" +authors = ["JÄ™drzej StuczyÅ„ski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# TODO: (for this and other crates), similarly to 'tokio', import only required "futures" modules rather than +# the entire crate +futures = "0.3" +tokio = { version = "0.2", features = ["macros", "rt-core", "stream", "sync", "time"] } +tokio-tungstenite = "0.10" + +# internal +gateway-requests = { path = "../../../gateway/gateway-requests" } +nymsphinx = { path = "../../nymsphinx" } + +[dev-dependencies] +# for tests +#url = "2.1" diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs new file mode 100644 index 0000000000..ed024544e5 --- /dev/null +++ b/common/client-libs/gateway-client/src/lib.rs @@ -0,0 +1,449 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt}; +use gateway_requests::auth_token::{AuthToken, AuthTokenConversionError}; +use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; +use nymsphinx::DestinationAddressBytes; +use std::convert::TryFrom; +use std::fmt::{self, Error, Formatter}; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio::sync::Notify; +use tokio_tungstenite::{ + connect_async, + tungstenite::{client::IntoClientRequest, protocol::Message, Error as WsError}, + WebSocketStream, +}; + +// TODO: combine the duplicate reading procedure, i.e. +/* +msg read from conn.next().await: + if msg.is_none() { + res = Some(Err(GatewayClientError::ConnectionAbruptlyClosed)); + break; + } + let msg = match msg.unwrap() { + Ok(msg) => msg, + Err(err) => { + res = Some(Err(GatewayClientError::NetworkError(err))); + break; + } + }; + match msg { + // specific handling + } +*/ + +// TODO: some batching mechanism to allow reading and sending more than a single packet through +pub type SphinxPacketSender = mpsc::UnboundedSender>>; +pub type SphinxPacketReceiver = mpsc::UnboundedReceiver>>; + +// type alias for not having to type the whole thing every single time +type WsConn = WebSocketStream; + +#[derive(Debug)] +pub enum GatewayClientError { + ConnectionNotEstablished, + GatewayError(String), + NetworkError(WsError), + NoAuthTokenAvailable, + ConnectionAbruptlyClosed, + MalformedResponse, + NotAuthenticated, + ConnectionInInvalidState, + AuthenticationFailure, + Timeout, +} + +impl From for GatewayClientError { + fn from(err: WsError) -> Self { + GatewayClientError::NetworkError(err) + } +} + +impl From for GatewayClientError { + fn from(_err: AuthTokenConversionError) -> Self { + GatewayClientError::MalformedResponse + } +} + +// better human readable representation of the error, mostly so that GatewayClientError +// would implement std::error::Error +impl fmt::Display for GatewayClientError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + match self { + GatewayClientError::ConnectionNotEstablished => { + write!(f, "connection to the gateway is not established") + } + GatewayClientError::NoAuthTokenAvailable => { + write!(f, "no AuthToken was provided or obtained") + } + GatewayClientError::NotAuthenticated => write!(f, "client is not authenticated"), + GatewayClientError::NetworkError(err) => { + write!(f, "there was a network error - {}", err) + } + GatewayClientError::ConnectionAbruptlyClosed => { + write!(f, "connection was abruptly closed") + } + GatewayClientError::Timeout => write!(f, "timed out"), + GatewayClientError::MalformedResponse => write!(f, "received response was malformed"), + GatewayClientError::ConnectionInInvalidState => write!( + f, + "connection is in an invalid state - please send a bug report" + ), + GatewayClientError::AuthenticationFailure => write!(f, "authentication failure"), + GatewayClientError::GatewayError(err) => { + write!(f, "gateway returned an error response - {}", err) + } + } + } +} + +// we can either have the stream itself or an option to re-obtain it +// by notifying the future owning it to finish the execution and awaiting the result +// which should be almost immediate (or an invalid state which should never, ever happen) +// TODO: perhaps restore the previous idea of Split(Stream, Sink) state to allow for +// sending messages without waiting for any responses and having no effect on rate of +// messages being pushed to us +enum SocketState<'a> { + Available(WsConn), + Delegated( + BoxFuture<'a, Result>, + Arc, + ), + NotConnected, + Invalid, +} + +impl<'a> SocketState<'a> { + fn is_available(&self) -> bool { + match self { + SocketState::Available(_) => true, + _ => false, + } + } + + fn is_delegated(&self) -> bool { + match self { + SocketState::Delegated(_, _) => true, + _ => false, + } + } + + fn is_established(&self) -> bool { + match self { + SocketState::Available(_) | SocketState::Delegated(_, _) => true, + _ => false, + } + } +} + +pub struct GatewayClient<'a, R: IntoClientRequest + Unpin + Clone> { + authenticated: bool, + // can be String, string slices, `url::Url`, `http::Uri`, etc. + gateway_address: R, + our_address: DestinationAddressBytes, + auth_token: Option, + connection: SocketState<'a>, + sphinx_packet_sender: SphinxPacketSender, + response_timeout_duration: Duration, +} + +impl<'a, R: IntoClientRequest + Unpin + Clone> Drop for GatewayClient<'a, R> { + fn drop(&mut self) { + // TODO to fix forcibly closing connection (although now that I think about it, + // I'm not sure this would do it, as to fix the said issue we'd need graceful shutdowns) + } +} + +impl<'a, R> GatewayClient<'static, R> +where + R: IntoClientRequest + Unpin + Clone, +{ + pub fn new( + gateway_address: R, + our_address: DestinationAddressBytes, + auth_token: Option, + sphinx_packet_sender: SphinxPacketSender, + response_timeout_duration: Duration, + ) -> Self { + GatewayClient { + authenticated: false, + gateway_address, + our_address, + auth_token, + connection: SocketState::NotConnected, + sphinx_packet_sender, + response_timeout_duration, + } + } + + pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { + let ws_stream = match connect_async(self.gateway_address.clone()).await { + Ok((ws_stream, _)) => ws_stream, + Err(e) => return Err(GatewayClientError::NetworkError(e)), + }; + + self.connection = SocketState::Available(ws_stream); + Ok(()) + } + + async fn read_control_response(&mut self) -> Result { + // we use the fact that all request responses are Message::Text and only pushed + // sphinx packets are Message::Binary + + let conn = match self.connection { + SocketState::Available(ref mut conn) => conn, + _ => return Err(GatewayClientError::ConnectionInInvalidState), + }; + + let mut timeout = tokio::time::delay_for(self.response_timeout_duration); + + let mut res = None; + while res.is_none() { + tokio::select! { + _ = &mut timeout => { + res = Some(Err(GatewayClientError::Timeout)) + } + // just keep getting through socket buffer until we get to what we want... + // (or we time out) + msg = conn.next() => { + if msg.is_none() { + res = Some(Err(GatewayClientError::ConnectionAbruptlyClosed)); + break; + } + let msg = match msg.unwrap() { + Ok(msg) => msg, + Err(err) => { + res = Some(Err(GatewayClientError::NetworkError(err))); + break; + } + }; + match msg { + Message::Binary(bin_msg) => { + self.sphinx_packet_sender.unbounded_send(vec![bin_msg]).unwrap() + } + Message::Text(txt_msg) => { + res = Some(ServerResponse::try_from(txt_msg).map_err(|_| GatewayClientError::MalformedResponse)); + } + _ => (), + } + } + } + } + + res.expect("response value should have been written in one of the branches!. If you see this error, please report a bug!") + } + + // If we want to send a message, we need to have a full control over the socket, + // as we need to be able to write the request and read the subsequent response + async fn send_websocket_message( + &mut self, + msg: Message, + ) -> Result { + let mut should_restart_sphinx_listener = false; + if self.connection.is_delegated() { + self.recover_socket_connection().await?; + should_restart_sphinx_listener = true; + } + + let conn = match self.connection { + SocketState::Available(ref mut conn) => conn, + SocketState::Delegated(_, _) => { + return Err(GatewayClientError::ConnectionInInvalidState) + } + SocketState::Invalid => return Err(GatewayClientError::ConnectionInInvalidState), + SocketState::NotConnected => return Err(GatewayClientError::ConnectionNotEstablished), + }; + conn.send(msg).await?; + let response = self.read_control_response().await; + + if should_restart_sphinx_listener { + self.start_listening_for_sphinx_packets()?; + } + response + } + + // next on TODO list: + // so that presumably we could increase our sending/receiving rate + async fn send_websocket_message_without_response( + &mut self, + msg: Message, + ) -> Result<(), GatewayClientError> { + unimplemented!() + } + + pub async fn register(&mut self) -> Result { + if !self.connection.is_established() { + return Err(GatewayClientError::ConnectionNotEstablished); + } + let msg = ClientControlRequest::new_register(self.our_address.clone()).into(); + let token = match self.send_websocket_message(msg).await? { + ServerResponse::Register { token } => { + self.authenticated = true; + Ok(AuthToken::try_from_base58_string(token)?) + } + ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + _ => unreachable!(), + }?; + self.start_listening_for_sphinx_packets()?; + Ok(token) + } + + pub async fn authenticate( + &mut self, + auth_token: Option, + ) -> Result { + if auth_token.is_none() && self.auth_token.is_none() { + return Err(GatewayClientError::NoAuthTokenAvailable); + } + if !self.connection.is_established() { + return Err(GatewayClientError::ConnectionNotEstablished); + } + // because of the previous check one of the unwraps MUST succeed + let auth_token = auth_token.unwrap_or_else(|| self.auth_token.unwrap()); + + let msg = + ClientControlRequest::new_authenticate(self.our_address.clone(), auth_token).into(); + let authenticated = match self.send_websocket_message(msg).await? { + ServerResponse::Authenticate { status } => { + self.authenticated = status; + Ok(status) + } + ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + _ => unreachable!(), + }?; + self.start_listening_for_sphinx_packets()?; + Ok(authenticated) + } + + // just a helper method to either call register or authenticate based on self.auth_token value + pub async fn perform_initial_authentication( + &mut self, + ) -> Result { + if self.auth_token.is_some() { + self.authenticate(None).await?; + } else { + self.register().await?; + } + if self.authenticated { + // if we are authenticated it means we MUST have an associated auth_token + Ok(self.auth_token.clone().unwrap()) + } else { + Err(GatewayClientError::AuthenticationFailure) + } + } + + // TODO: make it optionally use `send_websocket_message_without_response` + pub async fn send_sphinx_packet( + &mut self, + address: SocketAddr, + packet: Vec, + ) -> Result { + if !self.authenticated { + return Err(GatewayClientError::NotAuthenticated); + } + if !self.connection.is_established() { + return Err(GatewayClientError::ConnectionNotEstablished); + } + let msg = BinaryRequest::new_forward_request(address, packet).into(); + match self.send_websocket_message(msg).await? { + ServerResponse::Send { status } => Ok(status), + ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + _ => unreachable!(), + } + } + + async fn recover_socket_connection(&mut self) -> Result<(), GatewayClientError> { + if self.connection.is_available() { + return Ok(()); + } + if !self.connection.is_delegated() { + return Err(GatewayClientError::ConnectionInInvalidState); + } + + let (conn_fut, notify) = match std::mem::replace(&mut self.connection, SocketState::Invalid) + { + SocketState::Delegated(conn_fut, notify) => (conn_fut, notify), + _ => unreachable!(), + }; + + // tell the future to wrap up whatever it's doing now + notify.notify(); + self.connection = SocketState::Available(conn_fut.await?); + Ok(()) + } + + // TODO: this can be potentially bad as we have no direct restrictions of ensuring it's called + // within tokio runtime. Perhaps we should use the "old" way of passing explicit + // runtime handle to the constructor and using that instead? + fn start_listening_for_sphinx_packets(&mut self) -> Result<(), GatewayClientError> { + if !self.connection.is_available() { + return Err(GatewayClientError::ConnectionInInvalidState); + } + + // when called for, it NEEDS TO yield back the stream so that we could merge it and + // read control request responses. + let notify = Arc::new(Notify::new()); + let notify_clone = Arc::clone(¬ify); + + let mut extracted_connection = + match std::mem::replace(&mut self.connection, SocketState::Invalid) { + SocketState::Available(conn) => conn, + _ => unreachable!(), // impossible due to initial check + }; + + let sphinx_packet_sender = self.sphinx_packet_sender.clone(); + let sphinx_receiver_future = async move { + let mut should_return = false; + while !should_return { + tokio::select! { + _ = notify_clone.notified() => { + should_return = true; + } + msg = extracted_connection.next() => { + if msg.is_none() { + return Err(GatewayClientError::ConnectionAbruptlyClosed); + } + let msg = match msg.unwrap() { + Ok(msg) => msg, + Err(err) => { + return Err(GatewayClientError::NetworkError(err)); + } + }; + match msg { + Message::Binary(bin_msg) => { + sphinx_packet_sender.unbounded_send(vec![bin_msg]).unwrap() + } + _ => (), + }; + } + }; + } + Ok(extracted_connection) + }; + + let spawned_boxed_task = tokio::spawn(sphinx_receiver_future) + .map(|join_handle| { + join_handle.expect("task must have not failed to finish its execution!") + }) + .boxed(); + + self.connection = SocketState::Delegated(spawned_boxed_task, notify); + Ok(()) + } +} diff --git a/common/client-libs/mix-client/src/packet.rs b/common/client-libs/mix-client/src/packet.rs index 633d7aced6..cb26b8e501 100644 --- a/common/client-libs/mix-client/src/packet.rs +++ b/common/client-libs/mix-client/src/packet.rs @@ -13,9 +13,7 @@ // limitations under the License. use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; -use nymsphinx::{ - delays, Destination, DestinationAddressBytes, SURBIdentifier, SphinxPacket, SphinxUnwrapError, -}; +use nymsphinx::{delays, Destination, DestinationAddressBytes, SURBIdentifier, SphinxPacket}; use std::convert::TryFrom; use std::net::SocketAddr; use std::time; @@ -27,7 +25,7 @@ pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; pub enum SphinxPacketEncapsulationError { NoValidProvidersError, InvalidTopologyError, - SphinxEncapsulationError(SphinxUnwrapError), + SphinxError(nymsphinx::Error), InvalidFirstMixAddress, } @@ -38,12 +36,9 @@ impl From for SphinxPacketEncapsulationError { } } -// it is correct error we're converting from, it just has an unfortunate name -// related issue: https://github.com/nymtech/sphinx/issues/40 -impl From for SphinxPacketEncapsulationError { - fn from(err: SphinxUnwrapError) -> Self { - use SphinxPacketEncapsulationError::*; - SphinxEncapsulationError(err) +impl From for SphinxPacketEncapsulationError { + fn from(err: nymsphinx::Error) -> Self { + SphinxPacketEncapsulationError::SphinxError(err) } } @@ -107,7 +102,7 @@ pub fn encapsulate_message( let delays = delays::generate_from_average_duration(route.len(), average_delay); // build the packet - let packet = SphinxPacket::new(message, &route[..], &recipient, &delays)?; + let packet = SphinxPacket::new(message, &route[..], &recipient, &delays, None)?; // we know the mix route must be valid otherwise we would have already returned an error let first_node_address = @@ -125,7 +120,7 @@ pub fn encapsulate_message_route( let delays = delays::generate_from_average_duration(route.len(), average_delay); // build the packet - let packet = SphinxPacket::new(message, &route[..], &recipient, &delays)?; + let packet = SphinxPacket::new(message, &route[..], &recipient, &delays, None)?; let first_node_address = NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone())?; diff --git a/common/healthcheck/src/path_check.rs b/common/healthcheck/src/path_check.rs index d5dd7e5df0..9f0d54ed27 100644 --- a/common/healthcheck/src/path_check.rs +++ b/common/healthcheck/src/path_check.rs @@ -259,6 +259,7 @@ impl PathChecker { &path[..], &self.our_destination, &delays, + None, ) .unwrap(); diff --git a/common/healthcheck/src/result.rs b/common/healthcheck/src/result.rs index 3b25791088..6b5f20d160 100644 --- a/common/healthcheck/src/result.rs +++ b/common/healthcheck/src/result.rs @@ -95,6 +95,21 @@ impl HealthCheckResult { } }) .collect(); + + let filtered_gateway_nodes = topology + .gateways() + .into_iter() + .filter(|node| { + match self.node_score(NodeAddressBytes::from_base58_string(node.pub_key.clone())) { + None => { + error!("Unknown node in topology - {:?}", node); + false + } + Some(score) => score > score_threshold, + } + }) + .collect(); + // coco nodes remain unchanged as no healthcheck is being run on them or time being let filtered_coco_nodes = topology.coco_nodes(); @@ -102,6 +117,7 @@ impl HealthCheckResult { filtered_mix_nodes, filtered_provider_nodes, filtered_coco_nodes, + filtered_gateway_nodes, ) } diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index 3649294f8a..fb3be3da06 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -11,5 +11,5 @@ log = "0.4.8" rand = {version = "0.7.3", features = ["wasm-bindgen"]} ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="298c7fda6a577daf6d9bb955fa52c2bffecf6926" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="72368ea048472823f7f9e78eed77a2c3d30a471f" } diff --git a/common/nymsphinx/src/lib.rs b/common/nymsphinx/src/lib.rs index d85d3b08ce..5488c2b08d 100644 --- a/common/nymsphinx/src/lib.rs +++ b/common/nymsphinx/src/lib.rs @@ -27,10 +27,10 @@ pub use sphinx::{ constants::{ DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH, }, - header::{delays, delays::Delay, ProcessedHeader, SphinxHeader, SphinxUnwrapError}, - payload::{Payload, PayloadEncapsulationError}, + header::{delays, delays::Delay, ProcessedHeader, SphinxHeader}, + payload::Payload, route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier}, - ProcessedPacket, ProcessingError, SphinxPacket, PACKET_SIZE, + Error, ProcessedPacket, Result, SphinxPacket, PACKET_SIZE, }; // re-exporting this separately to remember to put special attention to below diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs new file mode 100644 index 0000000000..aca4da3fcb --- /dev/null +++ b/common/topology/src/gateway.rs @@ -0,0 +1,61 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::filter; +use nymsphinx::addressing::nodes::NymNodeRoutingAddress; +use nymsphinx::Node as SphinxNode; +use std::convert::TryInto; +use std::net::SocketAddr; + +#[derive(Debug, Clone)] +pub struct Client { + pub pub_key: String, +} + +#[derive(Debug, Clone)] +pub struct Node { + pub location: String, + pub client_listener: String, + pub mixnet_listener: SocketAddr, + pub pub_key: String, + pub registered_clients: Vec, + pub last_seen: u64, + pub version: String, +} + +impl Node { + pub fn get_pub_key_bytes(&self) -> [u8; 32] { + let mut key_bytes = [0; 32]; + bs58::decode(&self.pub_key).into(&mut key_bytes).unwrap(); + key_bytes + } +} + +impl filter::Versioned for Node { + fn version(&self) -> String { + self.version.clone() + } +} + +impl Into for Node { + fn into(self) -> SphinxNode { + let node_address_bytes = NymNodeRoutingAddress::from(self.mixnet_listener) + .try_into() + .unwrap(); + let key_bytes = self.get_pub_key_bytes(); + let key = nymsphinx::key::new(key_bytes); + + SphinxNode::new(node_address_bytes, key) + } +} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index f77a4654c8..790d683f27 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -21,6 +21,7 @@ use std::collections::HashMap; pub mod coco; mod filter; +pub mod gateway; pub mod mix; pub mod provider; @@ -32,9 +33,11 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { mix_nodes: Vec, mix_provider_nodes: Vec, coco_nodes: Vec, + gateway_nodes: Vec, ) -> Self; fn mix_nodes(&self) -> Vec; fn providers(&self) -> Vec; + fn gateways(&self) -> Vec; fn coco_nodes(&self) -> Vec; fn make_layered_topology(&self) -> Result>, NymTopologyError> { let mut layered_topology: HashMap> = HashMap::new(); @@ -94,14 +97,14 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { fn all_paths(&self) -> Result>, NymTopologyError> { let mut layered_topology = self.make_layered_topology()?; - let providers = self.providers(); + let gateways = self.gateways(); let sorted_layers: Vec> = (1..=layered_topology.len() as u64) .map(|layer| layered_topology.remove(&layer).unwrap()) // get all nodes per layer .map(|layer_nodes| layer_nodes.into_iter().map(|node| node.into()).collect()) // convert them into 'proper' sphinx nodes .chain(std::iter::once( - providers.into_iter().map(|node| node.into()).collect(), - )) // append all providers to the end + gateways.into_iter().map(|node| node.into()).collect(), + )) // append all gateways to the end .collect(); let all_paths = sorted_layers @@ -112,19 +115,30 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { Ok(all_paths) } + fn filter_system_version(&self, expected_version: &str) -> Self { + self.filter_node_versions( + expected_version, + expected_version, + expected_version, + expected_version, + ) + } + fn filter_node_versions( &self, expected_mix_version: &str, expected_provider_version: &str, + expected_gateway_version: &str, expected_coco_version: &str, ) -> Self { let mixes = self.mix_nodes().filter_by_version(expected_mix_version); let providers = self .providers() .filter_by_version(expected_provider_version); + let gateways = self.gateways().filter_by_version(expected_gateway_version); let cocos = self.coco_nodes().filter_by_version(expected_coco_version); - Self::new_from_nodes(mixes, providers, cocos) + Self::new_from_nodes(mixes, providers, cocos, gateways) } fn can_construct_path_through(&self) -> bool { diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 0fc5e0c9cb..27fed8288c 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -1,30 +1,52 @@ [package] -name = "gateway" -version = "0.1.0" -authors = ["Dave Hrycyszyn "] +build = "build.rs" +name = "nym-gateway" +version = "0.7.0-dev" +authors = ["Dave Hrycyszyn ", "JÄ™drzej StuczyÅ„ski "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = "2.33.0" +dirs = "2.0.2" dotenv = "0.15.0" futures = "0.3" +hmac = "0.7.1" log = "0.4" pretty_env_logger = "0.3" rand = "0.7.2" serde = { version = "1.0.104", features = ["derive"] } -#serde_json = "1.0.44" +sha2 = "0.8.1" sled = "0.31" tokio = { version = "0.2", features = ["full"] } tokio-tungstenite = "0.10.1" # internal +config = { path = "../common/config" } crypto = { path = "../common/crypto" } -mix-client = { path = "../common/client-libs/mix-client" } # to be removed very soon +directory-client = { path = "../common/client-libs/directory-client" } +gateway-requests = { path = "gateway-requests" } multi-tcp-client = { path = "../common/client-libs/multi-tcp-client" } nymsphinx = { path = "../common/nymsphinx" } +pemstore = { path = "../common/pemstore" } + +# this dependency is due to requiring to know content of loop message. however, mix-client module itself +# is going to be removed or renamed at some point as it no longer servers its purpose of sending traffic to mix network +# and only provides utility functions +mix-client = { path = "../common/client-libs/mix-client" } [dependencies.tungstenite] version = "0.10.0" -default-features = false \ No newline at end of file +default-features = false + +[build-dependencies] +built = "0.3.2" + +[dev-dependencies] +tempfile = "3.1.0" + +[features] +qa = [] +local = [] \ No newline at end of file diff --git a/gateway/src/client_handling/websocket/types.rs b/gateway/build.rs similarity index 57% rename from gateway/src/client_handling/websocket/types.rs rename to gateway/build.rs index 4f6867b2d1..56d753472c 100644 --- a/gateway/src/client_handling/websocket/types.rs +++ b/gateway/build.rs @@ -12,21 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; -use tokio_tungstenite::tungstenite::protocol::Message; +use built; -#[derive(Serialize, Deserialize, Debug)] -pub(crate) enum Request { - Send, - Register { address: String }, - Authenticate { token: String }, -} - -#[derive(Serialize, Deserialize, Debug)] -pub(crate) enum Response { - Send, - Register { token: String }, - Authenticate { status: bool }, - Error { message: String }, +fn main() { + built::write_built_file().expect("Failed to acquire build-time information"); } diff --git a/gateway/gateway-requests/Cargo.toml b/gateway/gateway-requests/Cargo.toml new file mode 100644 index 0000000000..9948964040 --- /dev/null +++ b/gateway/gateway-requests/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "gateway-requests" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bs58 = "0.3" +nymsphinx = { path = "../../common/nymsphinx" } +serde = { version = "1.0.104", features = ["derive"] } +serde_json = "1.0.44" +tokio-tungstenite = "0.10.1" diff --git a/gateway/gateway-requests/src/auth_token.rs b/gateway/gateway-requests/src/auth_token.rs new file mode 100644 index 0000000000..92768c1dbe --- /dev/null +++ b/gateway/gateway-requests/src/auth_token.rs @@ -0,0 +1,125 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub const AUTH_TOKEN_SIZE: usize = 32; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct AuthToken([u8; AUTH_TOKEN_SIZE]); + +#[derive(Debug)] +pub enum AuthTokenConversionError { + InvalidStringError, + StringOfInvalidLengthError, +} + +impl AuthToken { + pub fn from_bytes(bytes: [u8; AUTH_TOKEN_SIZE]) -> Self { + AuthToken(bytes) + } + + pub fn to_bytes(&self) -> [u8; AUTH_TOKEN_SIZE] { + self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn try_from_base58_string>( + val: S, + ) -> Result { + let decoded = match bs58::decode(val.into()).into_vec() { + Ok(decoded) => decoded, + Err(_) => return Err(AuthTokenConversionError::InvalidStringError), + }; + + if decoded.len() != AUTH_TOKEN_SIZE { + return Err(AuthTokenConversionError::StringOfInvalidLengthError); + } + + let mut token = [0u8; AUTH_TOKEN_SIZE]; + token.copy_from_slice(&decoded[..]); + Ok(AuthToken(token)) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(self.0).into_string() + } +} + +impl Into for AuthToken { + fn into(self) -> String { + self.to_base58_string() + } +} + +#[cfg(test)] +mod auth_token_conversion { + use super::*; + + #[test] + fn it_is_possible_to_recover_it_from_valid_b58_string() { + let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); + let auth_token_string = auth_token.to_base58_string(); + assert_eq!( + auth_token, + AuthToken::try_from_base58_string(auth_token_string).unwrap() + ) + } + + #[test] + fn it_is_possible_to_recover_it_from_valid_b58_str_ref() { + let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); + let auth_token_string = auth_token.to_base58_string(); + let auth_token_str_ref: &str = &auth_token_string; + assert_eq!( + auth_token, + AuthToken::try_from_base58_string(auth_token_str_ref).unwrap() + ) + } + + #[test] + fn it_returns_error_on_b58_with_invalid_characters() { + let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); + let auth_token_string = auth_token.to_base58_string(); + + let mut chars = auth_token_string.chars(); + let _consumed_first_char = chars.next().unwrap(); + + let invalid_chars_token = "=".to_string() + chars.as_str(); + assert!(AuthToken::try_from_base58_string(invalid_chars_token).is_err()) + } + + #[test] + fn it_returns_error_on_too_long_b58_string() { + let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); + let mut auth_token_string = auth_token.to_base58_string(); + auth_token_string.push('f'); + + assert!(AuthToken::try_from_base58_string(auth_token_string).is_err()) + } + + #[test] + fn it_returns_error_on_too_short_b58_string() { + let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); + let auth_token_string = auth_token.to_base58_string(); + + let mut chars = auth_token_string.chars(); + let _consumed_first_char = chars.next().unwrap(); + let _consumed_second_char = chars.next().unwrap(); + let invalid_chars_token = chars.as_str(); + + assert!(AuthToken::try_from_base58_string(invalid_chars_token).is_err()) + } +} diff --git a/gateway/gateway-requests/src/lib.rs b/gateway/gateway-requests/src/lib.rs new file mode 100644 index 0000000000..f437b47ddf --- /dev/null +++ b/gateway/gateway-requests/src/lib.rs @@ -0,0 +1,22 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod auth_token; +pub mod types; + +pub const DUMMY_MESSAGE_CONTENT: &[u8] = + b"[DUMMY MESSAGE] Wanting something does not give you the right to have it."; + +pub use auth_token::AuthToken; +pub use types::*; diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs new file mode 100644 index 0000000000..9caad4d937 --- /dev/null +++ b/gateway/gateway-requests/src/types.rs @@ -0,0 +1,194 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::auth_token::AuthToken; +use crate::types::BinaryRequest::ForwardSphinx; +use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nymsphinx::DestinationAddressBytes; +use serde::{Deserialize, Serialize}; +use std::{ + convert::TryFrom, + fmt::{self, Error, Formatter}, + net::SocketAddr, +}; +use tokio_tungstenite::tungstenite::protocol::Message; + +#[derive(Debug)] +pub enum GatewayRequestsError { + IncorrectlyEncodedAddress, + RequestOfInvalidSize(usize, usize), +} + +// to use it as `std::error::Error`, and we don't want to just derive is because we want +// the message to convey meanings of the usize tuple in RequestOfInvalidSize. +impl fmt::Display for GatewayRequestsError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + use GatewayRequestsError::*; + match self { + IncorrectlyEncodedAddress => write!(f, "address field was incorrectly encoded"), + RequestOfInvalidSize(actual, expected) => write!( + f, + "received request had invalid size. (actual: {}, expected: {})", + actual, expected + ), + } + } +} + +impl From for GatewayRequestsError { + fn from(_: NymNodeRoutingAddressError) -> Self { + GatewayRequestsError::IncorrectlyEncodedAddress + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum ClientControlRequest { + Authenticate { address: String, token: String }, + Register { address: String }, +} + +impl ClientControlRequest { + pub fn new_authenticate(address: DestinationAddressBytes, token: AuthToken) -> Self { + ClientControlRequest::Authenticate { + address: address.to_base58_string(), + token: token.to_base58_string(), + } + } + + pub fn new_register(address: DestinationAddressBytes) -> Self { + ClientControlRequest::Register { + address: address.to_base58_string(), + } + } +} + +impl Into for ClientControlRequest { + fn into(self) -> Message { + // it should be safe to call `unwrap` here as the message is generated by the server + // so if it fails (and consequently panics) it's a bug that should be resolved + let str_req = serde_json::to_string(&self).unwrap(); + Message::Text(str_req) + } +} + +impl TryFrom for ClientControlRequest { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result { + serde_json::from_str(&msg) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum ServerResponse { + Authenticate { status: bool }, + Register { token: String }, + Send { status: bool }, + Error { message: String }, +} + +impl ServerResponse { + pub fn new_error>(msg: S) -> Self { + ServerResponse::Error { + message: msg.into(), + } + } + + pub fn is_error(&self) -> bool { + match self { + ServerResponse::Error { .. } => true, + _ => false, + } + } + + pub fn implies_successful_authentication(&self) -> bool { + match self { + ServerResponse::Authenticate { status, .. } => *status, + ServerResponse::Register { .. } => true, + _ => false, + } + } +} + +impl Into for ServerResponse { + fn into(self) -> Message { + // it should be safe to call `unwrap` here as the message is generated by the server + // so if it fails (and consequently panics) it's a bug that should be resolved + let str_res = serde_json::to_string(&self).unwrap(); + Message::Text(str_res) + } +} + +impl TryFrom for ServerResponse { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result { + serde_json::from_str(&msg) + } +} + +pub enum BinaryRequest { + ForwardSphinx { address: SocketAddr, data: Vec }, +} + +impl BinaryRequest { + pub fn try_from_bytes(raw_req: &[u8]) -> Result { + // right now there's only a single option possible which significantly simplifies the logic + // if we decided to allow for more 'binary' messages, the API wouldn't need to change + let address = NymNodeRoutingAddress::try_from_bytes(&raw_req)?; + let addr_offset = address.bytes_min_len(); + + if raw_req[addr_offset..].len() != nymsphinx::PACKET_SIZE { + Err(GatewayRequestsError::RequestOfInvalidSize( + raw_req[addr_offset..].len(), + nymsphinx::PACKET_SIZE, + )) + } else { + Ok(ForwardSphinx { + address: address.into(), + data: raw_req[addr_offset..].into(), + }) + } + } + + pub fn into_bytes(self) -> Vec { + match self { + BinaryRequest::ForwardSphinx { address, data } => { + // TODO: using intermediate `NymNodeRoutingAddress` here is just temporary, because + // it happens to do exactly what we needed, but we don't really want to be + // dependant on what it does + let wrapped_address = NymNodeRoutingAddress::from(address); + wrapped_address + .as_bytes() + .into_iter() + .chain(data.into_iter()) + .collect() + } + } + } + + pub fn new_forward_request(address: SocketAddr, data: Vec) -> BinaryRequest { + BinaryRequest::ForwardSphinx { address, data } + } +} + +impl Into for BinaryRequest { + fn into(self) -> Message { + Message::Binary(self.into_bytes()) + } +} + +// TODO: tests... diff --git a/gateway/src/built_info.rs b/gateway/src/built_info.rs new file mode 100644 index 0000000000..38b46d59e6 --- /dev/null +++ b/gateway/src/built_info.rs @@ -0,0 +1,16 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The file has been placed there by the build script. +include!(concat!(env!("OUT_DIR"), "/built.rs")); diff --git a/gateway/src/client_handling/clients_handler.rs b/gateway/src/client_handling/clients_handler.rs deleted file mode 100644 index 91bd1aba0e..0000000000 --- a/gateway/src/client_handling/clients_handler.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::client_handling::ledger::ClientLedger; -use crate::client_handling::websocket::message_receiver::MixMessageSender; -use futures::channel::{mpsc, oneshot}; -use futures::StreamExt; -use log::*; -use nymsphinx::DestinationAddressBytes; -use std::collections::HashMap; -use tokio::task::JoinHandle; - -type temp_AuthToken = String; - -pub(crate) type ClientsHandlerRequestSender = mpsc::UnboundedSender; -pub(crate) type ClientsHandlerRequestReceiver = mpsc::UnboundedReceiver; - -pub(crate) type ClientsHandlerResponseSender = oneshot::Sender; -pub(crate) type ClientsHandlerResponseReceiver = oneshot::Receiver; - -pub(crate) enum ClientsHandlerRequest { - // client - Register( - DestinationAddressBytes, - MixMessageSender, - ClientsHandlerResponseSender, - ), - Authenticate( - temp_AuthToken, - MixMessageSender, - ClientsHandlerResponseSender, - ), - - // mix - IsOnline(DestinationAddressBytes, ClientsHandlerResponseSender), -} - -pub(crate) enum ClientsHandlerResponse { - Register(Option), - Authenticate(bool), - IsOnline(Option), -} - -pub(crate) struct ClientsHandler { - open_connections: HashMap, // clients_ledger: unimplemented!(), - clients_ledger: ClientLedger, - request_receiver_channel: ClientsHandlerRequestReceiver, -} - -impl ClientsHandler { - pub(crate) fn new(request_receiver_channel: ClientsHandlerRequestReceiver) -> Self { - ClientsHandler { - open_connections: HashMap::new(), - request_receiver_channel, - clients_ledger: unimplemented!(), - } - } - - pub(crate) async fn run(&mut self) { - while let Some(request) = self.request_receiver_channel.next().await { - // handle request - } - } - - pub(crate) fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { self.run().await }) - } -} diff --git a/gateway/src/client_handling/ledger.rs b/gateway/src/client_handling/ledger.rs deleted file mode 100644 index dd19b429c6..0000000000 --- a/gateway/src/client_handling/ledger.rs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//use directory_client::presence::providers::MixProviderClient; -//use log::*; -//use nymsphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; -//use sfw_provider_requests::auth_token::{AuthToken, AUTH_TOKEN_SIZE}; -//use std::path::PathBuf; - -#[derive(Debug)] -pub(crate) enum ClientLedgerError { - DbReadError(sled::Error), - DbWriteError(sled::Error), - DbOpenError(sled::Error), -} - -#[derive(Debug, Clone)] -// Note: you should NEVER create more than a single instance of this using 'new()'. -// You should always use .clone() to create additional instances -pub(crate) struct ClientLedger { - db: sled::Db, -} -// -//impl ClientLedger { -// pub(crate) fn load(file: PathBuf) -> Result { -// let db = match sled::open(file) { -// Err(e) => return Err(ClientLedgerError::DbOpenError(e)), -// Ok(db) => db, -// }; -// -// let ledger = ClientLedger { db }; -// -// ledger.db.iter().keys().for_each(|key| { -// println!( -// "key: {:?}", -// ledger -// .read_destination_address_bytes(key.unwrap()) -// .to_base58_string() -// ); -// }); -// -// Ok(ledger) -// } -// -// fn read_auth_token(&self, raw_token: sled::IVec) -> AuthToken { -// let token_bytes_ref = raw_token.as_ref(); -// // if this fails it means we have some database corruption and we -// // absolutely can't continue -// if token_bytes_ref.len() != AUTH_TOKEN_SIZE { -// error!("CLIENT LEDGER DATA CORRUPTION - TOKEN HAS INVALID LENGTH"); -// panic!("CLIENT LEDGER DATA CORRUPTION - TOKEN HAS INVALID LENGTH"); -// } -// -// let mut token_bytes = [0u8; AUTH_TOKEN_SIZE]; -// token_bytes.copy_from_slice(token_bytes_ref); -// AuthToken::from_bytes(token_bytes) -// } -// -// fn read_destination_address_bytes( -// &self, -// raw_destination: sled::IVec, -// ) -> DestinationAddressBytes { -// let destination_ref = raw_destination.as_ref(); -// // if this fails it means we have some database corruption and we -// // absolutely can't continue -// if destination_ref.len() != DESTINATION_ADDRESS_LENGTH { -// error!("CLIENT LEDGER DATA CORRUPTION - CLIENT ADDRESS HAS INVALID LENGTH"); -// panic!("CLIENT LEDGER DATA CORRUPTION - CLIENT ADDRESS HAS INVALID LENGTH"); -// } -// -// let mut destination_bytes = [0u8; DESTINATION_ADDRESS_LENGTH]; -// destination_bytes.copy_from_slice(destination_ref); -// DestinationAddressBytes::from_bytes(destination_bytes) -// } -// -// pub(crate) fn verify_token( -// &self, -// auth_token: &AuthToken, -// client_address: &DestinationAddressBytes, -// ) -> Result { -// match self.db.get(&client_address.to_bytes()) { -// Err(e) => Err(ClientLedgerError::DbReadError(e)), -// Ok(token) => match token { -// Some(token_ivec) => Ok(&self.read_auth_token(token_ivec) == auth_token), -// None => Ok(false), -// }, -// } -// } -// -// pub(crate) fn insert_token( -// &mut self, -// auth_token: AuthToken, -// client_address: DestinationAddressBytes, -// ) -> Result, ClientLedgerError> { -// let insertion_result = match self -// .db -// .insert(&client_address.to_bytes(), &auth_token.to_bytes()) -// { -// Err(e) => Err(ClientLedgerError::DbWriteError(e)), -// Ok(existing_token) => { -// Ok(existing_token.map(|existing_token| self.read_auth_token(existing_token))) -// } -// }; -// -// // registration doesn't happen that often so might as well flush it to the disk to be sure -// self.db.flush().unwrap(); -// insertion_result -// } -// -// pub(crate) fn remove_token( -// &mut self, -// client_address: &DestinationAddressBytes, -// ) -> Result, ClientLedgerError> { -// let removal_result = match self.db.remove(&client_address.to_bytes()) { -// Err(e) => Err(ClientLedgerError::DbWriteError(e)), -// Ok(existing_token) => { -// Ok(existing_token.map(|existing_token| self.read_auth_token(existing_token))) -// } -// }; -// -// // removing of tokens happens extremely rarely, so flush is also fine here -// self.db.flush().unwrap(); -// removal_result -// } -// -// pub(crate) fn current_clients(&self) -> Result, ClientLedgerError> { -// let clients = self.db.iter().keys(); -// -// let mut client_vec = Vec::new(); -// for client in clients { -// match client { -// Err(e) => return Err(ClientLedgerError::DbWriteError(e)), -// Ok(client_entry) => client_vec.push(MixProviderClient { -// pub_key: self -// .read_destination_address_bytes(client_entry) -// .to_base58_string(), -// }), -// } -// } -// -// Ok(client_vec) -// } -// -// #[cfg(test)] -// pub(crate) fn create_temporary() -> Self { -// let cfg = sled::Config::new().temporary(true); -// ClientLedger { -// db: cfg.open().unwrap(), -// } -// } -//} diff --git a/gateway/src/client_handling/mod.rs b/gateway/src/client_handling/mod.rs deleted file mode 100644 index 15706c869d..0000000000 --- a/gateway/src/client_handling/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod clients_handler; -pub(crate) mod ledger; -pub(crate) mod websocket; diff --git a/gateway/src/client_handling/websocket/connection_handler.rs b/gateway/src/client_handling/websocket/connection_handler.rs deleted file mode 100644 index fb83a1073d..0000000000 --- a/gateway/src/client_handling/websocket/connection_handler.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::client_handling::clients_handler::ClientsHandlerRequestSender; -use log::*; -use tokio::{prelude::*, stream::StreamExt}; -use tokio_tungstenite::{ - tungstenite::{protocol::Message, Error as WsError}, - WebSocketStream, -}; - -enum SocketStream { - RawTCP(S), - UpgradedWebSocket(WebSocketStream), - Invalid, -} - -pub(crate) struct Handle { - socket_connection: SocketStream, - clients_handler_sender: ClientsHandlerRequestSender, -} - -impl Handle -where - S: AsyncRead + AsyncWrite + Unpin, -{ - // for time being we assume handle is always constructed from raw socket. - // if we decide we want to change it, that's not too difficult - pub(crate) fn new(conn: S, clients_handler_sender: ClientsHandlerRequestSender) -> Self { - Handle { - socket_connection: SocketStream::RawTCP(conn), - clients_handler_sender, - } - } - - async fn perform_websocket_handshake(&mut self) { - self.socket_connection = - match std::mem::replace(&mut self.socket_connection, SocketStream::Invalid) { - SocketStream::RawTCP(conn) => { - // TODO: perhaps in the future, rather than panic here (and uncleanly shut tcp stream) - // return a result with an error? - let ws_stream = tokio_tungstenite::accept_async(conn) - .await - .expect("Failed to perform websocket handshake"); - SocketStream::UpgradedWebSocket(ws_stream) - } - other => other, - } - } - - async fn next_websocket_request(&mut self) -> Option> { - match self.socket_connection { - SocketStream::UpgradedWebSocket(ref mut ws_stream) => ws_stream.next().await, - _ => panic!("impossible state - websocket handshake was somehow reverted"), - } - } - - async fn listen_for_requests(&mut self) { - trace!("Started listening for incoming requests..."); - - while let Some(msg) = self.next_websocket_request().await { - // start handling here - // let msg = msg?; - // if msg.is_binary() { - // mixnet_client::forward_to_mixnode(msg.into_data(), Arc::clone(&client_ref)).await; - // } - } - - trace!("The stream was closed!"); - } - - pub(crate) async fn start_handling(&mut self) { - self.perform_websocket_handshake().await; - trace!("Managed to perform websocket handshake!"); - self.listen_for_requests().await; - } -} diff --git a/gateway/src/client_handling/websocket/message_receiver.rs b/gateway/src/client_handling/websocket/message_receiver.rs deleted file mode 100644 index 568b0d367d..0000000000 --- a/gateway/src/client_handling/websocket/message_receiver.rs +++ /dev/null @@ -1,4 +0,0 @@ -use futures::channel::mpsc; - -pub(crate) type MixMessageSender = mpsc::UnboundedSender>>; -pub(crate) type MixMessageReceiver = mpsc::UnboundedReceiver>>; diff --git a/gateway/src/client_handling/websocket/mod.rs b/gateway/src/client_handling/websocket/mod.rs deleted file mode 100644 index b585e2b23b..0000000000 --- a/gateway/src/client_handling/websocket/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub(crate) mod connection_handler; -pub(crate) mod listener; -pub(crate) mod message_receiver; -pub(crate) mod types; - -pub(crate) use listener::Listener; diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs new file mode 100644 index 0000000000..54752d5f63 --- /dev/null +++ b/gateway/src/commands/init.rs @@ -0,0 +1,131 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::commands::override_config; +use crate::config::persistence::pathfinder::GatewayPathfinder; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use crypto::encryption; +use pemstore::pemstore::PemStore; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("init") + .about("Initialise the gateway") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the gateway we want to create config for.") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("location") + .long("location") + .help("Optional geographical location of this provider") + .takes_value(true), + ) + .arg( + Arg::with_name("mix-host") + .long("mix-host") + .help("The custom host on which the gateway will be running for receiving sphinx packets") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("mix-port") + .long("mix-port") + .help("The port on which the gateway will be listening for sphinx packets") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-host") + .long("clients-host") + .help("The custom host on which the gateway will be running for receiving clients gateway-requests") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("clients-port") + .long("clients-port") + .help("The port on which the gateway will be listening for clients gateway-requests") + .takes_value(true) + ) + .arg( + Arg::with_name("mix-announce-host") + .long("mix-announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("mix-announce-port") + .long("mix-announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("clients-announce-host") + .long("clients-announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("clients-announce-port") + .long("clients-announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("inboxes") + .long("inboxes") + .help("Directory with inboxes where all packets for the clients are stored") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-ledger") + .long("clients-ledger") + .help("Ledger directory containing registered clients") + .takes_value(true) + ) + .arg( + Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the node is sending presence data to") + .takes_value(true), + ) +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + println!("Initialising gateway {}...", id); + + let mut config = crate::config::Config::new(id); + + config = override_config(config, matches); + + let sphinx_keys = encryption::KeyPair::new(); + let pathfinder = GatewayPathfinder::new_from_config(&config); + let pem_store = PemStore::new(pathfinder); + pem_store + .write_encryption_keys(sphinx_keys) + .expect("Failed to save sphinx keys"); + println!("Saved mixnet sphinx keypair"); + + let config_save_location = config.get_config_file_save_location(); + config + .save_to_file(None) + .expect("Failed to save the config file"); + println!("Saved configuration file to {:?}", config_save_location); + + println!("Gateway configuration completed.\n\n\n") +} diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs new file mode 100644 index 0000000000..4a8c3eeff8 --- /dev/null +++ b/gateway/src/commands/mod.rs @@ -0,0 +1,105 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::Config; +use clap::ArgMatches; + +pub mod init; +pub mod run; + +pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { + let mut was_mix_host_overridden = false; + if let Some(mix_host) = matches.value_of("mix-host") { + config = config.with_mix_listening_host(mix_host); + was_mix_host_overridden = true; + } + + if let Some(mix_port) = matches.value_of("mix-port").map(|port| port.parse::()) { + if let Err(err) = mix_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_mix_listening_port(mix_port.unwrap()); + } + let mut was_clients_host_overridden = false; + if let Some(clients_host) = matches.value_of("clients-host") { + config = config.with_clients_listening_host(clients_host); + was_clients_host_overridden = true; + } + + if let Some(clients_port) = matches + .value_of("clients-port") + .map(|port| port.parse::()) + { + if let Err(err) = clients_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_clients_listening_port(clients_port.unwrap()); + } + + if let Some(mix_announce_host) = matches.value_of("mix-announce-host") { + config = config.with_mix_announce_host(mix_announce_host); + } else if was_mix_host_overridden { + // make sure our 'mix-announce-host' always defaults to 'mix-host' + config = config.mix_announce_host_from_listening_host() + } + + if let Some(mix_announce_port) = matches + .value_of("mix-announce-port") + .map(|port| port.parse::()) + { + if let Err(err) = mix_announce_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_mix_announce_port(mix_announce_port.unwrap()); + } + + if let Some(clients_announce_host) = matches.value_of("clients-announce-host") { + config = config.with_clients_announce_host(clients_announce_host); + } else if was_clients_host_overridden { + // make sure our 'clients-announce-host' always defaults to 'clients-host' + config = config.clients_announce_host_from_listening_host() + } + + if let Some(clients_announce_port) = matches + .value_of("clients-announce-port") + .map(|port| port.parse::()) + { + if let Err(err) = clients_announce_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_clients_announce_port(clients_announce_port.unwrap()); + } + + if let Some(directory) = matches.value_of("directory") { + config = config.with_custom_directory(directory); + } + + if let Some(inboxes_dir) = matches.value_of("inboxes") { + config = config.with_custom_clients_inboxes(inboxes_dir); + } + + if let Some(clients_ledger) = matches.value_of("clients-ledger") { + config = config.with_custom_clients_ledger(clients_ledger); + } + + if let Some(location) = matches.value_of("location") { + config = config.with_location(location); + } + + config +} diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs new file mode 100644 index 0000000000..529a8b3dc5 --- /dev/null +++ b/gateway/src/commands/run.rs @@ -0,0 +1,233 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::commands::override_config; +use crate::config::persistence::pathfinder::GatewayPathfinder; +use crate::config::Config; +use crate::node::Gateway; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use crypto::encryption; +use directory_client::requests::presence_topology_get::PresenceTopologyGetRequester; +use directory_client::DirectoryClient; +use pemstore::pemstore::PemStore; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("run") + .about("Starts the gateway") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the gateway we want to run") + .takes_value(true) + .required(true), + ) + // the rest of arguments are optional, they are used to override settings in config file + .arg( + Arg::with_name("location") + .long("location") + .help("Optional geographical location of this gateway") + .takes_value(true), + ) + .arg( + Arg::with_name("config") + .long("config") + .help("Custom path to the nym gateway configuration file") + .takes_value(true), + ) + .arg( + Arg::with_name("mix-host") + .long("mix-host") + .help("The custom host on which the gateway will be running for receiving sphinx packets") + .takes_value(true) + ) + .arg( + Arg::with_name("mix-port") + .long("mix-port") + .help("The port on which the gateway will be listening for sphinx packets") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-host") + .long("clients-host") + .help("The custom host on which the gateway will be running for receiving clients gateway-requests") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-port") + .long("clients-port") + .help("The port on which the gateway will be listening for clients gateway-requests") + .takes_value(true) + ) + .arg( + Arg::with_name("mix-announce-host") + .long("mix-announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("mix-announce-port") + .long("mix-announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("clients-announce-host") + .long("clients-announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("clients-announce-port") + .long("clients-announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("inboxes") + .long("inboxes") + .help("Directory with inboxes where all packets for the clients are stored") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-ledger") + .long("clients-ledger") + .help("Ledger file containing registered clients") + .takes_value(true) + ) + .arg( + Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the gateway is sending presence data to") + .takes_value(true), + ) +} + +fn show_binding_warning(address: String) { + println!("\n##### NOTE #####"); + println!( + "\nYou are trying to bind to {} - you might not be accessible to other nodes\n\ + You can ignore this warning if you're running setup on a local network \n\ + or have set a custom 'announce-host'", + address + ); + println!("\n\n"); +} + +fn special_addresses() -> Vec<&'static str> { + vec!["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"] +} + +fn load_sphinx_keys(config_file: &Config) -> encryption::KeyPair { + let sphinx_keypair = PemStore::new(GatewayPathfinder::new_from_config(&config_file)) + .read_encryption() + .expect("Failed to read stored sphinx key files"); + println!( + "Public key: {}\n", + sphinx_keypair.public_key().to_base58_string() + ); + sphinx_keypair +} + +fn check_if_same_ip_gateway_exists( + directory_server: String, + announced_mix_host: String, + announced_clients_host: String, +) -> Option { + let directory_client_cfg = directory_client::Config::new(directory_server); + let topology = directory_client::Client::new(directory_client_cfg) + .presence_topology + .get() + .expect("Failed to retrieve network topology"); + + let existing_gateways = topology.gateway_nodes; + existing_gateways + .iter() + .find(|node| { + node.mixnet_listener == announced_mix_host + || node.client_listener == announced_clients_host + }) + .map(|node| node.pub_key.clone()) +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + + println!("Starting gateway {}...", id); + + let mut config = + Config::load_from_file(matches.value_of("config").map(|path| path.into()), Some(id)) + .expect("Failed to load config file"); + + config = override_config(config, matches); + + if let Some(duplicate_gateway_key) = check_if_same_ip_gateway_exists( + config.get_presence_directory_server(), + config.get_mix_announce_address(), + config.get_clients_announce_address(), + ) { + println!( + "Our announce-host is identical to an existing node's announce-host! (its key is {:?}", + duplicate_gateway_key + ); + return; + } + + let sphinx_keypair = load_sphinx_keys(&config); + + let mix_listening_ip_string = config.get_mix_listening_address().ip().to_string(); + if special_addresses().contains(&mix_listening_ip_string.as_ref()) { + show_binding_warning(mix_listening_ip_string); + } + + let clients_listening_ip_string = config.get_clients_listening_address().ip().to_string(); + if special_addresses().contains(&clients_listening_ip_string.as_ref()) { + show_binding_warning(clients_listening_ip_string); + } + + println!( + "Directory server [presence]: {}", + config.get_presence_directory_server() + ); + + println!( + "Listening for incoming sphinx packets on {}", + config.get_mix_listening_address() + ); + println!( + "Announcing the following socket address for sphinx packets: {}", + config.get_mix_announce_address() + ); + + println!( + "Listening for incoming clients packets on {}", + config.get_clients_listening_address() + ); + println!( + "Announcing the following socket address for clients packets: {}", + config.get_clients_announce_address() + ); + + println!( + "Inboxes directory is: {:?}", + config.get_clients_inboxes_dir() + ); + + println!( + "Clients ledger is stored at: {:?}", + config.get_clients_ledger_path() + ); + + Gateway::new(config, sphinx_keypair).run(); +} diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs new file mode 100644 index 0000000000..110dba7a19 --- /dev/null +++ b/gateway/src/config/mod.rs @@ -0,0 +1,586 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::template::config_template; +use config::NymConfig; +use log::*; +use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; +use std::str::FromStr; +use std::time; + +pub mod persistence; +mod template; + +// 'GATEWAY' +const DEFAULT_MIX_LISTENING_PORT: u16 = 1789; +const DEFAULT_CLIENT_LISTENING_PORT: u16 = 9000; +// 'DEBUG' +// where applicable, the below are defined in milliseconds +const DEFAULT_PRESENCE_SENDING_DELAY: u64 = 1500; // 1.5s +const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: u64 = 10_000; // 10s +const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: u64 = 300_000; // 5min +const DEFAULT_INITIAL_CONNECTION_TIMEOUT: u64 = 1_500; // 1.5s + +const DEFAULT_STORED_MESSAGE_FILENAME_LENGTH: u16 = 16; +const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: u16 = 5; + +#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + gateway: Gateway, + + mixnet_endpoint: MixnetEndpoint, + + clients_endpoint: ClientsEndpoint, + + #[serde(default)] + logging: Logging, + #[serde(default)] + debug: Debug, +} + +impl NymConfig for Config { + fn template() -> &'static str { + config_template() + } + + fn config_file_name() -> String { + "config.toml".to_string() + } + + fn default_root_directory() -> PathBuf { + dirs::home_dir() + .expect("Failed to evaluate $HOME value") + .join(".nym") + .join("gateways") + } + + fn root_directory(&self) -> PathBuf { + self.gateway.nym_root_directory.clone() + } + + fn config_directory(&self) -> PathBuf { + self.gateway + .nym_root_directory + .join(&self.gateway.id) + .join("config") + } + + fn data_directory(&self) -> PathBuf { + self.gateway + .nym_root_directory + .join(&self.gateway.id) + .join("data") + } +} + +impl Config { + pub fn new>(id: S) -> Self { + Config::default().with_id(id) + } + + // builder methods + pub fn with_id>(mut self, id: S) -> Self { + let id = id.into(); + if self.gateway.private_sphinx_key_file.as_os_str().is_empty() { + self.gateway.private_sphinx_key_file = + self::Gateway::default_private_sphinx_key_file(&id); + } + if self.gateway.public_sphinx_key_file.as_os_str().is_empty() { + self.gateway.public_sphinx_key_file = + self::Gateway::default_public_sphinx_key_file(&id); + } + if self + .clients_endpoint + .inboxes_directory + .as_os_str() + .is_empty() + { + self.clients_endpoint.inboxes_directory = + self::ClientsEndpoint::default_inboxes_directory(&id); + } + if self.clients_endpoint.ledger_path.as_os_str().is_empty() { + self.clients_endpoint.ledger_path = self::ClientsEndpoint::default_ledger_path(&id); + } + + self.gateway.id = id; + self + } + + pub fn with_custom_directory>(mut self, directory_server: S) -> Self { + self.debug.presence_directory_server = directory_server.into(); + self + } + + pub fn with_location>(mut self, location: S) -> Self { + self.gateway.location = location.into(); + self + } + + pub fn with_mix_listening_host>(mut self, host: S) -> Self { + // see if the provided `host` is just an ip address or ip:port + let host = host.into(); + + // is it ip:port? + match SocketAddr::from_str(host.as_ref()) { + Ok(socket_addr) => { + self.mixnet_endpoint.listening_address = socket_addr; + self + } + // try just for ip + Err(_) => match IpAddr::from_str(host.as_ref()) { + Ok(ip_addr) => { + self.mixnet_endpoint.listening_address.set_ip(ip_addr); + self + } + Err(_) => { + error!( + "failed to make any changes to config - invalid host {}", + host + ); + self + } + }, + } + } + + pub fn with_mix_listening_port(mut self, port: u16) -> Self { + self.mixnet_endpoint.listening_address.set_port(port); + self + } + + pub fn with_mix_announce_host>(mut self, host: S) -> Self { + // this is slightly more complicated as we store announce information as String, + // since it might not necessarily be a valid SocketAddr (say `nymtech.net:8080` is a valid + // announce address, yet invalid SocketAddr` + + // first lets see if we received host:port or just host part of an address + let host = host.into(); + let split_host: Vec<_> = host.split(':').collect(); + match split_host.len() { + 1 => { + // we provided only 'host' part so we are going to reuse existing port + self.mixnet_endpoint.announce_address = + format!("{}:{}", host, self.mixnet_endpoint.listening_address.port()); + self + } + 2 => { + // we provided 'host:port' so just put the whole thing there + self.mixnet_endpoint.announce_address = host; + self + } + _ => { + // we provided something completely invalid, so don't try to parse it + error!( + "failed to make any changes to config - invalid announce host {}", + host + ); + self + } + } + } + + pub fn mix_announce_host_from_listening_host(mut self) -> Self { + self.mixnet_endpoint.announce_address = self.mixnet_endpoint.listening_address.to_string(); + self + } + + pub fn with_mix_announce_port(mut self, port: u16) -> Self { + let current_host: Vec<_> = self.mixnet_endpoint.announce_address.split(':').collect(); + debug_assert_eq!(current_host.len(), 2); + self.mixnet_endpoint.announce_address = format!("{}:{}", current_host[0], port); + self + } + + pub fn with_clients_listening_host>(mut self, host: S) -> Self { + // see if the provided `host` is just an ip address or ip:port + let host = host.into(); + + // is it ip:port? + match SocketAddr::from_str(host.as_ref()) { + Ok(socket_addr) => { + self.clients_endpoint.listening_address = socket_addr; + self + } + // try just for ip + Err(_) => match IpAddr::from_str(host.as_ref()) { + Ok(ip_addr) => { + self.clients_endpoint.listening_address.set_ip(ip_addr); + self + } + Err(_) => { + error!( + "failed to make any changes to config - invalid host {}", + host + ); + self + } + }, + } + } + + pub fn clients_announce_host_from_listening_host(mut self) -> Self { + self.clients_endpoint.announce_address = format!( + "ws://{}", + self.clients_endpoint.listening_address.to_string() + ); + self + } + + pub fn with_clients_listening_port(mut self, port: u16) -> Self { + self.clients_endpoint.listening_address.set_port(port); + self + } + + pub fn with_clients_announce_host>(mut self, host: S) -> Self { + // this is slightly more complicated as we store announce information as String, + // since it might not necessarily be a valid SocketAddr (say `nymtech.net:8080` is a valid + // announce address, yet invalid SocketAddr` + + // first lets see if we received host:port or just host part of an address + let host = host.into(); + let split_host: Vec<_> = host.split(':').collect(); + match split_host.len() { + 1 => { + // we provided only 'host' part so we are going to reuse existing port + self.clients_endpoint.announce_address = format!( + "{}:{}", + host, + self.clients_endpoint.listening_address.port() + ); + // make sure it has 'ws' prefix (by extension it also includes 'wss') + if !self.clients_endpoint.announce_address.starts_with("ws") { + self.clients_endpoint.announce_address = + format!("ws://{}", self.clients_endpoint.announce_address); + } + self + } + 2 => { + // we provided 'host:port' so just put the whole thing there + self.clients_endpoint.announce_address = host; + // make sure it has 'ws' prefix (by extension it also includes 'wss') + if !self.clients_endpoint.announce_address.starts_with("ws") { + self.clients_endpoint.announce_address = + format!("ws://{}", self.clients_endpoint.announce_address); + } + self + } + _ => { + // we provided something completely invalid, so don't try to parse it + error!( + "failed to make any changes to config - invalid announce host {}", + host + ); + self + } + } + } + + pub fn with_clients_announce_port(mut self, port: u16) -> Self { + let current_host: Vec<_> = self.clients_endpoint.announce_address.split(':').collect(); + debug_assert_eq!(current_host.len(), 2); + self.clients_endpoint.announce_address = format!("{}:{}", current_host[0], port); + self + } + + pub fn with_custom_clients_inboxes>(mut self, inboxes_dir: S) -> Self { + self.clients_endpoint.inboxes_directory = PathBuf::from(inboxes_dir.into()); + self + } + + pub fn with_custom_clients_ledger>(mut self, ledger_path: S) -> Self { + self.clients_endpoint.ledger_path = PathBuf::from(ledger_path.into()); + self + } + + // getters + pub fn get_config_file_save_location(&self) -> PathBuf { + self.config_directory().join(Self::config_file_name()) + } + + pub fn get_location(&self) -> String { + self.gateway.location.clone() + } + + pub fn get_private_sphinx_key_file(&self) -> PathBuf { + self.gateway.private_sphinx_key_file.clone() + } + + pub fn get_public_sphinx_key_file(&self) -> PathBuf { + self.gateway.public_sphinx_key_file.clone() + } + + pub fn get_presence_directory_server(&self) -> String { + self.debug.presence_directory_server.clone() + } + + pub fn get_presence_sending_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.presence_sending_delay) + } + + pub fn get_mix_listening_address(&self) -> SocketAddr { + self.mixnet_endpoint.listening_address + } + + pub fn get_mix_announce_address(&self) -> String { + self.mixnet_endpoint.announce_address.clone() + } + + pub fn get_clients_listening_address(&self) -> SocketAddr { + self.clients_endpoint.listening_address + } + + pub fn get_clients_announce_address(&self) -> String { + self.clients_endpoint.announce_address.clone() + } + + pub fn get_clients_inboxes_dir(&self) -> PathBuf { + self.clients_endpoint.inboxes_directory.clone() + } + + pub fn get_clients_ledger_path(&self) -> PathBuf { + self.clients_endpoint.ledger_path.clone() + } + + pub fn get_packet_forwarding_initial_backoff(&self) -> time::Duration { + time::Duration::from_millis(self.debug.packet_forwarding_initial_backoff) + } + + pub fn get_packet_forwarding_maximum_backoff(&self) -> time::Duration { + time::Duration::from_millis(self.debug.packet_forwarding_maximum_backoff) + } + + pub fn get_initial_connection_timeout(&self) -> time::Duration { + time::Duration::from_millis(self.debug.initial_connection_timeout) + } + + pub fn get_message_retrieval_limit(&self) -> u16 { + self.debug.message_retrieval_limit + } + + pub fn get_stored_messages_filename_length(&self) -> u16 { + self.debug.stored_messages_filename_length + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Gateway { + /// ID specifies the human readable ID of this particular gateway. + id: String, + + /// Completely optional value specifying geographical location of this particular gateway. + /// Currently it's used entirely for debug purposes, as there are no mechanisms implemented + /// to verify correctness of the information provided. However, feel free to fill in + /// this field with as much accuracy as you wish to share. + location: String, + + /// Path to file containing private sphinx key. + private_sphinx_key_file: PathBuf, + + /// Path to file containing public sphinx key. + public_sphinx_key_file: PathBuf, + + /// nym_home_directory specifies absolute path to the home nym gateways directory. + /// It is expected to use default value and hence .toml file should not redefine this field. + nym_root_directory: PathBuf, +} + +impl Gateway { + fn default_private_sphinx_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("private_sphinx.pem") + } + + fn default_public_sphinx_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("public_sphinx.pem") + } + + fn default_location() -> String { + "unknown".into() + } +} + +impl Default for Gateway { + fn default() -> Self { + Gateway { + id: "".to_string(), + location: Self::default_location(), + private_sphinx_key_file: Default::default(), + public_sphinx_key_file: Default::default(), + nym_root_directory: Config::default_root_directory(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnetEndpoint { + /// Socket address to which this gateway will bind to + /// and will be listening for sphinx packets coming from the mixnet. + listening_address: SocketAddr, + + /// Optional address announced to the directory server for the clients to connect to. + /// It is useful, say, in NAT scenarios or wanting to more easily update actual IP address + /// later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. + /// Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` + /// are valid announce addresses, while the later will default to whatever port is used for + /// `listening_address`. + announce_address: String, +} + +impl Default for MixnetEndpoint { + fn default() -> Self { + MixnetEndpoint { + listening_address: format!("0.0.0.0:{}", DEFAULT_MIX_LISTENING_PORT) + .parse() + .unwrap(), + announce_address: format!("127.0.0.1:{}", DEFAULT_MIX_LISTENING_PORT), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ClientsEndpoint { + /// Socket address to which this gateway will bind to + /// and will be listening for data packets coming from the clients. + listening_address: SocketAddr, + + /// Optional address announced to the directory server for the clients to connect to. + /// It is useful, say, in NAT scenarios or wanting to more easily update actual IP address + /// later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. + /// Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` + /// are valid announce addresses, while the later will default to whatever port is used for + /// `listening_address`. + announce_address: String, + + /// Path to the directory with clients inboxes containing messages stored for them. + inboxes_directory: PathBuf, + + /// Full path to a file containing mapping of + /// client addresses to their access tokens. + ledger_path: PathBuf, +} + +impl ClientsEndpoint { + fn default_inboxes_directory(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("inboxes") + } + + fn default_ledger_path(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("client_ledger.sled") + } +} + +impl Default for ClientsEndpoint { + fn default() -> Self { + ClientsEndpoint { + listening_address: format!("0.0.0.0:{}", DEFAULT_CLIENT_LISTENING_PORT) + .parse() + .unwrap(), + announce_address: format!("ws://127.0.0.1:{}", DEFAULT_CLIENT_LISTENING_PORT), + inboxes_directory: Default::default(), + ledger_path: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Logging {} + +impl Default for Logging { + fn default() -> Self { + Logging {} + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct Debug { + /// Directory server to which the server will be reporting their presence data. + presence_directory_server: String, + + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + /// The provided value is interpreted as milliseconds. + packet_forwarding_initial_backoff: u64, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + /// The provided value is interpreted as milliseconds. + packet_forwarding_maximum_backoff: u64, + + /// Timeout for establishing initial connection when trying to forward a sphinx packet. + /// The provider value is interpreted as milliseconds. + initial_connection_timeout: u64, + + /// Delay between each subsequent presence data being sent. + presence_sending_delay: u64, + + /// Length of filenames for new client messages. + stored_messages_filename_length: u16, + + /// Number of messages client gets on each request + /// if there are no real messages, dummy ones are create to always return + /// `message_retrieval_limit` total messages + message_retrieval_limit: u16, +} + +impl Debug { + fn default_directory_server() -> String { + #[cfg(feature = "qa")] + return "https://qa-directory.nymtech.net".to_string(); + #[cfg(feature = "local")] + return "http://localhost:8080".to_string(); + + "https://directory.nymtech.net".to_string() + } +} + +impl Default for Debug { + fn default() -> Self { + Debug { + presence_directory_server: Self::default_directory_server(), + 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, + stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH, + message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[cfg(test)] +mod gateway_config { + use super::*; + + #[test] + fn after_saving_default_config_the_loaded_one_is_identical() { + // need to figure out how to do something similar but without touching the disk + // or the file system at all... + let temp_location = tempfile::tempdir().unwrap().path().join("config.toml"); + let default_config = Config::default().with_id("foomp".to_string()); + default_config + .save_to_file(Some(temp_location.clone())) + .unwrap(); + + let loaded_config = Config::load_from_file(Some(temp_location), None).unwrap(); + + assert_eq!(default_config, loaded_config); + } +} diff --git a/gateway/src/config/persistence/mod.rs b/gateway/src/config/persistence/mod.rs new file mode 100644 index 0000000000..97f4c000a4 --- /dev/null +++ b/gateway/src/config/persistence/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod pathfinder; diff --git a/gateway/src/config/persistence/pathfinder.rs b/gateway/src/config/persistence/pathfinder.rs new file mode 100644 index 0000000000..81d3d2d69b --- /dev/null +++ b/gateway/src/config/persistence/pathfinder.rs @@ -0,0 +1,58 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::Config; +use pemstore::pathfinder::PathFinder; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct GatewayPathfinder { + pub config_dir: PathBuf, + pub private_sphinx_key: PathBuf, + pub public_sphinx_key: PathBuf, +} + +impl GatewayPathfinder { + pub fn new_from_config(config: &Config) -> Self { + GatewayPathfinder { + config_dir: config.get_config_file_save_location(), + private_sphinx_key: config.get_private_sphinx_key_file(), + public_sphinx_key: config.get_public_sphinx_key_file(), + } + } +} + +impl PathFinder for GatewayPathfinder { + fn config_dir(&self) -> PathBuf { + self.config_dir.clone() + } + + fn private_identity_key(&self) -> PathBuf { + // TEMPORARILY USE SAME KEYS AS ENCRYPTION + self.private_sphinx_key.clone() + } + + fn public_identity_key(&self) -> PathBuf { + // TEMPORARILY USE SAME KEYS AS ENCRYPTION + self.public_sphinx_key.clone() + } + + fn private_encryption_key(&self) -> Option { + Some(self.private_sphinx_key.clone()) + } + + fn public_encryption_key(&self) -> Option { + Some(self.public_sphinx_key.clone()) + } +} diff --git a/gateway/src/config/template.rs b/gateway/src/config/template.rs new file mode 100644 index 0000000000..9e7f71c3c3 --- /dev/null +++ b/gateway/src/config/template.rs @@ -0,0 +1,92 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(crate) fn config_template() -> &'static str { + // While using normal toml marshalling would have been way simpler with less overhead, + // I think it's useful to have comments attached to the saved config file to explain behaviour of + // particular fields. + // Note: any changes to the template must be reflected in the appropriate structs in mod.rs. + r#" +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### main base mixnode config options ##### + +[gateway] +# Human readable ID of this particular gateway. +id = '{{ gateway.id }}' + +# Completely optional value specifying geographical location of this particular node. +# Currently it's used entirely for debug purposes, as there are no mechanisms implemented +# to verify correctness of the information provided. However, feel free to fill in +# this field with as much accuracy as you wish to share. +location = '{{ gateway.location }}' + +# Path to file containing private sphinx key. +private_sphinx_key_file = '{{ gateway.private_sphinx_key_file }}' + +# Path to file containing public sphinx key. +public_sphinx_key_file = '{{ gateway.public_sphinx_key_file }}' + +# nym_home_directory specifies absolute path to the home nym gateway directory. +# It is expected to use default value and hence .toml file should not redefine this field. +nym_root_directory = '{{ gateway.nym_root_directory }}' + + +##### Mixnet endpoint config options ##### + +[mixnet_endpoint] +# Socket address to which this gateway will bind to +# and will be listening for sphinx packets coming from the mixnet. +listening_address = '{{ mixnet_endpoint.listening_address }}' + +# Optional address announced to the directory server for the clients to connect to. +# It is useful, say, in NAT scenarios or wanting to more easily update actual IP address +# later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. +# Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` +# are valid announce addresses, while the later will default to whatever port is used for +# `listening_address`. +announce_address = '{{ mixnet_endpoint.announce_address }}' + + +#### Clients endpoint config options ##### + +[clients_endpoint] +# Socket address to which this gateway will bind to +# and will be listening for sphinx packets coming from the mixnet. +listening_address = '{{ clients_endpoint.listening_address }}' + +# Optional address announced to the directory server for the clients to connect to. +# It is useful, say, in NAT scenarios or wanting to more easily update actual IP address +# later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. +# Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` +# are valid announce addresses, while the later will default to whatever port is used for +# `listening_address`. +announce_address = '{{ clients_endpoint.announce_address }}' + +# Path to the directory with clients inboxes containing messages stored for them. +inboxes_directory = '{{ clients_endpoint.inboxes_directory }}' + +# Full path to a file containing mapping of client addresses to their access tokens. +ledger_path = '{{ clients_endpoint.ledger_path }}' + + +##### logging configuration options ##### + +[logging] + +# TODO + +"# +} diff --git a/gateway/src/main.rs b/gateway/src/main.rs index dff9e54de7..c921aaa100 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -12,45 +12,56 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::client_handling::websocket; -use crate::mixnet_handling::receiver::packet_processing::PacketProcessor; -use crate::storage::ClientStorage; -use log::*; +use clap::{App, ArgMatches}; -mod client_handling; -mod mixnet_client; -mod mixnet_handling; -pub(crate) mod storage; +pub mod built_info; +mod commands; +mod config; +mod node; -#[tokio::main] -async fn main() { +fn main() { dotenv::dotenv().ok(); setup_logging(); - let addr = "127.0.0.1:1793".parse().unwrap(); - info!("Listening on: {}", addr); + println!("{}", banner()); - let (dummy_clients_handler_tx, _) = futures::channel::mpsc::unbounded(); - let client_storage = ClientStorage::new(42, 42, "foomp".into()); - let dummy_keypair = crypto::encryption::KeyPair::new(); - let dummy_mix_packet_processor = PacketProcessor::new( - dummy_keypair.private_key().to_owned(), - dummy_clients_handler_tx.clone(), - client_storage, - ); + let arg_matches = App::new("Nym Mixnet Gateway") + .version(built_info::PKG_VERSION) + .author("Nymtech") + .about("Implementation of the Nym Mixnet Gateway") + .subcommand(commands::init::command_args()) + .subcommand(commands::run::command_args()) + .get_matches(); - websocket::Listener::new(addr, dummy_clients_handler_tx.clone()).start(); - mixnet_handling::Listener::new(addr).start(dummy_mix_packet_processor); + execute(arg_matches); +} - if let Err(e) = tokio::signal::ctrl_c().await { - error!( - "There was an error while capturing SIGINT - {:?}. We will terminate regardless", - e - ); +fn execute(matches: ArgMatches) { + match matches.subcommand() { + ("init", Some(m)) => commands::init::execute(m), + ("run", Some(m)) => commands::run::execute(m), + _ => println!("{}", usage()), } +} - println!( - "Received SIGINT - the provider will terminate now (threads are not YET nicely stopped)" - ); +fn usage() -> &'static str { + "usage: --help to see available options.\n\n" +} + +fn banner() -> String { + format!( + r#" + + _ __ _ _ _ __ ___ + | '_ \| | | | '_ \ _ \ + | | | | |_| | | | | | | + |_| |_|\__, |_| |_| |_| + |___/ + + (gateway - version {:}) + + "#, + built_info::PKG_VERSION + ) } fn setup_logging() { @@ -68,5 +79,8 @@ fn setup_logging() { .filter_module("reqwest", log::LevelFilter::Warn) .filter_module("mio", log::LevelFilter::Warn) .filter_module("want", log::LevelFilter::Warn) + .filter_module("sled", log::LevelFilter::Warn) + .filter_module("tungstenite", log::LevelFilter::Warn) + .filter_module("tokio_tungstenite", log::LevelFilter::Warn) .init(); } diff --git a/gateway/src/mixnet_client.rs b/gateway/src/mixnet_client.rs deleted file mode 100644 index 3094d6c4bd..0000000000 --- a/gateway/src/mixnet_client.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// use log::*; -use futures::lock::Mutex; -use multi_tcp_client::Client as MultiClient; -use nymsphinx::addressing::nodes::NymNodeRoutingAddress; -use nymsphinx::NODE_ADDRESS_LENGTH; -use std::sync::Arc; -use std::time::Duration; - -pub fn new() -> Arc> { - let config = multi_tcp_client::Config::new( - Duration::from_millis(200), - Duration::from_secs(86400), - Duration::from_secs(2), - ); - let client = multi_tcp_client::Client::new(config); - Arc::new(Mutex::new(client)) -} - -pub async fn forward_to_mixnode(mut payload: Vec, client_ref: Arc>) { - let mut address_buffer = [0; NODE_ADDRESS_LENGTH]; - let packet = payload.split_off(NODE_ADDRESS_LENGTH); - address_buffer.copy_from_slice(payload.as_slice()); - let address = NymNodeRoutingAddress::try_from_bytes(&address_buffer) - .unwrap() - .into(); - - let mut client = client_ref.lock().await; - client.send(address, packet, false).await.unwrap(); -} diff --git a/gateway/src/mixnet_handling/mod.rs b/gateway/src/mixnet_handling/mod.rs deleted file mode 100644 index 2209256561..0000000000 --- a/gateway/src/mixnet_handling/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod receiver; -pub(crate) mod sender; - -pub(crate) use receiver::listener::Listener; diff --git a/gateway/src/node/client_handling/clients_handler.rs b/gateway/src/node/client_handling/clients_handler.rs new file mode 100644 index 0000000000..273d6b70b2 --- /dev/null +++ b/gateway/src/node/client_handling/clients_handler.rs @@ -0,0 +1,313 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::node::{ + client_handling::websocket::message_receiver::MixMessageSender, + storage::{inboxes::ClientStorage, ClientLedger}, +}; +use crypto::encryption; +use futures::{ + channel::{mpsc, oneshot}, + StreamExt, +}; +use gateway_requests::auth_token::AuthToken; +use hmac::{Hmac, Mac}; +use log::*; +use nymsphinx::DestinationAddressBytes; +use sha2::Sha256; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::task::JoinHandle; + +pub(crate) type ClientsHandlerRequestSender = mpsc::UnboundedSender; +pub(crate) type ClientsHandlerRequestReceiver = mpsc::UnboundedReceiver; + +pub(crate) type ClientsHandlerResponseSender = oneshot::Sender; + +#[derive(Debug)] +pub(crate) enum ClientsHandlerRequest { + // client + Register( + DestinationAddressBytes, + MixMessageSender, + ClientsHandlerResponseSender, + ), + Authenticate( + DestinationAddressBytes, + AuthToken, + MixMessageSender, + ClientsHandlerResponseSender, + ), + Disconnect(DestinationAddressBytes), + + // mix + IsOnline(DestinationAddressBytes, ClientsHandlerResponseSender), +} + +#[derive(Debug)] +pub(crate) enum ClientsHandlerResponse { + Register(AuthToken), + Authenticate(bool), + IsOnline(Option), + Error(Box), +} + +pub(crate) struct ClientsHandler { + secret_key: Arc, + open_connections: HashMap, + clients_ledger: ClientLedger, + clients_inbox_storage: ClientStorage, +} + +impl ClientsHandler { + pub(crate) fn new( + secret_key: Arc, + clients_ledger: ClientLedger, + clients_inbox_storage: ClientStorage, + ) -> Self { + ClientsHandler { + secret_key, + open_connections: HashMap::new(), + clients_ledger, + clients_inbox_storage, + } + } + + fn make_error_response(&self, err: E) -> ClientsHandlerResponse + where + E: Into>, + { + ClientsHandlerResponse::Error(err.into()) + } + + // best effort sending error responses + fn send_error_response(&self, err: E, res_channel: ClientsHandlerResponseSender) + where + E: Into>, + { + if let Err(_) = res_channel.send(self.make_error_response(err)) { + error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); + } + } + + fn generate_new_auth_token(&self, client_address: DestinationAddressBytes) -> AuthToken { + type HmacSha256 = Hmac; + + // note that `new_varkey` doesn't even have an execution branch returning an error + // (true as of hmac 0.7.1) + let mut auth_token_raw = HmacSha256::new_varkey(&self.secret_key.to_bytes()).unwrap(); + auth_token_raw.input(client_address.as_bytes()); + let mut auth_token = [0u8; 32]; + auth_token.copy_from_slice(auth_token_raw.result().code().as_slice()); + AuthToken::from_bytes(auth_token) + } + + async fn push_stored_messages_to_client_and_save_channel( + &mut self, + client_address: DestinationAddressBytes, + comm_channel: MixMessageSender, + ) { + // TODO: it is possible that during a small window some of client messages will be "lost", + // i.e. be stored on the disk rather than pushed to the client, reason for this is as follows: + // now we push all stored messages from client's inbox to its websocket connection + // however, say, at the same time there's new message to the client - it gets stored on the disk! + // And only after this methods exits, mix receivers will become aware of the client + // connection going online and being able to forward traffic there. + // + // possible solution: spawn a future to empty inbox in X seconds rather than immediately + // JS: I will most likely do that (with including entries to config, etc.) once the + // basic version is up and running as not to waste time on it now + + // NOTE: THIS IGNORES MESSAGE RETRIEVAL LIMIT AND TAKES EVERYTHING! + let all_stored_messages = match self + .clients_inbox_storage + .retrieve_all_client_messages(client_address.clone()) + .await + { + Ok(msgs) => msgs, + Err(e) => { + error!( + "failed to retrieve client messages. {:?} inbox might be corrupted now - {:?}", + client_address.to_base58_string(), + e + ); + return; + } + }; + + let (messages, paths): (Vec<_>, Vec<_>) = all_stored_messages + .into_iter() + .map(|c| c.into_tuple()) + .unzip(); + + if let Err(_) = comm_channel.unbounded_send(messages) { + error!("Somehow we failed to stored messages to a fresh client channel - there seem to be a weird bug present!"); + } else { + // but if all went well, we can now delete it + if let Err(e) = self.clients_inbox_storage.delete_files(paths).await { + error!( + "Failed to remove client ({:?}) files - {:?}", + client_address.to_base58_string(), + e + ); + } else { + // finally, everything was fine - we retrieved everything, we deleted everything, + // we assume we can now safely delegate client message pushing + self.open_connections.insert(client_address, comm_channel); + } + } + } + + async fn handle_register_request( + &mut self, + address: DestinationAddressBytes, + comm_channel: MixMessageSender, + res_channel: ClientsHandlerResponseSender, + ) { + debug!( + "Processing register new client request: {:?}", + address.to_base58_string() + ); + + if self.open_connections.get(&address).is_some() { + warn!( + "Tried to process register request for a client with an already opened connection!" + ); + self.send_error_response("duplicate connection detected", res_channel); + return; + } + + // I presume some additional checks will go here: + // ... + let auth_token = self.generate_new_auth_token(address.clone()); + if self + .clients_ledger + .insert_token(auth_token.clone(), address.clone()) + .unwrap() + .is_some() + { + info!( + "Client {:?} was already registered before!", + address.to_base58_string() + ) + } else if let Err(e) = self + .clients_inbox_storage + .create_storage_dir(address.clone()) + .await + { + error!("We failed to create inbox directory for the client -{:?}\nReverting issued token...", e); + // we must revert our changes if this operation failed + self.clients_ledger.remove_token(&address).unwrap(); + self.send_error_response("failed to issue an auth token", res_channel); + return; + } + + self.push_stored_messages_to_client_and_save_channel(address, comm_channel) + .await; + + if let Err(_) = res_channel.send(ClientsHandlerResponse::Register(auth_token)) { + error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); + } + } + + async fn handle_authenticate_request( + &mut self, + address: DestinationAddressBytes, + token: AuthToken, + comm_channel: MixMessageSender, + res_channel: ClientsHandlerResponseSender, + ) { + debug!( + "Processing authenticate client request: {:?}", + address.to_base58_string() + ); + + if self.open_connections.get(&address).is_some() { + warn!("Tried to process authenticate request for a client with an already opened connection!"); + self.send_error_response("duplicate connection detected", res_channel); + return; + } + + if self.clients_ledger.verify_token(&token, &address).unwrap() { + self.push_stored_messages_to_client_and_save_channel(address, comm_channel) + .await; + if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(true)) { + error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); + } + } else { + if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(false)) { + error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); + } + } + } + + fn handle_disconnect(&mut self, address: DestinationAddressBytes) { + debug!( + "Processing disconnect client request: {:?}", + address.to_base58_string() + ); + self.open_connections.remove(&address); + } + + fn handle_is_online_request( + &self, + address: DestinationAddressBytes, + res_channel: ClientsHandlerResponseSender, + ) { + debug!( + "Processing is online request for: {:?}", + address.to_base58_string() + ); + + let response_value = self + .open_connections + .get(&address) + .map(|channel| channel.clone()); + // if this fails, it's a critical failure, because mix handlers should ALWAYS be online + res_channel + .send(ClientsHandlerResponse::IsOnline(response_value)) + .unwrap(); + } + + pub(crate) async fn run( + &mut self, + mut request_receiver_channel: ClientsHandlerRequestReceiver, + ) { + while let Some(request) = request_receiver_channel.next().await { + match request { + ClientsHandlerRequest::Register(address, comm_channel, res_channel) => { + self.handle_register_request(address, comm_channel, res_channel) + .await + } + ClientsHandlerRequest::Authenticate(address, token, comm_channel, res_channel) => { + self.handle_authenticate_request(address, token, comm_channel, res_channel) + .await + } + ClientsHandlerRequest::Disconnect(address) => self.handle_disconnect(address), + ClientsHandlerRequest::IsOnline(address, res_channel) => { + self.handle_is_online_request(address, res_channel) + } + }; + } + error!("Something bad has happened and we stopped listening for requests!"); + } + + pub(crate) fn start(mut self) -> (JoinHandle<()>, ClientsHandlerRequestSender) { + let (sender, receiver) = mpsc::unbounded(); + ( + tokio::spawn(async move { self.run(receiver).await }), + sender, + ) + } +} diff --git a/gateway/src/node/client_handling/mod.rs b/gateway/src/node/client_handling/mod.rs new file mode 100644 index 0000000000..1256cb2691 --- /dev/null +++ b/gateway/src/node/client_handling/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(crate) mod clients_handler; +pub(crate) mod websocket; diff --git a/gateway/src/node/client_handling/websocket/connection_handler.rs b/gateway/src/node/client_handling/websocket/connection_handler.rs new file mode 100644 index 0000000000..3ecffe5f2e --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler.rs @@ -0,0 +1,379 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use futures::{ + channel::{mpsc, oneshot}, + SinkExt, +}; +use log::*; +use std::convert::TryFrom; +use tokio::{prelude::*, stream::StreamExt}; +use tokio_tungstenite::{ + tungstenite::{protocol::Message, Error as WsError}, + WebSocketStream, +}; + +use gateway_requests::auth_token::AuthToken; +use gateway_requests::types::{BinaryRequest, ClientControlRequest, ServerResponse}; +use nymsphinx::DestinationAddressBytes; + +use crate::node::client_handling::clients_handler::{ + ClientsHandlerRequest, ClientsHandlerRequestSender, ClientsHandlerResponse, +}; +use crate::node::client_handling::websocket::message_receiver::{ + MixMessageReceiver, MixMessageSender, +}; +use crate::node::mixnet_handling::sender::OutboundMixMessageSender; + +//// TODO: note for my future self to consider the following idea: +//// split the socket connection into sink and stream +//// stream will be for reading explicit requests +//// and sink for pumping responses AND mix traffic +//// but as byproduct this might (or might not) break the clean "SocketStream" enum here + +enum SocketStream { + RawTCP(S), + UpgradedWebSocket(WebSocketStream), + Invalid, +} + +pub(crate) struct Handle { + address: Option, + clients_handler_sender: ClientsHandlerRequestSender, + outbound_mix_sender: OutboundMixMessageSender, + socket_connection: SocketStream, +} + +impl Handle +where + S: AsyncRead + AsyncWrite + Unpin, +{ + // for time being we assume handle is always constructed from raw socket. + // if we decide we want to change it, that's not too difficult + pub(crate) fn new( + conn: S, + clients_handler_sender: ClientsHandlerRequestSender, + outbound_mix_sender: OutboundMixMessageSender, + ) -> Self { + Handle { + address: None, + clients_handler_sender, + outbound_mix_sender, + socket_connection: SocketStream::RawTCP(conn), + } + } + + async fn perform_websocket_handshake(&mut self) { + self.socket_connection = + match std::mem::replace(&mut self.socket_connection, SocketStream::Invalid) { + SocketStream::RawTCP(conn) => { + // TODO: perhaps in the future, rather than panic here (and uncleanly shut tcp stream) + // return a result with an error? + let ws_stream = tokio_tungstenite::accept_async(conn) + .await + .expect("Failed to perform websocket handshake"); + SocketStream::UpgradedWebSocket(ws_stream) + } + other => other, + } + } + + async fn next_websocket_request(&mut self) -> Option> { + match self.socket_connection { + SocketStream::UpgradedWebSocket(ref mut ws_stream) => ws_stream.next().await, + _ => panic!("impossible state - websocket handshake was somehow reverted"), + } + } + + async fn send_websocket_response(&mut self, msg: Message) -> Result<(), WsError> { + match self.socket_connection { + // TODO: more closely investigate difference between `Sink::send` and `Sink::send_all` + // it got something to do with batching and flushing - it might be important if it + // turns out somehow we've got a bottleneck here + SocketStream::UpgradedWebSocket(ref mut ws_stream) => ws_stream.send(msg).await, + _ => panic!("impossible state - websocket handshake was somehow reverted"), + } + } + + async fn send_websocket_sphinx_packets( + &mut self, + packets: Vec>, + ) -> Result<(), WsError> { + let messages: Vec> = packets + .into_iter() + .map(|packet| Ok(Message::Binary(packet))) + .collect(); + let mut send_stream = futures::stream::iter(messages); + match self.socket_connection { + SocketStream::UpgradedWebSocket(ref mut ws_stream) => { + ws_stream.send_all(&mut send_stream).await + } + _ => panic!("impossible state - websocket handshake was somehow reverted"), + } + } + + fn disconnect(&self) { + // if we never established what is the address of the client, its connection was never + // announced hence we do not need to send 'disconnect' message + self.address.as_ref().map(|addr| { + self.clients_handler_sender + .unbounded_send(ClientsHandlerRequest::Disconnect(addr.clone())) + .unwrap(); + }); + } + + async fn handle_binary(&self, bin_msg: Vec) -> Message { + trace!("Handling binary message (presumably sphinx packet)"); + + match BinaryRequest::try_from_bytes(&bin_msg) { + Err(e) => ServerResponse::new_error(e.to_string()), + Ok(request) => match request { + // currently only a single type exists + BinaryRequest::ForwardSphinx { address, data } => { + // we know data has correct size (but nothing else besides of it) + self.outbound_mix_sender + .unbounded_send((address, data)) + .unwrap(); + ServerResponse::Send { status: true } + } + }, + } + .into() + } + + async fn handle_authenticate( + &mut self, + address: String, + token: String, + mix_sender: MixMessageSender, + ) -> ServerResponse { + let address = match DestinationAddressBytes::try_from_base58_string(address) { + Ok(address) => address, + Err(e) => { + trace!("failed to parse received DestinationAddress: {:?}", e); + return ServerResponse::new_error("malformed destination address").into(); + } + }; + let token = match AuthToken::try_from_base58_string(token) { + Ok(token) => token, + Err(e) => { + trace!("failed to parse received AuthToken: {:?}", e); + return ServerResponse::new_error("malformed authentication token").into(); + } + }; + + let (res_sender, res_receiver) = oneshot::channel(); + let clients_handler_request = + ClientsHandlerRequest::Authenticate(address.clone(), token, mix_sender, res_sender); + self.clients_handler_sender + .unbounded_send(clients_handler_request) + .unwrap(); // the receiver MUST BE alive + + match res_receiver.await.unwrap() { + ClientsHandlerResponse::Authenticate(authenticated) => { + if authenticated { + self.address = Some(address); + } + ServerResponse::Authenticate { + status: authenticated, + } + } + ClientsHandlerResponse::Error(e) => { + error!("Authentication unexpectedly failed - {}", e); + ServerResponse::Error { + message: "unexpected failure".into(), + } + } + _ => panic!("received response to wrong query!"), // this should NEVER happen + } + } + + async fn handle_register( + &mut self, + address: String, + mix_sender: MixMessageSender, + ) -> ServerResponse { + let address = match DestinationAddressBytes::try_from_base58_string(address) { + Ok(address) => address, + Err(e) => { + trace!("failed to parse received DestinationAddress: {:?}", e); + return ServerResponse::new_error("malformed destination address").into(); + } + }; + + let (res_sender, res_receiver) = oneshot::channel(); + let clients_handler_request = + ClientsHandlerRequest::Register(address.clone(), mix_sender, res_sender); + self.clients_handler_sender + .unbounded_send(clients_handler_request) + .unwrap(); // the receiver MUST BE alive + + match res_receiver.await.unwrap() { + // currently register can't fail (as in if all machines are working correctly and you + // send valid address, you will receive a valid token) + ClientsHandlerResponse::Register(token) => { + self.address = Some(address); + ServerResponse::Register { + token: token.to_base58_string(), + } + } + ClientsHandlerResponse::Error(e) => { + error!("Authentication unexpectedly failed - {}", e); + ServerResponse::Error { + message: "unexpected failure".into(), + } + } + _ => panic!("received response to wrong query!"), // this should NEVER happen + } + } + + // currently there are no valid control messages you can send after authentication + async fn handle_text(&mut self, _: String) -> Message { + trace!("Handling text message (presumably control message)"); + + error!("Currently there are no text messages besides 'Authenticate' and 'Register' and they were already dealt with!"); + ServerResponse::new_error("invalid request").into() + } + + async fn handle_request(&mut self, raw_request: Message) -> Option { + // apparently tungstenite auto-handles ping/pong/close messages so for now let's ignore + // them and let's test that claim. If that's not the case, just copy code from + // desktop nym-client websocket as I've manually handled everything there + match raw_request { + Message::Binary(bin_msg) => Some(self.handle_binary(bin_msg).await), + Message::Text(text_msg) => Some(self.handle_text(text_msg).await), + _ => None, + } + } + + async fn wait_for_initial_authentication(&mut self) -> Option { + trace!("Started waiting for authenticate/register request..."); + + while let Some(msg) = self.next_websocket_request().await { + let msg = match msg { + Ok(msg) => msg, + Err(err) => { + error!("failed to obtain message from websocket stream! stopping connection handler: {}", err); + break; + } + }; + + if msg.is_close() { + break; + } + + let (mix_sender, mix_receiver) = mpsc::unbounded(); + + // ONLY handle 'Authenticate' or 'Register' requests, ignore everything else + let response = match msg { + Message::Close(_) => break, + Message::Text(text_msg) => { + if let Ok(request) = ClientControlRequest::try_from(text_msg) { + match request { + ClientControlRequest::Authenticate { address, token } => { + self.handle_authenticate(address, token, mix_sender).await + } + ClientControlRequest::Register { address } => { + self.handle_register(address, mix_sender).await + } + } + } else { + ServerResponse::new_error("malformed request") + } + } + Message::Binary(_) => { + // perhaps logging level should be reduced here, let's leave it for now and see what happens + warn!("possibly received a sphinx packet without prior authentication. Request is going to be ignored"); + ServerResponse::new_error("binary request without prior authentication") + } + + _ => continue, + }; + + let is_done = response.implies_successful_authentication(); + + if let Err(err) = self.send_websocket_response(response.into()).await { + warn!( + "Failed to send message over websocket: {}. Assuming the connection is dead.", + err + ); + break; + } + + // it means we successfully managed to perform authentication and announce our + // presence to ClientsHandler + if is_done { + return Some(mix_receiver); + } + } + None + } + + async fn listen_for_requests(&mut self, mut mix_receiver: MixMessageReceiver) { + trace!("Started listening for ALL incoming requests..."); + + loop { + tokio::select! { + socket_msg = self.next_websocket_request() => { + if socket_msg.is_none() { + break; + } + let socket_msg = match socket_msg.unwrap() { + Ok(socket_msg) => socket_msg, + Err(err) => { + error!("failed to obtain message from websocket stream! stopping connection handler: {}", err); + break; + } + }; + + if socket_msg.is_close() { + break; + } + + if let Some(response) = self.handle_request(socket_msg).await { + if let Err(err) = self.send_websocket_response(response).await { + warn!( + "Failed to send message over websocket: {}. Assuming the connection is dead.", + err + ); + break; + } + } + }, + mix_messages = mix_receiver.next() => { + let mix_messages = mix_messages.expect("sender was unexpectedly closed! this shouldn't have ever happened!"); + if let Err(e) = self.send_websocket_sphinx_packets(mix_messages).await { + warn!("failed to send sphinx packets back to the client - {:?}, assuming the connection is dead", e); + break; + } + } + } + } + + self.disconnect(); + trace!("The stream was closed!"); + } + + pub(crate) async fn start_handling(&mut self) { + self.perform_websocket_handshake().await; + trace!("Managed to perform websocket handshake!"); + let mix_receiver = self.wait_for_initial_authentication().await; + trace!("Performed initial authentication"); + match mix_receiver { + Some(receiver) => self.listen_for_requests(receiver).await, + None => trace!("But connection was closed during the process"), + } + trace!("The handler is done!"); + } +} diff --git a/gateway/src/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs similarity index 56% rename from gateway/src/client_handling/websocket/listener.rs rename to gateway/src/node/client_handling/websocket/listener.rs index e7cef58a97..970333ca87 100644 --- a/gateway/src/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -12,29 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::client_handling::clients_handler::ClientsHandlerRequestSender; -use crate::client_handling::websocket::connection_handler::Handle; +use crate::node::client_handling::clients_handler::ClientsHandlerRequestSender; +use crate::node::client_handling::websocket::connection_handler::Handle; +use crate::node::mixnet_handling::sender::OutboundMixMessageSender; use log::*; use std::net::SocketAddr; use tokio::task::JoinHandle; pub(crate) struct Listener { address: SocketAddr, - clients_handler_sender: ClientsHandlerRequestSender, } impl Listener { - pub(crate) fn new( - address: SocketAddr, - clients_handler_sender: ClientsHandlerRequestSender, - ) -> Self { - Listener { - address, - clients_handler_sender, - } + pub(crate) fn new(address: SocketAddr) -> Self { + Listener { address } } - pub(crate) async fn run(&mut self) { + pub(crate) async fn run( + &mut self, + clients_handler_sender: ClientsHandlerRequestSender, + outbound_mix_sender: OutboundMixMessageSender, + ) { info!("Starting websocket listener at {}", self.address); let mut tcp_listener = tokio::net::TcpListener::bind(self.address) .await @@ -44,7 +42,13 @@ impl Listener { match tcp_listener.accept().await { Ok((socket, remote_addr)) => { trace!("received a socket connection from {}", remote_addr); - let mut handle = Handle::new(socket, self.clients_handler_sender.clone()); + // TODO: I think we need a mechanism for having a maximum number of connected + // clients or spawned tokio tasks -> perhaps a worker system? + let mut handle = Handle::new( + socket, + clients_handler_sender.clone(), + outbound_mix_sender.clone(), + ); tokio::spawn(async move { handle.start_handling().await }); } Err(e) => warn!("failed to get client: {:?}", e), @@ -52,7 +56,11 @@ impl Listener { } } - pub(crate) fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { self.run().await }) + pub(crate) fn start( + mut self, + clients_handler_sender: ClientsHandlerRequestSender, + outbound_mix_sender: OutboundMixMessageSender, + ) -> JoinHandle<()> { + tokio::spawn(async move { self.run(clients_handler_sender, outbound_mix_sender).await }) } } diff --git a/gateway/src/node/client_handling/websocket/message_receiver.rs b/gateway/src/node/client_handling/websocket/message_receiver.rs new file mode 100644 index 0000000000..ad69330270 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/message_receiver.rs @@ -0,0 +1,18 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use futures::channel::mpsc; + +pub(crate) type MixMessageSender = mpsc::UnboundedSender>>; +pub(crate) type MixMessageReceiver = mpsc::UnboundedReceiver>>; diff --git a/gateway/src/node/client_handling/websocket/mod.rs b/gateway/src/node/client_handling/websocket/mod.rs new file mode 100644 index 0000000000..babcbc7d80 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(crate) mod connection_handler; +pub(crate) mod listener; +pub(crate) mod message_receiver; + +pub(crate) use listener::Listener; diff --git a/gateway/src/node/mixnet_handling/mod.rs b/gateway/src/node/mixnet_handling/mod.rs new file mode 100644 index 0000000000..c4d03e42fa --- /dev/null +++ b/gateway/src/node/mixnet_handling/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(crate) mod receiver; +pub(crate) mod sender; + +pub(crate) use receiver::listener::Listener; +pub(crate) use receiver::packet_processing::PacketProcessor; diff --git a/gateway/src/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs similarity index 81% rename from gateway/src/mixnet_handling/receiver/connection_handler.rs rename to gateway/src/node/mixnet_handling/receiver/connection_handler.rs index 4255f9beff..2eb1107a54 100644 --- a/gateway/src/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -1,12 +1,19 @@ -use crate::client_handling::clients_handler::{ - ClientsHandlerRequest, ClientsHandlerRequestSender, ClientsHandlerResponse, -}; -use crate::client_handling::websocket::message_receiver::MixMessageSender; -use crate::mixnet_handling::receiver::packet_processing::PacketProcessor; -use futures::channel::{mpsc, oneshot}; +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::node::mixnet_handling::receiver::packet_processing::PacketProcessor; use log::*; -use nymsphinx::DestinationAddressBytes; -use std::collections::HashMap; use std::net::SocketAddr; use tokio::{io::AsyncReadExt, prelude::*}; diff --git a/gateway/src/mixnet_handling/receiver/listener.rs b/gateway/src/node/mixnet_handling/receiver/listener.rs similarity index 92% rename from gateway/src/mixnet_handling/receiver/listener.rs rename to gateway/src/node/mixnet_handling/receiver/listener.rs index 52a5c13cb7..9532a37af5 100644 --- a/gateway/src/mixnet_handling/receiver/listener.rs +++ b/gateway/src/node/mixnet_handling/receiver/listener.rs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::mixnet_handling::receiver::connection_handler::Handle; -use crate::mixnet_handling::receiver::packet_processing::PacketProcessor; +use crate::node::mixnet_handling::receiver::{ + connection_handler::Handle, packet_processing::PacketProcessor, +}; use log::*; use std::net::SocketAddr; use tokio::task::JoinHandle; diff --git a/gateway/src/mixnet_handling/receiver/mod.rs b/gateway/src/node/mixnet_handling/receiver/mod.rs similarity index 100% rename from gateway/src/mixnet_handling/receiver/mod.rs rename to gateway/src/node/mixnet_handling/receiver/mod.rs diff --git a/gateway/src/mixnet_handling/receiver/packet_processing.rs b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs similarity index 90% rename from gateway/src/mixnet_handling/receiver/packet_processing.rs rename to gateway/src/node/mixnet_handling/receiver/packet_processing.rs index 64a2092ec2..33e7cce31b 100644 --- a/gateway/src/mixnet_handling/receiver/packet_processing.rs +++ b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs @@ -12,17 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::client_handling::clients_handler::{ +use crate::node::client_handling::clients_handler::{ ClientsHandlerRequest, ClientsHandlerRequestSender, ClientsHandlerResponse, }; -use crate::client_handling::websocket::message_receiver::MixMessageSender; -use crate::storage::{ClientStorage, StoreData}; +use crate::node::client_handling::websocket::message_receiver::MixMessageSender; +use crate::node::storage::inboxes::{ClientStorage, StoreData}; use crypto::encryption; use futures::channel::oneshot; use futures::lock::Mutex; use log::*; use mix_client::packet::LOOP_COVER_MESSAGE_PAYLOAD; -use nymsphinx::{DestinationAddressBytes, Payload, ProcessedPacket, SURBIdentifier, SphinxPacket}; +use nymsphinx::{DestinationAddressBytes, Error as SphinxError, ProcessedPacket, SphinxPacket}; use std::collections::HashMap; use std::io; use std::ops::Deref; @@ -33,22 +33,16 @@ pub enum MixProcessingError { ReceivedForwardHopError, NonMatchingRecipient, InvalidPayload, - SphinxProcessingError, + SphinxProcessingError(SphinxError), IOError(String), } -pub enum MixProcessingResult { - #[allow(dead_code)] - ForwardHop, - FinalHop, -} - -impl From for MixProcessingError { - // for time being just have a single error instance for all possible results of nymsphinx::ProcessingError - fn from(_: nymsphinx::ProcessingError) -> Self { +impl From for MixProcessingError { + // for time being just have a single error instance for all possible results of SphinxError + fn from(err: SphinxError) -> Self { use MixProcessingError::*; - SphinxProcessingError + SphinxProcessingError(err) } } @@ -73,7 +67,7 @@ pub struct PacketProcessor { impl PacketProcessor { pub(crate) fn new( - secret_key: encryption::PrivateKey, + secret_key: Arc, clients_handler_sender: ClientsHandlerRequestSender, client_store: ClientStorage, ) -> Self { @@ -81,7 +75,7 @@ impl PacketProcessor { available_socket_senders_cache: Arc::new(Mutex::new(HashMap::new())), clients_handler_sender, client_store, - secret_key: Arc::new(secret_key), + secret_key, } } @@ -172,7 +166,7 @@ impl PacketProcessor { warn!("Received a forward hop message - those are not implemented for providers"); Err(MixProcessingError::ReceivedForwardHopError) } - Ok(ProcessedPacket::ProcessedPacketFinalHop(client_address, surb_id, payload)) => { + Ok(ProcessedPacket::ProcessedPacketFinalHop(client_address, _surb_id, payload)) => { // in our current design, we do not care about the 'surb_id' in the header // as it will always be empty anyway let (payload_destination, message) = payload @@ -185,7 +179,7 @@ impl PacketProcessor { } Err(e) => { warn!("Failed to unwrap Sphinx packet: {:?}", e); - Err(MixProcessingError::SphinxProcessingError) + Err(MixProcessingError::SphinxProcessingError(e)) } } } diff --git a/gateway/src/mixnet_handling/sender/mod.rs b/gateway/src/node/mixnet_handling/sender/mod.rs similarity index 62% rename from gateway/src/mixnet_handling/sender/mod.rs rename to gateway/src/node/mixnet_handling/sender/mod.rs index e6dc32e0f0..7329f524b3 100644 --- a/gateway/src/mixnet_handling/sender/mod.rs +++ b/gateway/src/node/mixnet_handling/sender/mod.rs @@ -19,11 +19,15 @@ use futures::StreamExt; use log::*; use std::net::SocketAddr; use std::time::Duration; +use tokio::task::JoinHandle; + +pub(crate) type OutboundMixMessageSender = mpsc::UnboundedSender<(SocketAddr, Vec)>; +pub(crate) type OutboundMixMessageReceiver = mpsc::UnboundedReceiver<(SocketAddr, Vec)>; pub(crate) struct PacketForwarder { tcp_client: multi_tcp_client::Client, - conn_tx: mpsc::UnboundedSender<(SocketAddr, Vec)>, - conn_rx: mpsc::UnboundedReceiver<(SocketAddr, Vec)>, + conn_tx: OutboundMixMessageSender, + conn_rx: OutboundMixMessageReceiver, } impl PacketForwarder { @@ -47,17 +51,19 @@ impl PacketForwarder { } } - pub(crate) fn start(mut self) -> mpsc::UnboundedSender<(SocketAddr, Vec)> { - // TODO: what to do with the lost JoinHandle - do we even care? + pub(crate) fn start(mut self) -> (JoinHandle<()>, OutboundMixMessageSender) { let sender_channel = self.conn_tx.clone(); - tokio::spawn(async move { - while let Some((address, packet)) = self.conn_rx.next().await { - trace!("Going to forward packet to {:?}", address); - // as a mix node we don't care about responses, we just want to fire packets - // as quickly as possible - self.tcp_client.send(address, packet, false).await.unwrap(); // if we're not waiting for response, we MUST get an Ok - } - }); - sender_channel + ( + tokio::spawn(async move { + while let Some((address, packet)) = self.conn_rx.next().await { + trace!("Going to forward packet to {:?}", address); + // as a mix node we don't care about responses, we just want to fire packets + // as quickly as possible + self.tcp_client.send(address, packet, false).await.unwrap(); + // if we're not waiting for response, we MUST get an Ok + } + }), + sender_channel, + ) } } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs new file mode 100644 index 0000000000..4eb912b241 --- /dev/null +++ b/gateway/src/node/mod.rs @@ -0,0 +1,161 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::Config; +use crate::node::client_handling::clients_handler::{ClientsHandler, ClientsHandlerRequestSender}; +use crate::node::client_handling::websocket; +use crate::node::mixnet_handling::sender::{OutboundMixMessageSender, PacketForwarder}; +use crate::node::storage::{inboxes, ClientLedger}; +use crypto::encryption; +use log::*; +use std::sync::Arc; +use tokio::runtime::Runtime; + +pub(crate) mod client_handling; +pub(crate) mod mixnet_handling; +mod presence; +pub(crate) mod storage; + +// current issues in this file: +// - two calls to `Arc::new(self.sphinx_keypair.private_key().clone()),` - basically 2 separate +// Arcs to the same underlying data (well, after a clone), so what it ends up resulting in is +// private key being in 3 different places in memory rather than in a single location. +// Does it affect performance? No, not really. Is it *SUPER* insecure? Also, not as much, because +// if somebody could read memory of the machine, they probably got better attack vectors. +// Should it get fixed? Probably. But it's very low priority for time being. + +pub struct Gateway { + config: Config, + sphinx_keypair: encryption::KeyPair, + registered_clients_ledger: ClientLedger, + client_inbox_storage: inboxes::ClientStorage, +} + +impl Gateway { + // the constructor differs from mixnodes and providers in that it takes keys directly + // as opposed to having `Self::load_sphinx_keys(cfg: &Config)` method. Let's see + // how it works out, because I'm not sure which one would be "better", but when I think about it, + // I kinda prefer to delegate having to load the keys to outside the gateway + pub fn new(config: Config, sphinx_keypair: encryption::KeyPair) -> Self { + let registered_clients_ledger = match ClientLedger::load(config.get_clients_ledger_path()) { + Err(e) => panic!(format!("Failed to load the ledger - {:?}", e)), + Ok(ledger) => ledger, + }; + let client_inbox_storage = inboxes::ClientStorage::new( + config.get_message_retrieval_limit() as usize, + config.get_stored_messages_filename_length(), + config.get_clients_inboxes_dir(), + ); + Gateway { + config, + sphinx_keypair, + client_inbox_storage, + registered_clients_ledger, + } + } + + fn start_mix_socket_listener(&self, clients_handler_sender: ClientsHandlerRequestSender) { + info!("Starting mix socket listener..."); + + let packet_processor = mixnet_handling::PacketProcessor::new( + Arc::new(self.sphinx_keypair.private_key().clone()), + clients_handler_sender, + self.client_inbox_storage.clone(), + ); + + mixnet_handling::Listener::new(self.config.get_mix_listening_address()) + .start(packet_processor); + } + + fn start_client_websocket_listener( + &self, + forwarding_channel: OutboundMixMessageSender, + clients_handler_sender: ClientsHandlerRequestSender, + ) { + info!("Starting client [web]socket listener..."); + + websocket::Listener::new(self.config.get_clients_listening_address()) + .start(clients_handler_sender, forwarding_channel); + } + + fn start_packet_forwarder(&self) -> OutboundMixMessageSender { + info!("Starting mix packet forwarder..."); + + let (_, forwarding_channel) = PacketForwarder::new( + self.config.get_packet_forwarding_initial_backoff(), + self.config.get_packet_forwarding_maximum_backoff(), + self.config.get_initial_connection_timeout(), + ) + .start(); + forwarding_channel + } + + fn start_clients_handler(&self) -> ClientsHandlerRequestSender { + info!("Starting clients handler"); + let (_, clients_handler_sender) = ClientsHandler::new( + Arc::new(self.sphinx_keypair.private_key().clone()), + self.registered_clients_ledger.clone(), + self.client_inbox_storage.clone(), + ) + .start(); + clients_handler_sender + } + + fn start_presence_notifier(&self) { + info!("Starting presence notifier..."); + let notifier_config = presence::NotifierConfig::new( + self.config.get_location(), + self.config.get_presence_directory_server(), + self.config.get_mix_announce_address(), + self.config.get_clients_announce_address(), + self.sphinx_keypair.public_key().to_base58_string(), + self.config.get_presence_sending_delay(), + ); + presence::Notifier::new(notifier_config, self.registered_clients_ledger.clone()).start(); + } + + async fn wait_for_interrupt(&self) { + if let Err(e) = tokio::signal::ctrl_c().await { + error!( + "There was an error while capturing SIGINT - {:?}. We will terminate regardless", + e + ); + } + println!( + "Received SIGINT - the gateway will terminate now (threads are not YET nicely stopped)" + ); + } + + // Rather than starting all futures with explicit `&Handle` argument, let's see how it works + // out if we make it implicit using `tokio::spawn` inside Runtime context. + // Basically more or less equivalent of using #[tokio::main] attribute. + pub fn run(&mut self) { + info!("Starting nym gateway!"); + let mut runtime = Runtime::new().unwrap(); + + runtime.block_on(async { + let mix_forwarding_channel = self.start_packet_forwarder(); + let clients_handler_sender = self.start_clients_handler(); + + self.start_mix_socket_listener(clients_handler_sender.clone()); + self.start_client_websocket_listener(mix_forwarding_channel, clients_handler_sender); + + self.start_presence_notifier(); + + info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!"); + + self.wait_for_interrupt().await + }); + } +} diff --git a/gateway/src/node/presence/mod.rs b/gateway/src/node/presence/mod.rs new file mode 100644 index 0000000000..3ab06c487d --- /dev/null +++ b/gateway/src/node/presence/mod.rs @@ -0,0 +1,120 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::built_info; +use crate::node::storage::ClientLedger; +use directory_client::presence::gateways::{GatewayClient, GatewayPresence}; +use directory_client::requests::presence_gateways_post::PresenceGatewayPoster; +use directory_client::DirectoryClient; +use log::{error, trace}; +use std::time::Duration; +use tokio::task::JoinHandle; + +pub(crate) struct NotifierConfig { + location: String, + directory_server: String, + mix_announce_host: String, + clients_announce_host: String, + pub_key_string: String, + sending_delay: Duration, +} + +impl NotifierConfig { + pub(crate) fn new( + location: String, + directory_server: String, + mix_announce_host: String, + clients_announce_host: String, + pub_key_string: String, + sending_delay: Duration, + ) -> Self { + NotifierConfig { + location, + directory_server, + mix_announce_host, + clients_announce_host, + pub_key_string, + sending_delay, + } + } +} + +pub(crate) struct Notifier { + location: String, + net_client: directory_client::Client, + client_ledger: ClientLedger, + sending_delay: Duration, + client_listener: String, + mixnet_listener: String, + pub_key_string: String, +} + +impl Notifier { + pub(crate) fn new(config: NotifierConfig, client_ledger: ClientLedger) -> Notifier { + let directory_client_cfg = directory_client::Config { + base_url: config.directory_server, + }; + let net_client = directory_client::Client::new(directory_client_cfg); + + Notifier { + client_ledger, + net_client, + location: config.location, + client_listener: config.clients_announce_host, + mixnet_listener: config.mix_announce_host, + pub_key_string: config.pub_key_string, + sending_delay: config.sending_delay, + } + } + + async fn make_presence(&self) -> GatewayPresence { + let client_keys = self.client_ledger.current_clients().unwrap(); + let registered_clients = client_keys + .into_iter() + .map(|key_bytes| GatewayClient { + pub_key: key_bytes.to_base58_string(), + }) + .collect(); + + GatewayPresence { + location: self.location.clone(), + client_listener: self.client_listener.clone(), + mixnet_listener: self.mixnet_listener.clone(), + pub_key: self.pub_key_string.clone(), + registered_clients, + last_seen: 0, + version: built_info::PKG_VERSION.to_string(), + } + } + + pub(crate) fn notify(&self, presence: GatewayPresence) { + match self.net_client.presence_gateway_post.post(&presence) { + Err(err) => error!("failed to send presence - {:?}", err), + Ok(_) => trace!("sent presence information"), + } + } + + pub fn start(self) -> JoinHandle<()> { + tokio::spawn(async move { + loop { + // set the deadline in the future + let sending_delay = tokio::time::delay_for(self.sending_delay); + let presence = self.make_presence().await; + self.notify(presence); + // wait for however much is left + sending_delay.await; + } + }) + } +} diff --git a/gateway/src/storage/mod.rs b/gateway/src/node/storage/inboxes.rs similarity index 84% rename from gateway/src/storage/mod.rs rename to gateway/src/node/storage/inboxes.rs index b488dea5c8..bd165e5a81 100644 --- a/gateway/src/storage/mod.rs +++ b/gateway/src/node/storage/inboxes.rs @@ -14,10 +14,10 @@ use futures::lock::Mutex; use futures::StreamExt; +use gateway_requests::DUMMY_MESSAGE_CONTENT; use log::*; -use nymsphinx::{DestinationAddressBytes, SURBIdentifier}; +use nymsphinx::DestinationAddressBytes; use rand::Rng; -//use sfw_provider_requests::DUMMY_MESSAGE_CONTENT; use std::io; use std::path::PathBuf; use std::sync::Arc; @@ -25,8 +25,6 @@ use tokio::fs; use tokio::fs::File; use tokio::prelude::*; -const DUMMY_MESSAGE_CONTENT: &'static [u8] = b"foomp"; - fn dummy_message() -> ClientFile { ClientFile { content: DUMMY_MESSAGE_CONTENT.to_vec(), @@ -75,6 +73,8 @@ pub struct ClientStorage { // even though the data inside is extremely cheap to copy, we have to have a single mutex, // so might as well store the data behind it pub struct ClientStorageInner { + // basically part of rate limiting which does not exist anymore + #[allow(dead_code)] message_retrieval_limit: usize, filename_length: u16, main_store_path_dir: PathBuf, @@ -92,10 +92,6 @@ impl ClientStorage { } } - // TODO: does this method really require locking? - // The worst that can happen is client sending 2 requests: to pull messages and register - // if register does not lock, then under specific timing pull messages will fail, - // but can simply be retried with no issues pub(crate) async fn create_storage_dir( &self, client_address: DestinationAddressBytes, @@ -131,7 +127,7 @@ impl ClientStorage { file.write_all(store_data.message.as_ref()).await } - pub(crate) async fn retrieve_client_files( + pub(crate) async fn retrieve_all_client_messages( &self, client_address: DestinationAddressBytes, ) -> io::Result> { @@ -156,28 +152,11 @@ impl ClientStorage { if !Self::is_valid_file(&dir_entry).await { continue; } - // Do not delete the file itself here! - // Only do it after client has received it let client_file = ClientFile::new(fs::read(dir_entry.path()).await?, dir_entry.path()); msgs.push(client_file) } - if msgs.len() == inner_data.message_retrieval_limit { - break; - } } - - let dummy_message = dummy_message(); - - // make sure we always return as many messages as we need - if msgs.len() != inner_data.message_retrieval_limit as usize { - msgs = msgs - .into_iter() - .chain(std::iter::repeat(dummy_message)) - .take(inner_data.message_retrieval_limit) - .collect(); - } - Ok(msgs) } diff --git a/gateway/src/node/storage/ledger.rs b/gateway/src/node/storage/ledger.rs new file mode 100644 index 0000000000..df9d79d9d2 --- /dev/null +++ b/gateway/src/node/storage/ledger.rs @@ -0,0 +1,169 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//use directory_client::presence::providers::MixProviderClient; +//use log::*; +//use nymsphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; +//use sfw_provider_requests::auth_token::{AuthToken, AUTH_TOKEN_SIZE}; +//use std::path::PathBuf; + +use std::path::PathBuf; + +use log::*; + +use gateway_requests::auth_token::{AuthToken, AUTH_TOKEN_SIZE}; +use nymsphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; + +#[derive(Debug)] +pub(crate) enum ClientLedgerError { + DbReadError(sled::Error), + DbWriteError(sled::Error), + DbOpenError(sled::Error), +} + +#[derive(Debug, Clone)] +// Note: you should NEVER create more than a single instance of this using 'new()'. +// You should always use .clone() to create additional instances +pub(crate) struct ClientLedger { + db: sled::Db, +} + +impl ClientLedger { + pub(crate) fn load(file: PathBuf) -> Result { + let db = match sled::open(file) { + Err(e) => return Err(ClientLedgerError::DbOpenError(e)), + Ok(db) => db, + }; + + let ledger = ClientLedger { db }; + + ledger.db.iter().keys().for_each(|key| { + println!( + "key: {:?}", + ledger + .read_destination_address_bytes(key.unwrap()) + .to_base58_string() + ); + }); + + Ok(ledger) + } + + fn read_auth_token(&self, raw_token: sled::IVec) -> AuthToken { + let token_bytes_ref = raw_token.as_ref(); + // if this fails it means we have some database corruption and we + // absolutely can't continue + if token_bytes_ref.len() != AUTH_TOKEN_SIZE { + error!("CLIENT LEDGER DATA CORRUPTION - TOKEN HAS INVALID LENGTH"); + panic!("CLIENT LEDGER DATA CORRUPTION - TOKEN HAS INVALID LENGTH"); + } + + let mut token_bytes = [0u8; AUTH_TOKEN_SIZE]; + token_bytes.copy_from_slice(token_bytes_ref); + AuthToken::from_bytes(token_bytes) + } + + fn read_destination_address_bytes( + &self, + raw_destination: sled::IVec, + ) -> DestinationAddressBytes { + let destination_ref = raw_destination.as_ref(); + // if this fails it means we have some database corruption and we + // absolutely can't continue + if destination_ref.len() != DESTINATION_ADDRESS_LENGTH { + error!("CLIENT LEDGER DATA CORRUPTION - CLIENT ADDRESS HAS INVALID LENGTH"); + panic!("CLIENT LEDGER DATA CORRUPTION - CLIENT ADDRESS HAS INVALID LENGTH"); + } + + let mut destination_bytes = [0u8; DESTINATION_ADDRESS_LENGTH]; + destination_bytes.copy_from_slice(destination_ref); + DestinationAddressBytes::from_bytes(destination_bytes) + } + + pub(crate) fn verify_token( + &self, + auth_token: &AuthToken, + client_address: &DestinationAddressBytes, + ) -> Result { + match self.db.get(&client_address.to_bytes()) { + Err(e) => Err(ClientLedgerError::DbReadError(e)), + Ok(token) => match token { + Some(token_ivec) => Ok(&self.read_auth_token(token_ivec) == auth_token), + None => Ok(false), + }, + } + } + + pub(crate) fn insert_token( + &mut self, + auth_token: AuthToken, + client_address: DestinationAddressBytes, + ) -> Result, ClientLedgerError> { + let insertion_result = match self + .db + .insert(&client_address.to_bytes(), &auth_token.to_bytes()) + { + Err(e) => Err(ClientLedgerError::DbWriteError(e)), + Ok(existing_token) => { + Ok(existing_token.map(|existing_token| self.read_auth_token(existing_token))) + } + }; + + // registration doesn't happen that often so might as well flush it to the disk to be sure + self.db.flush().unwrap(); + insertion_result + } + + pub(crate) fn remove_token( + &mut self, + client_address: &DestinationAddressBytes, + ) -> Result, ClientLedgerError> { + let removal_result = match self.db.remove(&client_address.to_bytes()) { + Err(e) => Err(ClientLedgerError::DbWriteError(e)), + Ok(existing_token) => { + Ok(existing_token.map(|existing_token| self.read_auth_token(existing_token))) + } + }; + + // removing of tokens happens extremely rarely, so flush is also fine here + self.db.flush().unwrap(); + removal_result + } + + pub(crate) fn current_clients( + &self, + ) -> Result, ClientLedgerError> { + let clients = self.db.iter().keys(); + + let mut client_vec = Vec::new(); + for client in clients { + match client { + Err(e) => return Err(ClientLedgerError::DbWriteError(e)), + Ok(client_entry) => { + client_vec.push(self.read_destination_address_bytes(client_entry)) + } + } + } + + Ok(client_vec) + } + + #[cfg(test)] + pub(crate) fn create_temporary() -> Self { + let cfg = sled::Config::new().temporary(true); + ClientLedger { + db: cfg.open().unwrap(), + } + } +} diff --git a/gateway/src/node/storage/mod.rs b/gateway/src/node/storage/mod.rs new file mode 100644 index 0000000000..e62b1d562c --- /dev/null +++ b/gateway/src/node/storage/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod inboxes; +mod ledger; + +pub(crate) use ledger::ClientLedger; diff --git a/mixnode/src/node/packet_processing.rs b/mixnode/src/node/packet_processing.rs index eb514ca7f9..adba39dc64 100644 --- a/mixnode/src/node/packet_processing.rs +++ b/mixnode/src/node/packet_processing.rs @@ -16,7 +16,9 @@ use crate::node::metrics; use crypto::encryption; use log::*; use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; -use nymsphinx::{Delay as SphinxDelay, NodeAddressBytes, ProcessedPacket, SphinxPacket}; +use nymsphinx::{ + Delay as SphinxDelay, Error as SphinxError, NodeAddressBytes, ProcessedPacket, SphinxPacket, +}; use std::convert::TryFrom; use std::net::SocketAddr; use std::ops::Deref; @@ -24,9 +26,8 @@ use std::sync::Arc; #[derive(Debug)] pub enum MixProcessingError { - SphinxRecoveryError, ReceivedFinalHopError, - SphinxProcessingError, + SphinxProcessingError(SphinxError), InvalidHopAddress, } @@ -36,12 +37,12 @@ pub enum MixProcessingResult { LoopMessage, } -impl From for MixProcessingError { - // for time being just have a single error instance for all possible results of nymsphinx::ProcessingError - fn from(_: nymsphinx::ProcessingError) -> Self { +impl From for MixProcessingError { + // for time being just have a single error instance for all possible results of SphinxError + fn from(err: SphinxError) -> Self { use MixProcessingError::*; - SphinxRecoveryError + SphinxProcessingError(err) } } @@ -111,7 +112,7 @@ impl PacketProcessor { } Err(e) => { warn!("Failed to unwrap Sphinx packet: {:?}", e); - Err(MixProcessingError::SphinxProcessingError) + Err(MixProcessingError::SphinxProcessingError(e)) } } } diff --git a/sfw-provider/src/commands/init.rs b/sfw-provider/src/commands/init.rs index a2a56f7947..12d0f98dfa 100644 --- a/sfw-provider/src/commands/init.rs +++ b/sfw-provider/src/commands/init.rs @@ -94,7 +94,7 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .arg( Arg::with_name("clients-ledger") .long("clients-ledger") - .help("[UNIMPLEMENTED] Ledger file containing registered clients") + .help("Ledger file containing registered clients") .takes_value(true) ) .arg( diff --git a/sfw-provider/src/commands/run.rs b/sfw-provider/src/commands/run.rs index 4e864516c9..446ae73fa6 100644 --- a/sfw-provider/src/commands/run.rs +++ b/sfw-provider/src/commands/run.rs @@ -24,7 +24,7 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .arg( Arg::with_name("id") .long("id") - .help("Id of the nym-mixnode we want to run") + .help("Id of the nym-provider we want to run") .takes_value(true) .required(true), ) @@ -98,7 +98,7 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .arg( Arg::with_name("clients-ledger") .long("clients-ledger") - .help("[UNIMPLEMENTED] Ledger file containing registered clients") + .help("Ledger file containing registered clients") .takes_value(true) ) .arg( diff --git a/sfw-provider/src/provider/mix_handling/packet_processing.rs b/sfw-provider/src/provider/mix_handling/packet_processing.rs index 27597bcbad..0ca79b4acd 100644 --- a/sfw-provider/src/provider/mix_handling/packet_processing.rs +++ b/sfw-provider/src/provider/mix_handling/packet_processing.rs @@ -16,7 +16,10 @@ use crate::provider::storage::{ClientStorage, StoreData}; use crypto::encryption; use log::*; use mix_client::packet::LOOP_COVER_MESSAGE_PAYLOAD; -use nymsphinx::{DestinationAddressBytes, Payload, ProcessedPacket, SURBIdentifier, SphinxPacket}; +use nymsphinx::{ + DestinationAddressBytes, Error as SphinxError, Payload, ProcessedPacket, SURBIdentifier, + SphinxPacket, +}; use std::io; use std::ops::Deref; use std::sync::Arc; @@ -26,7 +29,7 @@ pub enum MixProcessingError { ReceivedForwardHopError, NonMatchingRecipient, InvalidPayload, - SphinxProcessingError, + SphinxProcessingError(SphinxError), IOError(String), } @@ -36,12 +39,12 @@ pub enum MixProcessingResult { FinalHop, } -impl From for MixProcessingError { - // for time being just have a single error instance for all possible results of nymsphinx::ProcessingError - fn from(_: nymsphinx::ProcessingError) -> Self { +impl From for MixProcessingError { + // for time being just have a single error instance for all possible results of SphinxError + fn from(err: SphinxError) -> Self { use MixProcessingError::*; - SphinxProcessingError + SphinxProcessingError(err) } } @@ -115,7 +118,7 @@ impl PacketProcessor { } Err(e) => { warn!("Failed to unwrap Sphinx packet: {:?}", e); - Err(MixProcessingError::SphinxProcessingError) + Err(MixProcessingError::SphinxProcessingError(e)) } } } diff --git a/validator/src/services/mixmining/health_check_runner.rs b/validator/src/services/mixmining/health_check_runner.rs index e3e929f3e9..60d66a87ba 100644 --- a/validator/src/services/mixmining/health_check_runner.rs +++ b/validator/src/services/mixmining/health_check_runner.rs @@ -42,11 +42,8 @@ impl HealthCheckRunner { loop { let full_topology = directory_client::presence::Topology::new(self.directory_server.clone()); - let version_filtered_topology = full_topology.filter_node_versions( - crate::built_info::PKG_VERSION, - crate::built_info::PKG_VERSION, - crate::built_info::PKG_VERSION, - ); + let version_filtered_topology = + full_topology.filter_system_version(crate::built_info::PKG_VERSION); match self .health_checker .do_check(&version_filtered_topology)