From bb5cb1b5ad96b2c5473892f2eb3ea99a62963fb5 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Thu, 15 May 2025 21:55:46 +1000 Subject: [PATCH] chore(gui-client): greet GUI instance upon connect (#9151) The tunnel service of the GUI client can only handle one process at a time. The OS however will happily connect multiple clients to the socket / pipe. They will simply idle until the previous process disconnects. To avoid this situation, we introduce a `Hello` message from the tunnel service to the GUI client. If the GUI client doesn't receive this message within 5s, it considers the tunnel service be not responsive. If our duplicate instance detection works as intended, users are not expected to hit this. --- .../src-tauri/src/bin/firezone-gui-client.rs | 7 ++++ rust/gui-client/src-tauri/src/controller.rs | 39 +++++++++++++++++-- rust/gui-client/src-tauri/src/service.rs | 16 +++++++- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/rust/gui-client/src-tauri/src/bin/firezone-gui-client.rs b/rust/gui-client/src-tauri/src/bin/firezone-gui-client.rs index 8a1d6c8c7..e0ae0c08d 100644 --- a/rust/gui-client/src-tauri/src/bin/firezone-gui-client.rs +++ b/rust/gui-client/src-tauri/src/bin/firezone-gui-client.rs @@ -163,6 +163,13 @@ fn run_gui(config: RunConfig) -> Result<()> { return Err(anyhow); } + if anyhow.root_cause().is::() { + show_error_dialog( + "The Firezone Tunnel service is not responding. If the issue persists, contact your administrator.", + )?; + return Err(anyhow); + } + show_error_dialog( "An unexpected error occurred. Please try restarting Firezone. If the issue persists, contact your administrator.", )?; diff --git a/rust/gui-client/src-tauri/src/controller.rs b/rust/gui-client/src-tauri/src/controller.rs index 14272175c..3ea3e0e40 100644 --- a/rust/gui-client/src-tauri/src/controller.rs +++ b/rust/gui-client/src-tauri/src/controller.rs @@ -6,7 +6,7 @@ use crate::{ settings::{self, AdvancedSettings}, updates, uptime, }; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result, anyhow, bail}; use connlib_model::ResourceView; use firezone_bin_shared::DnsControlMethod; use firezone_logging::FilterReloadHandle; @@ -16,7 +16,13 @@ use futures::{ stream::{self, BoxStream}, }; use secrecy::{ExposeSecret as _, SecretString}; -use std::{collections::BTreeSet, ops::ControlFlow, path::PathBuf, task::Poll, time::Instant}; +use std::{ + collections::BTreeSet, + ops::ControlFlow, + path::PathBuf, + task::Poll, + time::{Duration, Instant}, +}; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::ReceiverStream; use url::Url; @@ -194,6 +200,10 @@ enum EventloopTick { ), } +#[derive(Debug, thiserror::Error)] +#[error("Failed to receive hello: {0:#}")] +pub struct FailedToReceiveHello(anyhow::Error); + impl Controller { pub(crate) async fn start( ctlr_tx: CtlrTx, @@ -206,9 +216,13 @@ impl Controller { ) -> Result<()> { tracing::debug!("Starting new instance of `Controller`"); - let (ipc_rx, ipc_client) = + let (mut ipc_rx, ipc_client) = ipc::connect(SocketId::Tunnel, ipc::ConnectOptions::default()).await?; + receive_hello(&mut ipc_rx) + .await + .map_err(FailedToReceiveHello)?; + let dns_notifier = new_dns_notifier().await?.boxed(); let network_notifier = new_network_notifier().await?.boxed(); @@ -664,6 +678,7 @@ impl Controller { )?; self.refresh_system_tray_menu(); } + service::ServerMsg::Hello => {} } Ok(ControlFlow::Continue(())) } @@ -914,3 +929,21 @@ async fn new_network_notifier() -> Result>> { Ok(Some(((), worker))) })) } + +async fn receive_hello(ipc_rx: &mut ipc::ClientRead) -> Result<()> { + const TIMEOUT: Duration = Duration::from_secs(5); + + let server_msg = tokio::time::timeout(TIMEOUT, ipc_rx.next()) + .await + .with_context(|| { + format!("Timeout while waiting for message from tunnel service for {TIMEOUT:?}") + })? + .context("No message received from tunnel service")? + .context("Failed to receive message from tunnel service")?; + + if !matches!(server_msg, service::ServerMsg::Hello) { + bail!("Expected `Hello` from tunnel service but got `{server_msg}`") + } + + Ok(()) +} diff --git a/rust/gui-client/src-tauri/src/service.rs b/rust/gui-client/src-tauri/src/service.rs index d886e9f5c..3ba690990 100644 --- a/rust/gui-client/src-tauri/src/service.rs +++ b/rust/gui-client/src-tauri/src/service.rs @@ -67,6 +67,7 @@ pub enum ClientMsg { /// Messages that end up in the GUI, either forwarded from connlib or from the IPC service. #[derive(Debug, serde::Deserialize, serde::Serialize, strum::Display)] pub enum ServerMsg { + Hello, /// The IPC service finished clearing its log dir. ClearedLogs(Result<(), String>), ConnectResult(Result<(), ConnectError>), @@ -150,7 +151,13 @@ async fn ipc_listen( tracing::info!("Caught SIGINT / SIGTERM / Ctrl+C while waiting on the next client."); break; }; - let mut handler = handler?; + let mut handler = match handler { + Ok(handler) => handler, + Err(e) => { + tracing::warn!("Failed to initialise IPC handler: {e:#}"); + continue; + } + }; if let HandlerOk::ServiceTerminating = handler.run(signals).await { break; } @@ -206,12 +213,17 @@ impl<'a> Handler<'a> { "Listening for GUI to connect over IPC..." ); - let (ipc_rx, ipc_tx) = server + let (ipc_rx, mut ipc_tx) = server .next_client_split() .await .context("Failed to wait for incoming IPC connection from a GUI")?; let tun_device = TunDeviceManager::new(ip_packet::MAX_IP_SIZE, 1)?; + ipc_tx + .send(&ServerMsg::Hello) + .await + .context("Failed to greet to new GUI process")?; // Greet the GUI process. If the GUI process doesn't receive this after connecting, it knows that the tunnel service isn't responding. + Ok(Self { dns_controller, ipc_rx,