From 9c2cf6bd2d2c632cd470cb9a1355c4bf71ccbeb9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 30 Jun 2026 13:00:24 +0200 Subject: [PATCH 01/11] linux stores service locations --- src-tauri/daemon/src/daemon.rs | 70 +++++---- src-tauri/enterprise/config-sync/src/lib.rs | 2 +- .../enterprise/service-locations/src/lib.rs | 2 + .../enterprise/service-locations/src/linux.rs | 145 ++++++++++++++++++ 4 files changed, 190 insertions(+), 29 deletions(-) create mode 100644 src-tauri/enterprise/service-locations/src/linux.rs diff --git a/src-tauri/daemon/src/daemon.rs b/src-tauri/daemon/src/daemon.rs index 175177a0..36476191 100644 --- a/src-tauri/daemon/src/daemon.rs +++ b/src-tauri/daemon/src/daemon.rs @@ -22,7 +22,7 @@ use defguard_client_proto::defguard::{ enterprise::posture::v2::DevicePostureData, }; use defguard_client_service_locations::ServiceLocationError; -#[cfg(windows)] +#[cfg(any(windows, target_os = "linux"))] use defguard_client_service_locations::ServiceLocationManager; #[cfg(not(target_os = "macos"))] use defguard_wireguard_rs::Kernel; @@ -84,7 +84,7 @@ pub(crate) struct DaemonService { wgapis: Arc>>, stats_period: Duration, stat_tasks: Arc>>>, - #[cfg(windows)] + #[cfg(any(windows, target_os = "linux"))] service_location_manager: Arc>, } @@ -92,13 +92,15 @@ impl DaemonService { #[must_use] pub fn new( config: &Config, - #[cfg(windows)] service_location_manager: Arc>, + #[cfg(any(windows, target_os = "linux"))] service_location_manager: Arc< + RwLock, + >, ) -> Self { Self { wgapis: Arc::new(RwLock::new(HashMap::new())), stats_period: Duration::from_secs(config.stats_period), stat_tasks: Arc::new(Mutex::new(HashMap::new())), - #[cfg(windows)] + #[cfg(any(windows, target_os = "linux"))] service_location_manager, } } @@ -178,7 +180,7 @@ pub(crate) fn setup_wgapi(ifname: &str) -> Result { impl DesktopDaemonService for DaemonService { type ReadInterfaceDataStream = InterfaceDataStream; - #[cfg(not(windows))] + #[cfg(all(not(windows), not(target_os = "linux")))] async fn save_service_locations( &self, _request: tonic::Request, @@ -187,7 +189,7 @@ impl DesktopDaemonService for DaemonService { Ok(Response::new(())) } - #[cfg(not(windows))] + #[cfg(all(not(windows), not(target_os = "linux")))] async fn delete_service_locations( &self, _request: tonic::Request, @@ -196,7 +198,7 @@ impl DesktopDaemonService for DaemonService { Ok(Response::new(())) } - #[cfg(windows)] + #[cfg(any(windows, target_os = "linux"))] async fn save_service_locations( &self, request: tonic::Request, @@ -224,25 +226,30 @@ impl DesktopDaemonService for DaemonService { } } - for saved_location in service_location.service_locations { - match self - .service_location_manager - .clone() - .write() - .unwrap() - .reset_service_location_state(&service_location.instance_id, &saved_location.pubkey) - { - Ok(()) => { - debug!( - "Service location '{}' state reset successfully", - saved_location.name - ); - } - Err(e) => { - error!( - "Failed to reset state for service location '{}': {e}", - saved_location.name - ); + #[cfg(windows)] + { + for saved_location in service_location.service_locations { + match self + .service_location_manager + .clone() + .write() + .unwrap() + .reset_service_location_state( + &service_location.instance_id, + &saved_location.pubkey, + ) { + Ok(()) => { + debug!( + "Service location '{}' state reset successfully", + saved_location.name + ); + } + Err(e) => { + error!( + "Failed to reset state for service location '{}': {e}", + saved_location.name + ); + } } } } @@ -263,7 +270,7 @@ impl DesktopDaemonService for DaemonService { )) } - #[cfg(windows)] + #[cfg(any(windows, target_os = "linux"))] async fn delete_service_locations( &self, request: tonic::Request, @@ -586,7 +593,14 @@ impl DesktopDaemonService for DaemonService { pub async fn run_server(config: Config) -> anyhow::Result<()> { debug!("Starting Defguard interface management daemon"); - let daemon_service = DaemonService::new(&config); + #[cfg(target_os = "linux")] + let service_location_manager = Arc::new(RwLock::new(ServiceLocationManager::init()?)); + + let daemon_service = DaemonService::new( + &config, + #[cfg(target_os = "linux")] + service_location_manager, + ); // Remove existing socket if it exists if Path::new(DAEMON_SOCKET_PATH).exists() { diff --git a/src-tauri/enterprise/config-sync/src/lib.rs b/src-tauri/enterprise/config-sync/src/lib.rs index c1fd79cd..c550b859 100644 --- a/src-tauri/enterprise/config-sync/src/lib.rs +++ b/src-tauri/enterprise/config-sync/src/lib.rs @@ -171,7 +171,7 @@ pub async fn poll_instance( fetched.response.device_config.as_ref().ok_or_else(|| { Error::InternalError("Device config not present in response".to_string()) })?; - + error!("DeviceConfig: {:#?}", device_config); if !config_changed(transaction, instance, device_config).await? { debug!( "Config for instance {}({}) didn't change", diff --git a/src-tauri/enterprise/service-locations/src/lib.rs b/src-tauri/enterprise/service-locations/src/lib.rs index 14e2d143..9685dfcb 100644 --- a/src-tauri/enterprise/service-locations/src/lib.rs +++ b/src-tauri/enterprise/service-locations/src/lib.rs @@ -14,6 +14,8 @@ use defguard_wireguard_rs::{error::WireguardInterfaceError, WGApi}; use log::warn; use serde::{Deserialize, Serialize}; +#[cfg(target_os = "linux")] +pub mod linux; #[cfg(windows)] pub mod windows; diff --git a/src-tauri/enterprise/service-locations/src/linux.rs b/src-tauri/enterprise/service-locations/src/linux.rs new file mode 100644 index 00000000..f1ce55c1 --- /dev/null +++ b/src-tauri/enterprise/service-locations/src/linux.rs @@ -0,0 +1,145 @@ +use std::{ + ffi::OsStr, + fs::{self, create_dir_all, set_permissions}, + os::unix::fs::PermissionsExt, + path::PathBuf, +}; + +use defguard_client_proto::defguard::client::v1::{ServiceLocation, ServiceLocationMode}; +use log::{debug, warn}; + +use crate::{ServiceLocationData, ServiceLocationError, ServiceLocationManager}; + +const DEFGUARD_DIR: &str = "/etc/defguard"; +const SERVICE_LOCATIONS_SUBDIR: &str = "service_locations"; +const SERVICE_LOCATION_DIR_PERMS: u32 = 0o700; +const SERVICE_LOCATION_FILE_PERMS: u32 = 0o600; + +fn get_shared_directory() -> PathBuf { + PathBuf::from(DEFGUARD_DIR).join(SERVICE_LOCATIONS_SUBDIR) +} + +fn get_instance_file_path(instance_id: &str) -> PathBuf { + get_shared_directory().join(format!("{instance_id}.json")) +} + +fn ensure_shared_directory() -> Result { + let path = get_shared_directory(); + create_dir_all(&path)?; + set_permissions( + &path, + fs::Permissions::from_mode(SERVICE_LOCATION_DIR_PERMS), + )?; + Ok(path) +} + +impl ServiceLocationManager { + pub fn init() -> Result { + debug!("Initializing Linux service location storage"); + ensure_shared_directory()?; + Ok(Self::default()) + } + + pub fn save_service_locations( + &self, + service_locations: &[ServiceLocation], + instance_id: &str, + private_key: &str, + ) -> Result<(), ServiceLocationError> { + debug!( + "Received a request to save {} service location(s) for instance {instance_id}", + service_locations.len(), + ); + + let service_locations = service_locations + .iter() + .filter(|location| location.mode == ServiceLocationMode::AlwaysOn as i32) + .cloned() + .collect::>(); + + if service_locations.is_empty() { + debug!("No Linux-supported service locations to save for instance {instance_id}"); + return self.delete_all_service_locations_for_instance(instance_id); + } + + let service_location_data = ServiceLocationData { + service_locations, + instance_id: instance_id.to_string(), + private_key: private_key.to_string(), + }; + + ensure_shared_directory()?; + let instance_file_path = get_instance_file_path(instance_id); + let json = serde_json::to_string_pretty(&service_location_data)?; + + debug!( + "Writing Linux service location data to file: {}", + instance_file_path.display() + ); + fs::write(&instance_file_path, json)?; + set_permissions( + &instance_file_path, + fs::Permissions::from_mode(SERVICE_LOCATION_FILE_PERMS), + )?; + + debug!("Service locations saved for instance {instance_id}"); + Ok(()) + } + + pub fn disconnect_service_locations_by_instance( + &mut self, + instance_id: &str, + ) -> Result<(), ServiceLocationError> { + debug!( + "Disconnect requested for Linux service locations for instance {instance_id}; no active \ + interface lifecycle is implemented yet" + ); + Ok(()) + } + + pub fn delete_all_service_locations_for_instance( + &self, + instance_id: &str, + ) -> Result<(), ServiceLocationError> { + debug!("Deleting Linux service locations for instance {instance_id}"); + + let instance_file_path = get_instance_file_path(instance_id); + if instance_file_path.exists() { + fs::remove_file(&instance_file_path)?; + debug!("Deleted Linux service locations for instance {instance_id}"); + } else { + debug!("No Linux service location file found for instance {instance_id}"); + } + + Ok(()) + } + + #[allow(dead_code)] + fn load_service_locations(&self) -> Result, ServiceLocationError> { + let base_dir = ensure_shared_directory()?; + let mut all_locations_data = Vec::new(); + + for entry in fs::read_dir(base_dir)? { + let entry = entry?; + let file_path = entry.path(); + + if file_path.is_file() && file_path.extension() == Some(OsStr::new("json")) { + match fs::read_to_string(&file_path) { + Ok(data) => match serde_json::from_str::(&data) { + Ok(locations_data) => all_locations_data.push(locations_data), + Err(err) => warn!( + "Failed to parse Linux service locations from file {}: {err}", + file_path.display() + ), + }, + Err(err) => warn!( + "Failed to read Linux service locations file {}: {err}", + file_path.display() + ), + } + } + } + + Ok(all_locations_data) + } +} From d86fab5cfe27eafc8a7cf16b6bd0505a2664923e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 1 Jul 2026 08:23:56 +0200 Subject: [PATCH 02/11] linux connects to service locations --- src-tauri/daemon/src/daemon.rs | 112 +++++++++++-- .../enterprise/service-locations/src/linux.rs | 157 +++++++++++++++++- 2 files changed, 250 insertions(+), 19 deletions(-) diff --git a/src-tauri/daemon/src/daemon.rs b/src-tauri/daemon/src/daemon.rs index 36476191..64c9c0b2 100644 --- a/src-tauri/daemon/src/daemon.rs +++ b/src-tauri/daemon/src/daemon.rs @@ -57,6 +57,11 @@ pub(super) const DAEMON_SOCKET_PATH: &str = "/var/run/defguard.socket"; #[cfg(target_os = "linux")] pub(super) const DAEMON_SOCKET_GROUP: &str = "defguard"; +#[cfg(target_os = "linux")] +const SERVICE_LOCATION_CONNECT_RETRY_COUNT: u32 = 5; +#[cfg(target_os = "linux")] +const SERVICE_LOCATION_CONNECT_RETRY_DELAY: Duration = Duration::from_secs(30); + #[derive(Debug, thiserror::Error)] pub enum DaemonError { #[error(transparent)] @@ -206,23 +211,56 @@ impl DesktopDaemonService for DaemonService { debug!("Received a request to save service location"); let service_location = request.into_inner(); - match self - .service_location_manager - .clone() - .read() - .unwrap() - .save_service_locations( - service_location.service_locations.as_slice(), - &service_location.instance_id, - &service_location.private_key, - ) { - Ok(()) => { - debug!("Service location saved successfully"); - } - Err(e) => { - let msg = format!("Failed to save service location: {e}"); + #[cfg(target_os = "linux")] + { + let mut manager = self.service_location_manager.write().unwrap(); + manager + .disconnect_service_locations_by_instance(&service_location.instance_id) + .map_err(|err| { + let msg = format!("Failed to disconnect service location: {err}"); + error!(msg); + Status::internal(msg) + })?; + manager + .save_service_locations( + service_location.service_locations.as_slice(), + &service_location.instance_id, + &service_location.private_key, + ) + .map_err(|err| { + let msg = format!("Failed to save service location: {err}"); + error!(msg); + Status::internal(msg) + })?; + manager.connect_to_service_locations().map_err(|err| { + let msg = format!("Failed to connect service location: {err}"); error!(msg); - return Err(Status::internal(msg)); + Status::internal(msg) + })?; + + debug!("Linux service locations saved and reconnected successfully"); + } + + #[cfg(windows)] + { + match self + .service_location_manager + .clone() + .read() + .unwrap() + .save_service_locations( + service_location.service_locations.as_slice(), + &service_location.instance_id, + &service_location.private_key, + ) { + Ok(()) => { + debug!("Service location saved successfully"); + } + Err(e) => { + let msg = format!("Failed to save service location: {e}"); + error!(msg); + return Err(Status::internal(msg)); + } } } @@ -595,6 +633,48 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { #[cfg(target_os = "linux")] let service_location_manager = Arc::new(RwLock::new(ServiceLocationManager::init()?)); + #[cfg(target_os = "linux")] + { + let service_location_manager_connect = service_location_manager.clone(); + tokio::spawn(async move { + for attempt in 1..=SERVICE_LOCATION_CONNECT_RETRY_COUNT { + info!( + "Attempting to auto-connect Linux service locations \ + (attempt {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT})" + ); + match service_location_manager_connect + .write() + .unwrap() + .connect_to_service_locations() + { + Ok(true) => { + info!( + "All Linux service locations connected successfully \ + (attempt {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT})" + ); + break; + } + Ok(false) => { + warn!( + "Linux service location auto-connect attempt \ + {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT} completed with some failures" + ); + } + Err(err) => { + warn!( + "Linux service location auto-connect attempt \ + {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT} failed: {err}" + ); + } + } + + if attempt < SERVICE_LOCATION_CONNECT_RETRY_COUNT { + tokio::time::sleep(SERVICE_LOCATION_CONNECT_RETRY_DELAY).await; + } + } + info!("Linux service location auto-connect task finished"); + }); + } let daemon_service = DaemonService::new( &config, diff --git a/src-tauri/enterprise/service-locations/src/linux.rs b/src-tauri/enterprise/service-locations/src/linux.rs index f1ce55c1..ad489b2b 100644 --- a/src-tauri/enterprise/service-locations/src/linux.rs +++ b/src-tauri/enterprise/service-locations/src/linux.rs @@ -3,10 +3,15 @@ use std::{ fs::{self, create_dir_all, set_permissions}, os::unix::fs::PermissionsExt, path::PathBuf, + str::FromStr, }; +use defguard_client_common::{dns_borrow, find_free_tcp_port, get_interface_name}; use defguard_client_proto::defguard::client::v1::{ServiceLocation, ServiceLocationMode}; -use log::{debug, warn}; +use defguard_wireguard_rs::{ + key::Key, net::IpAddrMask, peer::Peer, InterfaceConfiguration, WGApi, WireguardInterfaceApi, +}; +use log::{debug, error, warn}; use crate::{ServiceLocationData, ServiceLocationError, ServiceLocationManager}; @@ -14,6 +19,7 @@ const DEFGUARD_DIR: &str = "/etc/defguard"; const SERVICE_LOCATIONS_SUBDIR: &str = "service_locations"; const SERVICE_LOCATION_DIR_PERMS: u32 = 0o700; const SERVICE_LOCATION_FILE_PERMS: u32 = 0o600; +const DEFAULT_WIREGUARD_PORT: u16 = 51820; fn get_shared_directory() -> PathBuf { PathBuf::from(DEFGUARD_DIR).join(SERVICE_LOCATIONS_SUBDIR) @@ -86,17 +92,162 @@ impl ServiceLocationManager { Ok(()) } + fn add_connected_service_location(&mut self, instance_id: &str, location: &ServiceLocation) { + self.connected_service_locations + .entry(instance_id.to_string()) + .or_default() + .push(location.clone()); + + debug!( + "Added connected Linux service location for instance '{instance_id}', location '{}'", + location.name + ); + } + + fn is_service_location_connected(&self, instance_id: &str, location_pubkey: &str) -> bool { + self.connected_service_locations + .get(instance_id) + .is_some_and(|locations| { + locations + .iter() + .any(|location| location.pubkey == location_pubkey) + }) + } + pub fn disconnect_service_locations_by_instance( &mut self, instance_id: &str, ) -> Result<(), ServiceLocationError> { + debug!("Disconnecting Linux service locations for instance {instance_id}"); + + let Some(locations) = self.connected_service_locations.remove(instance_id) else { + debug!("No connected Linux service locations found for instance {instance_id}"); + return Ok(()); + }; + + for location in locations { + let ifname = get_interface_name(&location.name); + debug!("Tearing down Linux service location interface: {ifname}"); + if let Some(wgapi) = self.wgapis.remove(&ifname) { + if let Err(err) = wgapi.remove_interface() { + error!("Failed to remove Linux service location interface {ifname}: {err}"); + } else { + debug!("Linux service location interface {ifname} removed successfully"); + } + } else { + debug!("Linux service location interface {ifname} was not tracked as connected"); + } + } + + Ok(()) + } + + fn setup_service_location_interface( + &mut self, + location: &ServiceLocation, + private_key: &str, + ) -> Result<(), ServiceLocationError> { + let peer_key = Key::from_str(&location.pubkey)?; + let mut peer = Peer::new(peer_key); + peer.set_endpoint(&location.endpoint)?; + peer.persistent_keepalive_interval = location.keepalive_interval.try_into().ok(); + + for allowed_ip in location.allowed_ips.split(',').map(str::trim) { + if allowed_ip.is_empty() { + continue; + } + match IpAddrMask::from_str(allowed_ip) { + Ok(addr) => peer.allowed_ips.push(addr), + Err(err) => error!( + "Error parsing allowed IP {allowed_ip} while setting up Linux service location {}: {err}", + location.name + ), + } + } + + let addresses = location + .address + .split(',') + .map(str::trim) + .filter(|address| !address.is_empty()) + .map(IpAddrMask::from_str) + .collect::, _>>()?; + + let ifname = get_interface_name(&location.name); + let config = InterfaceConfiguration { + name: ifname.clone(), + prvkey: private_key.to_string(), + addresses, + port: find_free_tcp_port().unwrap_or(DEFAULT_WIREGUARD_PORT), + peers: vec![peer], + mtu: None, + fwmark: None, + }; + + let mut wgapi = WGApi::new(&ifname).map_err(|err| { + ServiceLocationError::InterfaceError(format!( + "Failed to setup Linux WireGuard API for interface {ifname}: {err}" + )) + })?; + + wgapi.create_interface()?; + let dns_config = Some(location.dns.clone()); + let (dns, search_domains) = dns_borrow(&dns_config); debug!( - "Disconnect requested for Linux service locations for instance {instance_id}; no active \ - interface lifecycle is implemented yet" + "Configuring Linux service location interface {ifname} with DNS: {dns:?} and search domains: {search_domains:?}" ); + wgapi.configure_interface(&config)?; + wgapi.configure_dns(&dns, &search_domains)?; + self.wgapis.insert(ifname.clone(), wgapi); + + debug!("Linux service location interface {ifname} configured successfully"); Ok(()) } + pub fn connect_to_service_locations(&mut self) -> Result { + debug!("Attempting to auto-connect Linux Always-on service locations"); + + let data = self.load_service_locations()?; + let mut all_connected = true; + + for instance_data in data { + for location in instance_data.service_locations { + if location.mode != ServiceLocationMode::AlwaysOn as i32 { + debug!( + "Skipping Linux service location '{}' because only Always-on is supported", + location.name + ); + continue; + } + + if self.is_service_location_connected(&instance_data.instance_id, &location.pubkey) + { + debug!( + "Skipping Linux service location '{}' because it's already connected", + location.name + ); + continue; + } + + if let Err(err) = + self.setup_service_location_interface(&location, &instance_data.private_key) + { + warn!( + "Failed to setup Linux service location interface for '{}': {err:?}", + location.name + ); + all_connected = false; + continue; + } + + self.add_connected_service_location(&instance_data.instance_id, &location); + debug!("Connected Linux service location '{}'", location.name); + } + } + + Ok(all_connected) + } + pub fn delete_all_service_locations_for_instance( &self, instance_id: &str, From 226595af7431dd919482369d80da6f78bb917262 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 1 Jul 2026 08:45:45 +0200 Subject: [PATCH 03/11] separate functions for linux and windows --- src-tauri/daemon/src/daemon.rs | 167 +++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 70 deletions(-) diff --git a/src-tauri/daemon/src/daemon.rs b/src-tauri/daemon/src/daemon.rs index 64c9c0b2..9c910834 100644 --- a/src-tauri/daemon/src/daemon.rs +++ b/src-tauri/daemon/src/daemon.rs @@ -203,7 +203,7 @@ impl DesktopDaemonService for DaemonService { Ok(Response::new(())) } - #[cfg(any(windows, target_os = "linux"))] + #[cfg(target_os = "linux")] async fn save_service_locations( &self, request: tonic::Request, @@ -211,83 +211,82 @@ impl DesktopDaemonService for DaemonService { debug!("Received a request to save service location"); let service_location = request.into_inner(); - #[cfg(target_os = "linux")] - { - let mut manager = self.service_location_manager.write().unwrap(); - manager - .disconnect_service_locations_by_instance(&service_location.instance_id) - .map_err(|err| { - let msg = format!("Failed to disconnect service location: {err}"); - error!(msg); - Status::internal(msg) - })?; - manager - .save_service_locations( - service_location.service_locations.as_slice(), - &service_location.instance_id, - &service_location.private_key, - ) - .map_err(|err| { - let msg = format!("Failed to save service location: {err}"); - error!(msg); - Status::internal(msg) - })?; - manager.connect_to_service_locations().map_err(|err| { - let msg = format!("Failed to connect service location: {err}"); + let mut manager = self.service_location_manager.write().unwrap(); + manager + .disconnect_service_locations_by_instance(&service_location.instance_id) + .map_err(|err| { + let msg = format!("Failed to disconnect service locations: {err}"); + error!(msg); + Status::internal(msg) + })?; + manager + .save_service_locations( + service_location.service_locations.as_slice(), + &service_location.instance_id, + &service_location.private_key, + ) + .map_err(|err| { + let msg = format!("Failed to save service locations: {err}"); error!(msg); Status::internal(msg) })?; + manager.connect_to_service_locations().map_err(|err| { + let msg = format!("Failed to connect service locations: {err}"); + error!(msg); + Status::internal(msg) + })?; - debug!("Linux service locations saved and reconnected successfully"); - } + debug!("Service locations saved and reconnected successfully"); + Ok(Response::new(())) + } - #[cfg(windows)] - { + #[cfg(windows)] + async fn save_service_locations( + &self, + request: tonic::Request, + ) -> Result, Status> { + debug!("Received a request to save service locations"); + let service_location = request.into_inner(); + + match self + .service_location_manager + .clone() + .read() + .unwrap() + .save_service_locations( + service_location.service_locations.as_slice(), + &service_location.instance_id, + &service_location.private_key, + ) { + Ok(()) => { + debug!("Service locations saved successfully"); + } + Err(e) => { + let msg = format!("Failed to save service locations: {e}"); + error!(msg); + return Err(Status::internal(msg)); + } + }; + + for saved_location in service_location.service_locations { match self .service_location_manager .clone() - .read() + .write() .unwrap() - .save_service_locations( - service_location.service_locations.as_slice(), - &service_location.instance_id, - &service_location.private_key, - ) { + .reset_service_location_state(&service_location.instance_id, &saved_location.pubkey) + { Ok(()) => { - debug!("Service location saved successfully"); + debug!( + "Service location '{}' state reset successfully", + saved_location.name + ); } Err(e) => { - let msg = format!("Failed to save service location: {e}"); - error!(msg); - return Err(Status::internal(msg)); - } - } - } - - #[cfg(windows)] - { - for saved_location in service_location.service_locations { - match self - .service_location_manager - .clone() - .write() - .unwrap() - .reset_service_location_state( - &service_location.instance_id, - &saved_location.pubkey, - ) { - Ok(()) => { - debug!( - "Service location '{}' state reset successfully", - saved_location.name - ); - } - Err(e) => { - error!( - "Failed to reset state for service location '{}': {e}", - saved_location.name - ); - } + error!( + "Failed to reset state for service location '{}': {e}", + saved_location.name + ); } } } @@ -308,12 +307,40 @@ impl DesktopDaemonService for DaemonService { )) } - #[cfg(any(windows, target_os = "linux"))] + #[cfg(target_os = "linux")] + async fn delete_service_locations( + &self, + request: tonic::Request, + ) -> Result, Status> { + debug!("Received a request to delete service locations"); + let instance_id = request.into_inner().instance_id; + + let mut manager = self.service_location_manager.write().unwrap(); + manager + .disconnect_service_locations_by_instance(&instance_id) + .map_err(|err| { + let msg = format!("Failed to disconnect service locations: {err}"); + error!(msg); + Status::internal(msg) + })?; + manager + .delete_all_service_locations_for_instance(&instance_id) + .map_err(|err| { + let msg = format!("Failed to delete service locations: {err}"); + error!(msg); + Status::internal(msg) + })?; + + debug!("Service locations deleted successfully"); + Ok(Response::new(())) + } + + #[cfg(windows)] async fn delete_service_locations( &self, request: tonic::Request, ) -> Result, Status> { - debug!("Received a request to delete service location"); + debug!("Received a request to delete service locations"); let instance_id = request.into_inner().instance_id; self.service_location_manager @@ -322,7 +349,7 @@ impl DesktopDaemonService for DaemonService { .unwrap() .disconnect_service_locations_by_instance(&instance_id) .map_err(|err| { - let msg = format!("Failed to disconnect service location: {err}"); + let msg = format!("Failed to disconnect service locations: {err}"); error!(msg); Status::internal(msg) })?; @@ -341,7 +368,7 @@ impl DesktopDaemonService for DaemonService { Err(err) => { error!("Failed to delete service location: {err}"); Err(Status::internal(format!( - "Failed to delete service location: {err}" + "Failed to delete service locations: {err}" ))) } } From c649008a8cd171dfa272d44f99174ea86c87f457 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 1 Jul 2026 12:05:15 +0200 Subject: [PATCH 04/11] disconnect stale service locations on windows; shared daemon functions --- src-tauri/daemon/src/daemon.rs | 119 ++---------------- .../enterprise/service-locations/src/linux.rs | 114 +++++++++++++++-- .../service-locations/src/windows.rs | 50 +++++++- 3 files changed, 161 insertions(+), 122 deletions(-) diff --git a/src-tauri/daemon/src/daemon.rs b/src-tauri/daemon/src/daemon.rs index 9c910834..c01e4cb2 100644 --- a/src-tauri/daemon/src/daemon.rs +++ b/src-tauri/daemon/src/daemon.rs @@ -203,23 +203,17 @@ impl DesktopDaemonService for DaemonService { Ok(Response::new(())) } - #[cfg(target_os = "linux")] + #[cfg(any(windows, target_os = "linux"))] async fn save_service_locations( &self, request: tonic::Request, ) -> Result, Status> { - debug!("Received a request to save service location"); + debug!("Received a request to save service locations"); let service_location = request.into_inner(); - let mut manager = self.service_location_manager.write().unwrap(); - manager - .disconnect_service_locations_by_instance(&service_location.instance_id) - .map_err(|err| { - let msg = format!("Failed to disconnect service locations: {err}"); - error!(msg); - Status::internal(msg) - })?; - manager + self.service_location_manager + .write() + .unwrap() .save_service_locations( service_location.service_locations.as_slice(), &service_location.instance_id, @@ -230,67 +224,8 @@ impl DesktopDaemonService for DaemonService { error!(msg); Status::internal(msg) })?; - manager.connect_to_service_locations().map_err(|err| { - let msg = format!("Failed to connect service locations: {err}"); - error!(msg); - Status::internal(msg) - })?; - - debug!("Service locations saved and reconnected successfully"); - Ok(Response::new(())) - } - - #[cfg(windows)] - async fn save_service_locations( - &self, - request: tonic::Request, - ) -> Result, Status> { - debug!("Received a request to save service locations"); - let service_location = request.into_inner(); - - match self - .service_location_manager - .clone() - .read() - .unwrap() - .save_service_locations( - service_location.service_locations.as_slice(), - &service_location.instance_id, - &service_location.private_key, - ) { - Ok(()) => { - debug!("Service locations saved successfully"); - } - Err(e) => { - let msg = format!("Failed to save service locations: {e}"); - error!(msg); - return Err(Status::internal(msg)); - } - }; - - for saved_location in service_location.service_locations { - match self - .service_location_manager - .clone() - .write() - .unwrap() - .reset_service_location_state(&service_location.instance_id, &saved_location.pubkey) - { - Ok(()) => { - debug!( - "Service location '{}' state reset successfully", - saved_location.name - ); - } - Err(e) => { - error!( - "Failed to reset state for service location '{}': {e}", - saved_location.name - ); - } - } - } + debug!("Service locations saved successfully"); Ok(Response::new(())) } @@ -307,7 +242,7 @@ impl DesktopDaemonService for DaemonService { )) } - #[cfg(target_os = "linux")] + #[cfg(any(windows, target_os = "linux"))] async fn delete_service_locations( &self, request: tonic::Request, @@ -323,6 +258,7 @@ impl DesktopDaemonService for DaemonService { error!(msg); Status::internal(msg) })?; + manager .delete_all_service_locations_for_instance(&instance_id) .map_err(|err| { @@ -335,45 +271,6 @@ impl DesktopDaemonService for DaemonService { Ok(Response::new(())) } - #[cfg(windows)] - async fn delete_service_locations( - &self, - request: tonic::Request, - ) -> Result, Status> { - debug!("Received a request to delete service locations"); - let instance_id = request.into_inner().instance_id; - - self.service_location_manager - .clone() - .write() - .unwrap() - .disconnect_service_locations_by_instance(&instance_id) - .map_err(|err| { - let msg = format!("Failed to disconnect service locations: {err}"); - error!(msg); - Status::internal(msg) - })?; - - match self - .service_location_manager - .clone() - .read() - .unwrap() - .delete_all_service_locations_for_instance(&instance_id) - { - Ok(()) => { - debug!("Service location deleted successfully"); - Ok(Response::new(())) - } - Err(err) => { - error!("Failed to delete service location: {err}"); - Err(Status::internal(format!( - "Failed to delete service locations: {err}" - ))) - } - } - } - async fn create_interface( &self, request: tonic::Request, diff --git a/src-tauri/enterprise/service-locations/src/linux.rs b/src-tauri/enterprise/service-locations/src/linux.rs index ad489b2b..b9bd7c6d 100644 --- a/src-tauri/enterprise/service-locations/src/linux.rs +++ b/src-tauri/enterprise/service-locations/src/linux.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashSet, ffi::OsStr, fs::{self, create_dir_all, set_permissions}, os::unix::fs::PermissionsExt, @@ -47,7 +48,7 @@ impl ServiceLocationManager { } pub fn save_service_locations( - &self, + &mut self, service_locations: &[ServiceLocation], instance_id: &str, private_key: &str, @@ -57,19 +58,34 @@ impl ServiceLocationManager { service_locations.len(), ); + let old_locations = self + .load_service_locations_for_instance(instance_id)? + .map_or_else(Vec::new, |data| data.service_locations); + let old_pubkeys = old_locations + .iter() + .map(|location| location.pubkey.clone()) + .collect::>(); + let service_locations = service_locations .iter() .filter(|location| location.mode == ServiceLocationMode::AlwaysOn as i32) .cloned() .collect::>(); + let new_pubkeys = service_locations + .iter() + .map(|location| location.pubkey.clone()) + .collect::>(); if service_locations.is_empty() { debug!("No Linux-supported service locations to save for instance {instance_id}"); + for removed_pubkey in old_pubkeys { + self.disconnect_service_location(instance_id, &removed_pubkey)?; + } return self.delete_all_service_locations_for_instance(instance_id); } let service_location_data = ServiceLocationData { - service_locations, + service_locations: service_locations.clone(), instance_id: instance_id.to_string(), private_key: private_key.to_string(), }; @@ -89,6 +105,15 @@ impl ServiceLocationManager { )?; debug!("Service locations saved for instance {instance_id}"); + + for removed_pubkey in old_pubkeys.difference(&new_pubkeys) { + self.disconnect_service_location(instance_id, removed_pubkey)?; + } + for location in &service_locations { + self.disconnect_service_location(instance_id, &location.pubkey)?; + self.connect_service_location(instance_id, location, private_key)?; + } + Ok(()) } @@ -142,6 +167,46 @@ impl ServiceLocationManager { Ok(()) } + fn disconnect_service_location( + &mut self, + instance_id: &str, + location_pubkey: &str, + ) -> Result<(), ServiceLocationError> { + let Some(locations) = self.connected_service_locations.get_mut(instance_id) else { + debug!("No connected Linux service locations found for instance {instance_id}"); + return Ok(()); + }; + + let Some(position) = locations + .iter() + .position(|location| location.pubkey == location_pubkey) + else { + debug!( + "Linux service location with pubkey {location_pubkey} for instance {instance_id} is not connected" + ); + return Ok(()); + }; + + let location = locations.remove(position); + if locations.is_empty() { + self.connected_service_locations.remove(instance_id); + } + + let ifname = get_interface_name(&location.name); + debug!("Tearing down Linux service location interface: {ifname}"); + if let Some(wgapi) = self.wgapis.remove(&ifname) { + if let Err(err) = wgapi.remove_interface() { + error!("Failed to remove Linux service location interface {ifname}: {err}"); + } else { + debug!("Linux service location interface {ifname} removed successfully"); + } + } else { + debug!("Linux service location interface {ifname} was not tracked as connected"); + } + + Ok(()) + } + fn setup_service_location_interface( &mut self, location: &ServiceLocation, @@ -204,6 +269,26 @@ impl ServiceLocationManager { Ok(()) } + fn connect_service_location( + &mut self, + instance_id: &str, + location: &ServiceLocation, + private_key: &str, + ) -> Result<(), ServiceLocationError> { + if self.is_service_location_connected(instance_id, &location.pubkey) { + debug!( + "Skipping Linux service location '{}' because it's already connected", + location.name + ); + return Ok(()); + } + + self.setup_service_location_interface(location, private_key)?; + self.add_connected_service_location(instance_id, location); + debug!("Connected Linux service location '{}'", location.name); + Ok(()) + } + pub fn connect_to_service_locations(&mut self) -> Result { debug!("Attempting to auto-connect Linux Always-on service locations"); @@ -229,19 +314,17 @@ impl ServiceLocationManager { continue; } - if let Err(err) = - self.setup_service_location_interface(&location, &instance_data.private_key) - { + if let Err(err) = self.connect_service_location( + &instance_data.instance_id, + &location, + &instance_data.private_key, + ) { warn!( "Failed to setup Linux service location interface for '{}': {err:?}", location.name ); all_connected = false; - continue; } - - self.add_connected_service_location(&instance_data.instance_id, &location); - debug!("Connected Linux service location '{}'", location.name); } } @@ -293,4 +376,17 @@ impl ServiceLocationManager { Ok(all_locations_data) } + + fn load_service_locations_for_instance( + &self, + instance_id: &str, + ) -> Result, ServiceLocationError> { + let instance_file_path = get_instance_file_path(instance_id); + if !instance_file_path.exists() { + return Ok(None); + } + + let data = fs::read_to_string(instance_file_path)?; + Ok(Some(serde_json::from_str::(&data)?)) + } } diff --git a/src-tauri/enterprise/service-locations/src/windows.rs b/src-tauri/enterprise/service-locations/src/windows.rs index 23c9dd18..371f9f0d 100644 --- a/src-tauri/enterprise/service-locations/src/windows.rs +++ b/src-tauri/enterprise/service-locations/src/windows.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, ffi::OsStr, fs::{self, create_dir_all}, path::PathBuf, @@ -820,7 +820,7 @@ impl ServiceLocationManager { } pub fn save_service_locations( - &self, + &mut self, service_locations: &[ServiceLocation], instance_id: &str, private_key: &str, @@ -831,6 +831,17 @@ impl ServiceLocationManager { ); debug!("Service locations to save: {service_locations:?}"); + let old_locations = self + .load_service_locations_for_instance(instance_id)? + .map_or_else(Vec::new, |data| data.service_locations); + let old_pubkeys = old_locations + .iter() + .map(|location| location.pubkey.clone()) + .collect::>(); + let new_pubkeys = service_locations + .iter() + .map(|location| location.pubkey.clone()) + .collect::>(); create_dir_all(get_shared_directory()?)?; @@ -869,6 +880,28 @@ impl ServiceLocationManager { "Service locations saved successfully for instance {instance_id} to {}", instance_file_path.display() ); + + for removed_pubkey in old_pubkeys.difference(&new_pubkeys) { + self.disconnect_service_location(instance_id, removed_pubkey)?; + } + + for saved_location in service_locations { + match self.reset_service_location_state(instance_id, &saved_location.pubkey) { + Ok(()) => { + debug!( + "Service location '{}' state reset successfully", + saved_location.name + ); + } + Err(err) => { + error!( + "Failed to reset state for service location '{}': {err}", + saved_location.name + ); + } + } + } + Ok(()) } @@ -949,6 +982,19 @@ impl ServiceLocationManager { } } + fn load_service_locations_for_instance( + &self, + instance_id: &str, + ) -> Result, ServiceLocationError> { + let instance_file_path = get_instance_file_path(instance_id)?; + if !instance_file_path.exists() { + return Ok(None); + } + + let data = fs::read_to_string(instance_file_path)?; + Ok(Some(serde_json::from_str::(&data)?)) + } + pub fn delete_all_service_locations_for_instance( &self, instance_id: &str, From 9bde71f0ee39649bb30aeb3da0977fa7cacc72d9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 1 Jul 2026 14:09:49 +0200 Subject: [PATCH 05/11] fix service locations autoconnect --- .../enterprise/config-sync/src/commands.rs | 29 +++++++++++++++---- src-tauri/enterprise/config-sync/src/lib.rs | 5 +++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src-tauri/enterprise/config-sync/src/commands.rs b/src-tauri/enterprise/config-sync/src/commands.rs index f74fc8b4..4a35936d 100644 --- a/src-tauri/enterprise/config-sync/src/commands.rs +++ b/src-tauri/enterprise/config-sync/src/commands.rs @@ -83,8 +83,6 @@ pub async fn do_update_instance( "A new base configuration has been applied to instance {instance}, even if nothing changed" ); - let mut service_locations = Vec::new(); - if locations_changed_val { debug!( "Updating locations for instance {}({}).", @@ -130,10 +128,9 @@ pub async fn do_update_instance( if saved_location.is_service_location() { debug!( - "Adding service location {}({}) for instance {}({}) to be saved to the daemon.", + "Location {}({}) for instance {}({}) is a service location.", saved_location.name, saved_location.id, instance.name, instance.id, ); - service_locations.push(to_service_location(&saved_location)?); } } @@ -149,6 +146,28 @@ pub async fn do_update_instance( info!("Locations for instance {instance} didn't change. Not updating them."); } + sync_service_locations(transaction, instance).await?; + + Ok(locations_changed_val) +} + +pub async fn sync_service_locations( + transaction: &mut Transaction<'_, Sqlite>, + instance: &Instance, +) -> Result<(), Error> { + let mut service_locations = Vec::new(); + let current_locations = + Location::find_by_instance_id(transaction.as_mut(), instance.id, true).await?; + for location in current_locations { + if location.is_service_location() { + debug!( + "Adding service location {}({}) for instance {}({}) to be saved to the daemon.", + location.name, location.id, instance.name, instance.id, + ); + service_locations.push(to_service_location(&location)?); + } + } + if service_locations.is_empty() { debug!( "No service locations for instance {}({}), removing all existing service locations.", @@ -232,7 +251,7 @@ pub async fn do_update_instance( } } - Ok(locations_changed_val) + Ok(()) } pub async fn disable_enterprise_features<'e, E>( diff --git a/src-tauri/enterprise/config-sync/src/lib.rs b/src-tauri/enterprise/config-sync/src/lib.rs index c550b859..8fe971b5 100644 --- a/src-tauri/enterprise/config-sync/src/lib.rs +++ b/src-tauri/enterprise/config-sync/src/lib.rs @@ -20,7 +20,7 @@ use semver::Version; use serde::Serialize; use sqlx::{Sqlite, Transaction}; -use crate::commands::{disable_enterprise_features, do_update_instance}; +use crate::commands::{disable_enterprise_features, do_update_instance, sync_service_locations}; static POLLING_ENDPOINT: &str = "/api/v1/poll"; @@ -177,6 +177,9 @@ pub async fn poll_instance( "Config for instance {}({}) didn't change", instance.name, instance.id ); + if !has_active_connections { + sync_service_locations(transaction, instance).await?; + } return Ok(PollInstanceResult::Unchanged { version_mismatch }); } From 5fbb0e7897b6a7a74ad57bb17be7e2c40df375c2 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 2 Jul 2026 09:12:07 +0200 Subject: [PATCH 06/11] shared reconnect constants --- src-tauri/daemon/src/daemon.rs | 8 ++++---- src-tauri/daemon/src/windows.rs | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src-tauri/daemon/src/daemon.rs b/src-tauri/daemon/src/daemon.rs index c01e4cb2..6fd85175 100644 --- a/src-tauri/daemon/src/daemon.rs +++ b/src-tauri/daemon/src/daemon.rs @@ -57,10 +57,10 @@ pub(super) const DAEMON_SOCKET_PATH: &str = "/var/run/defguard.socket"; #[cfg(target_os = "linux")] pub(super) const DAEMON_SOCKET_GROUP: &str = "defguard"; -#[cfg(target_os = "linux")] -const SERVICE_LOCATION_CONNECT_RETRY_COUNT: u32 = 5; -#[cfg(target_os = "linux")] -const SERVICE_LOCATION_CONNECT_RETRY_DELAY: Duration = Duration::from_secs(30); +#[cfg(any(windows, target_os = "linux"))] +pub(crate) const SERVICE_LOCATION_CONNECT_RETRY_COUNT: u32 = 5; +#[cfg(any(windows, target_os = "linux"))] +pub(crate) const SERVICE_LOCATION_CONNECT_RETRY_DELAY: Duration = Duration::from_secs(30); #[derive(Debug, thiserror::Error)] pub enum DaemonError { diff --git a/src-tauri/daemon/src/windows.rs b/src-tauri/daemon/src/windows.rs index e986982c..8230a02d 100644 --- a/src-tauri/daemon/src/windows.rs +++ b/src-tauri/daemon/src/windows.rs @@ -24,15 +24,16 @@ use windows_service::{ use crate::{ config::Config, - daemon::{run_server, DaemonError}, + daemon::{ + run_server, DaemonError, SERVICE_LOCATION_CONNECT_RETRY_COUNT, + SERVICE_LOCATION_CONNECT_RETRY_DELAY, + }, utils::logging_setup, }; static SERVICE_NAME: &str = "DefguardService"; const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; const LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS: Duration = Duration::from_secs(5); -const SERVICE_LOCATION_CONNECT_RETRY_COUNT: u32 = 5; -const SERVICE_LOCATION_CONNECT_RETRY_DELAY: Duration = Duration::from_secs(30); pub fn run() -> Result<(), windows_service::Error> { // Register generated `ffi_service_main` with the system and start the service, blocking From 98b4e4ca1ac83383f9dd1ca57cdc6c44c58190da Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 2 Jul 2026 09:59:39 +0200 Subject: [PATCH 07/11] make windows and linux service location reconnect beharior consistent --- .../enterprise/service-locations/src/linux.rs | 37 +++++++++++++------ .../service-locations/src/windows.rs | 8 ++++ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src-tauri/enterprise/service-locations/src/linux.rs b/src-tauri/enterprise/service-locations/src/linux.rs index b9bd7c6d..d0dd6c8b 100644 --- a/src-tauri/enterprise/service-locations/src/linux.rs +++ b/src-tauri/enterprise/service-locations/src/linux.rs @@ -58,6 +58,7 @@ impl ServiceLocationManager { service_locations.len(), ); + debug!("Service locations to save: {service_locations:?}"); let old_locations = self .load_service_locations_for_instance(instance_id)? .map_or_else(Vec::new, |data| data.service_locations); @@ -76,14 +77,6 @@ impl ServiceLocationManager { .map(|location| location.pubkey.clone()) .collect::>(); - if service_locations.is_empty() { - debug!("No Linux-supported service locations to save for instance {instance_id}"); - for removed_pubkey in old_pubkeys { - self.disconnect_service_location(instance_id, &removed_pubkey)?; - } - return self.delete_all_service_locations_for_instance(instance_id); - } - let service_location_data = ServiceLocationData { service_locations: service_locations.clone(), instance_id: instance_id.to_string(), @@ -95,7 +88,7 @@ impl ServiceLocationManager { let json = serde_json::to_string_pretty(&service_location_data)?; debug!( - "Writing Linux service location data to file: {}", + "Writing service location data to file: {}", instance_file_path.display() ); fs::write(&instance_file_path, json)?; @@ -109,9 +102,31 @@ impl ServiceLocationManager { for removed_pubkey in old_pubkeys.difference(&new_pubkeys) { self.disconnect_service_location(instance_id, removed_pubkey)?; } + + let mut reset_failed = false; for location in &service_locations { - self.disconnect_service_location(instance_id, &location.pubkey)?; - self.connect_service_location(instance_id, location, private_key)?; + if let Err(err) = self.disconnect_service_location(instance_id, &location.pubkey) { + error!( + "Failed to disconnect Linux service location '{}' before reconnecting: {err}", + location.name + ); + reset_failed = true; + continue; + } + + if let Err(err) = self.connect_service_location(instance_id, location, private_key) { + warn!( + "Failed to connect Linux service location '{}' after saving: {err}", + location.name + ); + reset_failed = true; + } + } + + if reset_failed { + return Err(ServiceLocationError::InterfaceError(format!( + "Failed to connect one or more Linux service locations for instance {instance_id}" + ))); } Ok(()) diff --git a/src-tauri/enterprise/service-locations/src/windows.rs b/src-tauri/enterprise/service-locations/src/windows.rs index 371f9f0d..3133ef4d 100644 --- a/src-tauri/enterprise/service-locations/src/windows.rs +++ b/src-tauri/enterprise/service-locations/src/windows.rs @@ -885,6 +885,7 @@ impl ServiceLocationManager { self.disconnect_service_location(instance_id, removed_pubkey)?; } + let mut reset_failed = false; for saved_location in service_locations { match self.reset_service_location_state(instance_id, &saved_location.pubkey) { Ok(()) => { @@ -898,10 +899,17 @@ impl ServiceLocationManager { "Failed to reset state for service location '{}': {err}", saved_location.name ); + reset_failed = true; } } } + if reset_failed { + return Err(ServiceLocationError::InterfaceError(format!( + "Failed to reset one or more service locations for instance {instance_id}" + ))); + } + Ok(()) } From bf76b77aa835b7f55705fc9cb47d15504c3e5cdd Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 2 Jul 2026 10:04:17 +0200 Subject: [PATCH 08/11] reset_service_location_state for linux --- .../enterprise/service-locations/src/linux.rs | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src-tauri/enterprise/service-locations/src/linux.rs b/src-tauri/enterprise/service-locations/src/linux.rs index d0dd6c8b..90ad2ddf 100644 --- a/src-tauri/enterprise/service-locations/src/linux.rs +++ b/src-tauri/enterprise/service-locations/src/linux.rs @@ -105,18 +105,10 @@ impl ServiceLocationManager { let mut reset_failed = false; for location in &service_locations { - if let Err(err) = self.disconnect_service_location(instance_id, &location.pubkey) { - error!( - "Failed to disconnect Linux service location '{}' before reconnecting: {err}", - location.name - ); - reset_failed = true; - continue; - } - - if let Err(err) = self.connect_service_location(instance_id, location, private_key) { + if let Err(err) = self.reset_service_location_state(instance_id, location, private_key) + { warn!( - "Failed to connect Linux service location '{}' after saving: {err}", + "Failed to reset Linux service location '{}' after saving: {err}", location.name ); reset_failed = true; @@ -132,6 +124,27 @@ impl ServiceLocationManager { Ok(()) } + fn reset_service_location_state( + &mut self, + instance_id: &str, + location: &ServiceLocation, + private_key: &str, + ) -> Result<(), ServiceLocationError> { + debug!( + "Resetting Linux service location '{}' for instance {instance_id}", + location.name + ); + + self.disconnect_service_location(instance_id, &location.pubkey)?; + self.connect_service_location(instance_id, location, private_key)?; + + debug!( + "Linux service location '{}' state reset successfully", + location.name + ); + Ok(()) + } + fn add_connected_service_location(&mut self, instance_id: &str, location: &ServiceLocation) { self.connected_service_locations .entry(instance_id.to_string()) From 7d8f63b9fe83f5bba9f5b5aa267dfba93b935156 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 2 Jul 2026 10:42:37 +0200 Subject: [PATCH 09/11] shared connect_service_locations function with retry loop --- src-tauri/Cargo.lock | 1 + src-tauri/daemon/src/daemon.rs | 48 +++------------ src-tauri/daemon/src/windows.rs | 60 ++++--------------- .../enterprise/service-locations/Cargo.toml | 1 + .../enterprise/service-locations/src/lib.rs | 45 ++++++++++++++ 5 files changed, 67 insertions(+), 88 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6126e667..76003bd3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1831,6 +1831,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", + "tokio", "windows 0.62.2", "windows-acl", "windows-service", diff --git a/src-tauri/daemon/src/daemon.rs b/src-tauri/daemon/src/daemon.rs index 6fd85175..ff94edb1 100644 --- a/src-tauri/daemon/src/daemon.rs +++ b/src-tauri/daemon/src/daemon.rs @@ -558,47 +558,13 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { #[cfg(target_os = "linux")] let service_location_manager = Arc::new(RwLock::new(ServiceLocationManager::init()?)); #[cfg(target_os = "linux")] - { - let service_location_manager_connect = service_location_manager.clone(); - tokio::spawn(async move { - for attempt in 1..=SERVICE_LOCATION_CONNECT_RETRY_COUNT { - info!( - "Attempting to auto-connect Linux service locations \ - (attempt {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT})" - ); - match service_location_manager_connect - .write() - .unwrap() - .connect_to_service_locations() - { - Ok(true) => { - info!( - "All Linux service locations connected successfully \ - (attempt {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT})" - ); - break; - } - Ok(false) => { - warn!( - "Linux service location auto-connect attempt \ - {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT} completed with some failures" - ); - } - Err(err) => { - warn!( - "Linux service location auto-connect attempt \ - {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT} failed: {err}" - ); - } - } - - if attempt < SERVICE_LOCATION_CONNECT_RETRY_COUNT { - tokio::time::sleep(SERVICE_LOCATION_CONNECT_RETRY_DELAY).await; - } - } - info!("Linux service location auto-connect task finished"); - }); - } + tokio::spawn( + defguard_client_service_locations::connect_service_locations( + service_location_manager.clone(), + SERVICE_LOCATION_CONNECT_RETRY_COUNT, + SERVICE_LOCATION_CONNECT_RETRY_DELAY, + ), + ); let daemon_service = DaemonService::new( &config, diff --git a/src-tauri/daemon/src/windows.rs b/src-tauri/daemon/src/windows.rs index 8230a02d..a453253b 100644 --- a/src-tauri/daemon/src/windows.rs +++ b/src-tauri/daemon/src/windows.rs @@ -10,7 +10,7 @@ use defguard_client_service_locations::{ windows::{watch_for_login_logoff, watch_for_network_change}, ServiceLocationError, ServiceLocationManager, }; -use tokio::{runtime::Runtime, time::sleep}; +use tokio::runtime::Runtime; use tracing::{error, info, warn}; use windows_service::{ define_windows_service, @@ -128,52 +128,18 @@ fn run_service() -> Result<(), DaemonError> { }) .expect("Failed to spawn network change monitor thread"); - // Spawn service location auto-connect task with retries. - // Each attempt skips locations that are already connected, so it is safe to call - // connect_to_service_locations repeatedly. The retry loop exists to handle the case - // where the connection may fail initially at startup because the network - // (e.g. Wi-Fi) is not yet available (mainly DNS resolution issues), and serves as - // a backstop for any network events missed by the watcher above. - // If all locations connect successfully on a given attempt, no further retries are made. - let service_location_manager_connect = service_location_manager.clone(); - runtime.spawn(async move { - for attempt in 1..=SERVICE_LOCATION_CONNECT_RETRY_COUNT { - info!( - "Attempting to auto-connect to service locations \ - (attempt {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT})" - ); - match service_location_manager_connect - .write() - .unwrap() - .connect_to_service_locations() - { - Ok(true) => { - info!( - "All service locations connected successfully \ - (attempt {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT})" - ); - break; - } - Ok(false) => { - warn!( - "Auto-connect attempt {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT} \ - completed with some failures" - ); - } - Err(err) => { - warn!( - "Auto-connect attempt {attempt}/{SERVICE_LOCATION_CONNECT_RETRY_COUNT} \ - failed: {err}" - ); - } - } - - if attempt < SERVICE_LOCATION_CONNECT_RETRY_COUNT { - sleep(SERVICE_LOCATION_CONNECT_RETRY_DELAY).await; - } - } - info!("Service location auto-connect task finished"); - }); + // Spawn the service location auto-connect task with retries. Each attempt skips locations + // that are already connected, so it is safe to call repeatedly. The retry loop handles the + // case where the connection fails initially at startup because the network (e.g. Wi-Fi) is + // not yet available (mainly DNS resolution issues), and serves as a backstop for any + // network events missed by the watcher above. + runtime.spawn( + defguard_client_service_locations::connect_service_locations( + service_location_manager.clone(), + SERVICE_LOCATION_CONNECT_RETRY_COUNT, + SERVICE_LOCATION_CONNECT_RETRY_DELAY, + ), + ); // Spawn login/logoff monitoring on a dedicated OS thread so the blocking // WTSWaitSystemEvent syscall does not stall Tokio's async worker threads. diff --git a/src-tauri/enterprise/service-locations/Cargo.toml b/src-tauri/enterprise/service-locations/Cargo.toml index ba88f497..9d231fe8 100644 --- a/src-tauri/enterprise/service-locations/Cargo.toml +++ b/src-tauri/enterprise/service-locations/Cargo.toml @@ -19,6 +19,7 @@ prost = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } +tokio = { workspace = true, features = ["time"] } [target.'cfg(windows)'.dependencies] known-folders = "1.4" diff --git a/src-tauri/enterprise/service-locations/src/lib.rs b/src-tauri/enterprise/service-locations/src/lib.rs index 9685dfcb..311094c2 100644 --- a/src-tauri/enterprise/service-locations/src/lib.rs +++ b/src-tauri/enterprise/service-locations/src/lib.rs @@ -1,4 +1,9 @@ use std::{collections::HashMap, fmt}; +#[cfg(any(windows, target_os = "linux"))] +use std::{ + sync::{Arc, RwLock}, + time::Duration, +}; use defguard_client_core::{ database::models::{ @@ -11,6 +16,8 @@ use defguard_client_proto::defguard::client::v1::{ ServiceLocation, ServiceLocationMode as ProtoServiceLocationMode, }; use defguard_wireguard_rs::{error::WireguardInterfaceError, WGApi}; +#[cfg(any(windows, target_os = "linux"))] +use log::info; use log::warn; use serde::{Deserialize, Serialize}; @@ -124,3 +131,41 @@ pub fn to_service_location(location: &Location) -> Result>, + retry_count: u32, + retry_delay: Duration, +) { + for attempt in 1..=retry_count { + info!("Attempting to auto-connect service locations (attempt {attempt}/{retry_count})"); + match manager.write().unwrap().connect_to_service_locations() { + Ok(true) => { + info!( + "All service locations connected successfully (attempt {attempt}/{retry_count})" + ); + break; + } + Ok(false) => warn!( + "Service location auto-connect attempt {attempt}/{retry_count} completed with some \ + failures" + ), + Err(err) => { + warn!("Service location auto-connect attempt {attempt}/{retry_count} failed: {err}") + } + } + + if attempt < retry_count { + tokio::time::sleep(retry_delay).await; + } + } + + info!("Service location auto-connect task finished"); +} From 60f911792541aa99e121f86c13a9c8aa09d2b9cc Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 2 Jul 2026 11:56:50 +0200 Subject: [PATCH 10/11] avoid reconnecting service locations on each poll --- src-tauri/enterprise/config-sync/src/lib.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src-tauri/enterprise/config-sync/src/lib.rs b/src-tauri/enterprise/config-sync/src/lib.rs index 8fe971b5..53da4ec5 100644 --- a/src-tauri/enterprise/config-sync/src/lib.rs +++ b/src-tauri/enterprise/config-sync/src/lib.rs @@ -20,7 +20,7 @@ use semver::Version; use serde::Serialize; use sqlx::{Sqlite, Transaction}; -use crate::commands::{disable_enterprise_features, do_update_instance, sync_service_locations}; +use crate::commands::{disable_enterprise_features, do_update_instance}; static POLLING_ENDPOINT: &str = "/api/v1/poll"; @@ -171,15 +171,11 @@ pub async fn poll_instance( fetched.response.device_config.as_ref().ok_or_else(|| { Error::InternalError("Device config not present in response".to_string()) })?; - error!("DeviceConfig: {:#?}", device_config); if !config_changed(transaction, instance, device_config).await? { debug!( "Config for instance {}({}) didn't change", instance.name, instance.id ); - if !has_active_connections { - sync_service_locations(transaction, instance).await?; - } return Ok(PollInstanceResult::Unchanged { version_mismatch }); } From 40541189016862ae856bfaea65e105a5e912eb97 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 2 Jul 2026 12:32:52 +0200 Subject: [PATCH 11/11] rustdoc comments --- src-tauri/enterprise/config-sync/src/commands.rs | 5 +++++ src-tauri/enterprise/service-locations/src/linux.rs | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src-tauri/enterprise/config-sync/src/commands.rs b/src-tauri/enterprise/config-sync/src/commands.rs index 4a35936d..17df0334 100644 --- a/src-tauri/enterprise/config-sync/src/commands.rs +++ b/src-tauri/enterprise/config-sync/src/commands.rs @@ -151,6 +151,11 @@ pub async fn do_update_instance( Ok(locations_changed_val) } +/// Synchronizes the daemon's persisted service-location state from the current database state. +/// +/// This is called after a real config update has been applied locally. It sends all currently +/// persisted service locations for the instance to the daemon, or asks the daemon to delete its +/// service-location state when none remain. pub async fn sync_service_locations( transaction: &mut Transaction<'_, Sqlite>, instance: &Instance, diff --git a/src-tauri/enterprise/service-locations/src/linux.rs b/src-tauri/enterprise/service-locations/src/linux.rs index 90ad2ddf..39b41be2 100644 --- a/src-tauri/enterprise/service-locations/src/linux.rs +++ b/src-tauri/enterprise/service-locations/src/linux.rs @@ -47,6 +47,11 @@ impl ServiceLocationManager { Ok(Self::default()) } + /// Persists Linux-supported service locations and resets their runtime connection state. + /// + /// Linux supports Always-on service locations only. Unsupported modes are filtered out before + /// storage, stale previously-saved locations are disconnected, and every saved Always-on location + /// is reset. All resets are attempted before returning an aggregate error. pub fn save_service_locations( &mut self, service_locations: &[ServiceLocation], @@ -124,6 +129,7 @@ impl ServiceLocationManager { Ok(()) } + /// Reconnects one Linux always-on service location. fn reset_service_location_state( &mut self, instance_id: &str, @@ -145,6 +151,7 @@ impl ServiceLocationManager { Ok(()) } + /// Records a service location as connected in the in-memory daemon state. fn add_connected_service_location(&mut self, instance_id: &str, location: &ServiceLocation) { self.connected_service_locations .entry(instance_id.to_string()) @@ -317,6 +324,10 @@ impl ServiceLocationManager { Ok(()) } + /// Attempts to connect all persisted Linux always-on service locations. + /// + /// Returns `Ok(true)` when every supported location is connected or already connected, and + /// `Ok(false)` when at least one supported location failed so the caller can retry later. pub fn connect_to_service_locations(&mut self) -> Result { debug!("Attempting to auto-connect Linux Always-on service locations"); @@ -377,6 +388,7 @@ impl ServiceLocationManager { } #[allow(dead_code)] + /// Loads persisted service-location data for all Linux instances. fn load_service_locations(&self) -> Result, ServiceLocationError> { let base_dir = ensure_shared_directory()?; let mut all_locations_data = Vec::new(); @@ -405,6 +417,7 @@ impl ServiceLocationManager { Ok(all_locations_data) } + /// Loads persisted service-location data for one Linux instance, if present. fn load_service_locations_for_instance( &self, instance_id: &str,