diff --git a/Cargo.lock b/Cargo.lock index 7239d23431..c076c822d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4702,6 +4702,7 @@ name = "nym-vpnc" version = "0.1.0" dependencies = [ "anyhow", + "bs58 0.5.1", "clap", "nym-vpn-proto", "parity-tokio-ipc", diff --git a/nym-vpnc/Cargo.toml b/nym-vpnc/Cargo.toml index 41f0a4d0ef..ec332c597f 100644 --- a/nym-vpnc/Cargo.toml +++ b/nym-vpnc/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true [dependencies] anyhow.workspace = true +bs58.workspace = true clap = { workspace = true, features = ["derive"] } parity-tokio-ipc.workspace = true prost.workspace = true diff --git a/nym-vpnc/src/main.rs b/nym-vpnc/src/main.rs index e6e7f0ab43..8590ba3755 100644 --- a/nym-vpnc/src/main.rs +++ b/nym-vpnc/src/main.rs @@ -4,9 +4,10 @@ use std::path::{Path, PathBuf}; use anyhow::Context; -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; use nym_vpn_proto::{ - nym_vpnd_client::NymVpndClient, ConnectRequest, DisconnectRequest, StatusRequest, + nym_vpnd_client::NymVpndClient, ConnectRequest, DisconnectRequest, ImportUserCredentialRequest, + StatusRequest, }; use parity_tokio_ipc::Endpoint as IpcEndpoint; use tonic::transport::{Channel as TonicChannel, Endpoint as TonicEndpoint}; @@ -27,6 +28,49 @@ enum Command { Connect, Disconnect, Status, + ImportCredential(ImportCredentialArgs), +} + +#[derive(Args)] +pub(crate) struct ImportCredentialArgs { + #[command(flatten)] + pub(crate) credential_type: ImportCredentialType, + + // currently hidden as there exists only a single serialization standard + #[arg(long, hide = true)] + pub(crate) version: Option, +} + +#[derive(Args, Clone)] +#[group(required = true, multiple = false)] +pub(crate) struct ImportCredentialType { + /// Credential encoded using base58. + #[arg(long)] + pub(crate) credential_data: Option, + + /// Path to the credential file. + #[arg(long)] + pub(crate) credential_path: Option, +} + +fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { + bs58::decode(raw).into_vec() +} + +// Workaround until clap supports enums for ArgGroups +pub(crate) enum ImportCredentialTypeEnum { + Path(PathBuf), + Data(String), +} + +impl From for ImportCredentialTypeEnum { + fn from(ict: ImportCredentialType) -> Self { + match (ict.credential_data, ict.credential_path) { + (Some(data), None) => ImportCredentialTypeEnum::Data(data), + (None, Some(path)) => ImportCredentialTypeEnum::Path(path), + _ => unreachable!(), + } + } } #[tokio::main] @@ -36,6 +80,7 @@ async fn main() -> anyhow::Result<()> { Command::Connect => connect(&args).await?, Command::Disconnect => disconnect(&args).await?, Command::Status => status(&args).await?, + Command::ImportCredential(ref import_args) => import_credential(&args, import_args).await?, } Ok(()) } @@ -97,3 +142,22 @@ async fn status(args: &CliArgs) -> anyhow::Result<()> { println!("{:?}", response); Ok(()) } + +async fn import_credential( + args: &CliArgs, + import_args: &ImportCredentialArgs, +) -> anyhow::Result<()> { + let import_type: ImportCredentialTypeEnum = import_args.credential_type.clone().into(); + let request = match import_type { + ImportCredentialTypeEnum::Path(_path) => todo!(), + ImportCredentialTypeEnum::Data(data) => { + let bin = parse_encoded_credential_data(&data)?; + tonic::Request::new(ImportUserCredentialRequest { credential: bin }) + } + }; + + let mut client = get_client(args).await?; + let response = client.import_user_credential(request).await?.into_inner(); + println!("{:?}", response); + Ok(()) +} diff --git a/nym-vpnd/src/command_interface/connection_handler.rs b/nym-vpnd/src/command_interface/connection_handler.rs index ecf7918bf2..0d3b1443f2 100644 --- a/nym-vpnd/src/command_interface/connection_handler.rs +++ b/nym-vpnd/src/command_interface/connection_handler.rs @@ -5,7 +5,8 @@ use tokio::sync::{mpsc::UnboundedSender, oneshot}; use tracing::{info, warn}; use crate::service::{ - VpnServiceCommand, VpnServiceConnectResult, VpnServiceDisconnectResult, VpnServiceStatusResult, + ConnectArgs, VpnServiceCommand, VpnServiceConnectResult, VpnServiceDisconnectResult, + VpnServiceImportUserCredentialResult, VpnServiceStatusResult, }; pub(super) struct CommandInterfaceConnectionHandler { @@ -20,8 +21,9 @@ impl CommandInterfaceConnectionHandler { pub(crate) async fn handle_connect(&self) -> VpnServiceConnectResult { info!("Starting VPN"); let (tx, rx) = oneshot::channel(); + let connect_args = ConnectArgs::Default; self.vpn_command_tx - .send(VpnServiceCommand::Connect(tx)) + .send(VpnServiceCommand::Connect(tx, connect_args)) .unwrap(); info!("Sent start command to VPN"); info!("Waiting for response"); @@ -70,4 +72,19 @@ impl CommandInterfaceConnectionHandler { info!("VPN status: {:?}", status); status } + + pub(crate) async fn handle_import_credential( + &self, + credential: Vec, + ) -> VpnServiceImportUserCredentialResult { + let (tx, rx) = oneshot::channel(); + self.vpn_command_tx + .send(VpnServiceCommand::ImportCredential(tx, credential)) + .unwrap(); + info!("Sent import credential command to VPN"); + info!("Waiting for response"); + let result = rx.await.unwrap(); + info!("VPN import credential result: {:?}", result); + result + } } diff --git a/nym-vpnd/src/command_interface/listener.rs b/nym-vpnd/src/command_interface/listener.rs index 73bc4d05b6..ea784c60b5 100644 --- a/nym-vpnd/src/command_interface/listener.rs +++ b/nym-vpnd/src/command_interface/listener.rs @@ -9,7 +9,8 @@ use std::{ use nym_vpn_proto::{ nym_vpnd_server::NymVpnd, ConnectRequest, ConnectResponse, ConnectionStatus, DisconnectRequest, - DisconnectResponse, StatusRequest, StatusResponse, + DisconnectResponse, Error as ProtoError, ImportUserCredentialRequest, + ImportUserCredentialResponse, StatusRequest, StatusResponse, }; use tokio::sync::mpsc::UnboundedSender; use tracing::{error, info}; @@ -48,7 +49,7 @@ impl CommandInterface { } } - fn remove_previous_socket_file(&self) { + pub(super) fn remove_previous_socket_file(&self) { if let ListenerType::Path(ref socket_path) = self.listener { match fs::remove_file(socket_path) { Ok(_) => info!( @@ -117,9 +118,37 @@ impl NymVpnd for CommandInterface { .handle_status() .await; + let error = match status { + VpnServiceStatusResult::NotConnected => None, + VpnServiceStatusResult::Connecting => None, + VpnServiceStatusResult::Connected => None, + VpnServiceStatusResult::Disconnecting => None, + VpnServiceStatusResult::ConnectionFailed(ref reason) => Some(reason.clone()), + } + .map(|reason| ProtoError { message: reason }); + info!("Returning status response"); Ok(tonic::Response::new(StatusResponse { status: ConnectionStatus::from(status) as i32, + error, + })) + } + + async fn import_user_credential( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + info!("Got import credential request"); + + let credential = request.into_inner().credential; + + let status = CommandInterfaceConnectionHandler::new(self.vpn_command_tx.clone()) + .handle_import_credential(credential) + .await; + + info!("Returning import credential response"); + Ok(tonic::Response::new(ImportUserCredentialResponse { + success: status.is_success(), })) } } @@ -131,6 +160,7 @@ impl From for ConnectionStatus { VpnServiceStatusResult::Connecting => ConnectionStatus::Connecting, VpnServiceStatusResult::Connected => ConnectionStatus::Connected, VpnServiceStatusResult::Disconnecting => ConnectionStatus::Disconnecting, + VpnServiceStatusResult::ConnectionFailed(_reason) => ConnectionStatus::ConnectionFailed, } } } diff --git a/nym-vpnd/src/command_interface/start.rs b/nym-vpnd/src/command_interface/start.rs index 97ef2f82ef..215f7a98e6 100644 --- a/nym-vpnd/src/command_interface/start.rs +++ b/nym-vpnd/src/command_interface/start.rs @@ -32,6 +32,7 @@ fn spawn_socket_listener(vpn_command_tx: UnboundedSender, soc info!("Starting socket listener on: {}", socket_path.display()); tokio::task::spawn(async move { let command_interface = CommandInterface::new_with_path(vpn_command_tx, &socket_path); + command_interface.remove_previous_socket_file(); let incoming = setup_socket_stream(&socket_path); Server::builder() .add_service(NymVpndServer::new(command_interface)) diff --git a/nym-vpnd/src/service/config.rs b/nym-vpnd/src/service/config.rs index ddcd72ac46..79d217e2a3 100644 --- a/nym-vpnd/src/service/config.rs +++ b/nym-vpnd/src/service/config.rs @@ -90,3 +90,12 @@ pub(super) fn read_config_file( error, }) } + +pub(super) fn create_data_dir(data_dir: &PathBuf) -> Result<(), ConfigSetupError> { + fs::create_dir_all(data_dir).map_err(|error| ConfigSetupError::CreateDirectory { + dir: data_dir.clone(), + error, + })?; + info!("Data directory created at {:?}", data_dir); + Ok(()) +} diff --git a/nym-vpnd/src/service/exit_listener.rs b/nym-vpnd/src/service/exit_listener.rs index aba8867c53..eed28b4f58 100644 --- a/nym-vpnd/src/service/exit_listener.rs +++ b/nym-vpnd/src/service/exit_listener.rs @@ -29,7 +29,7 @@ impl VpnServiceExitListener { } nym_vpn_lib::NymVpnExitStatusMessage::Failed(err) => { error!("VPN exit: fail: {err}"); - self.set_shared_state(VpnState::NotConnected); + self.set_shared_state(VpnState::ConnectionFailed(err.to_string())); } }, Err(err) => { diff --git a/nym-vpnd/src/service/mod.rs b/nym-vpnd/src/service/mod.rs index 2f0d6cf043..84c03aada4 100644 --- a/nym-vpnd/src/service/mod.rs +++ b/nym-vpnd/src/service/mod.rs @@ -9,5 +9,6 @@ mod vpn_service; pub(crate) use start::start_vpn_service; pub(crate) use vpn_service::{ - VpnServiceCommand, VpnServiceConnectResult, VpnServiceDisconnectResult, VpnServiceStatusResult, + ConnectArgs, VpnServiceCommand, VpnServiceConnectResult, VpnServiceDisconnectResult, + VpnServiceImportUserCredentialResult, VpnServiceStatusResult, }; diff --git a/nym-vpnd/src/service/vpn_service.rs b/nym-vpnd/src/service/vpn_service.rs index 55c1bf4c9e..05f98a3417 100644 --- a/nym-vpnd/src/service/vpn_service.rs +++ b/nym-vpnd/src/service/vpn_service.rs @@ -6,13 +6,14 @@ use std::sync::Arc; use futures::channel::mpsc::UnboundedSender; use futures::SinkExt; -use nym_vpn_lib::gateway_directory; +use nym_vpn_lib::credentials::import_credential; +use nym_vpn_lib::gateway_directory::{self}; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::oneshot; use tracing::info; use super::config::{ - create_config_file, read_config_file, ConfigSetupError, NymVpnServiceConfig, + create_config_file, create_data_dir, read_config_file, ConfigSetupError, NymVpnServiceConfig, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE, DEFAULT_DATA_DIR, }; use super::exit_listener::VpnServiceExitListener; @@ -24,13 +25,28 @@ pub enum VpnState { Connecting, Connected, Disconnecting, + #[allow(unused)] + ConnectionFailed(String), } #[derive(Debug)] pub enum VpnServiceCommand { - Connect(oneshot::Sender), + Connect(oneshot::Sender, ConnectArgs), Disconnect(oneshot::Sender), Status(oneshot::Sender), + ImportCredential( + oneshot::Sender, + Vec, + ), +} + +#[derive(Debug)] +pub enum ConnectArgs { + // Read the entry and exit points from the config file. + Default, + + #[allow(unused)] + Custom(String, String), } #[derive(Debug)] @@ -59,12 +75,25 @@ impl VpnServiceDisconnectResult { } } -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub enum VpnServiceStatusResult { NotConnected, Connecting, Connected, Disconnecting, + ConnectionFailed(String), +} + +#[derive(Debug)] +pub enum VpnServiceImportUserCredentialResult { + Success, + Fail(String), +} + +impl VpnServiceImportUserCredentialResult { + pub fn is_success(&self) -> bool { + matches!(self, VpnServiceImportUserCredentialResult::Success) + } } pub(super) struct NymVpnService { @@ -72,7 +101,6 @@ pub(super) struct NymVpnService { vpn_command_rx: UnboundedReceiver, vpn_ctrl_sender: Option>, config_file: PathBuf, - #[allow(unused)] data_dir: PathBuf, } @@ -104,9 +132,11 @@ impl NymVpnService { Ok(config) } - async fn handle_connect(&mut self) -> VpnServiceConnectResult { + async fn handle_connect(&mut self, _connect_args: ConnectArgs) -> VpnServiceConnectResult { self.set_shared_state(VpnState::Connecting); + // TODO: use connect_args here + let config = match self.try_setup_config() { Ok(config) => config, Err(err) => { @@ -115,9 +145,22 @@ impl NymVpnService { } }; - let mut nym_vpn = nym_vpn_lib::NymVpn::new(config.entry_point, config.exit_point); + // Make sure the data dir exists + match create_data_dir(&self.data_dir) { + Ok(()) => {} + Err(err) => { + self.set_shared_state(VpnState::NotConnected); + return VpnServiceConnectResult::Fail(format!( + "Failed to create data directory {:?}: {}", + self.data_dir, err + )); + } + } + let mut nym_vpn = nym_vpn_lib::NymVpn::new(config.entry_point, config.exit_point); nym_vpn.gateway_config = gateway_directory::Config::new_from_env(); + nym_vpn.mixnet_data_path = Some(self.data_dir.clone()); + let handle = nym_vpn_lib::spawn_nym_vpn_with_new_runtime(nym_vpn).unwrap(); let nym_vpn_lib::NymVpnHandle { @@ -140,6 +183,7 @@ impl NymVpnService { } fn set_shared_state(&self, state: VpnState) { + info!("VPN: Setting shared state to {:?}", state); *self.shared_vpn_state.lock().unwrap() = state; } @@ -163,11 +207,30 @@ impl NymVpnService { } async fn handle_status(&self) -> VpnServiceStatusResult { - match *self.shared_vpn_state.lock().unwrap() { + match self.shared_vpn_state.lock().unwrap().clone() { VpnState::NotConnected => VpnServiceStatusResult::NotConnected, VpnState::Connecting => VpnServiceStatusResult::Connecting, VpnState::Connected => VpnServiceStatusResult::Connected, VpnState::Disconnecting => VpnServiceStatusResult::Disconnecting, + VpnState::ConnectionFailed(reason) => VpnServiceStatusResult::ConnectionFailed(reason), + } + } + + async fn handle_import_credential( + &mut self, + credential: Vec, + ) -> VpnServiceImportUserCredentialResult { + // BUG: this is not correct after a connect/disconnect cycle + let is_running = self.vpn_ctrl_sender.is_some(); + if is_running { + return VpnServiceImportUserCredentialResult::Fail( + "Can't import credential while VPN is running".to_string(), + ); + } + + match import_credential(credential, self.data_dir.clone()).await { + Ok(()) => VpnServiceImportUserCredentialResult::Success, + Err(err) => VpnServiceImportUserCredentialResult::Fail(err.to_string()), } } @@ -175,8 +238,8 @@ impl NymVpnService { while let Some(command) = self.vpn_command_rx.recv().await { info!("VPN: Received command: {:?}", command); match command { - VpnServiceCommand::Connect(tx) => { - let result = self.handle_connect().await; + VpnServiceCommand::Connect(tx, connect_args) => { + let result = self.handle_connect(connect_args).await; tx.send(result).unwrap(); } VpnServiceCommand::Disconnect(tx) => { @@ -187,6 +250,10 @@ impl NymVpnService { let result = self.handle_status().await; tx.send(result).unwrap(); } + VpnServiceCommand::ImportCredential(tx, credential) => { + let result = self.handle_import_credential(credential).await; + tx.send(result).unwrap(); + } } } Ok(()) diff --git a/proto/nym/vpn.proto b/proto/nym/vpn.proto index be34572829..557c6c1d0f 100644 --- a/proto/nym/vpn.proto +++ b/proto/nym/vpn.proto @@ -45,11 +45,12 @@ enum VpnMode { enum ConnectionStatus { STATUS_UNSPECIFIED = 0; - NOT_CONNECTED = 1; - CONNECTING = 2; - CONNECTED = 3; - DISCONNECTING = 4; - UNKNOWN = 5; // errored pending state etc + UNKNOWN = 1; + NOT_CONNECTED = 2; + CONNECTING = 3; + CONNECTED = 4; + DISCONNECTING = 5; + CONNECTION_FAILED = 6; } message LocationListResponse { @@ -71,6 +72,7 @@ message GatewayResponse { message StatusRequest {} message StatusResponse { ConnectionStatus status = 1; + Error error = 2; } message ConnectionStatusUpdate { @@ -93,11 +95,19 @@ message SetUserCredentialsRequest { string key = 1; } +message ImportUserCredentialRequest { + bytes credential = 1; +} + +message ImportUserCredentialResponse { + bool success = 1; +} + service NymVpnd { - // rpc SetUserCredentials (SetUserCredentialsRequest) returns (Empty) {} rpc VpnConnect (ConnectRequest) returns (ConnectResponse) {} rpc VpnDisconnect (DisconnectRequest) returns (DisconnectResponse) {} rpc VpnStatus (StatusRequest) returns (StatusResponse) {} + rpc ImportUserCredential (ImportUserCredentialRequest) returns (ImportUserCredentialResponse) {} // rpc ListenToConnectionStatus (Empty) returns (stream ConnectionStatusUpdate) {} // // Cancel any connection pending state (connecting, disconnecting etc) // // and return to disconnected state