Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change client disconnect/reconnect mechanic and notifications #367

Merged
merged 8 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src-tauri/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ pub static LOCATION_UPDATE: &str = "location-update";
pub static APP_VERSION_FETCH: &str = "app-version-fetch";
pub static CONFIG_CHANGED: &str = "config-changed";
pub static DEAD_CONNECTION_DROPPED: &str = "dead-connection-dropped";
pub static DEAD_CONNECTION_RECONNECTED: &str = "dead-connection-reconnected";
pub static APPLICATION_CONFIG_CHANGED: &str = "application-config-changed";

/// Used as payload for [`DEAD_CONNECTION_DROPPED`] event
#[derive(Serialize, Clone, Debug)]
pub struct DeadConnDroppedOut {
pub(crate) name: String,
pub(crate) con_type: ConnectionType,
pub(crate) peer_alive_period: i64,
}

impl DeadConnDroppedOut {
Expand All @@ -35,3 +37,27 @@ impl DeadConnDroppedOut {
}
}
}

/// Used as payload for [`DEAD_CONNECTION_RECONNECTED`] event
#[derive(Serialize, Clone, Debug)]
pub struct DeadConnReconnected {
pub(crate) name: String,
pub(crate) con_type: ConnectionType,
pub(crate) peer_alive_period: i64,
}

impl DeadConnReconnected {
/// Emits [`DEAD_CONNECTION_RECONNECTED`] event with corresponding side effects.
pub(crate) fn emit(self, app_handle: &AppHandle) {
if let Err(err) = Notification::new(&app_handle.config().tauri.bundle.identifier)
.title(format!("{} {} reconnected", self.con_type, self.name))
.body("Connection activity timeout")
.show()
{
warn!("Dead connection reconnected notification not shown. Reason: {err}");
}
if let Err(err) = app_handle.emit_all(DEAD_CONNECTION_RECONNECTED, self) {
error!("Event Dead Connection Reconnected was not emitted. Reason: {err}");
}
}
}
99 changes: 82 additions & 17 deletions src-tauri/src/periodic/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
Id,
},
error::Error,
events::DeadConnDroppedOut,
events::{DeadConnDroppedOut, DeadConnReconnected},
ConnectionType,
};

