Add client functionality into nym-network-requester (#2972)

* network-requester: replace websocket with mixnet client

* network-requester: mini tidy

* network-requester: add info line about own address

* network-requester: update to nym-crypto rename
This commit is contained in:
Jon Häggblad
2023-02-20 13:00:34 +01:00
committed by GitHub
parent 494f1d5199
commit bbb3287029
16 changed files with 781 additions and 347 deletions
Generated
+8
View File
@@ -3724,13 +3724,18 @@ dependencies = [
"build-information",
"clap 4.1.4",
"client-connections",
"client-core",
"completions",
"config",
"dirs",
"futures",
"ipnetwork 0.20.0",
"lazy_static",
"log",
"logging",
"network-defaults",
"nym-crypto",
"nym-sdk",
"nymsphinx",
"ordered-buffer",
"pretty_env_logger",
@@ -3743,10 +3748,13 @@ dependencies = [
"socks5-requests",
"sqlx 0.6.2",
"statistics-common",
"tap",
"task",
"thiserror",
"tokio",
"tokio-tungstenite 0.17.2",
"url",
"version-checker",
"websocket-requests",
]
@@ -342,7 +342,6 @@ where
if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) {
match id {
ConnectionCommand::Close(id) => self.on_close_connection(id),
ConnectionCommand::ActiveConnections(_) => panic!(),
}
}
@@ -421,7 +420,6 @@ where
if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) {
match id {
ConnectionCommand::Close(id) => self.on_close_connection(id),
ConnectionCommand::ActiveConnections(_) => panic!(),
}
}
+2 -2
View File
@@ -10,7 +10,7 @@ use client_core::client::{
};
use log::*;
use nymsphinx::addressing::clients::Recipient;
use proxy_helpers::connection_controller::{BroadcastActiveConnections, Controller};
use proxy_helpers::connection_controller::Controller;
use std::net::SocketAddr;
use tap::TapFallible;
use task::TaskClient;
@@ -69,7 +69,7 @@ impl SphinxSocksServer {
// controller for managing all active connections
let (mut active_streams_controller, controller_sender) = Controller::new(
client_connection_tx,
BroadcastActiveConnections::Off,
//BroadcastActiveConnections::Off,
self.shutdown.clone(),
);
tokio::spawn(async move {
-6
View File
@@ -25,12 +25,6 @@ pub enum ConnectionCommand {
// Announce that at a connection was closed. E.g the `OutQueueControl` uses this to discard
// transmission lanes.
Close(ConnectionId),
// In the network requester for example, we usually want to broadcast active connections
// regularly, so we know what connections we need to request lane queue lengths for from the
// client.
// In the socks5-client, this is not needed since have direct access to the lane queue lengths.
ActiveConnections(Vec<ConnectionId>),
}
// The `OutQueueControl` publishes the backlog per lane, primarily so that upstream can slow down
@@ -7,12 +7,8 @@ use futures::StreamExt;
use log::*;
use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData};
use socks5_requests::{ConnectionId, NetworkData, SendRequest};
use std::{
collections::{HashMap, HashSet},
time::Duration,
};
use std::collections::{HashMap, HashSet};
use task::TaskClient;
use tokio::time;
/// A generic message produced after reading from a socket/connection. It includes data that was
/// actually read alongside boolean indicating whether the connection got closed so that
@@ -116,10 +112,6 @@ pub struct Controller {
// Broadcast closed connections
client_connection_tx: ConnectionCommandSender,
// The controller can broadcast active connections. This is useful in the network-requester
// where its used to query the client for lane queue lengths
broadcast_connections: BroadcastActiveConnections,
// TODO: this can potentially be abused to ddos and kill provider. Not sure at this point
// how to handle it more gracefully
@@ -133,7 +125,6 @@ pub struct Controller {
impl Controller {
pub fn new(
client_connection_tx: ConnectionCommandSender,
broadcast_connections: BroadcastActiveConnections,
shutdown: TaskClient,
) -> (Self, ControllerSender) {
let (sender, receiver) = mpsc::unbounded();
@@ -143,7 +134,6 @@ impl Controller {
receiver,
recently_closed: HashSet::new(),
client_connection_tx,
broadcast_connections,
pending_messages: HashMap::new(),
shutdown,
},
@@ -194,15 +184,6 @@ impl Controller {
}
}
fn broadcast_active_connections(&mut self) {
// What about the recently closed ones? Hopefully we can ignore them ...
let conn_ids = self.active_connections.keys().copied().collect();
self.client_connection_tx
.unbounded_send(ConnectionCommand::ActiveConnections(conn_ids))
.unwrap();
}
fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec<u8>, is_closed: bool) {
if let Some(active_connection) = self.active_connections.get_mut(&conn_id) {
if !payload.is_empty() {
@@ -259,8 +240,6 @@ impl Controller {
}
pub async fn run(&mut self) {
let mut interval = time::interval(Duration::from_millis(500));
loop {
tokio::select! {
command = self.receiver.next() => match command {
@@ -276,11 +255,6 @@ impl Controller {
break;
}
},
_ = interval.tick() => {
if self.broadcast_connections == BroadcastActiveConnections::On {
self.broadcast_active_connections();
}
},
}
}
self.shutdown.recv_timeout().await;
+13 -6
View File
@@ -4,18 +4,19 @@
[package]
name = "nym-network-requester"
version = "1.1.9"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
authors.workspace = true
edition.workspace = true
rust-version = "1.65"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { version = "0.1.51" }
clap = {version = "4.0", features = ["derive"]}
clap = {version = "4.0", features = ["cargo", "derive"]}
dirs = "4.0"
futures = "0.3.24"
ipnetwork = "0.20.0"
lazy_static = { workspace = true }
log = { workspace = true }
pretty_env_logger = "0.4.0"
publicsuffix = "1.5" # Can't update this until bip updates to support newer idna version
@@ -23,22 +24,28 @@ rand = "0.7.3"
reqwest = { version = "0.11.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.6.1", features = ["runtime-tokio-rustls", "chrono"]}
tap = { workspace = true }
thiserror = "1.0"
tokio = { version = "1.24.1", features = [ "net", "rt-multi-thread", "macros" ] }
tokio-tungstenite = "0.17.2"
url = { workspace = true }
# internal
build-information = { path = "../../common/build-information" }
client-connections = { path = "../../common/client-connections" }
client-core = { path = "../../clients/client-core" }
completions = { path = "../../common/completions" }
network-defaults = { path = "../../common/network-defaults" }
nymsphinx = { path = "../../common/nymsphinx" }
config = { path = "../../common/config" }
nym-crypto = { path = "../../common/crypto" }
logging = { path = "../../common/logging"}
network-defaults = { path = "../../common/network-defaults" }
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
nymsphinx = { path = "../../common/nymsphinx" }
ordered-buffer = {path = "../../common/socks5/ordered-buffer"}
proxy-helpers = { path = "../../common/socks5/proxy-helpers" }
service-providers-common = { path = "../common" }
socks5-requests = { path = "../../common/socks5/requests" }
statistics-common = { path = "../../common/statistics" }
task = { path = "../../common/task" }
version-checker = { path = "../../common/version-checker" }
websocket-requests = { path = "../../clients/native/websocket-requests" }
@@ -0,0 +1,157 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
cli::{override_config, OverrideConfig},
config::Config,
error::NetworkRequesterError,
};
use clap::Args;
use config::NymConfig;
use nym_crypto::asymmetric::identity;
use nymsphinx::addressing::clients::Recipient;
use serde::Serialize;
use std::fmt::Display;
use tap::TapFallible;
#[derive(Args, Clone)]
pub(crate) struct Init {
/// Id of the nym-mixnet-client we want to create config for.
#[clap(long)]
id: String,
/// Id of the gateway we are going to connect to.
#[clap(long)]
gateway: Option<identity::PublicKey>,
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
/// potentially causing loss of access.
#[clap(long)]
force_register_gateway: bool,
/// Comma separated list of rest endpoints of the nyxd validators
#[clap(long, alias = "nymd_validators", value_delimiter = ',')]
nyxd_urls: Option<Vec<url::Url>>,
/// Comma separated list of rest endpoints of the API validators
#[clap(long, alias = "api_validators", value_delimiter = ',')]
// the alias here is included for backwards compatibility (1.1.4 and before)
nym_apis: Option<Vec<url::Url>>,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement.
#[clap(long)]
enabled_credentials_mode: Option<bool>,
/// Save a summary of the initialization to a json file
#[clap(long)]
output_json: bool,
}
impl From<Init> for OverrideConfig {
fn from(init_config: Init) -> Self {
OverrideConfig {
nym_apis: init_config.nym_apis,
fastmode: false,
no_cover: false,
nyxd_urls: init_config.nyxd_urls,
enabled_credentials_mode: init_config.enabled_credentials_mode,
}
}
}
#[derive(Debug, Serialize)]
pub struct InitResults {
#[serde(flatten)]
client_core: client_core::init::InitResults,
}
impl InitResults {
fn new(config: &Config, address: &Recipient) -> Self {
Self {
client_core: client_core::init::InitResults::new(config.get_base(), address),
}
}
}
impl Display for InitResults {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.client_core)
}
}
pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> {
println!("Initialising client...");
let id = &args.id;
let already_init = Config::default_config_file_path(id).exists();
if already_init {
println!("Client \"{id}\" was already initialised before");
}
// Usually you only register with the gateway on the first init, however you can force
// re-registering if wanted.
let user_wants_force_register = args.force_register_gateway;
if user_wants_force_register {
println!("Instructed to force registering gateway. This might overwrite keys!");
}
// If the client was already initialized, don't generate new keys and don't re-register with
// the gateway (because this would create a new shared key).
// Unless the user really wants to.
let register_gateway = !already_init || user_wants_force_register;
// Attempt to use a user-provided gateway, if possible
let user_chosen_gateway_id = args.gateway;
// Load and potentially override config
let mut config = override_config(Config::new(id), OverrideConfig::from(args.clone()));
// Setup gateway by either registering a new one, or creating a new config from the selected
// one but with keys kept, or reusing the gateway configuration.
let gateway = client_core::init::setup_gateway_from_config::<Config, _>(
register_gateway,
user_chosen_gateway_id,
config.get_base(),
)
.await
.map_err(|source| {
eprintln!("Failed to setup gateway\nError: {source}");
NetworkRequesterError::FailedToSetupGateway { source }
})?;
config.get_base_mut().set_gateway_endpoint(gateway);
config.save_to_file(None).tap_err(|_| {
log::error!("Failed to save the config file");
})?;
print_saved_config(&config);
let address = client_core::init::get_client_address_from_stored_keys(config.get_base())?;
let init_results = InitResults::new(&config, &address);
println!("{init_results}");
// Output summary to a json file, if specified
if args.output_json {
client_core::init::output_to_json(&init_results, "client_init_results.json");
}
println!("\nThe address of this client is: {address}\n");
Ok(())
}
fn print_saved_config(config: &Config) {
let config_save_location = config.get_config_file_save_location();
println!("Saved configuration file to {config_save_location:?}");
println!("Using gateway: {}", config.get_base().get_gateway_id());
log::debug!("Gateway id: {}", config.get_base().get_gateway_id());
log::debug!("Gateway owner: {}", config.get_base().get_gateway_owner());
log::debug!(
"Gateway listener: {}",
config.get_base().get_gateway_listener()
);
println!("Client configuration completed.\n");
}
@@ -0,0 +1,106 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use build_information::BinaryBuildInformation;
use clap::{CommandFactory, Parser, Subcommand};
use completions::{fig_generate, ArgShell};
use crate::{
config::{BaseConfig, Config},
error::NetworkRequesterError,
};
mod init;
mod run;
lazy_static::lazy_static! {
pub static ref PRETTY_BUILD_INFORMATION: String =
BinaryBuildInformation::new(env!("CARGO_PKG_VERSION")).pretty_print();
}
// Helper for passing LONG_VERSION to clap
fn pretty_build_info_static() -> &'static str {
&PRETTY_BUILD_INFORMATION
}
#[derive(Parser)]
#[clap(author = "Nymtech", version, about, long_version = pretty_build_info_static())]
pub(crate) struct Cli {
/// Path pointing to an env file that configures the client.
#[clap(short, long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
#[clap(subcommand)]
command: Commands,
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Initialize a network-requester. Do this first!
Init(init::Init),
/// Run the network requester with the provided configuration and optionally override
/// parameters.
Run(run::Run),
/// Generate shell completions
Completions(ArgShell),
/// Generate Fig specification
GenerateFigSpec,
}
// Configuration that can be overridden.
pub(crate) struct OverrideConfig {
nym_apis: Option<Vec<url::Url>>,
fastmode: bool,
no_cover: bool,
nyxd_urls: Option<Vec<url::Url>>,
enabled_credentials_mode: Option<bool>,
}
pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config {
config
.with_base(BaseConfig::with_high_default_traffic_volume, args.fastmode)
.with_base(BaseConfig::with_disabled_cover_traffic, args.no_cover)
.with_optional_custom_env_ext(
BaseConfig::with_custom_nym_apis,
args.nym_apis,
network_defaults::var_names::NYM_API,
config::parse_urls,
)
.with_optional_custom_env_ext(
BaseConfig::with_custom_nyxd,
args.nyxd_urls,
network_defaults::var_names::NYXD,
config::parse_urls,
)
.with_optional_ext(
BaseConfig::with_disabled_credentials,
args.enabled_credentials_mode.map(|b| !b),
)
}
pub(crate) async fn execute(args: Cli) -> Result<(), NetworkRequesterError> {
let bin_name = "nym-network-requester";
match &args.command {
Commands::Init(m) => init::execute(m).await?,
Commands::Run(m) => run::execute(m).await?,
Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
@@ -0,0 +1,157 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
cli::{override_config, OverrideConfig},
config::Config,
error::NetworkRequesterError,
};
use clap::Args;
use config::NymConfig;
use nymsphinx::addressing::clients::Recipient;
const ENABLE_STATISTICS: &str = "enable-statistics";
#[allow(clippy::struct_excessive_bools)]
#[derive(Args, Clone)]
pub(crate) struct Run {
/// Id of the nym-mixnet-client we want to run.
#[clap(long)]
id: String,
/// Specifies whether this network requester should run in 'open-proxy' mode
#[clap(long)]
open_proxy: bool,
/// Enable service anonymized statistics that get sent to a statistics aggregator server
#[clap(long)]
enable_statistics: bool,
/// Mixnet client address where a statistics aggregator is running. The default value is a Nym
/// aggregator client
#[clap(long)]
statistics_recipient: Option<String>,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement.
#[clap(long)]
enabled_credentials_mode: Option<bool>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[clap(long, hide = true)]
fastmode: bool,
/// Disable loop cover traffic and the Poisson rate limiter (for debugging only)
#[clap(long, hide = true)]
no_cover: bool,
}
impl From<Run> for OverrideConfig {
fn from(run_config: Run) -> Self {
OverrideConfig {
nym_apis: None,
fastmode: run_config.fastmode,
no_cover: run_config.no_cover,
nyxd_urls: None,
enabled_credentials_mode: run_config.enabled_credentials_mode,
}
}
}
// this only checks compatibility between config the binary. It does not take into consideration
// network version. It might do so in the future.
fn version_check(cfg: &Config) -> bool {
let binary_version = env!("CARGO_PKG_VERSION");
let config_version = cfg.get_base().get_version();
if binary_version == config_version {
true
} else {
log::warn!(
"The native-client binary has different version than what is specified \
in config file! {} and {}",
binary_version,
config_version
);
if version_checker::is_minor_version_compatible(binary_version, config_version) {
log::info!(
"but they are still semver compatible. \
However, consider running the `upgrade` command"
);
true
} else {
log::error!(
"and they are semver incompatible! - \
please run the `upgrade` command before attempting `run` again"
);
false
}
}
}
pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> {
if args.open_proxy {
println!(
"\n\nYOU HAVE STARTED IN 'OPEN PROXY' MODE. ANYONE WITH YOUR CLIENT ADDRESS \
CAN MAKE REQUESTS FROM YOUR MACHINE. PLEASE QUIT IF YOU DON'T UNDERSTAND WHAT \
YOU'RE DOING.\n\n"
);
}
if args.enable_statistics {
println!(
"\n\nTHE NETWORK REQUESTER STATISTICS ARE ENABLED. IT WILL COLLECT AND SEND \
ANONYMIZED STATISTICS TO A CENTRAL SERVER. PLEASE QUIT IF YOU DON'T WANT \
THIS TO HAPPEN AND START WITHOUT THE {ENABLE_STATISTICS} FLAG .\n\n"
);
}
let id = &args.id;
let mut config = match Config::load_from_file(id) {
Ok(cfg) => cfg,
Err(err) => {
log::error!(
"Failed to load config for {}. \
Are you sure you have run `init` before? (Error was: {err})",
id
);
return Err(NetworkRequesterError::FailedToLoadConfig(id.to_string()));
}
};
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
if config.get_base_mut().set_empty_fields_to_defaults() {
log::warn!(
"Some of the core config options were left unset. \
The default values are going to get used instead."
);
}
if !version_check(&config) {
log::error!("Failed the local version check");
return Err(NetworkRequesterError::FailedLocalVersionCheck);
}
// TODO: consider incorporating statistics_recipient, open_proxuy and enable_statistics in
// `Config`.
let stats_provider_addr = args
.statistics_recipient
.as_ref()
.map(Recipient::try_from_base58_string)
.transpose()
.unwrap_or(None);
log::info!("Starting socks5 service provider");
let server = crate::core::NRServiceProviderBuilder::new(
config,
args.open_proxy,
args.enable_statistics,
stats_provider_addr,
)
.await;
server.run_service_provider().await
}
@@ -0,0 +1,128 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config::template::config_template;
use client_core::config::ClientCoreConfigTrait;
use config::{NymConfig, OptionalSet};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::path::PathBuf;
use std::str::FromStr;
pub use client_core::config::Config as BaseConfig;
pub use client_core::config::MISSING_VALUE;
pub use client_core::config::{DebugConfig, GatewayEndpointConfig};
mod template;
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(flatten)]
base: BaseConfig<Config>,
}
impl NymConfig for Config {
fn template() -> &'static str {
config_template()
}
// TODO: merge base dir with `HostStore`.
fn default_root_directory() -> PathBuf {
dirs::home_dir()
.expect("Failed to evaluate $HOME value")
.join(".nym")
.join("service-providers")
.join("network-requester")
}
fn try_default_root_directory() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".nym").join("clients"))
}
fn root_directory(&self) -> PathBuf {
self.base.get_nym_root_directory()
}
fn config_directory(&self) -> PathBuf {
self.root_directory()
.join(self.base.get_id())
.join("config")
}
fn data_directory(&self) -> PathBuf {
self.root_directory().join(self.base.get_id()).join("data")
}
}
impl ClientCoreConfigTrait for Config {
fn get_gateway_endpoint(&self) -> &client_core::config::GatewayEndpointConfig {
self.base.get_gateway_endpoint()
}
}
impl Config {
pub fn new<S: Into<String>>(id: S) -> Self {
Config {
base: BaseConfig::new(id),
}
}
// getters
pub fn get_config_file_save_location(&self) -> PathBuf {
self.config_directory().join(Self::config_file_name())
}
pub fn get_base(&self) -> &BaseConfig<Self> {
&self.base
}
pub fn get_base_mut(&mut self) -> &mut BaseConfig<Self> {
&mut self.base
}
// poor man's 'builder' method
pub fn with_base<F, T>(mut self, f: F, val: T) -> Self
where
F: Fn(BaseConfig<Self>, T) -> BaseConfig<Self>,
{
self.base = f(self.base, val);
self
}
// helper methods to use `OptionalSet` trait. Those are defined due to very... ehm. 'specific' structure of this config
// (plz, lets refactor it)
pub fn with_optional_ext<F, T>(mut self, f: F, val: Option<T>) -> Self
where
F: Fn(BaseConfig<Self>, T) -> BaseConfig<Self>,
{
self.base = self.base.with_optional(f, val);
self
}
#[allow(dead_code)]
pub fn with_optional_env_ext<F, T>(mut self, f: F, val: Option<T>, env_var: &str) -> Self
where
F: Fn(BaseConfig<Self>, T) -> BaseConfig<Self>,
T: FromStr,
<T as FromStr>::Err: Debug,
{
self.base = self.base.with_optional_env(f, val, env_var);
self
}
pub fn with_optional_custom_env_ext<F, T, G>(
mut self,
f: F,
val: Option<T>,
env_var: &str,
parser: G,
) -> Self
where
F: Fn(BaseConfig<Self>, T) -> BaseConfig<Self>,
G: Fn(&str) -> T,
{
self.base = self.base.with_optional_custom_env(f, val, env_var, parser);
self
}
}
@@ -0,0 +1,99 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
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.
r#"
# This is a TOML config file.
# For more information, see https://github.com/toml-lang/toml
##### main base client config options #####
[client]
# Version of the client for which this configuration was created.
version = '{{ client.version }}'
# Human readable ID of this particular client.
id = '{{ client.id }}'
# Indicates whether this client is running in a disabled credentials mode, thus attempting
# to claim bandwidth without presenting bandwidth credentials.
disabled_credentials_mode = {{ client.disabled_credentials_mode }}
# Addresses to nyxd validators via which the client can communicate with the chain.
nyxd_urls = [{{#each client.nyxd_urls }}
'{{this}}',
{{/each}}]
# Addresses to APIs running on validator from which the client gets the view of the network.
nym_api_urls = [{{#each client.nym_api_urls }}
'{{this}}',
{{/each}}]
# Path to file containing private identity key.
private_identity_key_file = '{{ client.private_identity_key_file }}'
# Path to file containing public identity key.
public_identity_key_file = '{{ client.public_identity_key_file }}'
# Path to file containing private encryption key.
private_encryption_key_file = '{{ client.private_encryption_key_file }}'
# Path to file containing public encryption key.
public_encryption_key_file = '{{ client.public_encryption_key_file }}'
# Path to the database containing bandwidth credentials
database_path = '{{ client.database_path }}'
# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
reply_surb_database_path = '{{ client.reply_surb_database_path }}'
##### additional client config options #####
# A gateway specific, optional, base58 stringified shared key used for
# communication with particular gateway.
gateway_shared_key_file = '{{ client.gateway_shared_key_file }}'
# Path to file containing key used for encrypting and decrypting the content of an
# acknowledgement so that nobody besides the client knows which packet it refers to.
ack_key_file = '{{ client.ack_key_file }}'
##### advanced configuration options #####
# Absolute path to the home Nym Clients directory.
nym_root_directory = '{{ client.nym_root_directory }}'
[client.gateway_endpoint]
# ID of the gateway from which the client should be fetching messages.
gateway_id = '{{ client.gateway_endpoint.gateway_id }}'
# Address of the gateway owner to which the client should send messages.
gateway_owner = '{{ client.gateway_endpoint.gateway_owner }}'
# Address of the gateway listener to which all client requests should be sent.
gateway_listener = '{{ client.gateway_endpoint.gateway_listener }}'
##### logging configuration options #####
[logging]
# TODO
##### debug configuration options #####
# The following options should not be modified unless you know EXACTLY what you are doing
# as if set incorrectly, they may impact your anonymity.
[debug]
average_packet_delay = '{{ debug.average_packet_delay }}'
average_ack_delay = '{{ debug.average_ack_delay }}'
loop_cover_traffic_average_delay = '{{ debug.loop_cover_traffic_average_delay }}'
message_sending_average_delay = '{{ debug.message_sending_average_delay }}'
"#
}
+72 -159
View File
@@ -3,27 +3,19 @@
use crate::allowed_hosts;
use crate::allowed_hosts::OutboundRequestFilter;
use crate::config::Config;
use crate::error::NetworkRequesterError;
use crate::reply::MixnetMessage;
use crate::statistics::ServiceStatisticsCollector;
use crate::websocket;
use crate::websocket::TSWebsocketStream;
use crate::{reply, socks5};
use async_trait::async_trait;
use build_information::BinaryBuildInformation;
use client_connections::{
ConnectionCommand, ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane,
};
use client_connections::LaneQueueLengths;
use futures::channel::mpsc;
use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use log::warn;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::receiver::ReconstructedMessage;
use proxy_helpers::connection_controller::{
BroadcastActiveConnections, Controller, ControllerCommand, ControllerSender,
};
use proxy_helpers::connection_controller::{Controller, ControllerCommand, ControllerSender};
use proxy_helpers::proxy_runner::{MixProxyReader, MixProxySender};
use service_providers_common::interface::{
BinaryInformation, ProviderInterfaceVersion, Request, RequestVersion,
@@ -37,8 +29,6 @@ use statistics_common::collector::StatisticsSender;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use task::{TaskClient, TaskManager};
use tokio_tungstenite::tungstenite::protocol::Message;
use websocket_requests::{requests::ClientRequest, responses::ServerResponse};
// Since it's an atomic, it's safe to be kept static and shared across threads
static ACTIVE_PROXIES: AtomicUsize = AtomicUsize::new(0);
@@ -53,7 +43,7 @@ pub(crate) fn new_legacy_request_version() -> RequestVersion<Socks5Request> {
// TODO: 'Builder' is not the most appropriate name here, but I needed
// ... something ...
pub struct NRServiceProviderBuilder {
websocket_address: String,
config: Config,
outbound_request_filter: OutboundRequestFilter,
open_proxy: bool,
enable_statistics: bool,
@@ -63,11 +53,11 @@ pub struct NRServiceProviderBuilder {
struct NRServiceProvider {
outbound_request_filter: OutboundRequestFilter,
open_proxy: bool,
mixnet_client: nym_sdk::mixnet::MixnetClient,
websocket_stream: SplitStream<TSWebsocketStream>,
controller_sender: ControllerSender,
mix_input_sender: MixProxySender<MixnetMessage>,
shared_lane_queue_lengths: LaneQueueLengths,
//shared_lane_queue_lengths: LaneQueueLengths,
stats_collector: Option<ServiceStatisticsCollector>,
shutdown: TaskManager,
}
@@ -157,7 +147,7 @@ impl ServiceProvider<Socks5Request> for NRServiceProvider {
impl NRServiceProviderBuilder {
pub async fn new(
websocket_address: String,
config: Config,
open_proxy: bool,
enable_statistics: bool,
stats_provider_addr: Option<Recipient>,
@@ -180,7 +170,7 @@ impl NRServiceProviderBuilder {
let outbound_request_filter = OutboundRequestFilter::new(allowed_hosts, unknown_hosts);
NRServiceProviderBuilder {
websocket_address,
config,
outbound_request_filter,
open_proxy,
enable_statistics,
@@ -190,10 +180,8 @@ impl NRServiceProviderBuilder {
/// Start all subsystems
pub async fn run_service_provider(self) -> Result<(), NetworkRequesterError> {
let websocket_stream = Self::connect_websocket(&self.websocket_address).await?;
// split the websocket so that we could read and write from separate threads
let (websocket_writer, websocket_reader) = websocket_stream.split();
// Connect to the mixnet
let mixnet_client = create_mixnet_client(self.config.get_base()).await?;
// channels responsible for managing messages that are to be sent to the mix network. The receiver is
// going to be used by `mixnet_response_listener`
@@ -202,21 +190,9 @@ impl NRServiceProviderBuilder {
// Used to notify tasks to shutdown. Not all tasks fully supports this (yet).
let shutdown = task::TaskManager::default();
// Channel for announcing client connection state by the controller.
// The `mixnet_response_listener` will use this to either report closed connection to the
// client or request lane queue lengths.
let (client_connection_tx, client_connection_rx) = mpsc::unbounded();
// Shared queue length data. Published by the `OutQueueController` in the client, and used
// primarily to throttle incoming connections
let shared_lane_queue_lengths = LaneQueueLengths::new();
// Controller for managing all active connections.
// We provide it with a ShutdownListener since it requires it, even though for the network
// requester shutdown signalling is not yet fully implemented.
let (mut active_connections_controller, controller_sender) = Controller::new(
client_connection_tx,
BroadcastActiveConnections::On,
mixnet_client.connection_command_sender(),
shutdown.subscribe(),
);
@@ -240,13 +216,15 @@ impl NRServiceProviderBuilder {
};
let stats_collector_clone = stats_collector.clone();
let mixnet_client_sender = mixnet_client.sender();
let self_address = *mixnet_client.nym_address();
// start the listener for mix messages
tokio::spawn(async move {
NRServiceProvider::mixnet_response_listener(
websocket_writer,
mixnet_client_sender,
mix_input_receiver,
stats_collector_clone,
client_connection_rx,
)
.await;
});
@@ -254,70 +232,54 @@ impl NRServiceProviderBuilder {
let service_provider = NRServiceProvider {
outbound_request_filter: self.outbound_request_filter,
open_proxy: self.open_proxy,
websocket_stream: websocket_reader,
mixnet_client,
controller_sender,
mix_input_sender,
shared_lane_queue_lengths,
//shared_lane_queue_lengths: mixnet_client.shared_lane_queue_lengths(),
stats_collector,
shutdown,
};
log::info!("The address of this client is: {}", self_address);
log::info!("All systems go. Press CTRL-C to stop the server.");
service_provider.run().await
}
// Make the websocket connection so we can receive incoming Mixnet messages.
async fn connect_websocket(uri: &str) -> Result<TSWebsocketStream, NetworkRequesterError> {
match websocket::Connection::new(uri).connect().await {
Ok(ws_stream) => {
log::info!("* connected to local websocket server at {}", uri);
Ok(ws_stream)
}
Err(err) => {
log::error!(
"Error: websocket connection attempt failed, is the Nym client running?"
);
Err(err.into())
}
}
}
}
impl NRServiceProvider {
async fn run(mut self) -> Result<(), NetworkRequesterError> {
// TODO: incorporate graceful shutdowns
loop {
let Some(reconstructed) = self.read_websocket_message().await else {
log::error!("The websocket stream has finished!");
return Err(NetworkRequesterError::ConnectionClosed);
};
while let Some(reconstructed_messages) = self.mixnet_client.wait_for_messages().await {
for reconstructed in reconstructed_messages {
let sender = reconstructed.sender_tag;
let request = match Socks5ProviderRequest::try_from_bytes(&reconstructed.message) {
Ok(req) => req,
Err(err) => {
// TODO: or should it even be further lowered to debug/trace?
log::warn!("Failed to deserialize received message: {err}");
continue;
}
};
let sender = reconstructed.sender_tag;
let request = match Socks5ProviderRequest::try_from_bytes(&reconstructed.message) {
Ok(req) => req,
Err(err) => {
// TODO: or should it even be further lowered to debug/trace?
log::warn!("Failed to deserialize received message: {err}");
continue;
if let Err(err) = self.on_request(sender, request).await {
// TODO: again, should it be a warning?
// we should also probably log some information regarding the origin of the request
// so that it would be easier to debug it
log::warn!("failed to resolve the received request: {err}");
}
};
if let Err(err) = self.on_request(sender, request).await {
// TODO: again, should it be a warning?
// we should also probably log some information regarding the origin of the request
// so that it would be easier to debug it
log::warn!("failed to resolve the received request: {err}");
}
}
log::error!("Network requester exited unexpectedly");
Ok(())
}
/// Listens for any messages from `mix_reader` that should be written back to the mix network
/// via the `websocket_writer`.
async fn mixnet_response_listener(
mut websocket_writer: SplitSink<TSWebsocketStream, Message>,
mut mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender,
mut mix_input_reader: MixProxyReader<MixnetMessage>,
stats_collector: Option<ServiceStatisticsCollector>,
mut client_connection_rx: ConnectionCommandReceiver,
) {
loop {
tokio::select! {
@@ -338,97 +300,17 @@ impl NRServiceProvider {
}
}
// make 'request' to native-websocket client
let response_message = msg.into_client_request();
let message = Message::Binary(response_message.serialize());
websocket_writer.send(message).await.unwrap();
let response_message = msg.into_input_message();
mixnet_client_sender.send_input_message(response_message).await;
} else {
log::error!("Exiting: channel closed!");
break;
}
},
Some(command) = client_connection_rx.next() => {
match command {
ConnectionCommand::Close(id) => {
let msg = ClientRequest::ClosedConnection(id);
let ws_msg = Message::Binary(msg.serialize());
websocket_writer.send(ws_msg).await.unwrap();
}
ConnectionCommand::ActiveConnections(ids) => {
// We can optimize this by sending a single request, but this is
// usually in the low single digits, max a few tens, so we leave that
// for a rainy day.
// Also that means fiddling with the currently manual
// serialize/deserialize we do with ClientRequests ...
for id in ids {
log::trace!("Requesting lane queue length for: {}", id);
let msg = ClientRequest::GetLaneQueueLength(id);
let ws_msg = Message::Binary(msg.serialize());
websocket_writer.send(ws_msg).await.unwrap();
}
}
}
},
}
}
}
fn handle_lane_queue_length_response(
lane_queue_lengths: &LaneQueueLengths,
lane: u64,
queue_length: usize,
) {
log::trace!("Received LaneQueueLength lane: {lane}, queue_length: {queue_length}");
if let Ok(mut lane_queue_lengths) = lane_queue_lengths.lock() {
let lane = TransmissionLane::ConnectionId(lane);
lane_queue_lengths.map.insert(lane, queue_length);
} else {
log::warn!("Unable to lock lane queue lengths, skipping updating received lane length")
}
}
async fn read_websocket_message(&mut self) -> Option<ReconstructedMessage> {
while let Some(msg) = self.websocket_stream.next().await {
let data = match msg {
Ok(msg) => msg.into_data(),
Err(err) => {
log::error!("Failed to read from the websocket: {err}");
continue;
}
};
// try to recover the actual message from the mix network...
let deserialized_message = match ServerResponse::deserialize(&data) {
Ok(deserialized) => deserialized,
Err(err) => {
log::error!(
"Failed to deserialize received websocket message! - {}",
err
);
continue;
}
};
let received = match deserialized_message {
ServerResponse::Received(received) => received,
ServerResponse::LaneQueueLength { lane, queue_length } => {
Self::handle_lane_queue_length_response(
&self.shared_lane_queue_lengths,
lane,
queue_length,
);
continue;
}
ServerResponse::Error(err) => {
panic!("received error from native client! - {err}")
}
_ => unimplemented!("probably should never be reached?"),
};
return Some(received);
}
None
}
#[allow(clippy::too_many_arguments)]
async fn start_proxy(
remote_version: RequestVersion<Socks5Request>,
@@ -545,7 +427,7 @@ impl NRServiceProvider {
let controller_sender_clone = self.controller_sender.clone();
let mix_input_sender_clone = self.mix_input_sender.clone();
let lane_queue_lengths_clone = self.shared_lane_queue_lengths.clone();
let lane_queue_lengths_clone = self.mixnet_client.shared_lane_queue_lengths();
let shutdown = self.shutdown.subscribe();
// and start the proxy for this connection
@@ -568,3 +450,34 @@ impl NRServiceProvider {
self.controller_sender.unbounded_send(req.into()).unwrap()
}
}
// Helper function to create the mixnet client.
// This is NOT in the SDK since we don't want to expose any of the client-core config types.
// We could however consider moving it to a crate in common in the future.
async fn create_mixnet_client<T>(
config: &client_core::config::Config<T>,
) -> Result<nym_sdk::mixnet::MixnetClient, NetworkRequesterError> {
let nym_api_endpoints = config.get_nym_api_endpoints();
let debug_config = config.get_debug_config().clone();
let mixnet_config = nym_sdk::mixnet::Config {
user_chosen_gateway: None,
nym_api_endpoints,
debug_config,
};
let storage_paths = nym_sdk::mixnet::StoragePaths::from(config);
let mixnet_client = nym_sdk::mixnet::MixnetClientBuilder::new()
.config(mixnet_config)
.enable_storage(storage_paths)
.gateway_config(config.get_gateway_endpoint_config().clone())
.build::<nym_sdk::mixnet::ReplyStorage>()
.await
.map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { source: err })?;
mixnet_client
.connect_to_mixnet()
.await
.map_err(|err| NetworkRequesterError::FailedToConnectToMixnet { source: err })
}
@@ -1,4 +1,4 @@
use crate::websocket::WebsocketConnectionError;
use client_core::error::ClientCoreError;
use socks5_requests::Socks5RequestError;
#[derive(thiserror::Error, Debug)]
@@ -6,15 +6,27 @@ pub enum NetworkRequesterError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Websocket error")]
WebsocketConnectionError(#[from] WebsocketConnectionError),
#[error("Websocket connection closed")]
ConnectionClosed,
#[error("client-core error: {0}")]
ClientCoreError(#[from] ClientCoreError),
#[error("encountered an error while trying to handle a provider request: {source}")]
ProviderRequestError {
#[from]
source: Socks5RequestError,
},
#[error("failed to setup gateway: {source}")]
FailedToSetupGateway { source: ClientCoreError },
#[error("failed to load configuration file: {0}")]
FailedToLoadConfig(String),
#[error("failed local version check, client and config mismatch")]
FailedLocalVersionCheck,
#[error("failed to setup mixnet client: {source}")]
FailedToSetupMixnetClient { source: nym_sdk::Error },
#[error("failed to connect to mixnet: {source}")]
FailedToConnectToMixnet { source: nym_sdk::Error },
}
@@ -1,114 +1,28 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::CommandFactory;
use clap::Subcommand;
use clap::{Args, Parser};
use completions::{fig_generate, ArgShell};
use clap::{crate_name, crate_version, Parser};
use logging::setup_logging;
use network_defaults::setup_env;
use error::NetworkRequesterError;
use nymsphinx::addressing::clients::Recipient;
mod allowed_hosts;
mod cli;
mod config;
mod core;
mod error;
mod reply;
mod socks5;
mod statistics;
mod websocket;
const ENABLE_STATISTICS: &str = "enable-statistics";
#[derive(Args)]
struct Run {
/// Specifies whether this network requester should run in 'open-proxy' mode
#[clap(long)]
open_proxy: bool,
/// Websocket port to bind to.
#[clap(long)]
websocket_port: Option<String>,
/// Enable service anonymized statistics that get sent to a statistics aggregator server
#[clap(long)]
enable_statistics: bool,
/// Mixnet client address where a statistics aggregator is running. The default value is a Nym aggregator client
#[clap(long)]
statistics_recipient: Option<String>,
}
impl Run {
async fn execute(&self) -> Result<(), NetworkRequesterError> {
if self.open_proxy {
println!("\n\nYOU HAVE STARTED IN 'OPEN PROXY' MODE. ANYONE WITH YOUR CLIENT ADDRESS CAN MAKE REQUESTS FROM YOUR MACHINE. PLEASE QUIT IF YOU DON'T UNDERSTAND WHAT YOU'RE DOING.\n\n");
}
if self.enable_statistics {
println!("\n\nTHE NETWORK REQUESTER STATISTICS ARE ENABLED. IT WILL COLLECT AND SEND ANONYMIZED STATISTICS TO A CENTRAL SERVER. PLEASE QUIT IF YOU DON'T WANT THIS TO HAPPEN AND START WITHOUT THE {ENABLE_STATISTICS} FLAG .\n\n");
}
let stats_provider_addr = self
.statistics_recipient
.as_ref()
.map(Recipient::try_from_base58_string)
.transpose()
.unwrap_or(None);
let websocket_address = format!(
"ws://localhost:{}",
self.websocket_port
.as_ref()
.unwrap_or(&network_defaults::DEFAULT_WEBSOCKET_LISTENING_PORT.to_string())
);
log::info!("Starting socks5 service provider");
let server = core::NRServiceProviderBuilder::new(
websocket_address,
self.open_proxy,
self.enable_statistics,
stats_provider_addr,
)
.await;
server.run_service_provider().await
}
}
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Run network requester
Run(Run),
/// Generate shell completions
Completions(ArgShell),
/// Generate Fig specification
GenerateFigSpec,
}
#[derive(Parser)]
#[clap(author = "Nymtech", version, about)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
pub(crate) async fn execute(args: Cli) -> Result<(), NetworkRequesterError> {
let bin_name = "nym-network-requester";
match &args.command {
Commands::Run(r) => r.execute().await?,
Commands::Completions(s) => s.generate(&mut crate::Cli::command(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut crate::Cli::command(), bin_name),
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), NetworkRequesterError> {
setup_logging();
let args = Cli::parse();
println!("{}", logging::banner(crate_name!(), crate_version!()));
execute(args).await
let args = cli::Cli::parse();
setup_env(args.config_env_file.as_ref());
cli::execute(args).await
}
@@ -1,6 +1,8 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use client_connections::TransmissionLane;
use nym_sdk::mixnet::InputMessage;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use service_providers_common::interface::{
@@ -11,7 +13,6 @@ use socks5_requests::{
Socks5RequestContent, Socks5Response, Socks5ResponseContent,
};
use std::fmt::{Debug, Formatter};
use websocket_requests::requests::ClientRequest;
/// Generic data this service provider will send back to the mixnet via its connected native client.
/// It includes serialized socks5 proxy responses to its connected clients
@@ -146,7 +147,7 @@ impl MixnetMessage {
self.data.len()
}
pub(crate) fn into_client_request(self) -> ClientRequest {
pub(crate) fn into_input_message(self) -> InputMessage {
self.address.send_back_to(self.data, self.connection_id)
}
}
@@ -173,17 +174,17 @@ impl MixnetAddress {
None
}
pub(super) fn send_back_to(self, message: Vec<u8>, connection_id: u64) -> ClientRequest {
pub(super) fn send_back_to(self, message: Vec<u8>, connection_id: u64) -> InputMessage {
match self {
MixnetAddress::Known(recipient) => ClientRequest::Send {
MixnetAddress::Known(recipient) => InputMessage::Regular {
recipient: *recipient,
message,
connection_id: Some(connection_id),
data: message,
lane: TransmissionLane::ConnectionId(connection_id),
},
MixnetAddress::Anonymous(sender_tag) => ClientRequest::Reply {
message,
sender_tag,
connection_id: Some(connection_id),
MixnetAddress::Anonymous(sender_tag) => InputMessage::Reply {
recipient_tag: sender_tag,
data: message,
lane: TransmissionLane::ConnectionId(connection_id),
},
}
}
@@ -1,34 +0,0 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use tokio::net::TcpStream;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::{connect_async, MaybeTlsStream};
#[allow(clippy::upper_case_acronyms)]
pub(crate) type TSWebsocketStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
pub struct Connection {
uri: String,
}
impl Connection {
pub fn new(uri: &str) -> Connection {
Connection {
uri: String::from(uri),
}
}
pub async fn connect(&self) -> Result<TSWebsocketStream, WebsocketConnectionError> {
match connect_async(&self.uri).await {
Ok((ws_stream, _)) => Ok(ws_stream),
Err(e) => Err(WebsocketConnectionError::ConnectionNotEstablished(e)),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum WebsocketConnectionError {
#[error("Connection not established")]
ConnectionNotEstablished(tokio_tungstenite::tungstenite::Error),
}