diff --git a/Cargo.lock b/Cargo.lock index c076c822d5..d9a33b2693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4704,6 +4704,7 @@ dependencies = [ "anyhow", "bs58 0.5.1", "clap", + "nym-gateway-directory", "nym-vpn-proto", "parity-tokio-ipc", "prost 0.12.4", diff --git a/crates/nym-gateway-directory/src/entries/entry_point.rs b/crates/nym-gateway-directory/src/entries/entry_point.rs index f29dbf7cda..3bee19dc8a 100644 --- a/crates/nym-gateway-directory/src/entries/entry_point.rs +++ b/crates/nym-gateway-directory/src/entries/entry_point.rs @@ -19,7 +19,6 @@ pub enum EntryPoint { // An explicit entry gateway identity. Gateway { identity: NodeIdentity }, // Select a random entry gateway in a specific location. - // NOTE: Consider using a crate with strongly typed country codes instead of strings Location { location: String }, // Select a random entry gateway but increasey probability of selecting a low latency gateway // as determined by ping times. diff --git a/nym-vpn-cli/src/commands.rs b/nym-vpn-cli/src/commands.rs index 8a90a6ca22..f3cd6c373f 100644 --- a/nym-vpn-cli/src/commands.rs +++ b/nym-vpn-cli/src/commands.rs @@ -129,10 +129,11 @@ pub(crate) struct CliExit { #[clap(long, alias = "exit-address")] pub(crate) exit_router_address: Option, + /// Mixnet public ID of the exit gateway. #[clap(long, alias = "exit-id")] pub(crate) exit_gateway_id: Option, - /// Mixnet recipient address. + /// Auto-select exit gateway by country ISO. #[clap(long, alias = "exit-country")] pub(crate) exit_gateway_country: Option, } diff --git a/nym-vpnc/Cargo.toml b/nym-vpnc/Cargo.toml index ec332c597f..e64d4e2e8c 100644 --- a/nym-vpnc/Cargo.toml +++ b/nym-vpnc/Cargo.toml @@ -20,6 +20,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"]} tonic.workspace = true tower.workspace = true +nym-gateway-directory = { path = "../crates/nym-gateway-directory" } nym-vpn-proto = { path = "../crates/nym-vpn-proto" } [build-dependencies] diff --git a/nym-vpnc/src/cli.rs b/nym-vpnc/src/cli.rs new file mode 100644 index 0000000000..788a1727c3 --- /dev/null +++ b/nym-vpnc/src/cli.rs @@ -0,0 +1,177 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{anyhow, Result}; +use clap::{Args, Parser, Subcommand}; +use nym_gateway_directory::{EntryPoint, ExitPoint, NodeIdentity, Recipient}; +use std::path::PathBuf; + +#[derive(Parser)] +#[clap(author = "Nymtech", version, about)] +pub(crate) struct CliArgs { + /// Use HTTP instead of socket file for IPC with the daemon. + #[arg(long)] + pub(crate) http: bool, + + #[command(subcommand)] + pub(crate) command: Command, +} + +#[derive(Subcommand)] +pub(crate) enum Command { + Connect(ConnectArgs), + Disconnect, + Status, + ImportCredential(ImportCredentialArgs), +} + +#[derive(Args)] +pub(crate) struct ConnectArgs { + #[command(flatten)] + pub(crate) entry: CliEntry, + + #[command(flatten)] + pub(crate) exit: CliExit, + + /// Disable routing all traffic through the nym TUN device. When the flag is set, the nym TUN + /// device will be created, but to route traffic through it you will need to do it manually, + /// e.g. ping -Itun0. + #[arg(long)] + pub(crate) disable_routing: bool, + + /// Enable two-hop mixnet traffic. This means that traffic jumps directly from entry gateway to + /// exit gateway. + #[arg(long)] + pub(crate) enable_two_hop: bool, + + /// Enable Poisson process rate limiting of outbound traffic. + #[arg(long)] + pub(crate) enable_poisson_rate: bool, + + /// Disable constant rate background loop cover traffic. + #[arg(long)] + pub(crate) disable_background_cover_traffic: bool, + + /// Enable credentials mode. + #[arg(long)] + pub(crate) enable_credentials_mode: bool, +} + +#[derive(Args)] +#[group(multiple = false)] +pub(crate) struct CliEntry { + /// Mixnet public ID of the entry gateway. + #[clap(long, alias = "entry-id")] + pub(crate) entry_gateway_id: Option, + + /// Auto-select entry gateway by country ISO. + #[clap(long, alias = "entry-country")] + pub(crate) entry_gateway_country: Option, + + /// Auto-select entry gateway by latency + #[clap(long, alias = "entry-fastest")] + pub(crate) entry_gateway_low_latency: bool, + + /// Auto-select entry gateway randomly. + #[clap(long, alias = "entry-random")] + pub(crate) entry_gateway_random: bool, +} + +#[derive(Args)] +#[group(multiple = false)] +pub(crate) struct CliExit { + /// Mixnet recipient address. + #[clap(long, alias = "exit-address")] + pub(crate) exit_router_address: Option, + + /// Mixnet public ID of the exit gateway. + #[clap(long, alias = "exit-id")] + pub(crate) exit_gateway_id: Option, + + /// Auto-select exit gateway by country ISO. + #[clap(long, alias = "exit-country")] + pub(crate) exit_gateway_country: Option, + + /// Auto-select exit gateway randomly. + #[clap(long, alias = "exit-random")] + pub(crate) exit_gateway_random: bool, +} + +#[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, +} + +// 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!(), + } + } +} + +pub(crate) fn parse_entry_point(args: &ConnectArgs) -> Result> { + if let Some(ref entry_gateway_id) = args.entry.entry_gateway_id { + Ok(Some(EntryPoint::Gateway { + identity: NodeIdentity::from_base58_string(entry_gateway_id.clone()) + .map_err(|_| anyhow!("Failed to parse gateway id"))?, + })) + } else if let Some(ref entry_gateway_country) = args.entry.entry_gateway_country { + Ok(Some(EntryPoint::Location { + location: entry_gateway_country.clone(), + })) + } else if args.entry.entry_gateway_low_latency { + Ok(Some(EntryPoint::RandomLowLatency)) + } else if args.entry.entry_gateway_random { + Ok(Some(EntryPoint::Random)) + } else { + Ok(None) + } +} + +pub(crate) fn parse_exit_point(args: &ConnectArgs) -> Result> { + if let Some(ref exit_router_address) = args.exit.exit_router_address { + Ok(Some(ExitPoint::Address { + address: Recipient::try_from_base58_string(exit_router_address.clone()) + .map_err(|_| anyhow!("Failed to parse exit node address"))?, + })) + } else if let Some(ref exit_router_id) = args.exit.exit_gateway_id { + Ok(Some(ExitPoint::Gateway { + identity: NodeIdentity::from_base58_string(exit_router_id.clone()) + .map_err(|_| anyhow!("Failed to parse gateway id"))?, + })) + } else if let Some(ref exit_gateway_country) = args.exit.exit_gateway_country { + Ok(Some(ExitPoint::Location { + location: exit_gateway_country.clone(), + })) + } else if args.exit.exit_gateway_random { + Ok(Some(ExitPoint::Random)) + } else { + Ok(None) + } +} diff --git a/nym-vpnc/src/config.rs b/nym-vpnc/src/config.rs new file mode 100644 index 0000000000..9b9e5ed4eb --- /dev/null +++ b/nym-vpnc/src/config.rs @@ -0,0 +1,12 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::path::{Path, PathBuf}; + +pub(crate) fn get_socket_path() -> PathBuf { + Path::new("/var/run/nym-vpn.sock").to_path_buf() +} + +pub(crate) fn default_endpoint() -> String { + "http://[::1]:53181".to_string() +} diff --git a/nym-vpnc/src/main.rs b/nym-vpnc/src/main.rs index f436d688e8..248399fc41 100644 --- a/nym-vpnc/src/main.rs +++ b/nym-vpnc/src/main.rs @@ -1,142 +1,67 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::path::{Path, PathBuf}; - -use anyhow::Context; -use clap::{Args, Parser, Subcommand}; +use anyhow::Result; +use clap::Parser; use nym_vpn_proto::{ - nym_vpnd_client::NymVpndClient, ConnectRequest, DisconnectRequest, ImportUserCredentialRequest, - StatusRequest, + ConnectRequest, DisconnectRequest, ImportUserCredentialRequest, StatusRequest, }; -use parity_tokio_ipc::Endpoint as IpcEndpoint; -use tonic::transport::{Channel as TonicChannel, Endpoint as TonicEndpoint}; - -#[derive(Parser)] -#[clap(author = "Nymtech", version, about)] -struct CliArgs { - /// Use HTTP instead of socket file for IPC with the daemon. - #[arg(long)] - http: bool, - - #[command(subcommand)] - command: Command, -} - -#[derive(Subcommand)] -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, +use vpnd_client::ClientType; - /// 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() -} +use crate::{ + cli::{Command, ImportCredentialTypeEnum}, + protobuf_conversion::{into_entry_point, into_exit_point}, +}; -// 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!(), - } - } -} +mod cli; +mod config; +mod protobuf_conversion; +mod vpnd_client; #[tokio::main] -async fn main() -> anyhow::Result<()> { - let args = CliArgs::parse(); +async fn main() -> Result<()> { + let args = cli::CliArgs::parse(); + let client_type = if args.http { + vpnd_client::ClientType::Http + } else { + vpnd_client::ClientType::Ipc + }; match args.command { - 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?, + Command::Connect(ref connect_args) => connect(client_type, connect_args).await?, + Command::Disconnect => disconnect(client_type).await?, + Command::Status => status(client_type).await?, + Command::ImportCredential(ref import_args) => { + import_credential(client_type, import_args).await? + } } Ok(()) } -fn get_socket_path() -> PathBuf { - Path::new("/var/run/nym-vpn.sock").to_path_buf() -} - -async fn get_channel(socket_path: PathBuf) -> anyhow::Result { - // NOTE: the uri here is ignored - Ok(TonicEndpoint::from_static("http://[::1]:53181") - .connect_with_connector(tower::service_fn(move |_| { - IpcEndpoint::connect(socket_path.clone()) - })) - .await?) -} - -fn default_endpoint() -> String { - "http://[::1]:53181".to_string() -} +async fn connect(client_type: ClientType, connect_args: &cli::ConnectArgs) -> Result<()> { + let entry = cli::parse_entry_point(connect_args)?; + let exit = cli::parse_exit_point(connect_args)?; -async fn get_client(args: &CliArgs) -> anyhow::Result> { - if args.http { - let endpoint = default_endpoint(); - let client = NymVpndClient::connect(endpoint.clone()) - .await - .with_context(|| format!("Failed to connect to: {}", endpoint))?; - Ok(client) - } else { - let socket_path = get_socket_path(); - let channel = get_channel(socket_path.clone()) - .await - .with_context(|| format!("Failed to connect to: {:?}", socket_path))?; - let client = NymVpndClient::new(channel); - Ok(client) - } -} + let request = tonic::Request::new(ConnectRequest { + entry: entry.map(into_entry_point), + exit: exit.map(into_exit_point), + }); -async fn connect(args: &CliArgs) -> anyhow::Result<()> { - let mut client = get_client(args).await?; - let request = tonic::Request::new(ConnectRequest {}); + let mut client = vpnd_client::get_client(client_type).await?; let response = client.vpn_connect(request).await?.into_inner(); println!("{:?}", response); Ok(()) } -async fn disconnect(args: &CliArgs) -> anyhow::Result<()> { - let mut client = get_client(args).await?; +async fn disconnect(client_type: ClientType) -> Result<()> { + let mut client = vpnd_client::get_client(client_type).await?; let request = tonic::Request::new(DisconnectRequest {}); let response = client.vpn_disconnect(request).await?.into_inner(); println!("{:?}", response); Ok(()) } -async fn status(args: &CliArgs) -> anyhow::Result<()> { - let mut client = get_client(args).await?; +async fn status(client_type: ClientType) -> Result<()> { + let mut client = vpnd_client::get_client(client_type).await?; let request = tonic::Request::new(StatusRequest {}); let response = client.vpn_status(request).await?.into_inner(); println!("{:?}", response); @@ -144,9 +69,9 @@ async fn status(args: &CliArgs) -> anyhow::Result<()> { } async fn import_credential( - args: &CliArgs, - import_args: &ImportCredentialArgs, -) -> anyhow::Result<()> { + client_type: ClientType, + import_args: &cli::ImportCredentialArgs, +) -> Result<()> { let import_type: ImportCredentialTypeEnum = import_args.credential_type.clone().into(); let raw_credential = match import_type { ImportCredentialTypeEnum::Path(path) => std::fs::read(path)?, @@ -155,8 +80,12 @@ async fn import_credential( let request = tonic::Request::new(ImportUserCredentialRequest { credential: raw_credential, }); - let mut client = get_client(args).await?; + let mut client = vpnd_client::get_client(client_type).await?; let response = client.import_user_credential(request).await?.into_inner(); println!("{:?}", response); Ok(()) } + +fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { + bs58::decode(raw).into_vec() +} diff --git a/nym-vpnc/src/protobuf_conversion.rs b/nym-vpnc/src/protobuf_conversion.rs new file mode 100644 index 0000000000..2e53626eda --- /dev/null +++ b/nym-vpnc/src/protobuf_conversion.rs @@ -0,0 +1,96 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_gateway_directory::{EntryPoint, ExitPoint, NodeIdentity, Recipient}; + +fn new_entry_node_gateway(identity: &NodeIdentity) -> nym_vpn_proto::EntryNode { + nym_vpn_proto::EntryNode { + entry_node_enum: Some(nym_vpn_proto::entry_node::EntryNodeEnum::Gateway( + nym_vpn_proto::Gateway { + id: identity.to_base58_string(), + }, + )), + } +} + +fn new_entry_node_location(country_code: &str) -> nym_vpn_proto::EntryNode { + nym_vpn_proto::EntryNode { + entry_node_enum: Some(nym_vpn_proto::entry_node::EntryNodeEnum::Location( + nym_vpn_proto::Location { + two_letter_iso_country_code: country_code.to_string(), + }, + )), + } +} + +fn new_entry_node_random_low_latency() -> nym_vpn_proto::EntryNode { + nym_vpn_proto::EntryNode { + entry_node_enum: Some(nym_vpn_proto::entry_node::EntryNodeEnum::RandomLowLatency( + nym_vpn_proto::Empty {}, + )), + } +} + +fn new_entry_node_random() -> nym_vpn_proto::EntryNode { + nym_vpn_proto::EntryNode { + entry_node_enum: Some(nym_vpn_proto::entry_node::EntryNodeEnum::Random( + nym_vpn_proto::Empty {}, + )), + } +} + +pub(crate) fn into_entry_point(entry: EntryPoint) -> nym_vpn_proto::EntryNode { + match entry { + EntryPoint::Gateway { identity } => new_entry_node_gateway(&identity), + EntryPoint::Location { location } => new_entry_node_location(&location), + EntryPoint::RandomLowLatency => new_entry_node_random_low_latency(), + EntryPoint::Random => new_entry_node_random(), + } +} + +fn new_exit_node_address(address: &Recipient) -> nym_vpn_proto::ExitNode { + nym_vpn_proto::ExitNode { + exit_node_enum: Some(nym_vpn_proto::exit_node::ExitNodeEnum::Address( + nym_vpn_proto::Address { + nym_address: address.to_string(), + }, + )), + } +} + +fn new_exit_node_gateway(identity: &NodeIdentity) -> nym_vpn_proto::ExitNode { + nym_vpn_proto::ExitNode { + exit_node_enum: Some(nym_vpn_proto::exit_node::ExitNodeEnum::Gateway( + nym_vpn_proto::Gateway { + id: identity.to_base58_string(), + }, + )), + } +} + +fn new_exit_node_location(country_code: &str) -> nym_vpn_proto::ExitNode { + nym_vpn_proto::ExitNode { + exit_node_enum: Some(nym_vpn_proto::exit_node::ExitNodeEnum::Location( + nym_vpn_proto::Location { + two_letter_iso_country_code: country_code.to_string(), + }, + )), + } +} + +fn new_exit_node_random() -> nym_vpn_proto::ExitNode { + nym_vpn_proto::ExitNode { + exit_node_enum: Some(nym_vpn_proto::exit_node::ExitNodeEnum::Random( + nym_vpn_proto::Empty {}, + )), + } +} + +pub(crate) fn into_exit_point(exit: ExitPoint) -> nym_vpn_proto::ExitNode { + match exit { + ExitPoint::Address { address } => new_exit_node_address(&address), + ExitPoint::Gateway { identity } => new_exit_node_gateway(&identity), + ExitPoint::Location { location } => new_exit_node_location(&location), + ExitPoint::Random => new_exit_node_random(), + } +} diff --git a/nym-vpnc/src/vpnd_client.rs b/nym-vpnc/src/vpnd_client.rs new file mode 100644 index 0000000000..a360e22e3f --- /dev/null +++ b/nym-vpnc/src/vpnd_client.rs @@ -0,0 +1,51 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::path::PathBuf; + +use anyhow::Context; +use nym_vpn_proto::nym_vpnd_client::NymVpndClient; +use parity_tokio_ipc::Endpoint as IpcEndpoint; +use tonic::transport::{Channel as TonicChannel, Endpoint as TonicEndpoint}; + +use crate::config; + +pub(crate) enum ClientType { + Http, + Ipc, +} + +pub(crate) async fn get_client( + client_type: ClientType, +) -> anyhow::Result> { + match client_type { + ClientType::Http => get_http_client().await, + ClientType::Ipc => get_ipc_client().await, + } +} + +async fn get_channel(socket_path: PathBuf) -> anyhow::Result { + // NOTE: the uri here is ignored + Ok(TonicEndpoint::from_static("http://[::1]:53181") + .connect_with_connector(tower::service_fn(move |_| { + IpcEndpoint::connect(socket_path.clone()) + })) + .await?) +} + +async fn get_http_client() -> anyhow::Result> { + let endpoint = config::default_endpoint(); + let client = NymVpndClient::connect(endpoint.clone()) + .await + .with_context(|| format!("Failed to connect to: {}", endpoint))?; + Ok(client) +} + +async fn get_ipc_client() -> anyhow::Result> { + let socket_path = config::get_socket_path(); + let channel = get_channel(socket_path.clone()) + .await + .with_context(|| format!("Failed to connect to: {:?}", socket_path))?; + let client = NymVpndClient::new(channel); + Ok(client) +} diff --git a/nym-vpnd/src/command_interface/connection_handler.rs b/nym-vpnd/src/command_interface/connection_handler.rs index 0d3b1443f2..c4bf3e38cf 100644 --- a/nym-vpnd/src/command_interface/connection_handler.rs +++ b/nym-vpnd/src/command_interface/connection_handler.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_vpn_lib::gateway_directory::{EntryPoint, ExitPoint}; use tokio::sync::{mpsc::UnboundedSender, oneshot}; use tracing::{info, warn}; @@ -18,10 +19,14 @@ impl CommandInterfaceConnectionHandler { Self { vpn_command_tx } } - pub(crate) async fn handle_connect(&self) -> VpnServiceConnectResult { + pub(crate) async fn handle_connect( + &self, + entry: Option, + exit: Option, + ) -> VpnServiceConnectResult { info!("Starting VPN"); let (tx, rx) = oneshot::channel(); - let connect_args = ConnectArgs::Default; + let connect_args = ConnectArgs { entry, exit }; self.vpn_command_tx .send(VpnServiceCommand::Connect(tx, connect_args)) .unwrap(); diff --git a/nym-vpnd/src/command_interface/listener.rs b/nym-vpnd/src/command_interface/listener.rs index ea784c60b5..7b1c0775ec 100644 --- a/nym-vpnd/src/command_interface/listener.rs +++ b/nym-vpnd/src/command_interface/listener.rs @@ -7,6 +7,10 @@ use std::{ path::{Path, PathBuf}, }; +use nym_vpn_lib::{ + gateway_directory::{EntryPoint, ExitPoint}, + NodeIdentity, Recipient, +}; use nym_vpn_proto::{ nym_vpnd_server::NymVpnd, ConnectRequest, ConnectResponse, ConnectionStatus, DisconnectRequest, DisconnectResponse, Error as ProtoError, ImportUserCredentialRequest, @@ -82,8 +86,92 @@ impl NymVpnd for CommandInterface { ) -> Result, tonic::Status> { info!("Got connect request: {:?}", request); + let connect_request = request.into_inner(); + + let entry = if let Some(entry) = connect_request.entry { + if let Some(entry_node_enum) = entry.entry_node_enum { + match entry_node_enum { + nym_vpn_proto::entry_node::EntryNodeEnum::Location(location) => { + info!( + "Connecting to entry node in country: {:?}", + location.two_letter_iso_country_code + ); + Some(EntryPoint::Location { + location: location.two_letter_iso_country_code.to_string(), + }) + } + nym_vpn_proto::entry_node::EntryNodeEnum::Gateway(gateway) => { + info!("Connecting to entry node with gateway id: {:?}", gateway.id); + let identity = + NodeIdentity::from_base58_string(&gateway.id).map_err(|err| { + error!("Failed to parse gateway id: {:?}", err); + tonic::Status::invalid_argument("Invalid gateway id") + })?; + Some(EntryPoint::Gateway { identity }) + } + nym_vpn_proto::entry_node::EntryNodeEnum::RandomLowLatency(_) => { + info!("Connecting to low latency entry node"); + Some(EntryPoint::RandomLowLatency) + } + nym_vpn_proto::entry_node::EntryNodeEnum::Random(_) => { + info!("Connecting to random entry node"); + Some(EntryPoint::Random) + } + } + } else { + None + } + } else { + None + }; + + let exit = if let Some(exit) = connect_request.exit { + if let Some(exit_node_enum) = exit.exit_node_enum { + match exit_node_enum { + nym_vpn_proto::exit_node::ExitNodeEnum::Address(address) => { + info!( + "Connecting to exit node at address: {:?}", + address.nym_address + ); + let address = Recipient::try_from_base58_string(address.nym_address) + .map_err(|err| { + error!("Failed to parse exit node address: {:?}", err); + tonic::Status::invalid_argument("Invalid exit node address") + })?; + Some(ExitPoint::Address { address }) + } + nym_vpn_proto::exit_node::ExitNodeEnum::Gateway(gateway) => { + info!("Connecting to exit node with gateway id: {:?}", gateway.id); + let identity = + NodeIdentity::from_base58_string(&gateway.id).map_err(|err| { + error!("Failed to parse gateway id: {:?}", err); + tonic::Status::invalid_argument("Invalid gateway id") + })?; + Some(ExitPoint::Gateway { identity }) + } + nym_vpn_proto::exit_node::ExitNodeEnum::Location(location) => { + info!( + "Connecting to exit node in country: {:?}", + location.two_letter_iso_country_code + ); + Some(ExitPoint::Location { + location: location.two_letter_iso_country_code.to_string(), + }) + } + nym_vpn_proto::exit_node::ExitNodeEnum::Random(_) => { + info!("Connecting to low latency exit node"); + Some(ExitPoint::Random) + } + } + } else { + None + } + } else { + None + }; + let status = CommandInterfaceConnectionHandler::new(self.vpn_command_tx.clone()) - .handle_connect() + .handle_connect(entry, exit) .await; info!("Returning connect response"); diff --git a/nym-vpnd/src/service/config.rs b/nym-vpnd/src/service/config.rs index 79d217e2a3..27c15f8a29 100644 --- a/nym-vpnd/src/service/config.rs +++ b/nym-vpnd/src/service/config.rs @@ -91,6 +91,19 @@ pub(super) fn read_config_file( }) } +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(), + error, + })?; + info!("Config file updated at {:?}", config_file); + Ok(()) +} + 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(), diff --git a/nym-vpnd/src/service/vpn_service.rs b/nym-vpnd/src/service/vpn_service.rs index 9addc137c7..702094f103 100644 --- a/nym-vpnd/src/service/vpn_service.rs +++ b/nym-vpnd/src/service/vpn_service.rs @@ -7,14 +7,14 @@ use std::sync::Arc; use futures::channel::mpsc::UnboundedSender; use futures::SinkExt; use nym_vpn_lib::credentials::import_credential; -use nym_vpn_lib::gateway_directory::{self}; +use nym_vpn_lib::gateway_directory::{self, EntryPoint}; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::oneshot; use tracing::info; use super::config::{ - create_config_file, create_data_dir, read_config_file, ConfigSetupError, NymVpnServiceConfig, - DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE, DEFAULT_DATA_DIR, + create_config_file, create_data_dir, read_config_file, write_config_file, ConfigSetupError, + NymVpnServiceConfig, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE, DEFAULT_DATA_DIR, }; use super::exit_listener::VpnServiceExitListener; use super::status_listener::VpnServiceStatusListener; @@ -29,6 +29,7 @@ pub enum VpnState { ConnectionFailed(String), } +#[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum VpnServiceCommand { Connect(oneshot::Sender, ConnectArgs), @@ -41,12 +42,9 @@ pub enum VpnServiceCommand { } #[derive(Debug)] -pub enum ConnectArgs { - // Read the entry and exit points from the config file. - Default, - - #[allow(unused)] - Custom(String, String), +pub struct ConnectArgs { + pub entry: Option, + pub exit: Option, } #[derive(Debug)] @@ -122,22 +120,36 @@ impl NymVpnService { } } - fn try_setup_config(&self) -> std::result::Result { + fn try_setup_config( + &self, + entry: Option, + exit: Option, + ) -> std::result::Result { // If the config file does not exit, create it let config = if self.config_file.exists() { - read_config_file(&self.config_file)? + let mut read_config = read_config_file(&self.config_file)?; + 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)?; + read_config } else { - create_config_file(&self.config_file, NymVpnServiceConfig::default())? + let config = NymVpnServiceConfig { + entry_point: entry.unwrap_or(EntryPoint::Random), + ..Default::default() + }; + create_config_file(&self.config_file, config)? }; Ok(config) } - async fn handle_connect(&mut self, _connect_args: ConnectArgs) -> VpnServiceConnectResult { + async fn handle_connect(&mut self, connect_args: ConnectArgs) -> VpnServiceConnectResult { self.set_shared_state(VpnState::Connecting); - // TODO: use connect_args here + let ConnectArgs { entry, exit } = connect_args; + info!("Using entry point: {:?}", entry); + info!("Using exit point: {:?}", exit); - let config = match self.try_setup_config() { + let config = match self.try_setup_config(entry, exit) { Ok(config) => config, Err(err) => { self.set_shared_state(VpnState::NotConnected); diff --git a/proto/nym/vpn.proto b/proto/nym/vpn.proto index 6bbdefc92e..9249cfc4c1 100644 --- a/proto/nym/vpn.proto +++ b/proto/nym/vpn.proto @@ -2,28 +2,43 @@ syntax = "proto3"; package nym.vpn; -// message Empty {} +message Empty {} -// message Gateway { -// string id = 1; -// Location location = 2; -// } -// -// message Location { -// string two_letter_country_code = 1; -// } -// -// message Node { -// oneof node { -// Location location = 1; -// Gateway gateway = 2; -// Empty fastest = 3; -// } -// } +// Represents the identity of a gateway +message Gateway { + string id = 1; +} + +// Represents a nym-address of the form id.enc@gateway +message Address { + string nym_address = 1; +} + +message Location { + string two_letter_iso_country_code = 1; +} + +message EntryNode { + oneof entry_node_enum { + Gateway gateway = 1; + Location location = 2; + Empty random_low_latency = 3; + Empty random = 4; + } +} + +message ExitNode { + oneof exit_node_enum { + Address address = 1; + Gateway gateway = 2; + Location location = 3; + Empty random = 4; + } +} message ConnectRequest { - // Node entry = 1; - // Node exit = 2; + EntryNode entry = 1; + ExitNode exit = 2; } message ConnectResponse {