Expand All @@ -34,22 +34,24 @@ async fn reconnect(
con_interface_name: &str,
app_handle: &AppHandle,
con_type: ConnectionType,
peer_alive_period: &TimeDelta,
) {
debug!("Starting attempt to reconnect {con_interface_name} {con_type}({con_id})...");
match disconnect(con_id, con_type, app_handle.clone()).await {
Ok(()) => {
debug!("Connection for {con_type} {con_interface_name}({con_id}) disconnected successfully in path of reconnection.");
let payload = DeadConnReconnected {
name: con_interface_name.to_string(),
con_type,
peer_alive_period: peer_alive_period.num_seconds(),
};
payload.emit(app_handle);
match connect(con_id, con_type, None, app_handle.clone()).await {
Ok(()) => {
info!("Reconnect for {con_type} {con_interface_name} ({con_id}) succeeded.",);
}
Err(err) => {
error!("Reconnect attempt failed, disconnect succeeded but connect failed. Error: {err}");
let payload = DeadConnDroppedOut {
name: con_interface_name.to_string(),
con_type,
};
payload.emit(app_handle);
}
}
}
Expand All @@ -66,6 +68,7 @@ async fn disconnect_dead_connection(
con_interface_name: &str,
app_handle: AppHandle,
con_type: ConnectionType,
peer_alive_period: &TimeDelta,
) {
debug!(
"Attempting to disconnect dead connection for interface {con_interface_name}, {con_type}: {con_id}");
Expand All @@ -75,6 +78,7 @@ async fn disconnect_dead_connection(
let event_payload = DeadConnDroppedOut {
con_type,
name: con_interface_name.to_string(),
peer_alive_period: peer_alive_period.num_seconds(),
};
event_payload.emit(&app_handle);
}
Expand All @@ -91,7 +95,9 @@ pub async fn verify_active_connections(app_handle: AppHandle) -> Result<(), Erro
let pool = &app_state.db;
debug!("Active connections verification started.");

// Both vectors contain IDs.
// Both vectors contain (ID, allow_reconnect) tuples.
// If allow_reconnect is false, the connection will always be dropped without a reconnect attempt.
// Otherwise, the connection will be reconnected if nothing else prevents it (e.g. MFA).
let mut locations_to_disconnect = Vec::new();
let mut tunnels_to_disconnect = Vec::new();

Expand All @@ -101,6 +107,8 @@ pub async fn verify_active_connections(app_handle: AppHandle) -> Result<(), Erro
let connection_count = connections.len();
if connection_count == 0 {
debug!("Connections verification skipped, no active connections found, task will wait for next {CHECK_INTERVAL:?}");
} else {
debug!("Verifying state of {connection_count} active connections. Inactive connections will be disconnected and reconnected if possible.");
}
let peer_alive_period = TimeDelta::seconds(i64::from(
app_state.app_config.lock().unwrap().peer_alive_period,
Expand All @@ -117,15 +125,27 @@ pub async fn verify_active_connections(app_handle: AppHandle) -> Result<(), Erro
latest_stat.collected_at,
peer_alive_period,
) {
debug!("There wasn't any activity for Location {}; considering it being dead.", con.location_id);
locations_to_disconnect.push(con.location_id);
// Check if there was any traffic since the connection was established.
// If not, consider the location dead and disconnect it later without reconnecting.
if latest_stat.collected_at < con.start {
debug!("There wasn't any activity for Location {} since its connection at {}; considering it being dead and possibly broken. \
It will be disconnected without a further automatic reconnect.", con.location_id, con.start);
locations_to_disconnect.push((con.location_id, false));
} else {
debug!("There wasn't any activity for Location {} for the last {}s; considering it being dead.", con.location_id, peer_alive_period.num_seconds());
locations_to_disconnect.push((con.location_id, true));
}
}
}
Ok(None) => {
error!(
debug!(
"LocationStats not found in database for active connection {} {}({})",
con.connection_type, con.interface_name, con.location_id
);
if Utc::now() - con.start.and_utc() > peer_alive_period {
debug!("There wasn't any activity for Location {} since its connection at {}; considering it being dead.", con.location_id, con.start);
locations_to_disconnect.push((con.location_id, false));
}
}
Err(err) => {
warn!("Verification for location {}({}) skipped due to db error. Error: {err}", con.interface_name, con.location_id);
Expand All @@ -140,17 +160,28 @@ pub async fn verify_active_connections(app_handle: AppHandle) -> Result<(), Erro
latest_stat.collected_at,
peer_alive_period,
) {
debug!("There wasn't any activity for Tunnel {}; considering it being dead.", con.location_id);
tunnels_to_disconnect.push(con.location_id);
// Check if there was any traffic since the connection was established.
// If not, consider the location dead and disconnect it later without reconnecting.
if latest_stat.collected_at - con.start < TimeDelta::zero() {
debug!("There wasn't any activity for Tunnel {} since its connection at {}; considering it being dead and possibly broken. \
It will be disconnected without a further automatic reconnect.", con.location_id, con.start);
tunnels_to_disconnect.push((con.location_id, false));
} else {
debug!("There wasn't any activity for Tunnel {} for the last {}s; considering it being dead.", con.location_id, peer_alive_period.num_seconds());
tunnels_to_disconnect.push((con.location_id, true));
}
}
}
Ok(None) => {
warn!(
"TunnelStats not found in database for active connection Tunnel {}({})",
con.interface_name, con.location_id
);
if Utc::now() - con.start.and_utc() > peer_alive_period {
debug!("There wasn't any activity for Location {} since its connection at {}; considering it being dead.", con.location_id, con.start);
tunnels_to_disconnect.push((con.location_id, false));
}
}

Err(err) => {
warn!(
"Verification for tunnel {}({}) skipped due to db error. Error: {err}",
Expand All @@ -166,17 +197,29 @@ pub async fn verify_active_connections(app_handle: AppHandle) -> Result<(), Erro
drop(connections);

// Process locations
for location_id in locations_to_disconnect.drain(..) {
for (location_id, allow_reconnect) in locations_to_disconnect.drain(..) {
match Location::find_by_id(pool, location_id).await {
Ok(Some(location)) => {
if !allow_reconnect {
warn!("Automatic reconnect for location {}({}) is not possible due to lack of activity. Interface will be disconnected.", location.name, location.id);
disconnect_dead_connection(
location_id,
&location.name,
app_handle.clone(),
ConnectionType::Location,
&peer_alive_period,
)
.await;
} else if
// only try to reconnect when location is not protected behind MFA
if location.mfa_enabled {
location.mfa_enabled {
warn!("Automatic reconnect for location {}({}) is not possible due to enabled MFA. Interface will be disconnected.", location.name, location.id);
disconnect_dead_connection(
location_id,
&location.name,
app_handle.clone(),
ConnectionType::Location,
&peer_alive_period,
)
.await;
} else {
Expand All @@ -185,6 +228,7 @@ pub async fn verify_active_connections(app_handle: AppHandle) -> Result<(), Erro
&location.name,
&app_handle,
ConnectionType::Location,
&peer_alive_period,
)
.await;
}
Expand All @@ -200,17 +244,37 @@ pub async fn verify_active_connections(app_handle: AppHandle) -> Result<(), Erro
"DEAD LOCATION",
app_handle.clone(),
ConnectionType::Location,
&peer_alive_period,
)
.await;
}
}
}

// Process tunnels
for tunnel_id in tunnels_to_disconnect.drain(..) {
for (tunnel_id, allow_reconnect) in tunnels_to_disconnect.drain(..) {
match Tunnel::find_by_id(pool, tunnel_id).await {
Ok(Some(tunnel)) => {
reconnect(tunnel.id, &tunnel.name, &app_handle, ConnectionType::Tunnel).await;
if allow_reconnect {
reconnect(
tunnel.id,
&tunnel.name,
&app_handle,
ConnectionType::Tunnel,
&peer_alive_period,
)
.await;
} else {
debug!("Automatic reconnect for location {}({}) is not possible due to lack of activity since the connection start. Interface will be disconnected.", tunnel.name, tunnel.id);
disconnect_dead_connection(
tunnel_id,
"DEAD TUNNEL",
app_handle.clone(),
ConnectionType::Tunnel,
&peer_alive_period,
)
.await;
}
}
Ok(None) => {
// Unlikely due to ON DELETE CASCADE.
Expand All @@ -223,6 +287,7 @@ pub async fn verify_active_connections(app_handle: AppHandle) -> Result<(), Erro
"DEAD TUNNEL",
app_handle.clone(),
ConnectionType::Tunnel,
&peer_alive_period,
)
.await;
}
Expand Down
12 changes: 5 additions & 7 deletions src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ const en = {
networkError: "There was a network error. Can't reach proxy.",
configChanged:
'Configuration for instance {instance: string} has changed. Disconnect from all locations to apply changes.',
deadConDropped: '{con_type: string} {interface_name: string} disconnected.',
deadConDropped:
'Detected that the {con_type: string} {interface_name: string} has disconnected, trying to reconnect...',
},
},
components: {
Expand All @@ -64,14 +65,11 @@ const en = {
client: {
modals: {
deadConDropped: {
title: '{conType: string} disconnected',
title: '{conType: string} {name: string} disconnected',
tunnel: 'Tunnel',
location: 'Location',
body: {
periodic:
'{conType: string} {instanceName: string} was automatically disconnected because it exceeded the expected time for staying active without receiving confirmation from the server.',
connection: `{conType: string} {name: string} connection was automatically disconnected because it didn't complete the necessary setup in time. This can happen if the connection wasn't fully established`,
},
message:
'The {conType: string} {name: string} has been disconnected, since we have detected that the server is not responding with any traffic for {time: number}s. If this message keeps occurring, please contact your administrator and inform them about this fact.',
controls: {
close: 'Close',
},
Expand Down
Loading