diff --git a/package.json b/package.json index be08be0d..7b810c47 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@react-hook/resize-observer": "^2.0.2", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", - "@tanstack/query-core": "^5.90.12", + "@tanstack/query-core": "^5.90.14", "@tanstack/react-virtual": "3.13.13", "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", @@ -115,8 +115,8 @@ "@biomejs/biome": "^2.3.10", "@hookform/devtools": "^4.4.0", "@svgr/cli": "^8.1.0", - "@tanstack/react-query": "^5.90.12", - "@tanstack/react-query-devtools": "^5.91.1", + "@tanstack/react-query": "^5.90.14", + "@tanstack/react-query-devtools": "^5.91.2", "@tauri-apps/cli": "^2.9.6", "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95b49044..db69f83d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^2.0.1 version: 2.0.1 '@tanstack/query-core': - specifier: ^5.90.12 - version: 5.90.12 + specifier: ^5.90.14 + version: 5.90.14 '@tanstack/react-virtual': specifier: 3.13.13 version: 3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -187,7 +187,7 @@ importers: version: 3.25.76 zustand: specifier: ^5.0.9 - version: 5.0.9(@types/react@19.2.7)(immer@11.1.0)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) + version: 5.0.9(@types/react@19.2.7)(immer@11.1.3)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: '@biomejs/biome': specifier: ^2.3.10 @@ -199,11 +199,11 @@ importers: specifier: ^8.1.0 version: 8.1.0(typescript@5.9.3) '@tanstack/react-query': - specifier: ^5.90.12 - version: 5.90.12(react@19.2.3) + specifier: ^5.90.14 + version: 5.90.14(react@19.2.3) '@tanstack/react-query-devtools': - specifier: ^5.91.1 - version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3) + specifier: ^5.91.2 + version: 5.91.2(@tanstack/react-query@5.90.14(react@19.2.3))(react@19.2.3) '@tauri-apps/cli': specifier: ^2.9.6 version: 2.9.6 @@ -1092,20 +1092,20 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tanstack/query-core@5.90.12': - resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + '@tanstack/query-core@5.90.14': + resolution: {integrity: sha512-/6di2yNI+YxpVrH9Ig74Q+puKnkCE+D0LGyagJEGndJHJc6ahkcc/UqirHKy8zCYE/N9KLggxcQvzYCsUBWgdw==} - '@tanstack/query-devtools@5.91.1': - resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==} + '@tanstack/query-devtools@5.92.0': + resolution: {integrity: sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==} - '@tanstack/react-query-devtools@5.91.1': - resolution: {integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==} + '@tanstack/react-query-devtools@5.91.2': + resolution: {integrity: sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==} peerDependencies: - '@tanstack/react-query': ^5.90.10 + '@tanstack/react-query': ^5.90.14 react: ^18 || ^19 - '@tanstack/react-query@5.90.12': - resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} + '@tanstack/react-query@5.90.14': + resolution: {integrity: sha512-JAMuULej09hrZ14W9+mxoRZ44rR2BuZfCd6oKTQVNfynQxCN3muH3jh3W46gqZNw5ZqY0ZVaS43Imb3dMr6tgw==} peerDependencies: react: ^18 || ^19 @@ -1929,8 +1929,8 @@ packages: immer@10.2.0: resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - immer@11.1.0: - resolution: {integrity: sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==} + immer@11.1.3: + resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==} immutable@5.1.4: resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} @@ -3476,7 +3476,7 @@ snapshots: dependencies: '@standard-schema/spec': 1.1.0 '@standard-schema/utils': 0.3.0 - immer: 11.1.0 + immer: 11.1.3 redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.1 @@ -3760,19 +3760,19 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/query-core@5.90.12': {} + '@tanstack/query-core@5.90.14': {} - '@tanstack/query-devtools@5.91.1': {} + '@tanstack/query-devtools@5.92.0': {} - '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3)': + '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.14(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/query-devtools': 5.91.1 - '@tanstack/react-query': 5.90.12(react@19.2.3) + '@tanstack/query-devtools': 5.92.0 + '@tanstack/react-query': 5.90.14(react@19.2.3) react: 19.2.3 - '@tanstack/react-query@5.90.12(react@19.2.3)': + '@tanstack/react-query@5.90.14(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.90.12 + '@tanstack/query-core': 5.90.14 react: 19.2.3 '@tanstack/react-virtual@3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': @@ -4660,7 +4660,7 @@ snapshots: immer@10.2.0: {} - immer@11.1.0: {} + immer@11.1.3: {} immutable@5.1.4: {} @@ -5963,10 +5963,10 @@ snapshots: zod@3.25.76: {} - zustand@5.0.9(@types/react@19.2.7)(immer@11.1.0)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): + zustand@5.0.9(@types/react@19.2.7)(immer@11.1.3)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): optionalDependencies: '@types/react': 19.2.7 - immer: 11.1.0 + immer: 11.1.3 react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d14d81d1..d71b6dd0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -547,9 +547,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -723,7 +723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" dependencies = [ "rust_decimal", - "schemars 1.1.0", + "schemars 1.2.0", "serde", "utf8-width", ] @@ -846,9 +846,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.50" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -1746,9 +1746,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -1979,9 +1979,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fixedbitset" @@ -3046,9 +3046,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -3090,9 +3090,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "javascriptcore-rs" @@ -3301,13 +3301,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.6.0", + "redox_syscall 0.7.0", ] [[package]] @@ -4698,9 +4698,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -5051,9 +5051,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ "bitflags 2.10.0", ] @@ -5385,9 +5385,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -5436,9 +5436,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -5615,15 +5615,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.146" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -5679,7 +5679,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -5769,10 +5769,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -6833,9 +6834,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -8407,13 +8408,13 @@ dependencies = [ [[package]] name = "windows-service" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac" dependencies = [ "bitflags 2.10.0", "widestring 1.2.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9125,6 +9126,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zmij" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" + [[package]] name = "zune-core" version = "0.4.12" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 510bb550..a87bfc42 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -145,7 +145,7 @@ windows = { version = "0.62", features = [ "Win32_System_RemoteDesktop", ] } windows-acl = "0.3" -windows-service = "0.7" +windows-service = "0.8" windows-sys = { version = "0.61", features = [ # Core Win32 types "Win32_Foundation", diff --git a/src-tauri/cli/src/bin/dg.rs b/src-tauri/cli/src/bin/dg.rs index 94252fcc..eca53ba9 100644 --- a/src-tauri/cli/src/bin/dg.rs +++ b/src-tauri/cli/src/bin/dg.rs @@ -3,7 +3,6 @@ use std::os::unix::fs::PermissionsExt; use std::{ fmt, fs::{create_dir_all, OpenOptions}, - net::IpAddr, path::{Path, PathBuf}, str::FromStr, sync::Arc, @@ -11,7 +10,7 @@ use std::{ }; use clap::{builder::FalseyValueParser, command, value_parser, Arg, Command}; -use common::{find_free_tcp_port, get_interface_name}; +use common::{dns_borrow, find_free_tcp_port, get_interface_name}; #[cfg(not(target_os = "macos"))] use defguard_wireguard_rs::Kernel; #[cfg(target_os = "macos")] @@ -168,18 +167,8 @@ async fn connect(config: CliConfig, ifname: String, trigger: Arc) -> Res .expect("Failed to create WireGuard interface"); debug!("Preparing DNS configuration for interface {ifname}"); - let dns_string = config.device_config.dns.clone().unwrap_or_default(); - let dns_entries = dns_string.split(',').map(str::trim).collect::>(); // We assume that every entry that can't be parsed as an IP address is a domain name. - let mut dns = Vec::new(); - let mut search_domains = Vec::new(); - for entry in dns_entries { - if let Ok(ip) = entry.parse::() { - dns.push(ip); - } else { - search_domains.push(entry); - } - } + let (dns, search_domains) = dns_borrow(&config.device_config.dns); debug!( "DNS configuration for interface {ifname}: DNS: {dns:?}, Search domains: \ {search_domains:?}" @@ -227,7 +216,7 @@ async fn connect(config: CliConfig, ifname: String, trigger: Arc) -> Res .split(',') .filter_map(ip_addr_parser) .collect::>(); - debug!("Parsed assigned IPs: {:?}", addresses); + debug!("Parsed assigned IPs: {addresses:?}"); let config = InterfaceConfiguration { name: config.instance_info.name.clone(), diff --git a/src-tauri/common/src/lib.rs b/src-tauri/common/src/lib.rs index 4d2f836b..190fcbd3 100644 --- a/src-tauri/common/src/lib.rs +++ b/src-tauri/common/src/lib.rs @@ -32,6 +32,48 @@ pub fn get_interface_name(_name: &str) -> String { format!("{base_ifname}0") } +/// Split DNS settings into resolver IP addresses and search domains. +pub fn dns_owned(config: &Option) -> (Vec, Vec) { + let mut dns = Vec::new(); + let mut dns_search = Vec::new(); + + if let Some(dns_string) = config { + if !dns_string.is_empty() { + 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::() { + dns.push(ip); + } else { + dns_search.push(entry.into()); + } + } + } + } + + (dns, dns_search) +} + +/// Split DNS settings into resolver IP addresses and search domains. +pub fn dns_borrow(config: &Option) -> (Vec, Vec<&str>) { + let mut dns = Vec::new(); + let mut dns_search = Vec::new(); + + if let Some(dns_string) = config { + if !dns_string.is_empty() { + 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::() { + dns.push(ip); + } else { + dns_search.push(entry); + } + } + } + } + + (dns, dns_search) +} + /// Strips location name of all non-alphanumeric characters returning usable interface name. #[cfg(any(windows, target_os = "macos"))] #[must_use] diff --git a/src-tauri/src/apple.rs b/src-tauri/src/apple.rs index 4321adca..5d2958af 100644 --- a/src-tauri/src/apple.rs +++ b/src-tauri/src/apple.rs @@ -15,6 +15,7 @@ use std::{ }; use block2::RcBlock; +use common::dns_owned; use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask}; use objc2::{ rc::Retained, @@ -399,7 +400,7 @@ pub(crate) struct Stats { } /// Run [`NSRunLoop`] until semaphore becomes `true`. -pub fn spawn_runloop_and_wait_for(semaphore: Arc) { +pub fn spawn_runloop_and_wait_for(semaphore: &Arc) { const ONE_SECOND: f64 = 1.; let run_loop = NSRunLoop::currentRunLoop(); let mut date = NSDate::dateWithTimeIntervalSinceNow(ONE_SECOND); @@ -1096,7 +1097,7 @@ impl Location { error!("{msg}"); Error::InternalError(msg) })?; - let (dns, dns_search) = self.dns(); + let (dns, dns_search) = dns_owned(&self.dns); Ok(TunnelConfiguration { location_id: Some(self.id), tunnel_id: None, @@ -1180,7 +1181,7 @@ impl Tunnel { error!("{msg}"); Error::InternalError(msg) })?; - let (dns, dns_search) = self.dns(); + let (dns, dns_search) = dns_owned(&self.dns); Ok(TunnelConfiguration { location_id: None, tunnel_id: Some(self.id), diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 46039bda..e599fcde 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -103,7 +103,7 @@ async fn startup(app_handle: &AppHandle) { } semaphore_clone.store(true, Ordering::Release); }); - defguard_client::apple::spawn_runloop_and_wait_for(semaphore); + defguard_client::apple::spawn_runloop_and_wait_for(&semaphore); let _ = handle.await; let (tunnels, locations) = get_all_tunnels_locations().await; @@ -416,7 +416,7 @@ fn main() { }); // Obj-C API needs a runtime, but at this point Tauri has closed its runtime, so // create a temporary one. - defguard_client::apple::spawn_runloop_and_wait_for(semaphore); + defguard_client::apple::spawn_runloop_and_wait_for(&semaphore); tauri::async_runtime::block_on(async move { let _ = handle.await; }); diff --git a/src-tauri/src/database/models/location.rs b/src-tauri/src/database/models/location.rs index fdf7f876..53f15881 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -1,6 +1,4 @@ use std::fmt; -#[cfg(target_os = "macos")] -use std::net::IpAddr; #[cfg(not(target_os = "macos"))] use std::str::FromStr; @@ -241,26 +239,6 @@ impl Location { } } - /// Split DNS settings into resolver IP addresses and search domains. - #[cfg(target_os = "macos")] - pub(crate) fn dns(&self) -> (Vec, Vec) { - 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::() { - dns.push(ip); - } else { - dns_search.push(entry.into()); - } - } - } - - (dns, dns_search) - } - #[cfg(not(target_os = "macos"))] pub(crate) async fn interface_configuration<'e, E>( &self, diff --git a/src-tauri/src/database/models/tunnel.rs b/src-tauri/src/database/models/tunnel.rs index 7522e7c7..b53570d5 100644 --- a/src-tauri/src/database/models/tunnel.rs +++ b/src-tauri/src/database/models/tunnel.rs @@ -1,5 +1,3 @@ -#[cfg(target_os = "macos")] -use std::net::IpAddr; use std::{fmt, time::SystemTime}; use chrono::{NaiveDateTime, Utc}; @@ -254,28 +252,6 @@ impl Tunnel { } } -impl Tunnel { - /// Split DNS settings into resolver IP addresses and search domains. - #[cfg(target_os = "macos")] - pub(crate) fn dns(&self) -> (Vec, Vec) { - 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::() { - dns.push(ip); - } else { - dns_search.push(entry.into()); - } - } - } - - (dns, dns_search) - } -} - #[derive(Debug, Serialize, Deserialize)] pub struct TunnelStats { id: I, diff --git a/src-tauri/src/enterprise/service_locations/mod.rs b/src-tauri/src/enterprise/service_locations/mod.rs index b8dffa4c..f120d552 100644 --- a/src-tauri/src/enterprise/service_locations/mod.rs +++ b/src-tauri/src/enterprise/service_locations/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; use defguard_wireguard_rs::{error::WireguardInterfaceError, WGApi}; use serde::{Deserialize, Serialize}; @@ -63,8 +63,8 @@ pub(crate) struct SingleServiceLocationData { pub private_key: String, } -impl std::fmt::Debug for ServiceLocationData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for ServiceLocationData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ServiceLocationData") .field("service_locations", &self.service_locations) .field("instance_id", &self.instance_id) @@ -73,8 +73,8 @@ impl std::fmt::Debug for ServiceLocationData { } } -impl std::fmt::Debug for SingleServiceLocationData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for SingleServiceLocationData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SingleServiceLocationData") .field("service_locations", &self.service_location) .field("instance_id", &self.instance_id) @@ -88,18 +88,22 @@ impl Location { if !self.is_service_location() { warn!("Location {self} is not a service location, so it can't be converted to one."); return Err(crate::error::Error::ConversionError(format!( - "Failed to convert location {self} to a service location as it's either not marked as one or has MFA enabled." + "Failed to convert location {self} to a service location as it's either not marked \ + as one or has MFA enabled." ))); } let mode = match self.service_location_mode { ServiceLocationMode::Disabled => { warn!( - "Location {self} has an invalid service location mode, so it can't be converted to one." + "Location {self} has an invalid service location mode, so it can't be converted to \ + one." ); - return Err( - crate::error::Error::ConversionError(format!("Location {} has an invalid service location mode ({:?}), so it can't be converted to one.", self, self.service_location_mode)) - ); + return Err(crate::error::Error::ConversionError(format!( + "Location {self} has an invalid service location mode ({:?}), so it can't be \ + converted to one.", + self.service_location_mode + ))); } ServiceLocationMode::PreLogon => 0, ServiceLocationMode::AlwaysOn => 1, diff --git a/src-tauri/src/enterprise/service_locations/windows.rs b/src-tauri/src/enterprise/service_locations/windows.rs index 53f6ce67..d061a526 100644 --- a/src-tauri/src/enterprise/service_locations/windows.rs +++ b/src-tauri/src/enterprise/service_locations/windows.rs @@ -1,7 +1,6 @@ use std::{ collections::HashMap, fs::{self, create_dir_all}, - net::IpAddr, path::PathBuf, result::Result, str::FromStr, @@ -9,7 +8,7 @@ use std::{ time::Duration, }; -use common::{find_free_tcp_port, get_interface_name}; +use common::{dns_borrow, find_free_tcp_port, get_interface_name}; use defguard_wireguard_rs::{ host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WireguardInterfaceApi, }; @@ -351,7 +350,8 @@ impl ServiceLocationManager { location_pubkey: &str, ) -> Result<(), ServiceLocationError> { debug!( - "Reseting the state of service location for instance_id: {instance_id}, location_pubkey: {location_pubkey}" + "Reseting the state of service location for instance_id: {instance_id}, \ + location_pubkey: {location_pubkey}" ); let service_location_data = self @@ -364,19 +364,22 @@ impl ServiceLocationManager { })?; debug!( - "Disconnecting service location for instance_id: {instance_id}, location_pubkey: {location_pubkey} ({})", + "Disconnecting service location for instance_id: {instance_id}, location_pubkey: \ + {location_pubkey} ({})", service_location_data.service_location.name ); self.disconnect_service_location(instance_id, location_pubkey)?; debug!( - "Disconnected service location for instance_id: {instance_id}, location_pubkey: {location_pubkey} ({})", + "Disconnected service location for instance_id: {instance_id}, \ + location_pubkey: {location_pubkey} ({})", service_location_data.service_location.name ); debug!( - "Reconnecting service location if needed for instance_id: {instance_id}, location_pubkey: {location_pubkey} ({})", + "Reconnecting service location if needed for instance_id: {instance_id}, \ + location_pubkey: {location_pubkey} ({})", service_location_data.service_location.name ); @@ -388,7 +391,8 @@ impl ServiceLocationManager { && !is_user_logged_in()) { debug!( - "Reconnecting service location for instance_id: {instance_id}, location_pubkey: {location_pubkey} ({})", + "Reconnecting service location for instance_id: {instance_id}, location_pubkey: \ + {location_pubkey} ({})", service_location_data.service_location.name ); self.connect_to_service_location(&service_location_data)?; @@ -419,11 +423,13 @@ impl ServiceLocationManager { debug!("Interface {ifname} removed successfully"); } debug!( - "Removing connected service location for instance_id: {instance_id}, location_pubkey: {}", - location.pubkey - ); + "Removing connected service location for instance_id: {instance_id}, \ + location_pubkey: {}", + location.pubkey + ); debug!( - "Disconnected service location for instance_id: {instance_id}, location_pubkey: {}", + "Disconnected service location for instance_id: {instance_id}, \ + location_pubkey: {}", location.pubkey ); } else { @@ -450,7 +456,8 @@ impl ServiceLocationManager { location_pubkey: &str, ) -> Result<(), ServiceLocationError> { debug!( - "Disconnecting service location for instance_id: {instance_id}, location_pubkey: {location_pubkey}" + "Disconnecting service location for instance_id: {instance_id}, location_pubkey: \ + {location_pubkey}" ); if let Some(locations) = self.connected_service_locations.get_mut(instance_id) { @@ -472,19 +479,22 @@ impl ServiceLocationManager { } } else { debug!( - "Service location with pubkey {location_pubkey} for instance {instance_id} is not connected, skipping disconnect" + "Service location with pubkey {location_pubkey} for instance {instance_id} is \ + not connected, skipping disconnect" ); return Ok(()); } } else { debug!( - "No connected service locations found for instance_id: {instance_id}, skipping disconnect" + "No connected service locations found for instance_id: {instance_id}, skipping \ + disconnect" ); return Ok(()); } debug!( - "Disconnected service location for instance_id: {instance_id}, location_pubkey: {location_pubkey}" + "Disconnected service location for instance_id: {instance_id}, location_pubkey: \ + {location_pubkey}" ); Ok(()) @@ -552,24 +562,13 @@ impl ServiceLocationManager { wgapi.create_interface()?; // Extract DNS configuration if available - let dns_string = location.dns.clone(); - let dns_entries = dns_string.split(',').map(str::trim).collect::>(); - // We assume that every entry that can't be parsed as an IP address is a domain name. - let mut dns = Vec::new(); - let mut search_domains = Vec::new(); - for entry in dns_entries { - if let Ok(ip) = entry.parse::() { - dns.push(ip); - } else { - search_domains.push(entry); - } - } - + let dns_config = Some(location.dns.clone()); + let (dns, search_domains) = dns_borrow(&dns_config); debug!( - "Configuring interface {ifname} with DNS: {:?} and search domains: {:?}", - dns, search_domains + "Configuring interface {ifname} with DNS: {dns:?} and search domains: \ + {search_domains:?}", ); - debug!("Interface Configuration: {:?}", config); + debug!("Interface Configuration: {config:?}"); wgapi.configure_interface(&config)?; wgapi.configure_dns(&dns, &search_domains)?; @@ -587,13 +586,15 @@ impl ServiceLocationManager { let instance_id = &location_data.instance_id; let location_pubkey = &location_data.service_location.pubkey; debug!( - "Connecting to service location for instance_id: {instance_id}, location_pubkey: {location_pubkey}" + "Connecting to service location for instance_id: {instance_id}, location_pubkey: \ + {location_pubkey}" ); // Check if already connected to this service location if self.is_service_location_connected(instance_id, location_pubkey) { debug!( - "Service location with pubkey {location_pubkey} for instance {instance_id} is already connected, skipping" + "Service location with pubkey {location_pubkey} for instance {instance_id} is \ + already connected, skipping" ); return Ok(()); } @@ -602,8 +603,8 @@ impl ServiceLocationManager { .load_service_location(instance_id, location_pubkey)? .ok_or_else(|| { ServiceLocationError::LoadError(format!( - "Service location with pubkey {} for instance {} not found", - location_pubkey, instance_id + "Service location with pubkey {location_pubkey} for instance {instance_id} not \ + found", )) })?; @@ -630,14 +631,16 @@ impl ServiceLocationManager { for (instance, locations) in self.connected_service_locations.iter() { for location in locations { debug!( - "Found connected service location for instance_id: {instance}, location_pubkey: {}", + "Found connected service location for instance_id: {instance}, \ + location_pubkey: {}", location.pubkey ); if let Some(m) = mode { let location_mode: ServiceLocationMode = location.mode.try_into()?; if location_mode != m { debug!( - "Skipping interface {} due to the service location mode doesn't match the requested mode (expected {m:?}, found {:?})", + "Skipping interface {} due to the service location mode doesn't match the \ + requested mode (expected {m:?}, found {:?})", location.name, location.mode ); continue; @@ -702,9 +705,10 @@ impl ServiceLocationManager { continue; } debug!( - "Proceeding to connect pre-logon service location '{}' because no user is logged in", - location.name - ); + "Proceeding to connect pre-logon service location '{}' because no user \ + is logged in", + location.name + ); } if self.is_service_location_connected(&instance_data.instance_id, &location.pubkey) @@ -779,7 +783,8 @@ impl ServiceLocationManager { debug!("Setting ACLs on service location file: {file_path_str}"); if let Err(e) = set_protected_acls(file_path_str) { warn!( - "Failed to set ACLs on service location file {file_path_str}: {e}. File saved but may have insecure permissions." + "Failed to set ACLs on service location file {file_path_str}: {e}. File saved \ + but may have insecure permissions." ); } else { debug!("Successfully set ACLs on service location file"); @@ -850,7 +855,8 @@ impl ServiceLocationManager { for location in service_location_data.service_locations { if location.pubkey == location_pubkey { debug!( - "Successfully loaded service location for instance {instance_id} and pubkey {location_pubkey}" + "Successfully loaded service location for instance {instance_id} and \ + pubkey {location_pubkey}" ); return Ok(Some(SingleServiceLocationData { service_location: location, diff --git a/src-tauri/src/service/daemon.rs b/src-tauri/src/service/daemon.rs index 655ec3f3..6ecf251f 100644 --- a/src-tauri/src/service/daemon.rs +++ b/src-tauri/src/service/daemon.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - net::IpAddr, pin::Pin, sync::{Arc, Mutex, RwLock}, time::{Duration, SystemTime}, @@ -8,6 +7,7 @@ use std::{ #[cfg(unix)] use std::{fs, os::unix::fs::PermissionsExt, path::Path}; +use common::dns_borrow; use defguard_wireguard_rs::{ error::WireguardInterfaceError, InterfaceConfiguration, Kernel, WGApi, WireguardInterfaceApi, }; @@ -107,21 +107,10 @@ fn configure_new_interface( // The WireGuard DNS config value can be a list of IP addresses and domain names, which will // be used as DNS servers and search domains respectively. debug!("Preparing DNS configuration for interface {ifname}"); - let dns_string = request.dns.clone().unwrap_or_default(); - let dns_entries = dns_string.split(',').map(str::trim).collect::>(); - // We assume that every entry that can't be parsed as an IP address is a domain name. - let mut dns = Vec::new(); - let mut search_domains = Vec::new(); - for entry in dns_entries { - if let Ok(ip) = entry.parse::() { - dns.push(ip); - } else { - search_domains.push(entry); - } - } + let (dns, search_domains) = dns_borrow(&request.dns); debug!( "DNS configuration for interface {ifname}: DNS: {dns:?}, Search domains: \ - {search_domains:?}" + {search_domains:?}" ); let configure_interface_result = wgapi.configure_interface(interface_config); diff --git a/src-tauri/src/service/named_pipe.rs b/src-tauri/src/service/named_pipe.rs index 321d76cc..6b8670e2 100644 --- a/src-tauri/src/service/named_pipe.rs +++ b/src-tauri/src/service/named_pipe.rs @@ -1,4 +1,8 @@ -use std::{os::windows::io::RawHandle, pin::Pin}; +use std::{ + os::windows::io::RawHandle, + pin::Pin, + task::{Context, Poll}, +}; use async_stream::stream; use futures_core::stream::Stream; @@ -18,16 +22,16 @@ use windows_sys::Win32::{ }; // Named-pipe name used for IPC between defguard client and windows service. -pub static PIPE_NAME: &str = r"\\.\pipe\defguard_daemon"; +pub(super) static PIPE_NAME: &str = r"\\.\pipe\defguard_daemon"; -// SDDL defining named pipe ACL: +/// SDDL defining named pipe ACL: /// - `SY` (LocalSystem) - full control /// - `BA` (Administrators) - full control /// - `BU` (Built-in Users) - read/write -pub static SDDL: &str = "D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;BU)"; +static SDDL: &str = "D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;BU)"; /// Tonic-compatible wrapper around a Windows named pipe server handle. -pub struct TonicNamedPipeServer { +pub(crate) struct TonicNamedPipeServer { inner: NamedPipeServer, } @@ -46,9 +50,9 @@ impl Connected for TonicNamedPipeServer { impl AsyncRead for TonicNamedPipeServer { fn poll_read( mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, + cx: &mut Context<'_>, buf: &mut tokio::io::ReadBuf<'_>, - ) -> std::task::Poll> { + ) -> Poll> { Pin::new(&mut self.inner).poll_read(cx, buf) } } @@ -57,30 +61,30 @@ impl AsyncWrite for TonicNamedPipeServer { /// Delegate async write to the underlying pipe. fn poll_write( mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, + cx: &mut Context<'_>, buf: &[u8], - ) -> std::task::Poll> { + ) -> Poll> { Pin::new(&mut self.inner).poll_write(cx, buf) } /// Delegate flush to the underlying pipe. fn poll_flush( mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { + cx: &mut Context<'_>, + ) -> Poll> { Pin::new(&mut self.inner).poll_flush(cx) } /// Delegate shutdown to the underlying pipe. fn poll_shutdown( mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { + cx: &mut Context<'_>, + ) -> Poll> { Pin::new(&mut self.inner).poll_shutdown(cx) } } -/// Convert a Rust `&str` to a null-terminated UTF-16 buffer suitable for Win32 APIs. +/// Convert `&str` to a null-terminated UTF-16 buffer suitable for Win32 APIs. fn str_to_wide_null_terminated(s: &str) -> Vec { s.encode_utf16().chain(Some(0)).collect() } @@ -161,16 +165,15 @@ fn create_tokio_secure_pipe() -> Result { /// 1. Creates a fresh listening instance. /// 2. Awaits a client connection (`connect().await`). /// 3. Yields the connected `TonicNamedPipeServer`. -pub fn get_named_pipe_server_stream( +pub(crate) fn get_named_pipe_server_stream( ) -> Result>, std::io::Error> { debug!("Creating named pipe server stream"); let stream = stream! { - let mut server = create_tokio_secure_pipe()?; - + let mut server; loop { + server = create_tokio_secure_pipe()?; server.connect().await?; yield Ok(TonicNamedPipeServer::new(server)); - server = create_tokio_secure_pipe()?; } }; info!("Created named pipe server stream"); diff --git a/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx b/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx index 15e66114..ac04559b 100644 --- a/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx +++ b/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx @@ -149,7 +149,13 @@ export const EditTunnelFormCard = ({ tunnel, submitRef }: Props) => { return value === '' || patternValidWireguardKey.test(value); }, LL.form.errors.invalid()), address: z.string().refine((value) => { - return patternValidIp.test(value) || patternValidIpV6.test(value); + if (value) { + const ips = value.split(',').map((ip) => ip.trim()); + return ips.every( + (ip) => patternValidIp.test(ip) || patternValidIpV6.test(ip), + ); + } + return false; }, LL.form.errors.invalid()), endpoint: z .string() diff --git a/swift/extension/VPNExtension/PacketTunnelProvider.swift b/swift/extension/VPNExtension/PacketTunnelProvider.swift index 15231446..233ec53b 100644 --- a/swift/extension/VPNExtension/PacketTunnelProvider.swift +++ b/swift/extension/VPNExtension/PacketTunnelProvider.swift @@ -28,12 +28,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return } - log.info("Tunnel configuration parsed successfully") - let networkSettings = tunnelConfig.asNetworkSettings() self.setTunnelNetworkSettings(networkSettings) { error in if error != nil { - self.log.error("Set tunnel network settings error: \(String(describing: error))") + self.log.error("Failed to set tunnel network settings: \(String(describing: error))") } completionHandler(error) return diff --git a/swift/plugin/Sources/Wireguard.swift b/swift/plugin/Sources/Wireguard.swift deleted file mode 100644 index 16a20d78..00000000 --- a/swift/plugin/Sources/Wireguard.swift +++ /dev/null @@ -1,262 +0,0 @@ -// Functions to be called from Rust code. - -import NetworkExtension -import SwiftRs -import os - -let appId = Bundle.main.bundleIdentifier ?? "net.defguard" -let pluginAppId = "\(appId).VPNExtension" -let logger = Logger(subsystem: appId, category: "WireguardPlugin") - -/// From preferences load `NETunnelProviderManager` with a given `name. -func managerForName( - _ name: String, - completion: @escaping (NETunnelProviderManager?) -> Void -) { - var providerManager: NETunnelProviderManager? - NETunnelProviderManager.loadAllFromPreferences { managers, error in - guard let managers = managers else { - logger.info("No tunnel managers in user's settings") - return - } - guard error == nil else { - logger.warning( - "Error loading tunnel managers: \(error, privacy: .public)") - providerManager = nil - completion(nil) - return - } - logger.info("Loaded \(managers.count, privacy: .public) tunnel managers.") - - // Find the right protocol manager. - providerManager = nil - for manager in managers { - // Obtain named configuration. - if manager.localizedDescription != name { - continue - } - guard let tunnelProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol - else { - continue - } - // Sometimes all managers from all apps come through, so filter by bundle ID. - if tunnelProtocol.providerBundleIdentifier == pluginAppId { - providerManager = manager - break - } - } - if providerManager == nil { - logger.log("No VPN manager found") - } else { - logger.log( - "Loaded provider manager: \(String(describing: providerManager!.localizedDescription), privacy: .public)" - ) - } - completion(providerManager) - } -} - -@_cdecl("start_tunnel") -public func startTunnel(json: SRString) -> Bool { - let decoder = JSONDecoder() - guard let json_data = json.toString().data(using: .utf8) else { - logger.error("Failed to convert JSON string to data") - return false - } - let config: TunnelConfiguration - do { config = try decoder.decode(TunnelConfiguration.self, from: json_data) } catch { - logger.error( - "Failed to decode tunnel configuration: \(error.localizedDescription, privacy: .public)" - ) - return false - } - - if !config.isValidForClientConnection() { - logger.error("Invalid tunnel configuration: \(json.toString(), privacy: .public)") - return false - } - - logger.info("Saving tunnel with config: \(String(describing: config))") - saveConfig(config) - - // MFA is not that fast to propagate pre-shared key, so wait a moment here. - Thread.sleep(forTimeInterval: 1) - // Note: this will re-load configuration from preferneces which is a desired effect. - startVPN(name: config.name) - - return true -} - -@_cdecl("stop_tunnel") -public func stopTunnel(name: SRString) -> Bool { - // Blocking - let semaphore = DispatchSemaphore(value: 0) - - managerForName(name.toString()) { manager in - if let providerManager = manager { - providerManager.connection.stopVPNTunnel() - logger.info("VPN stopped") - } - semaphore.signal() - } - - semaphore.wait() - return true -} - -@_cdecl("tunnel_stats") -public func tunnelStats(name: SRString) -> Stats? { - // Blocking - let semaphore = DispatchSemaphore(value: 0) - var result: Stats? = nil - - managerForName(name.toString()) { manager in - if let providerManager = manager as NETunnelProviderManager? { - let session = providerManager.connection as! NETunnelProviderSession - do { - // TODO: data should contain a valid message. - let data = Data() - try session.sendProviderMessage(data) { response in - if let data = response { - let decoder = JSONDecoder() - result = try? decoder.decode(Stats.self, from: data) - } - semaphore.signal() - } - } catch { - logger.error("Failed to send message to tunnel extension \(error)") - semaphore.signal() - } - } - } - - semaphore.wait() - return result -} - -@_cdecl("all_tunnel_stats") -public func allTunnelStats() -> SRObjectArray { - // Blocking - let semaphore = DispatchSemaphore(value: 0) - var stats: [Stats] = [] - - // Get all tunnel provider managers. - NETunnelProviderManager.loadAllFromPreferences { managers, error in - guard let managers = managers else { - logger.info("No tunnel managers in user's settings") - return - } - guard error == nil else { - logger.warning( - "Error loading tunnel managers: \(error, privacy: .public)") - semaphore.signal() - return - } - logger.info("Loaded \(managers.count, privacy: .public) tunnel managers.") - - // `NETunnelProviderSession.sendProviderMessage()` is asynchronous, so use `DispatchGroup`. - let dispatchGroup = DispatchGroup() - - for manager in managers { - guard let tunnelProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol - else { - continue - } - // Sometimes all managers from all apps come through, so filter by bundle ID. - if tunnelProtocol.providerBundleIdentifier != pluginAppId { - continue - } - if let providerManager = manager as NETunnelProviderManager? { - let session = providerManager.connection as! NETunnelProviderSession - do { - // TODO: data should contain a valid message. - let data = Data() - dispatchGroup.enter() - try session.sendProviderMessage(data) { response in - if let data = response { - let decoder = JSONDecoder() - if let result = try? decoder.decode(Stats.self, from: data) { - stats.append(result) - } - } - dispatchGroup.leave() - } - } catch { - logger.error("Failed to send message to tunnel extension \(error)") - dispatchGroup.leave() - } - } - } - - // NOTE: `dispatchGroup.wait()` will cause a dead-lock, because it uses the same thread as - // `NETunnelProviderSession.sendProviderMessage()`. Use this pattern instead: - dispatchGroup.notify(queue: DispatchQueue.global()) { - semaphore.signal() - } - } - - semaphore.wait() - return SRObjectArray(stats) -} - -/// Save `TunnelConfiguration` to preferences. -func saveConfig(_ config: TunnelConfiguration) { - // Blocking - let semaphore = DispatchSemaphore(value: 0) - - managerForName(config.name) { manager in - let providerManager = manager ?? NETunnelProviderManager() - let tunnelProtocol = NETunnelProviderProtocol() - tunnelProtocol.providerBundleIdentifier = pluginAppId - // `serverAddress` must have a non-nil string value for the protocol configuration to be valid. - if let endpoint = config.peers[0].endpoint { - tunnelProtocol.serverAddress = endpoint.toString() - } else { - tunnelProtocol.serverAddress = "" - } - let configDict: [String: Any] - do { - configDict = try config.toDictionary() - } catch { - logger.log( - "Failed to convert config to dictionary: \(error.localizedDescription, privacy: .public)" - ) - // TODO: signal failure - semaphore.signal() - return - } - tunnelProtocol.providerConfiguration = configDict - providerManager.protocolConfiguration = tunnelProtocol - providerManager.localizedDescription = config.name - providerManager.isEnabled = true - - providerManager.saveToPreferences { error in - if let error = error { - logger.log("Failed to save provider manager: \(error, privacy: .public)") - // TODO: signal failure - } else { - logger.info("Config saved") - } - - semaphore.signal() - } - } - - semaphore.wait() -} - -/// Start VPN tunnel for a given `name`. -func startVPN(name: String) { - managerForName(name) { manager in - guard let providerManager = manager else { - logger.warning("Couldn't load \(name) configuration from preferences") - return - } - do { - try providerManager.connection.startVPNTunnel() - logger.info("VPN started") - } catch { - logger.error("Failed to start VPN: \(error, privacy: .public)") - } - } -}