From f68ea1393d3fda9ad70394221d74bd794c2b773c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Oct 2024 00:39:15 +0200 Subject: [PATCH] Add network discovery and selection capability to nym-vpnd (#1361) --- .../src/controller.rs | 1 + .../crates/nym-vpn-lib/src/mixnet/connect.rs | 2 +- nym-vpn-core/crates/nym-vpnc/src/cli.rs | 7 + nym-vpn-core/crates/nym-vpnc/src/main.rs | 14 +- nym-vpn-core/crates/nym-vpnd/Cargo.toml | 1 + .../command_interface/connection_handler.rs | 12 +- .../src/command_interface/listener.rs | 23 +- .../src/command_interface/protobuf/error.rs | 25 +- nym-vpn-core/crates/nym-vpnd/src/discovery.rs | 338 ++++++++++++++++++ nym-vpn-core/crates/nym-vpnd/src/logging.rs | 2 +- nym-vpn-core/crates/nym-vpnd/src/main.rs | 62 +++- .../crates/nym-vpnd/src/service/config.rs | 123 +++++-- .../crates/nym-vpnd/src/service/error.rs | 16 + .../crates/nym-vpnd/src/service/mod.rs | 8 +- .../nym-vpnd/src/service/vpn_service.rs | 75 ++-- nym-vpn-core/env/discovery.json | 5 + nym-vpn-core/env/env.json | 3 + proto/nym/vpn.proto | 28 ++ 18 files changed, 665 insertions(+), 80 deletions(-) create mode 100644 nym-vpn-core/crates/nym-vpnd/src/discovery.rs create mode 100644 nym-vpn-core/env/discovery.json create mode 100644 nym-vpn-core/env/env.json diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index 6ebc097728..1627bfcc04 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -726,6 +726,7 @@ where Some(command) = self.command_rx.recv() => { if let Err(err) = self.handle_command(command).await { tracing::error!("{err}"); + tracing::debug!("{err:#?}"); } } _ = update_shared_account_state_timer.tick() => { diff --git a/nym-vpn-core/crates/nym-vpn-lib/src/mixnet/connect.rs b/nym-vpn-core/crates/nym-vpn-lib/src/mixnet/connect.rs index 66e25e6b1c..78916fa7bc 100644 --- a/nym-vpn-core/crates/nym-vpn-lib/src/mixnet/connect.rs +++ b/nym-vpn-core/crates/nym-vpn-lib/src/mixnet/connect.rs @@ -89,7 +89,7 @@ pub(crate) async fn setup_mixnet_client( let storage = VpnClientOnDiskStorage::new(path.clone()); match storage.is_mnemonic_stored().await { Ok(is_stored) if !is_stored => { - tracing::error!("No credential stored"); + tracing::error!("No account stored"); task_client.disarm(); return Err(MixnetError::InvalidCredential); } diff --git a/nym-vpn-core/crates/nym-vpnc/src/cli.rs b/nym-vpn-core/crates/nym-vpnc/src/cli.rs index 4726480522..d4b705ed7a 100644 --- a/nym-vpn-core/crates/nym-vpnc/src/cli.rs +++ b/nym-vpn-core/crates/nym-vpnc/src/cli.rs @@ -24,6 +24,7 @@ pub(crate) enum Command { Disconnect, Status, Info, + SetNetwork(SetNetworkArgs), StoreAccount(StoreAccountArgs), RemoveAccount, GetLocalAccountState, @@ -134,6 +135,12 @@ pub(crate) struct CliExit { pub(crate) exit_gateway_random: bool, } +#[derive(Args)] +pub(crate) struct SetNetworkArgs { + /// The network to be set. + pub(crate) network: String, +} + #[derive(Args)] pub(crate) struct StoreAccountArgs { /// The account mnemonic to be stored. diff --git a/nym-vpn-core/crates/nym-vpnc/src/main.rs b/nym-vpn-core/crates/nym-vpnc/src/main.rs index e527aa7b72..64855b9cbf 100644 --- a/nym-vpn-core/crates/nym-vpnc/src/main.rs +++ b/nym-vpn-core/crates/nym-vpnc/src/main.rs @@ -8,7 +8,8 @@ use nym_vpn_proto::{ ConnectRequest, DisconnectRequest, Empty, GetAccountSummaryRequest, GetDeviceZkNymsRequest, GetDevicesRequest, GetLocalAccountStateRequest, InfoRequest, InfoResponse, IsReadyToConnectRequest, ListCountriesRequest, ListGatewaysRequest, RegisterDeviceRequest, - RemoveAccountRequest, RequestZkNymRequest, StatusRequest, StoreAccountRequest, UserAgent, + RemoveAccountRequest, RequestZkNymRequest, SetNetworkRequest, StatusRequest, + StoreAccountRequest, UserAgent, }; use protobuf_conversion::{into_gateway_type, into_threshold}; use sysinfo::System; @@ -39,6 +40,7 @@ async fn main() -> Result<()> { Command::Disconnect => disconnect(client_type).await?, Command::Status => status(client_type).await?, Command::Info => info(client_type).await?, + Command::SetNetwork(ref args) => set_network(client_type, args).await?, Command::StoreAccount(ref store_args) => store_account(client_type, store_args).await?, Command::RemoveAccount => remove_account(client_type).await?, Command::GetLocalAccountState => get_local_account_state(client_type).await?, @@ -164,6 +166,16 @@ async fn info(client_type: ClientType) -> Result<()> { Ok(()) } +async fn set_network(client_type: ClientType, args: &cli::SetNetworkArgs) -> Result<()> { + let mut client = vpnd_client::get_client(client_type).await?; + let request = tonic::Request::new(SetNetworkRequest { + network: args.network.clone(), + }); + let response = client.set_network(request).await?.into_inner(); + println!("{:#?}", response); + Ok(()) +} + async fn store_account(client_type: ClientType, store_args: &cli::StoreAccountArgs) -> Result<()> { let mut client = vpnd_client::get_client(client_type).await?; let request = tonic::Request::new(StoreAccountRequest { diff --git a/nym-vpn-core/crates/nym-vpnd/Cargo.toml b/nym-vpn-core/crates/nym-vpnd/Cargo.toml index 860f233575..0ed2233fb7 100644 --- a/nym-vpn-core/crates/nym-vpnd/Cargo.toml +++ b/nym-vpn-core/crates/nym-vpnd/Cargo.toml @@ -21,6 +21,7 @@ parity-tokio-ipc.workspace = true prost-types.workspace = true prost.workspace = true reqwest = { workspace = true, default-features = false, features = [ + "blocking", "rustls-tls", ] } serde.workspace = true diff --git a/nym-vpn-core/crates/nym-vpnd/src/command_interface/connection_handler.rs b/nym-vpn-core/crates/nym-vpnd/src/command_interface/connection_handler.rs index 10a0a5e7d6..baa4ba487b 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/command_interface/connection_handler.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/command_interface/connection_handler.rs @@ -12,8 +12,8 @@ use nym_vpn_lib::gateway_directory::{EntryPoint, ExitPoint, GatewayClient, Gatew use crate::{ service::{ - AccountError, ConnectArgs, ConnectOptions, VpnServiceCommand, VpnServiceConnectError, - VpnServiceDisconnectError, VpnServiceInfo, VpnServiceStatus, + AccountError, ConnectArgs, ConnectOptions, SetNetworkError, VpnServiceCommand, + VpnServiceConnectError, VpnServiceDisconnectError, VpnServiceInfo, VpnServiceStatus, }, types::gateway, }; @@ -77,6 +77,14 @@ impl CommandInterfaceConnectionHandler { self.send_and_wait(VpnServiceCommand::Info, ()).await } + pub(crate) async fn handle_set_network( + &self, + network: String, + ) -> Result, VpnCommandSendError> { + self.send_and_wait(VpnServiceCommand::SetNetwork, network) + .await + } + pub(crate) async fn handle_status(&self) -> Result { self.send_and_wait(VpnServiceCommand::Status, ()).await } diff --git a/nym-vpn-core/crates/nym-vpnd/src/command_interface/listener.rs b/nym-vpn-core/crates/nym-vpnd/src/command_interface/listener.rs index 279c99a2e0..5185d16edc 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/command_interface/listener.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/command_interface/listener.rs @@ -20,8 +20,8 @@ use nym_vpn_proto::{ InfoResponse, IsAccountStoredRequest, IsAccountStoredResponse, IsReadyToConnectRequest, IsReadyToConnectResponse, ListCountriesRequest, ListCountriesResponse, ListGatewaysRequest, ListGatewaysResponse, RegisterDeviceRequest, RegisterDeviceResponse, RemoveAccountRequest, - RemoveAccountResponse, RequestZkNymRequest, RequestZkNymResponse, StatusRequest, - StatusResponse, StoreAccountRequest, StoreAccountResponse, + RemoveAccountResponse, RequestZkNymRequest, RequestZkNymResponse, SetNetworkRequest, + SetNetworkResponse, StatusRequest, StatusResponse, StoreAccountRequest, StoreAccountResponse, }; use super::{ @@ -123,6 +123,25 @@ impl NymVpnd for CommandInterface { Ok(tonic::Response::new(response)) } + async fn set_network( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let network = request.into_inner().network; + + let status = CommandInterfaceConnectionHandler::new(self.vpn_command_tx.clone()) + .handle_set_network(network) + .await?; + + let response = nym_vpn_proto::SetNetworkResponse { + error: status + .err() + .map(nym_vpn_proto::SetNetworkRequestError::from), + }; + tracing::debug!("Returning set network response: {:?}", response); + Ok(tonic::Response::new(response)) + } + async fn vpn_connect( &self, request: tonic::Request, diff --git a/nym-vpn-core/crates/nym-vpnd/src/command_interface/protobuf/error.rs b/nym-vpn-core/crates/nym-vpnd/src/command_interface/protobuf/error.rs index e64f41315e..a0c1905ba3 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/command_interface/protobuf/error.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/command_interface/protobuf/error.rs @@ -5,7 +5,9 @@ use maplit::hashmap; use nym_vpn_account_controller::ReadyToConnect; use nym_vpn_proto::{account_error::AccountErrorType, error::ErrorType, Error as ProtoError}; -use crate::service::{AccountError, ConnectionFailedError, VpnServiceConnectError}; +use crate::service::{ + AccountError, ConnectionFailedError, SetNetworkError, VpnServiceConnectError, +}; impl From for nym_vpn_proto::ConnectRequestError { fn from(err: VpnServiceConnectError) -> Self { @@ -538,3 +540,24 @@ impl From for tonic::Status { } } } + +impl From for nym_vpn_proto::SetNetworkRequestError { + fn from(err: SetNetworkError) -> Self { + match err { + SetNetworkError::NetworkNotFound(ref err) => nym_vpn_proto::SetNetworkRequestError { + kind: nym_vpn_proto::set_network_request_error::SetNetworkRequestErrorType::InvalidNetworkName as i32, + message: err.to_string(), + }, + SetNetworkError::ReadConfig { .. } => nym_vpn_proto::SetNetworkRequestError { + kind: nym_vpn_proto::set_network_request_error::SetNetworkRequestErrorType::Internal + as i32, + message: err.to_string(), + }, + SetNetworkError::WriteConfig { .. } => nym_vpn_proto::SetNetworkRequestError { + kind: nym_vpn_proto::set_network_request_error::SetNetworkRequestErrorType::Internal + as i32, + message: err.to_string(), + }, + } + } +} diff --git a/nym-vpn-core/crates/nym-vpnd/src/discovery.rs b/nym-vpn-core/crates/nym-vpnd/src/discovery.rs new file mode 100644 index 0000000000..ae30680855 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpnd/src/discovery.rs @@ -0,0 +1,338 @@ +use std::{env, path::PathBuf}; + +use anyhow::Context; +use nym_vpn_lib::nym_config::defaults::{var_names, NymNetworkDetails}; +use url::Url; + +const DISCOVERY_FILE: &str = "discovery.json"; +const NETWORKS_SUBDIR: &str = "networks"; +const DISCOVERY_WELLKNOWN: &str = "https://nymvpn.com/api/public/v1/.wellknown"; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub(crate) struct GlobalConfigFile { + pub(crate) network_name: String, +} + +impl Default for GlobalConfigFile { + fn default() -> Self { + Self { + network_name: NymNetworkDetails::default().network_name, + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +struct Discovery { + network_name: String, + nym_api_url: String, + nym_vpn_api_url: String, +} + +impl Default for Discovery { + fn default() -> Self { + let default_network_details = NymNetworkDetails::default(); + Self { + network_name: default_network_details.network_name, + nym_api_url: default_network_details + .endpoints + .first() + .and_then(|e| e.api_url.clone()) + .expect("default network details not setup correctly"), + nym_vpn_api_url: default_network_details + .nym_vpn_api_url + .expect("default network details not setup correctly"), + } + } +} + +// This is the type we fetch remotely from nym-api +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +struct NetworkDetails { + pub network: NymNetworkDetails, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +struct NymVpnNetworkDetails { + pub nym_vpn_api_url: String, +} + +fn discovery_endpoint(network_name: &str) -> anyhow::Result { + format!( + "{}/{}/{}", + DISCOVERY_WELLKNOWN, network_name, DISCOVERY_FILE + ) + .parse() + .map_err(Into::into) +} + +fn fetch_discovery(network_name: &str) -> anyhow::Result { + let url = discovery_endpoint(network_name)?; + tracing::info!("Fetching nym network discovery from: {}", url); + reqwest::blocking::get(url.clone()) + .with_context(|| format!("Failed to fetch discovery from {}", url))? + .json() + .with_context(|| "Failed to parse discovery") +} + +fn discovery_file_path(network_name: &str) -> PathBuf { + crate::service::config_dir() + .join(NETWORKS_SUBDIR) + .join(format!("{}_{}", network_name, DISCOVERY_FILE)) +} + +fn check_if_discovery_file_exists(network_name: &str) -> bool { + discovery_file_path(network_name).exists() +} + +fn read_discovery_file(network_name: &str) -> anyhow::Result { + let discovery_path = discovery_file_path(network_name); + tracing::info!("Reading discovery file from: {}", discovery_path.display()); + + let file_str = std::fs::read_to_string(discovery_path)?; + let network: Discovery = serde_json::from_str(&file_str)?; + Ok(network) +} + +fn write_discovery_to_file(discovery: &Discovery) -> anyhow::Result<()> { + let discovery_path = discovery_file_path(&discovery.network_name); + tracing::info!("Writing discovery file to: {}", discovery_path.display()); + + // Create parent directories if they don't exist + if let Some(parent) = discovery_path.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!( + "Failed to create parent directories for {:?}", + discovery_path + ) + })?; + } + + let file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&discovery_path) + .with_context(|| format!("Failed to open discovery file at {:?}", discovery_path))?; + + serde_json::to_writer_pretty(&file, discovery) + .with_context(|| format!("Failed to write discovery file at {:?}", discovery_path))?; + + Ok(()) +} + +fn fetch_nym_network_details(nym_api_url: Url) -> anyhow::Result { + let url = format!("{}/v1/network/details", nym_api_url); + tracing::info!("Fetching nym network details from: {}", url); + reqwest::blocking::get(&url) + .with_context(|| format!("Failed to fetch network details from {}", url))? + .json() + .with_context(|| "Failed to parse network details") +} + +fn network_details_path(network_name: &str) -> PathBuf { + crate::service::config_dir() + .join(NETWORKS_SUBDIR) + .join(format!("{}.json", network_name)) +} + +fn check_if_nym_network_details_file_exists(network_name: &str) -> bool { + network_details_path(network_name).exists() +} + +fn read_nym_network_details_from_file(network_name: &str) -> anyhow::Result { + let network_details_path = network_details_path(network_name); + tracing::info!( + "Reading network details from: {}", + network_details_path.display() + ); + let file_str = std::fs::read_to_string(network_details_path)?; + let network: NymNetworkDetails = serde_json::from_str(&file_str)?; + Ok(network) +} + +fn write_nym_network_details_to_file(network: &NymNetworkDetails) -> anyhow::Result<()> { + let network_details_path = network_details_path(&network.network_name); + + // Create parent directories if they don't exist + if let Some(parent) = network_details_path.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!( + "Failed to create parent directories for {:?}", + network_details_path + ) + })?; + } + + let file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&network_details_path) + .with_context(|| { + format!( + "Failed to open network details file at {:?}", + network_details_path + ) + })?; + + serde_json::to_writer_pretty(&file, network).with_context(|| { + format!( + "Failed to write network details file at {:?}", + network_details_path + ) + })?; + + Ok(()) +} + +fn setup_nym_network_details(network_name: &str) -> anyhow::Result { + let network = read_nym_network_details_from_file(network_name)?; + export_nym_network_details_to_env(network.clone()); + Ok(network) +} + +fn setup_nym_vpn_network_details(nym_vpn_api_url: Url) -> anyhow::Result { + let vpn_network_details = NymVpnNetworkDetails { + nym_vpn_api_url: nym_vpn_api_url.to_string(), + }; + export_nym_vpn_network_details_to_env(vpn_network_details.clone()); + Ok(vpn_network_details) +} + +fn export_nym_network_details_to_env(network_details: NymNetworkDetails) { + fn set_optional_var(var_name: &str, value: Option) { + if let Some(value) = value { + env::set_var(var_name, value); + } + } + + env::set_var(var_names::NETWORK_NAME, network_details.network_name); + env::set_var( + var_names::BECH32_PREFIX, + network_details.chain_details.bech32_account_prefix, + ); + + env::set_var( + var_names::MIX_DENOM, + network_details.chain_details.mix_denom.base, + ); + env::set_var( + var_names::MIX_DENOM_DISPLAY, + network_details.chain_details.mix_denom.display, + ); + + env::set_var( + var_names::STAKE_DENOM, + network_details.chain_details.stake_denom.base, + ); + env::set_var( + var_names::STAKE_DENOM_DISPLAY, + network_details.chain_details.stake_denom.display, + ); + + env::set_var( + var_names::DENOMS_EXPONENT, + network_details + .chain_details + .mix_denom + .display_exponent + .to_string(), + ); + + env::set_var( + var_names::NYXD, + network_details.endpoints.first().unwrap().nyxd_url.clone(), + ); + set_optional_var( + var_names::NYM_API, + network_details.endpoints.first().unwrap().api_url.clone(), + ); + set_optional_var( + var_names::NYXD_WEBSOCKET, + network_details + .endpoints + .first() + .unwrap() + .websocket_url + .clone(), + ); + + set_optional_var( + var_names::MIXNET_CONTRACT_ADDRESS, + network_details.contracts.mixnet_contract_address, + ); + set_optional_var( + var_names::VESTING_CONTRACT_ADDRESS, + network_details.contracts.vesting_contract_address, + ); + set_optional_var( + var_names::ECASH_CONTRACT_ADDRESS, + network_details.contracts.ecash_contract_address, + ); + set_optional_var( + var_names::GROUP_CONTRACT_ADDRESS, + network_details.contracts.group_contract_address, + ); + set_optional_var( + var_names::MULTISIG_CONTRACT_ADDRESS, + network_details.contracts.multisig_contract_address, + ); + set_optional_var( + var_names::COCONUT_DKG_CONTRACT_ADDRESS, + network_details.contracts.coconut_dkg_contract_address, + ); + + set_optional_var(var_names::EXPLORER_API, network_details.explorer_api); + set_optional_var(var_names::NYM_VPN_API, network_details.nym_vpn_api_url); +} + +fn export_nym_vpn_network_details_to_env(vpn_network_details: NymVpnNetworkDetails) { + env::set_var(var_names::NYM_VPN_API, vpn_network_details.nym_vpn_api_url); +} + +pub fn read_global_config_file() -> anyhow::Result { + let global_config_file_path = + crate::service::config_dir().join(crate::service::DEFAULT_GLOBAL_CONFIG_FILE); + + crate::service::create_config_file(&global_config_file_path, &GlobalConfigFile::default())?; + crate::service::read_config_file(&global_config_file_path).map_err(Into::into) +} + +pub fn write_global_config_file( + global_config: GlobalConfigFile, +) -> anyhow::Result { + let global_config_file_path = + crate::service::config_dir().join(crate::service::DEFAULT_GLOBAL_CONFIG_FILE); + + crate::service::write_config_file(&global_config_file_path, global_config).map_err(Into::into) +} + +pub(crate) fn discover_env(network_name: &str) -> anyhow::Result<()> { + // Lookup network discovery to bootstrap + if !check_if_discovery_file_exists(network_name) { + let discovery = fetch_discovery(network_name)?; + if discovery.network_name != network_name { + anyhow::bail!("Network name mismatch between requested and fetched discovery") + } + write_discovery_to_file(&discovery)?; + } + let discovery = read_discovery_file(network_name)?; + + // Using discovery, fetch and setup nym network details + if !check_if_nym_network_details_file_exists(&discovery.network_name) { + let network_details = fetch_nym_network_details(discovery.nym_api_url.parse()?)?; + if network_details.network.network_name != discovery.network_name { + anyhow::bail!( + "Network name mismatch between discovery file and fetched network details" + ) + } + write_nym_network_details_to_file(&network_details.network)?; + } + let network_details = setup_nym_network_details(&discovery.network_name)?; + crate::set_global_network_details(network_details)?; + + // Using discovery, setup nym vpn network details + setup_nym_vpn_network_details(discovery.nym_vpn_api_url.parse()?)?; + + Ok(()) +} diff --git a/nym-vpn-core/crates/nym-vpnd/src/logging.rs b/nym-vpn-core/crates/nym-vpnd/src/logging.rs index b67e441914..5153759a1e 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/logging.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/logging.rs @@ -30,7 +30,7 @@ pub fn setup_logging(_as_service: bool) { #[allow(unused)] pub fn setup_logging_to_file() -> WorkerGuard { - let log_dir = service::default_log_dir(); + let log_dir = service::log_dir(); println!("log_dir: {}", log_dir.display()); diff --git a/nym-vpn-core/crates/nym-vpnd/src/main.rs b/nym-vpn-core/crates/nym-vpnd/src/main.rs index 651bb01181..45f490b331 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/main.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/main.rs @@ -1,9 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// mod account; mod cli; mod command_interface; +mod discovery; mod logging; mod runtime; mod service; @@ -13,35 +13,65 @@ mod util; #[cfg(windows)] mod windows_service; +use std::sync::OnceLock; + use clap::Parser; -use nym_vpn_lib::nym_config::defaults::setup_env; +use nym_vpn_lib::nym_config::defaults::NymNetworkDetails; use service::NymVpnService; use tokio::sync::broadcast; use tokio_util::sync::CancellationToken; -use crate::{ - cli::CliArgs, - command_interface::{start_command_interface, CommandInterfaceOptions}, - logging::setup_logging, -}; +use crate::{cli::CliArgs, command_interface::CommandInterfaceOptions}; + +// Lazy initialized global NymNetworkDetails +static GLOBAL_NETWORK_DETAILS: OnceLock = OnceLock::new(); -fn main() -> Result<(), Box> { +fn main() -> anyhow::Result<()> { run() } +fn set_global_network_details(network_details: NymNetworkDetails) -> anyhow::Result<()> { + GLOBAL_NETWORK_DETAILS + .set(network_details) + .map_err(|_| anyhow::anyhow!("Failed to set network details")) +} + #[cfg(unix)] -fn run() -> Result<(), Box> { +fn run() -> anyhow::Result<()> { let args = CliArgs::parse(); - setup_logging(args.command.run_as_service); - setup_env(args.config_env_file.as_ref()); + let global_config_file = discovery::read_global_config_file()?; + + logging::setup_logging(args.command.run_as_service); + + if let Some(ref env) = args.config_env_file { + nym_vpn_lib::nym_config::defaults::setup_env(Some(env)); + let network_details = NymNetworkDetails::new_from_env(); + set_global_network_details(network_details)?; + } else { + let network_name = global_config_file.network_name.clone(); + tracing::info!("Setting up environment by discovering the network: {network_name}"); + discovery::discover_env(&network_name)?; + } run_inner(args) } #[cfg(windows)] -fn run() -> Result<(), Box> { +fn run() -> anyhow::Result<()> { let args = CliArgs::parse(); - setup_env(args.config_env_file.as_ref()); + let global_config_file = discovery::read_global_config_file()?; + + if let Some(ref env) = args.config_env_file { + nym_vpn_lib::nym_config::defaults::setup_env(Some(env)); + let network_details = NymNetworkDetails::new_from_env(); + GLOBAL_NETWORK_DETAILS + .set(network_details) + .map_err(|_| anyhow::anyhow!("Failed to set network details"))?; + } else { + let network_name = global_config_file.network_name.clone(); + tracing::info!("Setting up environment from discovery file: {network_name}"); + discovery::discover_env(&network_name)?; + } if args.command.is_any() { Ok(windows_service::start(args)?) @@ -51,16 +81,16 @@ fn run() -> Result<(), Box> { } } -fn run_inner(args: CliArgs) -> Result<(), Box> { +fn run_inner(args: CliArgs) -> anyhow::Result<()> { runtime::new_runtime().block_on(run_inner_async(args)) } -async fn run_inner_async(args: CliArgs) -> Result<(), Box> { +async fn run_inner_async(args: CliArgs) -> anyhow::Result<()> { let (state_changes_tx, state_changes_rx) = broadcast::channel(10); let (status_tx, status_rx) = broadcast::channel(10); let shutdown_token = CancellationToken::new(); - let (command_handle, vpn_command_rx) = start_command_interface( + let (command_handle, vpn_command_rx) = command_interface::start_command_interface( state_changes_rx, status_rx, Some(CommandInterfaceOptions { diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/config.rs b/nym-vpn-core/crates/nym-vpnd/src/service/config.rs index 0f0b454eb0..c0c93316eb 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/config.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/config.rs @@ -2,10 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-only #[cfg(unix)] -use std::os::unix::fs::PermissionsExt as _; +use std::os::unix::fs::PermissionsExt; use std::{fmt, fs, path::PathBuf}; use nym_vpn_lib::gateway_directory; +use serde::{de::DeserializeOwned, Serialize}; use tracing::info; #[cfg(not(windows))] @@ -14,15 +15,47 @@ const DEFAULT_DATA_DIR: &str = "/var/lib/nym-vpnd"; const DEFAULT_LOG_DIR: &str = "/var/log/nym-vpnd"; #[cfg(not(windows))] const DEFAULT_CONFIG_DIR: &str = "/etc/nym"; -pub(super) const DEFAULT_CONFIG_FILE: &str = "nym-vpnd.toml"; +pub(crate) const DEFAULT_CONFIG_FILE: &str = "nym-vpnd.toml"; pub(crate) const DEFAULT_LOG_FILE: &str = "nym-vpnd.log"; +pub(crate) const DEFAULT_GLOBAL_CONFIG_FILE: &str = "config.toml"; + +#[derive(Debug, Clone)] +pub(crate) enum NetworkEnvironments { + Mainnet, + Qa, + Canary, +} + +impl fmt::Display for NetworkEnvironments { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NetworkEnvironments::Mainnet => write!(f, "mainnet"), + NetworkEnvironments::Qa => write!(f, "qa"), + NetworkEnvironments::Canary => write!(f, "canary"), + } + } +} + +impl TryFrom<&str> for NetworkEnvironments { + type Error = &'static str; + + fn try_from(env: &str) -> Result { + match env { + "mainnet" => Ok(NetworkEnvironments::Mainnet), + "qa" => Ok(NetworkEnvironments::Qa), + "canary" => Ok(NetworkEnvironments::Canary), + _ => Err("Invalid network environment"), + } + } +} + #[cfg(windows)] pub(crate) fn program_data_path() -> PathBuf { PathBuf::from(std::env::var("ProgramData").unwrap_or(std::env::var("PROGRAMDATA").unwrap())) } -pub(super) fn default_data_dir() -> PathBuf { +fn default_data_dir() -> PathBuf { #[cfg(windows)] return program_data_path().join("nym-vpnd").join("data"); @@ -30,7 +63,13 @@ pub(super) fn default_data_dir() -> PathBuf { return DEFAULT_DATA_DIR.into(); } -pub(crate) fn default_log_dir() -> PathBuf { +pub(crate) fn data_dir() -> PathBuf { + std::env::var("NYM_VPND_DATA_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| default_data_dir()) +} + +fn default_log_dir() -> PathBuf { #[cfg(windows)] return program_data_path().join("nym-vpnd").join("log"); @@ -38,7 +77,13 @@ pub(crate) fn default_log_dir() -> PathBuf { return DEFAULT_LOG_DIR.into(); } -pub(super) fn default_config_dir() -> PathBuf { +pub(crate) fn log_dir() -> PathBuf { + std::env::var("NYM_VPND_LOG_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| default_log_dir()) +} + +pub(crate) fn default_config_dir() -> PathBuf { #[cfg(windows)] return program_data_path().join("nym-vpnd").join("config"); @@ -46,6 +91,12 @@ pub(super) fn default_config_dir() -> PathBuf { return DEFAULT_CONFIG_DIR.into(); } +pub(crate) fn config_dir() -> PathBuf { + std::env::var("NYM_VPND_CONFIG_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| default_config_dir()) +} + #[derive(thiserror::Error, Debug)] pub enum ConfigSetupError { #[error("failed to parse config file {file}: {error}")] @@ -80,10 +131,13 @@ pub enum ConfigSetupError { FailedToInitKeys { source: nym_vpn_store::keys::persistence::OnDiskKeysError, }, + + #[error("global network details not set")] + GlobalNetworkNotSet, } #[derive(Debug, serde::Serialize, serde::Deserialize)] -pub(super) struct NymVpnServiceConfig { +pub(crate) struct NymVpnServiceConfig { pub(super) entry_point: gateway_directory::EntryPoint, pub(super) exit_point: gateway_directory::ExitPoint, } @@ -107,56 +161,61 @@ impl Default for NymVpnServiceConfig { } } -pub(super) fn create_config_file( - config_file: &PathBuf, - config: NymVpnServiceConfig, -) -> Result { +// Create the TOML representation of the provided config, only if it doesn't already exists +pub(crate) fn create_config_file(file_path: &PathBuf, config: C) -> Result +where + C: Serialize, +{ let config_str = toml::to_string(&config).unwrap(); + tracing::info!("Creating config file at {}", file_path.display()); // Create path - let config_dir = config_file + let config_dir = file_path .parent() .ok_or_else(|| ConfigSetupError::GetParentDirectory { - file: config_file.clone(), + file: file_path.clone(), })?; fs::create_dir_all(config_dir).map_err(|error| ConfigSetupError::CreateDirectory { dir: config_dir.to_path_buf(), error, })?; - fs::write(config_file, config_str).map_err(|error| ConfigSetupError::WriteFile { - file: config_file.clone(), - error, - })?; - info!("Config file created at {:?}", config_file); + if !file_path.exists() { + fs::write(file_path, config_str).map_err(|error| ConfigSetupError::WriteFile { + file: file_path.clone(), + error, + })?; + tracing::info!("Config file created at {:?}", file_path.display()); + } Ok(config) } -pub(super) fn read_config_file( - config_file: &PathBuf, -) -> Result { +pub(crate) fn read_config_file(file_path: &PathBuf) -> Result +where + C: DeserializeOwned, +{ let file_content = - fs::read_to_string(config_file).map_err(|error| ConfigSetupError::ReadConfig { - file: config_file.clone(), + fs::read_to_string(file_path).map_err(|error| ConfigSetupError::ReadConfig { + file: file_path.clone(), error, })?; toml::from_str(&file_content).map_err(|error| ConfigSetupError::Parse { - file: config_file.clone(), + file: file_path.clone(), error: Box::new(error), }) } -pub(super) fn write_config_file( - config_file: &PathBuf, - config: &NymVpnServiceConfig, -) -> Result<(), ConfigSetupError> { - let config_str = toml::to_string(config).unwrap(); - fs::write(config_file, config_str).map_err(|error| ConfigSetupError::WriteFile { - file: config_file.clone(), +pub(crate) fn write_config_file(file_path: &PathBuf, config: C) -> Result +where + C: Serialize, +{ + let config_str = toml::to_string(&config).unwrap(); + fs::write(file_path, config_str).map_err(|error| ConfigSetupError::WriteFile { + file: file_path.clone(), error, })?; - info!("Config file updated at {:?}", config_file); - Ok(()) + info!("Config file updated at {:?}", file_path); + Ok(config) } pub(super) fn create_data_dir(data_dir: &PathBuf) -> Result<(), ConfigSetupError> { diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/error.rs b/nym-vpn-core/crates/nym-vpnd/src/service/error.rs index bbe635915c..31126454e4 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/error.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/error.rs @@ -350,6 +350,22 @@ pub enum AccountError { }, } +#[derive(Debug, thiserror::Error)] +pub enum SetNetworkError { + #[error("failed to read config")] + ReadConfig { + source: Box, + }, + + #[error("failed to write config")] + WriteConfig { + source: Box, + }, + + #[error("failed to set network: {0}")] + NetworkNotFound(String), +} + #[derive(thiserror::Error, Debug)] pub enum Error { // FIXME: this variant should be constructed diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/mod.rs b/nym-vpn-core/crates/nym-vpnd/src/service/mod.rs index 9f53cfef7d..e5fe21c7f4 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/mod.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/mod.rs @@ -5,9 +5,13 @@ mod config; mod error; mod vpn_service; -pub(crate) use config::{default_log_dir, DEFAULT_LOG_FILE}; +pub(crate) use config::{ + config_dir, create_config_file, log_dir, read_config_file, write_config_file, + DEFAULT_GLOBAL_CONFIG_FILE, DEFAULT_LOG_FILE, +}; pub(crate) use error::{ - AccountError, ConnectionFailedError, VpnServiceConnectError, VpnServiceDisconnectError, + AccountError, ConnectionFailedError, SetNetworkError, VpnServiceConnectError, + VpnServiceDisconnectError, }; pub(crate) use vpn_service::{ ConnectArgs, ConnectOptions, ConnectedStateDetails, NymVpnService, VpnServiceCommand, diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs index 14b7c079c9..0644955b1a 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs @@ -37,12 +37,11 @@ use nym_vpn_lib::{ }; use nym_vpn_store::keys::KeyStore as _; +use crate::GLOBAL_NETWORK_DETAILS; + use super::{ - config::{ - self, create_config_file, create_data_dir, read_config_file, write_config_file, - ConfigSetupError, NymVpnServiceConfig, DEFAULT_CONFIG_FILE, - }, - error::{AccountError, ConnectionFailedError, Error, Result}, + config::{ConfigSetupError, NetworkEnvironments, NymVpnServiceConfig, DEFAULT_CONFIG_FILE}, + error::{AccountError, ConnectionFailedError, Error, Result, SetNetworkError}, VpnServiceConnectError, VpnServiceDisconnectError, }; @@ -96,6 +95,7 @@ pub enum VpnServiceCommand { Disconnect(oneshot::Sender>, ()), Status(oneshot::Sender, ()), Info(oneshot::Sender, ()), + SetNetwork(oneshot::Sender>, String), StoreAccount(oneshot::Sender>, String), IsAccountStored(oneshot::Sender>, ()), RemoveAccount(oneshot::Sender>, ()), @@ -123,6 +123,7 @@ impl fmt::Display for VpnServiceCommand { VpnServiceCommand::Disconnect(..) => write!(f, "Disconnect"), VpnServiceCommand::Status(..) => write!(f, "Status"), VpnServiceCommand::Info(..) => write!(f, "Info"), + VpnServiceCommand::SetNetwork(..) => write!(f, "SetNetwork"), VpnServiceCommand::StoreAccount(..) => write!(f, "StoreAccount"), VpnServiceCommand::IsAccountStored(..) => write!(f, "IsAccountStored"), VpnServiceCommand::RemoveAccount(..) => write!(f, "RemoveAccount"), @@ -395,20 +396,22 @@ impl NymVpnService { status_tx: broadcast::Sender, shutdown_token: CancellationToken, ) -> Result { - let config_dir = std::env::var("NYM_VPND_CONFIG_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| config::default_config_dir()); + let network_details = GLOBAL_NETWORK_DETAILS + .get() + .ok_or(Error::ConfigSetup(ConfigSetupError::GlobalNetworkNotSet))? + .clone(); + let network_name = network_details.network_name.clone(); + + let config_dir = super::config::config_dir().join(&network_name); let config_file = config_dir.join(DEFAULT_CONFIG_FILE); - let data_dir = std::env::var("NYM_VPND_DATA_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| config::default_data_dir()); + let data_dir = super::config::data_dir().join(&network_name); let storage = Arc::new(tokio::sync::Mutex::new( nym_vpn_lib::storage::VpnClientOnDiskStorage::new(data_dir.clone()), )); // Make sure the data dir exists - create_data_dir(&data_dir).map_err(Error::ConfigSetup)?; + super::config::create_data_dir(&data_dir).map_err(Error::ConfigSetup)?; // Generate the device keys if we don't already have them storage @@ -538,6 +541,10 @@ where let result = self.handle_info().await; let _ = tx.send(result); } + VpnServiceCommand::SetNetwork(tx, network) => { + let result = self.handle_set_network(network).await; + let _ = tx.send(result); + } VpnServiceCommand::StoreAccount(tx, account) => { let result = self.handle_store_account(account).await; let _ = tx.send(result); @@ -588,24 +595,27 @@ where ) -> Result { // If the config file does not exit, create it let config = if self.config_file.exists() { - let mut read_config = read_config_file(&self.config_file) - .map_err(|err| { - tracing::error!( - "Failed to read config file, resetting to defaults: {:?}", - err - ); - }) - .unwrap_or_default(); + let mut read_config: NymVpnServiceConfig = + super::config::read_config_file(&self.config_file) + .map_err(|err| { + tracing::error!( + "Failed to read config file, resetting to defaults: {:?}", + err + ); + }) + .unwrap_or_default(); read_config.entry_point = entry.unwrap_or(read_config.entry_point); read_config.exit_point = exit.unwrap_or(read_config.exit_point); - write_config_file(&self.config_file, &read_config).map_err(Error::ConfigSetup)?; + super::config::write_config_file(&self.config_file, &read_config) + .map_err(Error::ConfigSetup)?; read_config } else { let config = NymVpnServiceConfig { entry_point: entry.unwrap_or(EntryPoint::Random), exit_point: exit.unwrap_or(ExitPoint::Random), }; - create_config_file(&self.config_file, config).map_err(Error::ConfigSetup)? + super::config::create_config_file(&self.config_file, config) + .map_err(Error::ConfigSetup)? }; Ok(config) } @@ -741,6 +751,27 @@ where } } + async fn handle_set_network(&self, network: String) -> Result<(), SetNetworkError> { + let mut global_config = crate::discovery::read_global_config_file().map_err(|source| { + SetNetworkError::ReadConfig { + source: source.into(), + } + })?; + + // Manually restrict the set of possible network, until we handle this automatically + let network_selected = NetworkEnvironments::try_from(network.as_str()) + .map_err(|_err| SetNetworkError::NetworkNotFound(network.to_owned()))?; + global_config.network_name = network_selected.to_string(); + + crate::discovery::write_global_config_file(global_config).map_err(|source| { + SetNetworkError::WriteConfig { + source: source.into(), + } + })?; + + Ok(()) + } + async fn handle_store_account(&mut self, account: String) -> Result<(), AccountError> { self.storage .lock() diff --git a/nym-vpn-core/env/discovery.json b/nym-vpn-core/env/discovery.json new file mode 100644 index 0000000000..8018a62d4c --- /dev/null +++ b/nym-vpn-core/env/discovery.json @@ -0,0 +1,5 @@ +{ + "network_name": "mainnet", + "nym_api_url": "https://validator.nymtech.net/api/", + "nym_vpn_api_url": "https://nymvpn.com/api/" +} diff --git a/nym-vpn-core/env/env.json b/nym-vpn-core/env/env.json new file mode 100644 index 0000000000..7f9ef733de --- /dev/null +++ b/nym-vpn-core/env/env.json @@ -0,0 +1,3 @@ +{ + "environments": ["mainnet", "canary"] +} diff --git a/proto/nym/vpn.proto b/proto/nym/vpn.proto index abaef1d045..1d5d0ec32f 100644 --- a/proto/nym/vpn.proto +++ b/proto/nym/vpn.proto @@ -111,6 +111,31 @@ message InfoResponse { Url nym_vpn_api_url = 7; } +message SetNetworkRequest { + string network = 1; +} + +message SetNetworkResponse { + SetNetworkRequestError error = 1; +} + +message SetNetworkRequestError { + enum SetNetworkRequestErrorType { + SET_NETWORK_REQUEST_ERROR_TYPE_UNSPECIFIED = 0; + + // Unspecified internal error + INTERNAL = 1; + + // The network name provided is not valid + INVALID_NETWORK_NAME = 2; + } + + SetNetworkRequestErrorType kind = 1; + + // Internal message for logging and debugging + string message = 2; +} + message Threshold { uint32 min_performance = 1; } @@ -619,6 +644,9 @@ service NymVpnd { // Get info regarding the nym-vpnd in general, like version etc. rpc Info (InfoRequest) returns (InfoResponse) {} + // Set the network. This requires a restart to take effect + rpc SetNetwork (SetNetworkRequest) returns (SetNetworkResponse) {} + // Start the tunnel and connect rpc VpnConnect (ConnectRequest) returns (ConnectResponse) {}