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:
Jon Häggblad
2023-01-24 11:04:30 +01:00
committed by GitHub
parent 17771b5742
commit 95080c3ecc
10 changed files with 207 additions and 58 deletions
+20 -14
View File
@@ -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 {
+1
View File
@@ -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;
+8 -4
View File
@@ -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
+108 -38
View File
@@ -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,
);
}
}