Skip to content
Merged
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
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.3/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.6/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"react-auth-code-input": "^3.2.1",
"react-click-away-listener": "^2.4.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.66.0",
"react-hook-form": "^7.66.1",
"react-hotkeys-hook": "^5.2.1",
"react-loading-skeleton": "^3.5.0",
"react-markdown": "^10.1.0",
Expand All @@ -108,7 +108,7 @@
"zustand": "^5.0.8"
},
"devDependencies": {
"@biomejs/biome": "^2.3.5",
"@biomejs/biome": "^2.3.6",
"@hookform/devtools": "^4.4.0",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.90.10",
Expand All @@ -117,7 +117,7 @@
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react-swc": "^4.2.2",
Expand Down
386 changes: 193 additions & 193 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

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

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

121 changes: 100 additions & 21 deletions src-tauri/src/apple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@ use std::{
net::IpAddr,
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicUsize, Ordering},
mpsc::channel,
Arc,
Arc, Mutex,
},
};

use block2::RcBlock;
use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask};
use objc2::{rc::Retained, runtime::AnyObject};
use objc2_foundation::{
ns_string, NSArray, NSDictionary, NSError, NSMutableArray, NSMutableDictionary, NSNumber,
NSString,
ns_string, NSArray, NSData, NSDictionary, NSError, NSMutableArray, NSMutableDictionary,
NSNumber, NSString,
};
use objc2_network_extension::{NETunnelProviderManager, NETunnelProviderProtocol};
use serde::Serialize;
use objc2_network_extension::{
NETunnelProviderManager, NETunnelProviderProtocol, NETunnelProviderSession,
};
use serde::{Deserialize, Serialize};
use sqlx::SqliteExecutor;

