nym-connect: add connection health test (#2883)
* nym-connect: add connect health test * nym-connect: redo connection test * nym-connect: strongly typed response * Fix clippy * nym-connect: also send event on connection check success * nym-connect: tidy
This commit is contained in:
@@ -15,12 +15,12 @@ pub enum BackendError {
|
||||
#[from]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("String formatting error: {source}")]
|
||||
#[error("string formatting error: {source}")]
|
||||
FmtError {
|
||||
#[from]
|
||||
source: std::fmt::Error,
|
||||
},
|
||||
#[error("Tauri error: {source}")]
|
||||
#[error("tauri error: {source}")]
|
||||
TauriError {
|
||||
#[from]
|
||||
source: tauri::Error,
|
||||
@@ -46,30 +46,36 @@ pub enum BackendError {
|
||||
source: crate::operations::growth::api_client::ApiClientError,
|
||||
},
|
||||
|
||||
#[error("Could not send disconnect signal to the SOCKS5 client")]
|
||||
#[error("could not send disconnect signal to the SOCKS5 client")]
|
||||
CoundNotSendDisconnectSignal,
|
||||
#[error("No service provider set")]
|
||||
#[error("no service provider set")]
|
||||
NoServiceProviderSet,
|
||||
#[error("No gateway provider set")]
|
||||
#[error("no gateway provider set")]
|
||||
NoGatewaySet,
|
||||
#[error("Initialization failed with a panic")]
|
||||
#[error("initialization failed with a panic")]
|
||||
InitializationPanic,
|
||||
#[error("Could not get config id before gateway is set")]
|
||||
#[error("could not get config id before gateway is set")]
|
||||
CouldNotGetIdWithoutGateway,
|
||||
#[error("Could initialize without gateway set")]
|
||||
#[error("could initialize without gateway set")]
|
||||
CouldNotInitWithoutGateway,
|
||||
#[error("Could initialize without service provider set")]
|
||||
#[error("could initialize without service provider set")]
|
||||
CouldNotInitWithoutServiceProvider,
|
||||
#[error("Could not get file name")]
|
||||
#[error("could not get file name")]
|
||||
CouldNotGetFilename,
|
||||
#[error("Could not get config file location")]
|
||||
#[error("could not get config file location")]
|
||||
CouldNotGetConfigFilename,
|
||||
#[error("Could not load existing gateway configuration")]
|
||||
#[error("could not load existing gateway configuration")]
|
||||
CouldNotLoadExistingGatewayConfiguration(std::io::Error),
|
||||
#[error("Unable to open a new window")]
|
||||
#[error("unable to open a new window")]
|
||||
NewWindowError,
|
||||
#[error("Unable to parse the specified gateway")]
|
||||
#[error("unable to parse the specified gateway")]
|
||||
UnableToParseGateway,
|
||||
|
||||
#[error("HTTP get request failed: {status_code}")]
|
||||
RequestFail {
|
||||
url: reqwest::Url,
|
||||
status_code: reqwest::StatusCode,
|
||||
},
|
||||
}
|
||||
|
||||
impl Serialize for BackendError {
|
||||
|
||||
@@ -41,6 +41,7 @@ fn main() {
|
||||
crate::config::get_config_file_location,
|
||||
crate::config::get_config_id,
|
||||
crate::operations::connection::status::get_connection_status,
|
||||
crate::operations::connection::status::run_health_check,
|
||||
crate::operations::connection::connect::get_gateway,
|
||||
crate::operations::connection::connect::get_service_provider,
|
||||
crate::operations::connection::connect::set_gateway,
|
||||
|
||||
@@ -11,6 +11,7 @@ pub async fn start_connecting(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
window: tauri::Window<tauri::Wry>,
|
||||
) -> Result<ConnectResult> {
|
||||
log::trace!("Start connecting");
|
||||
let (msg_receiver, exit_status_receiver) = {
|
||||
let mut state_w = state.write().await;
|
||||
state_w.start_connecting(&window).await?
|
||||
@@ -18,8 +19,8 @@ pub async fn start_connecting(
|
||||
|
||||
// Setup task for checking status
|
||||
let state = state.inner().clone();
|
||||
tasks::start_disconnect_listener(state, window.clone(), exit_status_receiver);
|
||||
tasks::start_status_listener(window, msg_receiver);
|
||||
tasks::start_disconnect_listener(state.clone(), window.clone(), exit_status_receiver);
|
||||
tasks::start_status_listener(state, window.clone(), msg_receiver);
|
||||
|
||||
Ok(ConnectResult {
|
||||
address: "PLACEHOLDER".to_string(),
|
||||
|
||||
@@ -9,6 +9,7 @@ pub async fn start_disconnecting(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
window: tauri::Window<tauri::Wry>,
|
||||
) -> Result<ConnectResult> {
|
||||
log::trace!("Start disconnecting");
|
||||
let mut guard = state.write().await;
|
||||
|
||||
guard.start_disconnecting(&window).await?;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::error::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::models::ConnectionStatusKind;
|
||||
use crate::state::State;
|
||||
|
||||
static HEALTH_CHECK_URL: &str = "https://nymtech.net/.wellknown/connect/healthcheck.json";
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_connection_status(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
@@ -13,3 +16,27 @@ pub async fn get_connection_status(
|
||||
let state = state.read().await;
|
||||
Ok(state.get_status())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct ConnectionSuccess {
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_health_check() -> bool {
|
||||
log::info!("Running network health check");
|
||||
match crate::operations::http::socks5_get::<_, ConnectionSuccess>(HEALTH_CHECK_URL).await {
|
||||
Ok(res) if res.status == "ok" => {
|
||||
log::info!("Healthcheck success!");
|
||||
true
|
||||
}
|
||||
Ok(res) => {
|
||||
log::error!("Healthcheck failed with status: {}", res.status);
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Healthcheck failed: {err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ impl GrowthApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use the method in `operations::http` instead
|
||||
pub(crate) async fn post<REQ: Serialize + ?Sized, RESP: DeserializeOwned>(
|
||||
&self,
|
||||
url: &str,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use crate::error::{BackendError, Result};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tap::TapFallible;
|
||||
|
||||
pub async fn socks5_get<U, T>(url: U) -> Result<T>
|
||||
where
|
||||
U: reqwest::IntoUrl + std::fmt::Display,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
log::info!(">>> GET {url}");
|
||||
let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:1080")?;
|
||||
let client = reqwest::Client::builder()
|
||||
.proxy(proxy)
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
.build()?;
|
||||
|
||||
let resp = client.get(url).send().await.tap_err(|err| {
|
||||
log::error!("<<< Request send error: {err}");
|
||||
})?;
|
||||
|
||||
if resp.status().is_client_error() || resp.status().is_server_error() {
|
||||
log::error!("<<< {}", resp.status());
|
||||
return Err(BackendError::RequestFail {
|
||||
url: resp.url().clone(),
|
||||
status_code: resp.status(),
|
||||
});
|
||||
}
|
||||
|
||||
let response_body = resp.text().await.tap_err(|err| {
|
||||
log::error!("<<< Request error: {err}");
|
||||
})?;
|
||||
log::info!("<<< {response_body}");
|
||||
|
||||
Ok(serde_json::from_str(&response_body).tap_err(|err| {
|
||||
log::error!("<<< JSON parsing error: {err}");
|
||||
})?)
|
||||
}
|
||||
@@ -3,4 +3,5 @@ pub mod directory;
|
||||
pub mod export;
|
||||
pub mod growth;
|
||||
pub mod help;
|
||||
pub mod http;
|
||||
pub mod window;
|
||||
|
||||
@@ -43,7 +43,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_status(&self) -> ConnectionStatusKind {
|
||||
self.status.clone()
|
||||
}
|
||||
@@ -111,9 +110,7 @@ impl State {
|
||||
}
|
||||
|
||||
// Kick off the main task and get the channel for controlling it
|
||||
let (msg_receiver, exit_status_receiver) = self.start_nym_socks5_client()?;
|
||||
self.set_state(ConnectionStatusKind::Connected, window);
|
||||
Ok((msg_receiver, exit_status_receiver))
|
||||
self.start_nym_socks5_client()
|
||||
}
|
||||
|
||||
/// Create a configuration file
|
||||
@@ -142,9 +139,16 @@ impl State {
|
||||
Ok((msg_rx, exit_status_rx))
|
||||
}
|
||||
|
||||
/// Once the SOCKS5 client is operational, the status listener would call this
|
||||
pub fn mark_connected(&mut self, window: &tauri::Window<tauri::Wry>) {
|
||||
log::trace!("state::mark_connected");
|
||||
self.set_state(ConnectionStatusKind::Connected, window);
|
||||
}
|
||||
|
||||
/// Disconnect by sending a message to the SOCKS5 client thread. Once it has finished and is
|
||||
/// disconnected, the disconnect handler will mark it as disconnected.
|
||||
pub async fn start_disconnecting(&mut self, window: &tauri::Window<tauri::Wry>) -> Result<()> {
|
||||
log::trace!("state::start_disconnecting");
|
||||
self.set_state(ConnectionStatusKind::Disconnecting, window);
|
||||
|
||||
// Send shutdown message
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use client_core::config::{ClientCoreConfigTrait, GatewayEndpointConfig};
|
||||
use client_core::{
|
||||
config::{ClientCoreConfigTrait, GatewayEndpointConfig},
|
||||
error::ClientCoreStatusMessage,
|
||||
};
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use std::sync::Arc;
|
||||
use tap::TapFallible;
|
||||
use task::manager::TaskStatus;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use config_common::NymConfig;
|
||||
@@ -9,7 +13,7 @@ use config_common::NymConfig;
|
||||
use nym_socks5::client::NymClient as Socks5NymClient;
|
||||
use nym_socks5::client::{config::Config as Socks5Config, Socks5ControlMessageSender};
|
||||
|
||||
use crate::{error::Result, state::State};
|
||||
use crate::{error::Result, models::ConnectionStatusKind, operations::connection, state::State};
|
||||
|
||||
pub type ExitStatusReceiver = futures::channel::oneshot::Receiver<Socks5ExitStatusMessage>;
|
||||
|
||||
@@ -89,8 +93,78 @@ struct Payload {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
fn new(title: String, message: String) -> Self {
|
||||
Self { title, message }
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_event(event: &str, title: &str, msg: &str, window: &tauri::Window<tauri::Wry>) {
|
||||
if let Err(err) = window.emit(event, Payload::new(title.into(), msg.into())) {
|
||||
log::error!("Failed to emit tauri event: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_status_event(
|
||||
event: &str,
|
||||
msg: Box<dyn std::error::Error + Send + Sync>,
|
||||
window: &tauri::Window<tauri::Wry>,
|
||||
) {
|
||||
if let Err(err) = window.emit(event, Payload::new("SOCKS5 update".into(), msg.to_string())) {
|
||||
log::error!("Failed to emit tauri event: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_connection_check(state: Arc<RwLock<State>>, window: tauri::Window<tauri::Wry>) {
|
||||
log::debug!("Starting connection check handler");
|
||||
tokio::spawn(async move {
|
||||
if state.read().await.get_status() != ConnectionStatusKind::Connected {
|
||||
log::error!("SOCKS5 connection status check failed: not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
log::info!("Running connection health check");
|
||||
if connection::status::run_health_check().await {
|
||||
emit_event(
|
||||
"socks5-connection-success-event",
|
||||
"SOCKS5 success",
|
||||
"SOCKS5 connection health check successful",
|
||||
&window,
|
||||
);
|
||||
} else {
|
||||
if state.read().await.get_status() != ConnectionStatusKind::Connected {
|
||||
log::debug!("SOCKS5 connection status check cancelled: not connected");
|
||||
}
|
||||
log::error!("SOCKS5 connection health check failed");
|
||||
emit_event(
|
||||
"socks5-connection-fail-event",
|
||||
"SOCKS5 error",
|
||||
"SOCKS5 connection health check failed",
|
||||
&window,
|
||||
);
|
||||
}
|
||||
|
||||
log::debug!("Connection check handler exiting");
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_connection_ready(
|
||||
state: &Arc<RwLock<State>>,
|
||||
window: &tauri::Window,
|
||||
msg: Box<dyn std::error::Error + Send + Sync>,
|
||||
) {
|
||||
{
|
||||
let mut state_w = state.write().await;
|
||||
state_w.mark_connected(window);
|
||||
}
|
||||
|
||||
emit_status_event("socks5-connected-event", msg, window);
|
||||
start_connection_check(state.clone(), window.clone());
|
||||
}
|
||||
|
||||
/// The status listener listens for non-exit status messages from the background socks5 proxy task.
|
||||
pub fn start_status_listener(
|
||||
state: Arc<RwLock<State>>,
|
||||
window: tauri::Window<tauri::Wry>,
|
||||
mut msg_receiver: task::StatusReceiver,
|
||||
) {
|
||||
@@ -98,16 +172,21 @@ pub fn start_status_listener(
|
||||
tokio::spawn(async move {
|
||||
while let Some(msg) = msg_receiver.next().await {
|
||||
log::info!("SOCKS5 proxy sent status message: {}", msg);
|
||||
window
|
||||
.emit(
|
||||
"socks5-status-event",
|
||||
Payload {
|
||||
title: "SOCKS5 update".into(),
|
||||
message: msg.to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if let Some(TaskStatus::Ready) = msg.downcast_ref::<TaskStatus>() {
|
||||
handle_connection_ready(&state, &window, msg).await;
|
||||
} else if let Some(_gateway_status) = msg.downcast_ref::<ClientCoreStatusMessage>() {
|
||||
// TODO: use this instead once we change on the frontend too
|
||||
//let event_name = match gateway_status {
|
||||
// ClientCoreStatusMessage::GatewayIsSlow => "socks5-gateway-status",
|
||||
// ClientCoreStatusMessage::GatewayIsVerySlow => "socks5-gateway-status",
|
||||
//};
|
||||
emit_status_event("socks5-status-event", msg, &window);
|
||||
} else {
|
||||
emit_status_event("socks5-status-event", msg, &window);
|
||||
}
|
||||
}
|
||||
log::info!("Status listener exiting");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,39 +202,30 @@ pub fn start_disconnect_listener(
|
||||
match exit_status_receiver.await {
|
||||
Ok(Socks5ExitStatusMessage::Stopped) => {
|
||||
log::info!("SOCKS5 task reported it has finished");
|
||||
window
|
||||
.emit(
|
||||
"socks5-event",
|
||||
Payload {
|
||||
title: "SOCKS5 finished".into(),
|
||||
message: "SOCKS5 task reported it has finished".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
emit_event(
|
||||
"socks5-event",
|
||||
"SOCKS5 finished",
|
||||
"SOCKS5 task reported it has finished",
|
||||
&window,
|
||||
);
|
||||
}
|
||||
Ok(Socks5ExitStatusMessage::Failed(err)) => {
|
||||
log::info!("SOCKS5 task reported error: {err}");
|
||||
window
|
||||
.emit(
|
||||
"socks5-event",
|
||||
Payload {
|
||||
title: "SOCKS5 error".into(),
|
||||
message: format!("SOCKS5 failed: {err}"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
emit_event(
|
||||
"socks5-event",
|
||||
"SOCKS5 error",
|
||||
&format!("SOCKS5 failed: {err}"),
|
||||
&window,
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
log::info!("SOCKS5 task appears to have stopped abruptly");
|
||||
window
|
||||
.emit(
|
||||
"socks5-event",
|
||||
Payload {
|
||||
title: "SOCKS5 error".into(),
|
||||
message: "SOCKS5 stopped abruptly. Please try reconnecting.".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
emit_event(
|
||||
"socks5-event",
|
||||
"SOCKS5 error",
|
||||
"SOCKS5 stopped abruptly. Please try reconnecting.",
|
||||
&window,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user