From 899f5ea5e80984600c5cfe25ea0313c85638bc49 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 1 Jul 2025 16:11:29 +0200 Subject: [PATCH] fix(gui-client): ensure GUI client can access `firezone-id.json` (#9764) I believe some of the recent changes around how we load the `firezone-id.json` from the GUI client surfaced that we in fact don't always have access to it. Previously, this was silenced because we would only optionally add it as context to the Sentry client. Now, we need it to initialise telemetry so we know whether or not to send logs to Sentry. In order to be able to access the file, we need to change the config's directory and the file to be owned by the `firezone-client` group. --- rust/bin-shared/src/device_id.rs | 2 +- rust/gui-client/src-tauri/src/elevation.rs | 16 ++++------------ rust/gui-client/src-tauri/src/lib.rs | 13 +++++++++++++ rust/gui-client/src-tauri/src/service.rs | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/rust/bin-shared/src/device_id.rs b/rust/bin-shared/src/device_id.rs index 53186a931..5f3671da7 100644 --- a/rust/bin-shared/src/device_id.rs +++ b/rust/bin-shared/src/device_id.rs @@ -18,7 +18,7 @@ pub struct DeviceId { /// /// e.g. `C:\ProgramData\dev.firezone.client/firezone-id.json` or /// `/var/lib/dev.firezone.client/config/firezone-id.json`. -pub(crate) fn path() -> Result { +pub fn path() -> Result { let path = crate::known_dirs::tunnel_service_config() .context("Failed to compute path for firezone-id file")? .join("firezone-id.json"); diff --git a/rust/gui-client/src-tauri/src/elevation.rs b/rust/gui-client/src-tauri/src/elevation.rs index 57e133b51..de1eb8d80 100644 --- a/rust/gui-client/src-tauri/src/elevation.rs +++ b/rust/gui-client/src-tauri/src/elevation.rs @@ -2,10 +2,9 @@ pub use platform::gui_check; #[cfg(target_os = "linux")] mod platform { + use crate::FIREZONE_CLIENT_GROUP; use anyhow::{Context as _, Result}; - const FIREZONE_GROUP: &str = "firezone-client"; - /// Returns true if all permissions are correct for the GUI to run /// /// Everything that needs root / admin powers happens in the Tunnel services, @@ -17,7 +16,7 @@ mod platform { return Ok(false); } - let fz_gid = firezone_group()?.gid; + let fz_gid = crate::firezone_client_group()?.gid; let groups = nix::unistd::getgroups().context("Unable to read groups of current user")?; if !groups.contains(&fz_gid) { return Err(Error::UserNotInFirezoneGroup); @@ -26,16 +25,9 @@ mod platform { Ok(true) } - fn firezone_group() -> Result { - let group = nix::unistd::Group::from_name(FIREZONE_GROUP) - .context("can't get group by name")? - .with_context(|| format!("`{FIREZONE_GROUP}` group must exist on the system"))?; - Ok(group) - } - #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("User is not part of {FIREZONE_GROUP} group")] + #[error("User is not part of {FIREZONE_CLIENT_GROUP} group")] UserNotInFirezoneGroup, #[error(transparent)] Other(#[from] anyhow::Error), @@ -45,7 +37,7 @@ mod platform { pub fn user_friendly_msg(&self) -> String { match self { Error::UserNotInFirezoneGroup => format!( - "You are not a member of the group `{FIREZONE_GROUP}`. Try `sudo usermod -aG {FIREZONE_GROUP} $USER` and then reboot" + "You are not a member of the group `{FIREZONE_CLIENT_GROUP}`. Try `sudo usermod -aG {FIREZONE_CLIENT_GROUP} $USER` and then reboot" ), Error::Other(e) => format!("Failed to determine group ownership: {e:#}"), } diff --git a/rust/gui-client/src-tauri/src/lib.rs b/rust/gui-client/src-tauri/src/lib.rs index 8b6443295..61876ad8b 100644 --- a/rust/gui-client/src-tauri/src/lib.rs +++ b/rust/gui-client/src-tauri/src/lib.rs @@ -20,3 +20,16 @@ pub mod settings; /// Tunnel service and GUI client are always bundled into a single release. /// Hence, we have a single constant for Tunnel service and GUI client. pub const RELEASE: &str = concat!("gui-client@", env!("CARGO_PKG_VERSION")); + +pub const FIREZONE_CLIENT_GROUP: &str = "firezone-client"; + +#[cfg(target_os = "linux")] +pub fn firezone_client_group() -> anyhow::Result { + use anyhow::Context as _; + + let group = nix::unistd::Group::from_name(FIREZONE_CLIENT_GROUP) + .context("can't get group by name")? + .with_context(|| format!("`{FIREZONE_CLIENT_GROUP}` group must exist on the system"))?; + + Ok(group) +} diff --git a/rust/gui-client/src-tauri/src/service.rs b/rust/gui-client/src-tauri/src/service.rs index 4a634bda1..641ee7dee 100644 --- a/rust/gui-client/src-tauri/src/service.rs +++ b/rust/gui-client/src-tauri/src/service.rs @@ -107,6 +107,23 @@ async fn ipc_listen( // This also gives the GUI a safe place to put the log filter config let device_id = device_id::get_or_create().context("Failed to read / create device ID")?; + // Fix up the group of the device ID file and directory so the GUI client can access it. + #[cfg(target_os = "linux")] + { + let path = device_id::path().context("Failed to access device ID path")?; + let group_id = crate::firezone_client_group() + .context("Failed to get `firezone-client` group")? + .gid + .as_raw(); + + std::os::unix::fs::chown(&path, None, Some(group_id)) + .with_context(|| format!("Failed to change ownership of '{}'", path.display()))?; + + let dir = path.parent().context("No parent path")?; + std::os::unix::fs::chown(dir, None, Some(group_id)) + .with_context(|| format!("Failed to change ownership of '{}'", dir.display()))?; + } + let mut server = ipc::Server::new(SocketId::Tunnel)?; let mut dns_controller = DnsController { dns_control_method }; loop {