use crate::{
Expand All @@ -32,7 +34,9 @@ use crate::{
const PLUGIN_BUNDLE_ID: &str = "net.defguard.VPNExtension";

// Should match the declaration in Swift.
#[derive(Deserialize)]
#[repr(C)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Stats {
pub(crate) location_id: Option<Id>,
pub(crate) tunnel_id: Option<Id>,
Expand Down Expand Up @@ -75,7 +79,6 @@ fn manager_for_name(name: &str) -> Option<Retained<NETunnelProviderManager>> {
}
}
if let Some(descr) = unsafe { manager.localizedDescription() } {
error!("Descripion {descr}");
if descr == name_string {
tx.send(Some(manager)).unwrap();
return;
Expand All @@ -94,21 +97,17 @@ fn manager_for_name(name: &str) -> Option<Retained<NETunnelProviderManager>> {
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TunnelConfiguration {
#[serde(rename = "locationId")]
location_id: Option<Id>,
#[serde(rename = "tunnelId")]
tunnel_id: Option<Id>,
name: String,
#[serde(rename = "privateKey")]
private_key: String,
addresses: Vec<IpAddrMask>,
#[serde(rename = "listenPort")]
listen_port: Option<u16>,
peers: Vec<Peer>,
mtu: Option<u32>,
dns: Vec<IpAddr>,
#[serde(rename = "dnsSearch")]
dns_search: Vec<String>,
}

Expand Down Expand Up @@ -307,9 +306,93 @@ pub(crate) fn stop_tunnel(name: &str) -> bool {
}
}

/// IMPORTANT: This is currently for testing. Assume the config has been saved.
pub(crate) fn all_tunnel_stats() -> Vec<Stats> {
Vec::<Stats>::new()
pub(crate) fn tunnel_stats() -> Vec<Stats> {
let new_stats = Arc::new(Mutex::new(Vec::new()));
let plugin_bundle_id = NSString::from_str(PLUGIN_BUNDLE_ID);
let spinlock = Arc::new(AtomicUsize::new(1));

let new_stats_clone = Arc::clone(&new_stats);
let spinlock_for_response = Arc::clone(&spinlock);
let response_handler = RcBlock::new(move |data_ptr: *mut NSData| {
if let Some(data) = unsafe { data_ptr.as_ref() } {
if let Ok(stats) = serde_json::from_slice(data.to_vec().as_slice()) {
if let Ok(mut new_stats_locked) = new_stats_clone.lock() {
new_stats_locked.push(stats);
}
} else {
warn!("Failed to deserialize tunnel stats");
}
} else {
warn!("No data");
}
spinlock_for_response.fetch_sub(1, Ordering::Release);
});

let spinlock_for_handler = Arc::clone(&spinlock);
let handler = RcBlock::new(
move |managers_ptr: *mut NSArray<NETunnelProviderManager>, error_ptr: *mut NSError| {
if !error_ptr.is_null() {
error!("Failed to load tunnel provider managers.");
return;
}

let Some(managers) = (unsafe { managers_ptr.as_ref() }) else {
error!("No managers");
return;
};

for manager in managers {
let Some(vpn_protocol) = (unsafe { manager.protocolConfiguration() }) else {
continue;
};
let Ok(tunnel_protocol) = vpn_protocol.downcast::<NETunnelProviderProtocol>()
else {
error!("Failed to downcast to NETunnelProviderProtocol");
continue;
};
// Sometimes all managers from all apps come through, so filter by bundle ID.
if let Some(bundle_id) = unsafe { tunnel_protocol.providerBundleIdentifier() } {
if bundle_id != plugin_bundle_id {
continue;
}
}

let Ok(session) =
unsafe { manager.connection() }.downcast::<NETunnelProviderSession>()
else {
error!("Failed to downcast to NETunnelProviderSession");
continue;
};

let message_data = NSData::new();
if unsafe {
session.sendProviderMessage_returnError_responseHandler(
&message_data,
None,
Some(&response_handler),
)
} {
spinlock_for_handler.fetch_add(1, Ordering::Release);
info!("Message sent to NETunnelProviderSession");
} else {
error!("Failed to send to NETunnelProviderSession");
}
}
// Final release
spinlock_for_handler.fetch_sub(1, Ordering::Release);
},
);
unsafe {
NETunnelProviderManager::loadAllFromPreferencesWithCompletionHandler(&handler);
}

// Wait for all handlers to complete.
while spinlock.load(Ordering::Acquire) != 0 {
spin_loop();
}

let stats = new_stats.lock().unwrap().drain(..).collect();
stats
}

impl Location<Id> {
Expand Down Expand Up @@ -409,16 +492,12 @@ impl Location<Id> {
}

impl Tunnel<Id> {
pub(crate) async fn tunnel_configurarion<'e, E>(
pub(crate) fn tunnel_configurarion(
&self,
executor: E,
dns: Vec<IpAddr>,
dns_search: Vec<String>,
mtu: Option<u32>,
) -> Result<TunnelConfiguration, Error>
where
E: SqliteExecutor<'e>,
{
) -> Result<TunnelConfiguration, Error> {
// prepare peer config
debug!("Decoding tunnel {self} public key: {}.", self.server_pubkey);
let peer_key = Key::from_str(&self.server_pubkey)?;
Expand Down
21 changes: 13 additions & 8 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ const UPDATE_URL: &str = "https://pkgs.defguard.net/api/update/check";

#[cfg(target_os = "macos")]
use crate::apple::stop_tunnel;
#[cfg(not(target_os = "macos"))]
use crate::service::{
proto::{DeleteServiceLocationsRequest, RemoveInterfaceRequest, SaveServiceLocationsRequest},
utils::DAEMON_CLIENT,
};
use crate::{
active_connections::{find_connection, get_connection_id_by_type},
app_config::{AppConfig, AppConfigPatch},
Expand All @@ -46,13 +41,23 @@ use crate::{
proto::DeviceConfigResponse,
tray::{configure_tray_icon, reload_tray_menu},
utils::{
construct_platform_header, disconnect_interface, execute_command,
get_location_interface_details, get_tunnel_interface_details, get_tunnel_or_location_name,
handle_connection_for_location, handle_connection_for_tunnel,
construct_platform_header, disconnect_interface, get_location_interface_details,
get_tunnel_interface_details, get_tunnel_or_location_name, handle_connection_for_location,
handle_connection_for_tunnel,
},
wg_config::parse_wireguard_config,
CommonConnection, CommonConnectionInfo, CommonLocationStats, ConnectionType,
};
#[cfg(not(target_os = "macos"))]
use crate::{
service::{
proto::{
DeleteServiceLocationsRequest, RemoveInterfaceRequest, SaveServiceLocationsRequest,
},
utils::DAEMON_CLIENT,
},
utils::execute_command,
};

/// Open new WireGuard connection.
#[tauri::command(async)]
Expand Down
24 changes: 24 additions & 0 deletions src-tauri/src/database/models/tunnel.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(target_os = "macos")]
use std::net::IpAddr;
use std::{fmt, time::SystemTime};

use chrono::{NaiveDateTime, Utc};
Expand Down Expand Up @@ -252,6 +254,28 @@ impl Tunnel<NoId> {
}
}

impl Tunnel<Id> {
/// Split DNS settings into resolver IP addresses and search domains.
#[cfg(target_os = "macos")]
pub(crate) fn dns(&self) -> (Vec<IpAddr>, Vec<String>) {
let mut dns = Vec::new();
let mut dns_search = Vec::new();

if let Some(dns_string) = &self.dns {
for entry in dns_string.split(',').map(str::trim) {
// Assume that every entry that can't be parsed as an IP address is a domain name.
if let Ok(ip) = entry.parse::<IpAddr>() {
dns.push(ip);
} else {
dns_search.push(entry.into());
}
}
}

(dns, dns_search)
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TunnelStats<I = NoId> {
id: I,
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub struct CommonLocationStats<I = NoId> {
pub persistent_keepalive_interval: Option<u16>,
pub connection_type: ConnectionType,
}

// Common fields for ConnectionInfo and TunnelConnectionInfo due to shared command
#[derive(Debug, Serialize)]
pub struct CommonConnectionInfo {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/periodic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub async fn run_periodic_tasks(app_handle: &AppHandle) {
() = poll_config(app_handle.clone()) => {
error!("Config polling task has stopped unexpectedly");
}
() = verify_active_connections(app_handle.clone()), if cfg!(not(target_os = "macos")) => {
() = verify_active_connections(app_handle.clone()) => {
error!("Active connection verification task has stopped unexpectedly");
}
() = purge_stats() => {
Expand Down
Loading