Skip to content
Draft
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
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

122 changes: 53 additions & 69 deletions src-tauri/daemon/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(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 {
#[error(transparent)]
Expand Down Expand Up @@ -84,21 +89,23 @@ pub(crate) struct DaemonService {
wgapis: Arc<RwLock<HashMap<IfName, WG>>>,
stats_period: Duration,
stat_tasks: Arc<Mutex<HashMap<IfName, JoinHandle<()>>>>,
#[cfg(windows)]
#[cfg(any(windows, target_os = "linux"))]
service_location_manager: Arc<RwLock<ServiceLocationManager>>,
}

impl DaemonService {
#[must_use]
pub fn new(
config: &Config,
#[cfg(windows)] service_location_manager: Arc<RwLock<ServiceLocationManager>>,
#[cfg(any(windows, target_os = "linux"))] service_location_manager: Arc<
RwLock<ServiceLocationManager>,
>,
) -> 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,
}
}
Expand Down Expand Up @@ -178,7 +185,7 @@ pub(crate) fn setup_wgapi(ifname: &str) -> Result<WG, Status> {
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<SaveServiceLocationsRequest>,
Expand All @@ -187,7 +194,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<DeleteServiceLocationsRequest>,
Expand All @@ -196,57 +203,29 @@ impl DesktopDaemonService for DaemonService {
Ok(Response::new(()))
}

#[cfg(windows)]
#[cfg(any(windows, target_os = "linux"))]
async fn save_service_locations(
&self,
request: tonic::Request<SaveServiceLocationsRequest>,
) -> Result<Response<()>, Status> {
debug!("Received a request to save service location");
debug!("Received a request to save service locations");
let service_location = request.into_inner();

match self
.service_location_manager
.clone()
.read()
self.service_location_manager
.write()
.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}");
)
.map_err(|err| {
let msg = format!("Failed to save service locations: {err}");
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
);
}
}
}
Status::internal(msg)
})?;

debug!("Service locations saved successfully");
Ok(Response::new(()))
}

Expand All @@ -263,43 +242,33 @@ impl DesktopDaemonService for DaemonService {
))
}

#[cfg(windows)]
#[cfg(any(windows, target_os = "linux"))]
async fn delete_service_locations(
&self,
request: tonic::Request<DeleteServiceLocationsRequest>,
) -> Result<Response<()>, 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
.clone()
.write()
.unwrap()
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 location: {err}");
let msg = format!("Failed to disconnect service locations: {err}");
error!(msg);
Status::internal(msg)
})?;

match self
.service_location_manager
.clone()
.read()
.unwrap()
manager
.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 location: {err}"
)))
}
}
.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(()))
}

async fn create_interface(
Expand Down Expand Up @@ -586,7 +555,22 @@ 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()?));
#[cfg(target_os = "linux")]
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,
#[cfg(target_os = "linux")]
service_location_manager,
);

// Remove existing socket if it exists
if Path::new(DAEMON_SOCKET_PATH).exists() {
Expand Down
67 changes: 17 additions & 50 deletions src-tauri/daemon/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -127,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.
Expand Down
34 changes: 29 additions & 5 deletions src-tauri/enterprise/config-sync/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}({}).",
Expand Down Expand Up @@ -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)?);
}
}

Expand All @@ -149,6 +146,33 @@ 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)
}

/// 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<Id>,
) -> 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.",
Expand Down Expand Up @@ -232,7 +256,7 @@ pub async fn do_update_instance(
}
}

Ok(locations_changed_val)
Ok(())
}

pub async fn disable_enterprise_features<'e, E>(
Expand Down
1 change: 0 additions & 1 deletion src-tauri/enterprise/config-sync/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ pub async fn poll_instance(
fetched.response.device_config.as_ref().ok_or_else(|| {
Error::InternalError("Device config not present in response".to_string())
})?;

if !config_changed(transaction, instance, device_config).await? {
debug!(
"Config for instance {}({}) didn't change",
Expand Down
1 change: 1 addition & 0 deletions src-tauri/enterprise/service-locations/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading