diff --git a/.github/workflows/publish-nym-vpn-core.yml b/.github/workflows/publish-nym-vpn-core.yml index 298266b6f6..87ff4f3068 100644 --- a/.github/workflows/publish-nym-vpn-core.yml +++ b/.github/workflows/publish-nym-vpn-core.yml @@ -1,4 +1,4 @@ -name: publish-nym-vpn-core.yml +name: publish-nym-vpn-core on: schedule: - cron: "4 3 * * *" diff --git a/nym-vpn-core/Cargo.lock b/nym-vpn-core/Cargo.lock index 2c0968e462..462fde2c14 100644 --- a/nym-vpn-core/Cargo.lock +++ b/nym-vpn-core/Cargo.lock @@ -4820,6 +4820,7 @@ dependencies = [ "nym-vpn-proto", "parity-tokio-ipc", "prost 0.12.6", + "prost-types 0.12.6", "time", "tokio", "tonic 0.11.0", diff --git a/nym-vpn-core/nym-vpnc/Cargo.toml b/nym-vpn-core/nym-vpnc/Cargo.toml index eb87a1204b..9cc5feac5b 100644 --- a/nym-vpn-core/nym-vpnc/Cargo.toml +++ b/nym-vpn-core/nym-vpnc/Cargo.toml @@ -14,6 +14,7 @@ anyhow.workspace = true bs58.workspace = true clap = { workspace = true, features = ["derive"] } parity-tokio-ipc.workspace = true +prost-types.workspace = true prost.workspace = true time = { workspace = true, features = ["formatting"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread"]} diff --git a/nym-vpn-core/nym-vpnc/src/cli.rs b/nym-vpn-core/nym-vpnc/src/cli.rs index 47b5e8b33c..30fbda2112 100644 --- a/nym-vpn-core/nym-vpnc/src/cli.rs +++ b/nym-vpn-core/nym-vpnc/src/cli.rs @@ -22,6 +22,7 @@ pub(crate) enum Command { Connect(ConnectArgs), Disconnect, Status, + Info, ImportCredential(ImportCredentialArgs), ListenToStatus, ListenToStateChanges, diff --git a/nym-vpn-core/nym-vpnc/src/main.rs b/nym-vpn-core/nym-vpnc/src/main.rs index 0bc96354c2..144c46dead 100644 --- a/nym-vpn-core/nym-vpnc/src/main.rs +++ b/nym-vpn-core/nym-vpnc/src/main.rs @@ -4,13 +4,16 @@ use anyhow::Result; use clap::Parser; use nym_vpn_proto::{ - ConnectRequest, DisconnectRequest, Empty, ImportUserCredentialRequest, StatusRequest, + ConnectRequest, DisconnectRequest, Empty, ImportUserCredentialRequest, InfoRequest, + StatusRequest, }; use vpnd_client::ClientType; use crate::{ cli::{Command, ImportCredentialTypeEnum}, - protobuf_conversion::{into_entry_point, into_exit_point, ipaddr_into_string}, + protobuf_conversion::{ + into_entry_point, into_exit_point, ipaddr_into_string, parse_offset_datetime, + }, }; mod cli; @@ -30,6 +33,7 @@ async fn main() -> Result<()> { Command::Connect(ref connect_args) => connect(client_type, connect_args).await?, Command::Disconnect => disconnect(client_type).await?, Command::Status => status(client_type).await?, + Command::Info => info(client_type).await?, Command::ImportCredential(ref import_args) => { import_credential(client_type, import_args).await? } @@ -74,22 +78,30 @@ async fn status(client_type: ClientType) -> Result<()> { let response = client.vpn_status(request).await?.into_inner(); println!("{:?}", response); - let utc_since = response + if let Some(Ok(utc_since)) = response .details .and_then(|details| details.since) - .map(|timestamp| { - time::OffsetDateTime::from_unix_timestamp(timestamp.seconds) - .map(|t| t + time::Duration::nanoseconds(timestamp.nanos as i64)) - }); - - if let Some(utc_since) = utc_since { - match utc_since { - Ok(utc_since) => { - println!("since (utc): {:?}", utc_since); - println!("duration: {}", time::OffsetDateTime::now_utc() - utc_since); - } - Err(err) => eprintln!("failed to parse timestamp: {err}"), - } + .map(parse_offset_datetime) + { + println!("since (utc): {:?}", utc_since); + println!("duration: {}", time::OffsetDateTime::now_utc() - utc_since); + } + + Ok(()) +} + +async fn info(client_type: ClientType) -> Result<()> { + let mut client = vpnd_client::get_client(client_type).await?; + let request = tonic::Request::new(InfoRequest {}); + let response = client.info(request).await?.into_inner(); + println!("{:?}", response); + + if let Some(Ok(utc_build_timestamp)) = response.build_timestamp.map(parse_offset_datetime) { + println!("build timestamp (utc): {:?}", utc_build_timestamp); + println!( + "build age: {}", + time::OffsetDateTime::now_utc() - utc_build_timestamp + ); } Ok(()) } diff --git a/nym-vpn-core/nym-vpnc/src/protobuf_conversion.rs b/nym-vpn-core/nym-vpnc/src/protobuf_conversion.rs index 87ae6631c0..7f310e41f0 100644 --- a/nym-vpn-core/nym-vpnc/src/protobuf_conversion.rs +++ b/nym-vpn-core/nym-vpnc/src/protobuf_conversion.rs @@ -98,3 +98,11 @@ pub(crate) fn into_exit_point(exit: ExitPoint) -> nym_vpn_proto::ExitNode { pub(crate) fn ipaddr_into_string(ip: std::net::IpAddr) -> nym_vpn_proto::Dns { nym_vpn_proto::Dns { ip: ip.to_string() } } + +pub(crate) fn parse_offset_datetime( + timestamp: prost_types::Timestamp, +) -> Result { + time::OffsetDateTime::from_unix_timestamp(timestamp.seconds) + .map(|t| t + time::Duration::nanoseconds(timestamp.nanos as i64)) + .map_err(time::Error::from) +} diff --git a/nym-vpn-core/nym-vpnd/src/command_interface/connection_handler.rs b/nym-vpn-core/nym-vpnd/src/command_interface/connection_handler.rs index 94913613bf..9e96313f41 100644 --- a/nym-vpn-core/nym-vpnd/src/command_interface/connection_handler.rs +++ b/nym-vpn-core/nym-vpnd/src/command_interface/connection_handler.rs @@ -4,11 +4,11 @@ use nym_vpn_lib::gateway_directory::{EntryPoint, ExitPoint}; use time::OffsetDateTime; use tokio::sync::{mpsc::UnboundedSender, oneshot}; -use tracing::{info, warn}; +use tracing::{debug, info, warn}; use crate::service::{ ConnectArgs, ConnectOptions, ImportCredentialError, VpnServiceCommand, VpnServiceConnectResult, - VpnServiceDisconnectResult, VpnServiceStatusResult, + VpnServiceDisconnectResult, VpnServiceInfoResult, VpnServiceStatusResult, }; pub(super) struct CommandInterfaceConnectionHandler { @@ -36,8 +36,8 @@ impl CommandInterfaceConnectionHandler { self.vpn_command_tx .send(VpnServiceCommand::Connect(tx, connect_args)) .unwrap(); - info!("Sent start command to VPN"); - info!("Waiting for response"); + debug!("Sent start command to VPN"); + debug!("Waiting for response"); let result = rx.await.unwrap(); match result { VpnServiceConnectResult::Success(ref _connect_handle) => { @@ -55,12 +55,12 @@ impl CommandInterfaceConnectionHandler { self.vpn_command_tx .send(VpnServiceCommand::Disconnect(tx)) .unwrap(); - info!("Sent stop command to VPN"); - info!("Waiting for response"); + debug!("Sent stop command to VPN"); + debug!("Waiting for response"); let result = rx.await.unwrap(); match result { VpnServiceDisconnectResult::Success => { - info!("VPN disconnect command sent successfully"); + debug!("VPN disconnect command sent successfully"); } VpnServiceDisconnectResult::NotRunning => { info!("VPN can't stop - it's not running"); @@ -72,15 +72,27 @@ impl CommandInterfaceConnectionHandler { result } + pub(crate) async fn handle_info(&self) -> VpnServiceInfoResult { + let (tx, rx) = oneshot::channel(); + self.vpn_command_tx + .send(VpnServiceCommand::Info(tx)) + .unwrap(); + debug!("Sent info command to VPN"); + debug!("Waiting for response"); + let info = rx.await.unwrap(); + debug!("VPN info: {:?}", info); + info + } + pub(crate) async fn handle_status(&self) -> VpnServiceStatusResult { let (tx, rx) = oneshot::channel(); self.vpn_command_tx .send(VpnServiceCommand::Status(tx)) .unwrap(); - info!("Sent status command to VPN"); - info!("Waiting for response"); + debug!("Sent status command to VPN"); + debug!("Waiting for response"); let status = rx.await.unwrap(); - info!("VPN status: {}", status); + debug!("VPN status: {}", status); status } @@ -92,10 +104,10 @@ impl CommandInterfaceConnectionHandler { self.vpn_command_tx .send(VpnServiceCommand::ImportCredential(tx, credential)) .unwrap(); - info!("Sent import credential command to VPN"); - info!("Waiting for response"); + debug!("Sent import credential command to VPN"); + debug!("Waiting for response"); let result = rx.await.unwrap(); - info!("VPN import credential result: {:?}", result); + debug!("VPN import credential result: {:?}", result); result } } diff --git a/nym-vpn-core/nym-vpnd/src/command_interface/listener.rs b/nym-vpn-core/nym-vpnd/src/command_interface/listener.rs index cea2ab70e3..f428b08bef 100644 --- a/nym-vpn-core/nym-vpnd/src/command_interface/listener.rs +++ b/nym-vpn-core/nym-vpnd/src/command_interface/listener.rs @@ -14,6 +14,7 @@ use nym_vpn_proto::{ ConnectionStatusUpdate, DisconnectRequest, DisconnectResponse, Empty, ImportUserCredentialRequest, ImportUserCredentialResponse, StatusRequest, StatusResponse, }; +use nym_vpn_proto::{InfoRequest, InfoResponse}; use prost_types::Timestamp; use tokio::sync::{broadcast, mpsc::UnboundedSender}; use tracing::{error, info}; @@ -100,6 +101,21 @@ impl Drop for CommandInterface { #[tonic::async_trait] impl NymVpnd for CommandInterface { + async fn info( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + info!("Got info request: {:?}", request); + + let info = CommandInterfaceConnectionHandler::new(self.vpn_command_tx.clone()) + .handle_info() + .await; + + let response = InfoResponse::from(info); + info!("Returning info response: {:?}", response); + Ok(tonic::Response::new(response)) + } + async fn vpn_connect( &self, request: tonic::Request, @@ -143,8 +159,9 @@ impl NymVpnd for CommandInterface { .start(); } - info!("Returning connect response"); - Ok(tonic::Response::new(ConnectResponse { success })) + let response = ConnectResponse { success }; + info!("Returning connect response: {:?}", response); + Ok(tonic::Response::new(response)) } async fn vpn_disconnect( @@ -157,10 +174,11 @@ impl NymVpnd for CommandInterface { .handle_disconnect() .await; - info!("Returning disconnect response"); - Ok(tonic::Response::new(DisconnectResponse { + let response = DisconnectResponse { success: status.is_success(), - })) + }; + info!("Returning disconnect response: {:?}", response); + Ok(tonic::Response::new(response)) } async fn vpn_status( @@ -173,8 +191,9 @@ impl NymVpnd for CommandInterface { .handle_status() .await; - info!("Returning status response"); - Ok(tonic::Response::new(StatusResponse::from(status))) + let response = StatusResponse::from(status); + info!("Returning status response: {:?}", response); + Ok(tonic::Response::new(response)) } async fn import_user_credential( @@ -189,7 +208,6 @@ impl NymVpnd for CommandInterface { .handle_import_credential(credential) .await; - info!("Returning import credential response"); let response = match response { Ok(time) => ImportUserCredentialResponse { success: true, @@ -202,6 +220,7 @@ impl NymVpnd for CommandInterface { expiry: None, }, }; + info!("Returning import credential response: {:?}", response); Ok(tonic::Response::new(response)) } diff --git a/nym-vpn-core/nym-vpnd/src/command_interface/proto_response.rs b/nym-vpn-core/nym-vpnd/src/command_interface/proto_response.rs index 5208dcaff2..6bcd62ad04 100644 --- a/nym-vpn-core/nym-vpnd/src/command_interface/proto_response.rs +++ b/nym-vpn-core/nym-vpnd/src/command_interface/proto_response.rs @@ -1,8 +1,10 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::service::{VpnServiceStateChange, VpnServiceStatusResult}; -use nym_vpn_proto::{ConnectionStateChange, ConnectionStatus, Error as ProtoError, StatusResponse}; +use crate::service::{VpnServiceInfoResult, VpnServiceStateChange, VpnServiceStatusResult}; +use nym_vpn_proto::{ + ConnectionStateChange, ConnectionStatus, Error as ProtoError, InfoResponse, StatusResponse, +}; impl From for StatusResponse { fn from(status: VpnServiceStatusResult) -> Self { @@ -50,6 +52,21 @@ impl From for StatusResponse { } } +impl From for InfoResponse { + fn from(info: VpnServiceInfoResult) -> Self { + let build_timestamp = info.build_timestamp.map(|ts| prost_types::Timestamp { + seconds: ts.unix_timestamp(), + nanos: ts.nanosecond() as i32, + }); + InfoResponse { + version: info.version, + build_timestamp, + triple: info.triple, + git_commit: info.git_commit, + } + } +} + impl From for ConnectionStateChange { fn from(status: VpnServiceStateChange) -> Self { let mut error = None; diff --git a/nym-vpn-core/nym-vpnd/src/service/mod.rs b/nym-vpn-core/nym-vpnd/src/service/mod.rs index ead297cdcb..6c55e5a72d 100644 --- a/nym-vpn-core/nym-vpnd/src/service/mod.rs +++ b/nym-vpn-core/nym-vpnd/src/service/mod.rs @@ -13,5 +13,6 @@ pub(crate) use error::{ConnectionFailedError, ImportCredentialError}; pub(crate) use start::start_vpn_service; pub(crate) use vpn_service::{ ConnectArgs, ConnectOptions, VpnServiceCommand, VpnServiceConnectResult, - VpnServiceDisconnectResult, VpnServiceStateChange, VpnServiceStatusResult, + VpnServiceDisconnectResult, VpnServiceInfoResult, VpnServiceStateChange, + VpnServiceStatusResult, }; diff --git a/nym-vpn-core/nym-vpnd/src/service/vpn_service.rs b/nym-vpn-core/nym-vpnd/src/service/vpn_service.rs index ccda45555e..282b326e3b 100644 --- a/nym-vpn-core/nym-vpnd/src/service/vpn_service.rs +++ b/nym-vpn-core/nym-vpnd/src/service/vpn_service.rs @@ -13,10 +13,11 @@ use nym_vpn_lib::gateway_directory::{self, EntryPoint, ExitPoint}; use nym_vpn_lib::nym_bin_common::bin_info; use nym_vpn_lib::{NodeIdentity, Recipient}; use serde::{Deserialize, Serialize}; +use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::{broadcast, oneshot}; -use tracing::{error, info}; +use tracing::{debug, error, info}; use super::config::{ self, create_config_file, create_data_dir, create_device_keys, read_config_file, @@ -80,6 +81,7 @@ pub enum VpnServiceCommand { Connect(oneshot::Sender, ConnectArgs), Disconnect(oneshot::Sender), Status(oneshot::Sender), + Info(oneshot::Sender), ImportCredential( oneshot::Sender, ImportCredentialError>>, Vec, @@ -92,6 +94,7 @@ impl fmt::Display for VpnServiceCommand { VpnServiceCommand::Connect(_, args) => write!(f, "Connect {{ {args:?} }}"), VpnServiceCommand::Disconnect(_) => write!(f, "Disconnect"), VpnServiceCommand::Status(_) => write!(f, "Status"), + VpnServiceCommand::Info(_) => write!(f, "Info"), VpnServiceCommand::ImportCredential(_, _) => write!(f, "ImportCredential"), } } @@ -158,6 +161,14 @@ pub enum VpnServiceStatusResult { ConnectionFailed(ConnectionFailedError), } +#[derive(Clone, Debug)] +pub struct VpnServiceInfoResult { + pub version: String, + pub build_timestamp: Option, + pub triple: String, + pub git_commit: String, +} + impl fmt::Display for VpnServiceStatusResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -481,6 +492,16 @@ impl NymVpnService { self.shared_vpn_state.get().into() } + async fn handle_info(&self) -> VpnServiceInfoResult { + let bin_info = nym_vpn_lib::nym_bin_common::bin_info_local_vergen!(); + VpnServiceInfoResult { + version: bin_info.build_version.to_string(), + build_timestamp: time::OffsetDateTime::parse(bin_info.build_timestamp, &Rfc3339).ok(), + triple: bin_info.cargo_triple.to_string(), + git_commit: bin_info.commit_sha.to_string(), + } + } + async fn handle_import_credential( &mut self, credential: Vec, @@ -496,7 +517,7 @@ impl NymVpnService { pub(crate) async fn run(mut self) -> anyhow::Result<()> { while let Some(command) = self.vpn_command_rx.recv().await { - info!("VPN: Received command: {command}"); + debug!("VPN: Received command: {command}"); match command { VpnServiceCommand::Connect(tx, connect_args) => { let result = self.handle_connect(connect_args).await; @@ -510,6 +531,10 @@ impl NymVpnService { let result = self.handle_status().await; tx.send(result).unwrap(); } + VpnServiceCommand::Info(tx) => { + let result = self.handle_info().await; + tx.send(result).unwrap(); + } VpnServiceCommand::ImportCredential(tx, credential) => { let result = self.handle_import_credential(credential).await; tx.send(result).unwrap(); diff --git a/proto/nym/vpn.proto b/proto/nym/vpn.proto index d4d452d124..ba8b8e133d 100644 --- a/proto/nym/vpn.proto +++ b/proto/nym/vpn.proto @@ -40,6 +40,15 @@ message Dns { string ip = 1; } +message InfoRequest {} + +message InfoResponse { + string version = 1; + google.protobuf.Timestamp build_timestamp = 2; + string triple = 3; + string git_commit = 4; +} + message ConnectRequest { EntryNode entry = 1; ExitNode exit = 2; @@ -227,6 +236,7 @@ message ImportError { } service NymVpnd { + rpc Info (InfoRequest) returns (InfoResponse) {} rpc VpnConnect (ConnectRequest) returns (ConnectResponse) {} rpc VpnDisconnect (DisconnectRequest) returns (DisconnectResponse) {} rpc VpnStatus (StatusRequest) returns (StatusResponse) {}