Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion .trivyignore.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
vulnerabilities:
- id: GHSA-wrw7-89jp-8q8g
expired_at: 2026-04-18
expired_at: 2026-05-16
statement: 'glib is a transitive dependency of Tauri which we cannot update ourselves. Waiting for tauri to finish migration to gtk4-rs: https://github.com/tauri-apps/tauri/issues/12563'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"html-react-parser": "^5.2.17",
"itertools": "^2.6.0",
"js-base64": "^3.7.8",
"lodash-es": "^4.17.23",
"lodash-es": "^4.18.1",
"merge-refs": "^2.0.0",
"millify": "^6.1.0",
"motion": "^12.38.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

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

6 changes: 3 additions & 3 deletions src-tauri/Cargo.lock

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

3 changes: 3 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ windows-sys = { version = "0.61", features = [
# HANDLE & file functions
"Win32_System_IO",
"Win32_System_Threading",

# Network address change notifications (NotifyAddrChange)
"Win32_NetworkManagement_IpHelper",
] }

[features]
Expand Down
70 changes: 66 additions & 4 deletions src-tauri/src/enterprise/service_locations/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use defguard_wireguard_rs::{
};
use known_folders::get_known_folder_path;
use log::{debug, error, warn};
use tokio::time::sleep;
use windows::{
core::PSTR,
Win32::System::RemoteDesktop::{
Expand All @@ -23,6 +24,7 @@ use windows::{
},
};
use windows_acl::acl::ACL;
use windows_sys::Win32::NetworkManagement::IpHelper::NotifyAddrChange;

use crate::{
enterprise::service_locations::{
Expand All @@ -36,10 +38,62 @@ use crate::{
};

const LOGIN_LOGOFF_EVENT_RETRY_DELAY_SECS: u64 = 5;
// How long to wait after a network change before attempting to connect.
// Gives DHCP time to complete and DNS to become available.
const NETWORK_STABILIZATION_DELAY_SECS: u64 = 3;
Comment thread
t-aleksander marked this conversation as resolved.
Outdated
// How long to wait before restarting the network change watcher on error.
const NETWORK_CHANGE_MONITOR_RESTART_DELAY_SECS: u64 = 5;
Comment thread
t-aleksander marked this conversation as resolved.
Outdated
const DEFAULT_WIREGUARD_PORT: u16 = 51820;
const DEFGUARD_DIR: &str = "Defguard";
const SERVICE_LOCATIONS_SUBDIR: &str = "service_locations";

/// Watches for IP address changes on any network interface and attempts to connect to any
/// service locations that are not yet connected. This handles the case where the endpoint
/// hostname cannot be resolved at service startup because the network (e.g. Wi-Fi) is not
/// yet available. When the network comes up and an IP is assigned, this watcher fires and
/// retries the connection.
///
/// Note: `NotifyAddrChange` also fires when WireGuard interfaces are created. This is
/// harmless because `connect_to_service_locations` skips already-connected locations.
pub(crate) async fn watch_for_network_change(
service_location_manager: Arc<RwLock<ServiceLocationManager>>,
) -> Result<(), ServiceLocationError> {
loop {
// NotifyAddrChange blocks until any IP address is added or removed on any interface.
// Passing NULL for both handle and overlapped selects the synchronous (blocking) mode.
let result = unsafe { NotifyAddrChange(std::ptr::null_mut(), std::ptr::null()) };

if result != 0 {
error!("NotifyAddrChange failed with error code: {result}");
sleep(Duration::from_secs(
Comment thread
t-aleksander marked this conversation as resolved.
Outdated
NETWORK_CHANGE_MONITOR_RESTART_DELAY_SECS,
))
.await;
continue;
}

debug!(
"Network address change detected, waiting {NETWORK_STABILIZATION_DELAY_SECS}s for \
Comment thread
t-aleksander marked this conversation as resolved.
Outdated
network to stabilize before attempting service location connections..."
);
sleep(Duration::from_secs(NETWORK_STABILIZATION_DELAY_SECS)).await;
Comment thread
t-aleksander marked this conversation as resolved.
Outdated

debug!("Attempting to connect to service locations after network change");
match service_location_manager
.write()
.unwrap()
.connect_to_service_locations()
{
Ok(_) => {
debug!("Service location connect attempt after network change completed");
}
Err(err) => {
warn!("Failed to connect to service locations after network change: {err}");
}
}
}
}

pub(crate) async fn watch_for_login_logoff(
service_location_manager: Arc<RwLock<ServiceLocationManager>>,
) -> Result<(), ServiceLocationError> {
Expand All @@ -59,7 +113,7 @@ pub(crate) async fn watch_for_login_logoff(
}
Err(err) => {
error!("Failed waiting for login/logoff event: {err:?}");
tokio::time::sleep(Duration::from_secs(LOGIN_LOGOFF_EVENT_RETRY_DELAY_SECS)).await;
sleep(Duration::from_secs(LOGIN_LOGOFF_EVENT_RETRY_DELAY_SECS)).await;
continue;
}
};
Expand Down Expand Up @@ -680,12 +734,19 @@ impl ServiceLocationManager {
Ok(())
}

pub(crate) fn connect_to_service_locations(&mut self) -> Result<(), ServiceLocationError> {
/// Attempts to connect to all service locations that are not already connected.
///
/// Returns `Ok(true)` if every location is now connected (either it was already connected or
/// it was successfully connected during this call), and `Ok(false)` if at least one location
/// failed to connect (indicating that a retry may be worthwhile).
pub(crate) fn connect_to_service_locations(&mut self) -> Result<bool, ServiceLocationError> {
debug!("Attempting to auto-connect to VPN...");

let data = self.load_service_locations()?;
debug!("Loaded {} instance(s) from ServiceLocationApi", data.len());

let mut all_connected = true;

for instance_data in data {
debug!(
"Found service locations for instance ID: {}",
Expand Down Expand Up @@ -725,10 +786,11 @@ impl ServiceLocationManager {
if let Err(err) =
self.setup_service_location_interface(&location, &instance_data.private_key)
{
debug!(
warn!(
"Failed to setup service location interface for '{}': {err:?}",
location.name
);
all_connected = false;
continue;
}

Expand All @@ -749,7 +811,7 @@ impl ServiceLocationManager {

debug!("Auto-connect attempt completed");

Ok(())
Ok(all_connected)
}

pub fn save_service_locations(
Expand Down
99 changes: 84 additions & 15 deletions src-tauri/src/service/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::{

use clap::Parser;
use error;
use tokio::runtime::Runtime;
use tokio::{runtime::Runtime, time::sleep};
use windows_service::{
define_windows_service,
service::{
Expand All @@ -20,7 +20,8 @@ use windows_service::{

use crate::{
enterprise::service_locations::{
windows::watch_for_login_logoff, ServiceLocationError, ServiceLocationManager,
windows::{watch_for_login_logoff, watch_for_network_change},
ServiceLocationError, ServiceLocationManager,
},
service::{
config::Config,
Expand All @@ -32,6 +33,9 @@ use crate::{
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 NETWORK_CHANGE_MONITORING_RESTART_DELAY_SECS: Duration = Duration::from_secs(5);
const SERVICE_LOCATION_CONNECT_RETRY_COUNT: u32 = 5;
const SERVICE_LOCATION_CONNECT_RETRY_DELAY_SECS: u64 = 30;
Comment thread
t-aleksander marked this conversation as resolved.
Outdated

pub fn run() -> Result<(), windows_service::Error> {
// Register generated `ffi_service_main` with the system and start the service, blocking
Expand Down Expand Up @@ -112,25 +116,90 @@ fn run_service() -> Result<(), DaemonError> {

let service_location_manager = Arc::new(RwLock::new(service_location_manager));

// Spawn service location management task
// Spawn network change monitoring task first so NotifyAddrChange is registered as early
// as possible, minimising the window in which a network event could be missed before
// the watcher is listening. The retry task below is the backstop for any event that
// still slips through that window.
let service_location_manager_clone = service_location_manager.clone();
runtime.spawn(async move {
let manager = service_location_manager_clone;

info!("Starting service location management task");
info!("Starting network change monitoring");
loop {
match watch_for_network_change(manager.clone()).await {
Ok(()) => {
warn!(
"Network change monitoring ended unexpectedly. Restarting in \
{NETWORK_CHANGE_MONITORING_RESTART_DELAY_SECS:?}..."
);
sleep(NETWORK_CHANGE_MONITORING_RESTART_DELAY_SECS).await;
Comment thread
t-aleksander marked this conversation as resolved.
Outdated
}
Err(e) => {
error!(
"Error in network change monitoring: {e}. Restarting in \
{NETWORK_CHANGE_MONITORING_RESTART_DELAY_SECS:?}...",
);
sleep(NETWORK_CHANGE_MONITORING_RESTART_DELAY_SECS).await;
info!("Restarting network change monitoring");
}
}
}
});

info!("Attempting to auto-connect to service locations");
match manager.write().unwrap().connect_to_service_locations() {
Ok(()) => {
info!("Auto-connect to service locations completed successfully");
// 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}"
);
}
}
Err(err) => {
warn!(
"Error while trying to auto-connect to service locations: {err}. \
Will continue monitoring for login/logoff events.",
);

if attempt < SERVICE_LOCATION_CONNECT_RETRY_COUNT {
sleep(Duration::from_secs(
SERVICE_LOCATION_CONNECT_RETRY_DELAY_SECS,
))
.await;
}
}
info!("Service location auto-connect task finished");
});

// Spawn login/logoff monitoring task, runs concurrently with the tasks above.
let service_location_manager_clone = service_location_manager.clone();
runtime.spawn(async move {
let manager = service_location_manager_clone;

info!("Starting login/logoff event monitoring");
loop {
Expand All @@ -140,14 +209,14 @@ fn run_service() -> Result<(), DaemonError> {
"Login/logoff event monitoring ended unexpectedly. Restarting in \
{LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS:?}..."
);
tokio::time::sleep(LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS).await;
sleep(LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS).await;
}
Err(e) => {
error!(
"Error in login/logoff event monitoring: {e}. Restarting in \
{LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS:?}...",
);
tokio::time::sleep(LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS).await;
sleep(LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS).await;
info!("Restarting login/logoff event monitoring");
}
}
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/wg_config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{array::TryFromSliceError, net::IpAddr, path::Path};

use base64::{prelude::BASE64_STANDARD, DecodeError, Engine};
use std::path::Path;
use std::{array::TryFromSliceError, net::IpAddr};
use thiserror::Error;
use x25519_dalek::{PublicKey, StaticSecret};

Expand Down
Loading