diff --git a/new-ui/src/shared/components/LocationCard/api/connectError.ts b/new-ui/src/shared/components/LocationCard/api/connectError.ts
index 8f8913960..e500ca060 100644
--- a/new-ui/src/shared/components/LocationCard/api/connectError.ts
+++ b/new-ui/src/shared/components/LocationCard/api/connectError.ts
@@ -1,19 +1,17 @@
import z from 'zod';
-const connectErrorSchema = z.discriminatedUnion('kind', [
- z.object({
- kind: z.literal('postureCheckFailed'),
- message: z.string(),
- }),
- z.object({
- kind: z.literal('serviceUnavailable'),
- message: z.string(),
- }),
- z.object({
- kind: z.literal('other'),
- message: z.string(),
- }),
-]);
+export const DEFAULT_CONNECTION_ERROR =
+ 'One or more external services are unavailable or unreachable. This may be caused by a network issue or a temporary service outage. Please try again later.';
+
+const connectErrorSchema = z.object({
+ kind: z.enum([
+ 'postureCheckFailed',
+ 'serviceUnavailable',
+ 'allTrafficConflict',
+ 'other',
+ ]),
+ message: z.string(),
+});
export type ConnectError = z.infer
;
diff --git a/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx b/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx
index c079c8784..c77755a34 100644
--- a/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx
+++ b/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx
@@ -23,6 +23,8 @@ export const LocationCardConnectButton = () => {
) {
setPostureError(connectError.message);
setView(LocationCardViews.PostureCheckFail);
+ } else if (connectError?.kind === 'allTrafficConflict') {
+ setView(LocationCardViews.ConnectionError, connectError.message);
} else if (connectError?.kind === 'serviceUnavailable') {
setView(LocationCardViews.ConnectionError);
}
diff --git a/new-ui/src/shared/components/LocationCard/context/context.tsx b/new-ui/src/shared/components/LocationCard/context/context.tsx
index a88352b48..46bc2383f 100644
--- a/new-ui/src/shared/components/LocationCard/context/context.tsx
+++ b/new-ui/src/shared/components/LocationCard/context/context.tsx
@@ -20,10 +20,11 @@ interface LocationCardContextValue {
currentView: LocationCardViewsValue;
previousView: LocationCardViewsValue | null;
postureError: string | null;
+ connectionError: string | null;
autoConnectOpenid: boolean;
mfaMethod: MfaMethodValue;
setMfaMethod: (value: MfaMethodValue) => void;
- setView: (view: LocationCardViewsValue) => void;
+ setView: (view: LocationCardViewsValue, connectionError?: string) => void;
setPostureError: (error: string | null) => void;
startMfa: () => void;
}
@@ -54,6 +55,7 @@ export const LocationCardProvider = ({
const [autoConnectOpenid, setAutoConnectOpenid] = useState(false);
const [previousView, setPreviousView] = useState(null);
const [postureError, setPostureError] = useState(null);
+ const [connectionError, setConnectionError] = useState(null);
const [currentView, setCurrentView] = useState(
location.active ? LocationCardViews.Connected : LocationCardViews.Default,
);
@@ -62,9 +64,10 @@ export const LocationCardProvider = ({
);
const setView = useCallback(
- (view: LocationCardViewsValue) => {
+ (view: LocationCardViewsValue, connectionError?: string) => {
setPreviousView(currentView);
setCurrentView(view);
+ setConnectionError(connectionError ?? null);
},
[currentView],
);
@@ -123,6 +126,7 @@ export const LocationCardProvider = ({
currentView,
previousView,
postureError,
+ connectionError,
autoConnectOpenid,
location,
instance,
diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardConnectionErrorView/LocationCardConnectionErrorView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardConnectionErrorView/LocationCardConnectionErrorView.tsx
index a19ddd1c6..428dc667c 100644
--- a/new-ui/src/shared/components/LocationCard/views/LocationCardConnectionErrorView/LocationCardConnectionErrorView.tsx
+++ b/new-ui/src/shared/components/LocationCard/views/LocationCardConnectionErrorView/LocationCardConnectionErrorView.tsx
@@ -5,11 +5,12 @@ import { ButtonVariant } from '../../../Button/types';
import { Divider } from '../../../Divider/Divider';
import { Icon, IconKind } from '../../../Icon';
import { SizedBox } from '../../../SizedBox/SizedBox';
+import { DEFAULT_CONNECTION_ERROR } from '../../api/connectError';
import { useLocationCardContext } from '../../context/context';
import { LocationCardViews } from '../../context/types';
export const LocationCardConnectionErrorView = () => {
- const { setView } = useLocationCardContext();
+ const { setView, connectionError } = useLocationCardContext();
return (
@@ -17,10 +18,7 @@ export const LocationCardConnectionErrorView = () => {
-
- One or more external services are unavailable or unreachable. This may be caused
- by a network issue or a temporary service outage. Please try again later.
-
+
{connectionError ?? DEFAULT_CONNECTION_ERROR}
{
view: ConnectModalView.PostureCheckFail,
postureError: connectError.message,
});
+ } else if (connectError?.kind === 'allTrafficConflict') {
+ useConnectModal.getState().open({
+ location,
+ view: ConnectModalView.ConnectionError,
+ connectionError: connectError.message,
+ });
} else if (connectError?.kind === 'serviceUnavailable') {
useConnectModal.getState().open({
location,
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 473996d99..ec7517aa4 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -893,9 +893,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.4.2"
+version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
+checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -2238,9 +2238,9 @@ dependencies = [
[[package]]
name = "error-code"
-version = "3.3.2"
+version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
+checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
[[package]]
name = "etcetera"
@@ -2350,9 +2350,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
-version = "0.1.10"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "fixedbitset"
@@ -3059,9 +3059,9 @@ dependencies = [
[[package]]
name = "http-body-util"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [
"bytes",
"futures-core",
@@ -3219,9 +3219,9 @@ dependencies = [
[[package]]
name = "icu_collections"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
@@ -3233,9 +3233,9 @@ dependencies = [
[[package]]
name = "icu_locale_core"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
@@ -3246,9 +3246,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -3260,16 +3260,17 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
+ "displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
@@ -3280,15 +3281,15 @@ dependencies = [
[[package]]
name = "icu_properties_data"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428"
dependencies = [
"displaydoc",
"icu_locale_core",
@@ -3829,14 +3830,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
-version = "0.1.19"
+version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa"
+checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
dependencies = [
"bitflags 2.13.1",
"libc",
"plain",
- "redox_syscall 0.9.1",
+ "redox_syscall 0.9.2",
]
[[package]]
@@ -3870,9 +3871,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "litrs"
@@ -4143,9 +4144,9 @@ dependencies = [
[[package]]
name = "netlink-packet-wireguard"
-version = "0.4.2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8901a50aa367f28d70e8c2a6b379f51efef1e5acebbe05f940ae00363d7d83fd"
+checksum = "81b0e03593f61a7684836d73fdfef3dddae3f2dbc81896159a14bfafdb013567"
dependencies = [
"bitflags 2.13.1",
"libc",
@@ -5021,9 +5022,9 @@ dependencies = [
[[package]]
name = "pkg-config"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "plain"
@@ -5112,9 +5113,9 @@ dependencies = [
[[package]]
name = "potential_utf"
-version = "0.1.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
"zerovec",
]
@@ -5647,9 +5648,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.9.1"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e"
+checksum = "f1c93da5bb2c5d4e6c0ef7abeead62c89169a0a4882bfb83ac892f2423aea2fe"
dependencies = [
"bitflags 2.13.1",
]
@@ -7599,9 +7600,9 @@ dependencies = [
[[package]]
name = "tinystr"
-version = "0.8.3"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
"zerovec",
@@ -8251,9 +8252,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.24.0"
+version = "1.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
+checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -8471,9 +8472,9 @@ dependencies = [
[[package]]
name = "wayland-backend"
-version = "0.3.16"
+version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "016ccf01d1c58b6f8999612813e17c9b2390f7d70671428869913310f83f54b8"
+checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
dependencies = [
"cc",
"downcast-rs",
@@ -9432,9 +9433,9 @@ dependencies = [
[[package]]
name = "writeable"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "wry"
@@ -9689,9 +9690,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
-version = "0.2.4"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
@@ -9700,9 +9701,9 @@ dependencies = [
[[package]]
name = "zerovec"
-version = "0.11.6"
+version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8"
dependencies = [
"yoke",
"zerofrom",
@@ -9711,13 +9712,13 @@ dependencies = [
[[package]]
name = "zerovec-derive"
-version = "0.11.3"
+version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.3",
]
[[package]]
@@ -9781,9 +9782,9 @@ dependencies = [
[[package]]
name = "zvariant_utils"
-version = "4.0.0"
+version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "629d80ece222cad20fe0e8741be493c4ab166acf3b85341bdc2cdbcfd8f3c2d6"
+checksum = "6b84ebb462416c27cdb97f2e7f5f0ccc844da1fe2ecc7121e1b690b41318bf42"
dependencies = [
"proc-macro2",
"quote",
diff --git a/src-tauri/client-cli/src/commands/connect.rs b/src-tauri/client-cli/src/commands/connect.rs
index 92e20ebfc..beac5fd40 100644
--- a/src-tauri/client-cli/src/commands/connect.rs
+++ b/src-tauri/client-cli/src/commands/connect.rs
@@ -4,7 +4,7 @@ use defguard_client_posture::{authorize_posture_session, get_posture_data};
use defguard_client_proto::defguard::client_types::MfaMethod;
use defguard_core::{
connection::{active_state::active_state, bring_up, ConnectionTarget},
- database::models::instance::Instance,
+ database::models::{instance::Instance, Id},
ConnectionType,
};
use secrecy::ExposeSecret;
@@ -62,11 +62,12 @@ pub async fn handle(
ResolvedTarget::Location(loc) => (loc.id, ConnectionType::Location, loc.name.as_str()),
ResolvedTarget::Tunnel(tun) => (tun.id, ConnectionType::Tunnel, tun.name.as_str()),
};
- let active = active_state(&state.pool).await?;
- if active
+ let active: Vec<(Id, ConnectionType)> = active_state(&state.pool)
+ .await?
.iter()
- .any(|c| c.connection_type == target_connection_type && c.target_id == target_id)
- {
+ .map(|c| (c.target_id, c.connection_type))
+ .collect();
+ if active.contains(&(target_id, target_connection_type)) {
return Ok(ConnectResult::AlreadyConnected {
name: target_name.to_string(),
});
diff --git a/src-tauri/core/src/connection/active_connections.rs b/src-tauri/core/src/connection/active_connections.rs
index 0feaa8953..abe8ec54a 100644
--- a/src-tauri/core/src/connection/active_connections.rs
+++ b/src-tauri/core/src/connection/active_connections.rs
@@ -15,6 +15,15 @@ use crate::{
pub static ACTIVE_CONNECTIONS: LazyLock>> =
LazyLock::new(|| Mutex::new(Vec::new()));
+pub(crate) async fn active_connection_ids() -> Vec<(Id, ConnectionType)> {
+ ACTIVE_CONNECTIONS
+ .lock()
+ .await
+ .iter()
+ .map(|con| (con.location_id, con.connection_type))
+ .collect()
+}
+
pub async fn get_connection_id_by_type(connection_type: ConnectionType) -> Vec {
let active_connections = ACTIVE_CONNECTIONS.lock().await;
diff --git a/src-tauri/core/src/connection/mod.rs b/src-tauri/core/src/connection/mod.rs
index f8f2da2b2..d289f2d8c 100644
--- a/src-tauri/core/src/connection/mod.rs
+++ b/src-tauri/core/src/connection/mod.rs
@@ -20,11 +20,13 @@ pub use setup::{setup_interface, setup_interface_tunnel};
use tokio::time::sleep;
use crate::{
+ connection::active_connections::active_connection_ids,
database::{
models::{connection::ActiveConnection, location::Location, tunnel::Tunnel, Id},
DbPool,
},
error::Error,
+ ConnectionType,
};
#[cfg(target_os = "macos")]
@@ -36,8 +38,81 @@ pub enum ConnectionTarget {
Tunnel(Tunnel),
}
+impl ConnectionTarget {
+ pub async fn ensure_single_all_traffic_connection(
+ &self,
+ pool: &DbPool,
+ route_all_traffic: Option,
+ ) -> Result<(), Error> {
+ let (id, connection_type, name, holds_default_route) = match self {
+ Self::Location(location) => (
+ location.id,
+ ConnectionType::Location,
+ &location.name,
+ location
+ .holds_default_route(pool, route_all_traffic)
+ .await?,
+ ),
+ Self::Tunnel(tunnel) => (
+ tunnel.id,
+ ConnectionType::Tunnel,
+ &tunnel.name,
+ tunnel.holds_default_route(route_all_traffic),
+ ),
+ };
+
+ if !holds_default_route {
+ return Ok(());
+ }
+
+ if let Some((active_type, active_name)) =
+ find_default_route_owner(pool, (id, connection_type)).await?
+ {
+ error!(
+ "Refusing to connect {connection_type} \"{name}\" (ID {id}): it routes all \
+ traffic, but {active_type} \"{active_name}\" already holds the default route."
+ );
+ return Err(Error::AllTrafficConflict(format!(
+ "Can't connect to {connection_type} \"{name}\": {active_type} \"{active_name}\" \
+ is already routing all traffic. Only one connection can route all traffic at a \
+ time, so disconnect it first or turn off \"route all traffic\" for one of them."
+ )));
+ }
+
+ Ok(())
+ }
+}
+
+async fn find_default_route_owner(
+ pool: &DbPool,
+ exclude: (Id, ConnectionType),
+) -> Result, Error> {
+ for (id, connection_type) in active_connection_ids().await {
+ if (id, connection_type) == exclude {
+ continue;
+ }
+ let owner = match connection_type {
+ ConnectionType::Location => match Location::find_by_id(pool, id).await? {
+ Some(location) => location
+ .holds_default_route(pool, None)
+ .await?
+ .then_some(location.name),
+ None => None,
+ },
+ ConnectionType::Tunnel => match Tunnel::find_by_id(pool, id).await? {
+ Some(tunnel) => tunnel.holds_default_route(None).then_some(tunnel.name),
+ None => None,
+ },
+ };
+ if let Some(name) = owner {
+ return Ok(Some((connection_type, name)));
+ }
+ }
+
+ Ok(None)
+}
+
/// Bring a WireGuard interface up for the given target.
-#[cfg_attr(target_os = "macos", allow(unused_variables))]
pub async fn bring_up(
target: ConnectionTarget,
psk: Option,
@@ -45,6 +120,10 @@ pub async fn bring_up(
pool: &DbPool,
route_all_traffic: Option,
) -> Result {
+ target
+ .ensure_single_all_traffic_connection(pool, route_all_traffic)
+ .await?;
+
#[cfg(not(target_os = "macos"))]
{
match target {
diff --git a/src-tauri/core/src/connection/setup.rs b/src-tauri/core/src/connection/setup.rs
index 82d31d5ae..e98cf9fab 100644
--- a/src-tauri/core/src/connection/setup.rs
+++ b/src-tauri/core/src/connection/setup.rs
@@ -130,7 +130,7 @@ pub async fn setup_interface_tunnel(
"Parsing tunnel {tunnel} allowed ips: {:?}",
tunnel.allowed_ips
);
- let route_all_traffic = route_all_traffic.unwrap_or(tunnel.route_all_traffic);
+ let route_all_traffic = tunnel.effective_route_all_traffic(route_all_traffic);
let allowed_ips = if route_all_traffic {
debug!("Using all traffic routing for tunnel {tunnel}");
vec![DEFAULT_ROUTE_IPV4.into(), DEFAULT_ROUTE_IPV6.into()]
diff --git a/src-tauri/core/src/database/models/location.rs b/src-tauri/core/src/database/models/location.rs
index 297ecf179..cf8e9a9f8 100644
--- a/src-tauri/core/src/database/models/location.rs
+++ b/src-tauri/core/src/database/models/location.rs
@@ -11,6 +11,7 @@ use sqlx::{prelude::Type, query, query_as, query_scalar, SqliteExecutor};
use super::wireguard_keys::WireguardKeys;
use super::{Id, NoId};
use crate::{
+ contains_default_route,
database::{
models::instance::{ClientTrafficPolicy, Instance},
DbPool,
@@ -329,6 +330,36 @@ impl Location {
}
}
+ pub async fn effective_route_all_traffic(
+ &self,
+ pool: &DbPool,
+ route_all_traffic: Option,
+ ) -> Result {
+ let Some(instance) = Instance::find_by_id(pool, self.instance_id).await? else {
+ error!("Instance {} not found", self.instance_id);
+ return Err(Error::InternalError(format!(
+ "Instance {} not found",
+ self.instance_id
+ )));
+ };
+ Ok(match instance.client_traffic_policy {
+ ClientTrafficPolicy::ForceAllTraffic => true,
+ ClientTrafficPolicy::DisableAllTraffic => false,
+ ClientTrafficPolicy::None => route_all_traffic.unwrap_or(self.route_all_traffic),
+ })
+ }
+
+ pub async fn holds_default_route(
+ &self,
+ pool: &DbPool,
+ route_all_traffic: Option,
+ ) -> Result {
+ Ok(self
+ .effective_route_all_traffic(pool, route_all_traffic)
+ .await?
+ || contains_default_route(&self.allowed_ips))
+ }
+
#[cfg(not(target_os = "macos"))]
pub async fn interface_configuration(
&self,
@@ -338,8 +369,6 @@ impl Location {
mtu: Option,
route_all_traffic: Option,
) -> Result {
- use crate::database::models::instance::{ClientTrafficPolicy, Instance};
-
debug!("Looking for WireGuard keys for location {self} instance");
let Some(keys) = WireguardKeys::find_by_instance_id(pool, self.instance_id).await? else {
error!("No keys found for instance: {}", self.instance_id);
@@ -368,18 +397,9 @@ impl Location {
}
debug!("Parsing location {self} allowed IPs: {}", self.allowed_ips);
- let Some(instance) = Instance::find_by_id(pool, self.instance_id).await? else {
- error!("Instance {} not found", self.instance_id);
- return Err(Error::InternalError(format!(
- "Instance {} not found",
- self.instance_id
- )));
- };
- let route_all_traffic = match instance.client_traffic_policy {
- ClientTrafficPolicy::ForceAllTraffic => true,
- ClientTrafficPolicy::DisableAllTraffic => false,
- ClientTrafficPolicy::None => route_all_traffic.unwrap_or(self.route_all_traffic),
- };
+ let route_all_traffic = self
+ .effective_route_all_traffic(pool, route_all_traffic)
+ .await?;
let allowed_ips = if route_all_traffic {
debug!("Using all traffic routing for location {self}");
vec![DEFAULT_ROUTE_IPV4.into(), DEFAULT_ROUTE_IPV6.into()]
@@ -678,6 +698,65 @@ mod tests {
);
}
+ #[sqlx::test(migrations = "../migrations")]
+ async fn test_effective_route_all_traffic(pool: SqlitePool) {
+ use ClientTrafficPolicy::{DisableAllTraffic, ForceAllTraffic, None as NoPolicy};
+
+ // (policy, stored flag, per-call override, expected)
+ let cases = [
+ (NoPolicy, false, None, false),
+ (NoPolicy, false, Some(true), true),
+ (NoPolicy, true, None, true),
+ (NoPolicy, true, Some(false), false),
+ (ForceAllTraffic, false, None, true),
+ (ForceAllTraffic, false, Some(false), true),
+ (DisableAllTraffic, true, None, false),
+ (DisableAllTraffic, true, Some(true), false),
+ ];
+
+ let mut instance = new_instance().save(&pool).await.unwrap();
+ let mut location = new_location(instance.id).save(&pool).await.unwrap();
+
+ for (policy, route_all_traffic, override_value, expected) in cases {
+ instance.client_traffic_policy = policy.clone();
+ instance.save(&pool).await.unwrap();
+ location.route_all_traffic = route_all_traffic;
+
+ let effective = location
+ .effective_route_all_traffic(&pool, override_value)
+ .await
+ .unwrap();
+ assert_eq!(
+ effective, expected,
+ "policy {policy:?}, flag {route_all_traffic}, override {override_value:?}"
+ );
+ }
+ }
+
+ #[sqlx::test(migrations = "../migrations")]
+ async fn test_holds_default_route_detects_default_route_in_allowed_ips(pool: SqlitePool) {
+ let instance = new_instance().save(&pool).await.unwrap();
+ let mut location = new_location(instance.id);
+ location.allowed_ips = "10.0.0.0/8, 0.0.0.0/0".into();
+ let location = location.save(&pool).await.unwrap();
+
+ assert!(!location
+ .effective_route_all_traffic(&pool, None)
+ .await
+ .unwrap());
+ assert!(location.holds_default_route(&pool, None).await.unwrap());
+
+ let mut location = new_location(instance.id);
+ location.allowed_ips = "10.0.0.0/8, 192.168.1.0/24".into();
+ let location = location.save(&pool).await.unwrap();
+
+ assert!(!location.holds_default_route(&pool, None).await.unwrap());
+ assert!(location
+ .holds_default_route(&pool, Some(true))
+ .await
+ .unwrap());
+ }
+
#[test]
fn test_service_location_mode_from_proto() {
assert_eq!(
diff --git a/src-tauri/core/src/database/models/tunnel.rs b/src-tauri/core/src/database/models/tunnel.rs
index 03f47b0ba..62e0b9fed 100644
--- a/src-tauri/core/src/database/models/tunnel.rs
+++ b/src-tauri/core/src/database/models/tunnel.rs
@@ -8,8 +8,8 @@ use sqlx::{query, query_as, query_scalar, SqliteExecutor};
use super::{connection::ActiveConnection, Id, NoId, PURGE_DURATION};
use crate::{
- CommonConnection, CommonConnectionInfo, CommonLocationStats, ConnectionType,
- DateTimeAggregation,
+ contains_default_route, CommonConnection, CommonConnectionInfo, CommonLocationStats,
+ ConnectionType, DateTimeAggregation,
};
#[serde_as]
@@ -58,6 +58,20 @@ impl fmt::Display for Tunnel {
}
impl Tunnel {
+ #[must_use]
+ pub fn effective_route_all_traffic(&self, route_all_traffic: Option) -> bool {
+ route_all_traffic.unwrap_or(self.route_all_traffic)
+ }
+
+ #[must_use]
+ pub fn holds_default_route(&self, route_all_traffic: Option) -> bool {
+ self.effective_route_all_traffic(route_all_traffic)
+ || self
+ .allowed_ips
+ .as_deref()
+ .is_some_and(contains_default_route)
+ }
+
pub async fn save<'e, E>(&mut self, executor: E) -> sqlx::Result<()>
where
E: SqliteExecutor<'e>,
diff --git a/src-tauri/core/src/database/models/tunnel_configuration.rs b/src-tauri/core/src/database/models/tunnel_configuration.rs
index be13b4bd9..8f03590f3 100644
--- a/src-tauri/core/src/database/models/tunnel_configuration.rs
+++ b/src-tauri/core/src/database/models/tunnel_configuration.rs
@@ -22,13 +22,7 @@ use crate::{
manager_for_key_and_value, LOCATION_ID, OBSERVER_COMMS, PLUGIN_BUNDLE_ID, TUNNEL_ID,
},
database::{
- models::{
- instance::{ClientTrafficPolicy, Instance},
- location::Location,
- tunnel::Tunnel,
- wireguard_keys::WireguardKeys,
- Id,
- },
+ models::{location::Location, tunnel::Tunnel, wireguard_keys::WireguardKeys, Id},
DB_POOL,
},
error::Error,
@@ -313,18 +307,7 @@ impl Location {
}
debug!("Parsing location {self} allowed IPs: {}", self.allowed_ips);
- let Some(instance) = Instance::find_by_id(&*DB_POOL, self.instance_id).await? else {
- error!("Instance {} not found", self.instance_id);
- return Err(Error::InternalError(format!(
- "Instance {} not found",
- self.instance_id
- )));
- };
- let route_all_traffic = match instance.client_traffic_policy {
- ClientTrafficPolicy::ForceAllTraffic => true,
- ClientTrafficPolicy::DisableAllTraffic => false,
- ClientTrafficPolicy::None => self.route_all_traffic,
- };
+ let route_all_traffic = self.effective_route_all_traffic(&DB_POOL, None).await?;
let allowed_ips = if route_all_traffic {
debug!("Using all traffic routing for location {self}");
vec![DEFAULT_ROUTE_IPV4.into(), DEFAULT_ROUTE_IPV6.into()]
diff --git a/src-tauri/core/src/error.rs b/src-tauri/core/src/error.rs
index d869511b0..a3105c921 100644
--- a/src-tauri/core/src/error.rs
+++ b/src-tauri/core/src/error.rs
@@ -58,6 +58,8 @@ pub enum Error {
PostureCheckFailed(String),
#[error("Service unavailable: {0}")]
ServiceUnavailable(String),
+ #[error("{0}")]
+ AllTrafficConflict(String),
}
// we must manually implement serde::Serialize
diff --git a/src-tauri/core/src/lib.rs b/src-tauri/core/src/lib.rs
index b9c00fef2..dcdef4e69 100644
--- a/src-tauri/core/src/lib.rs
+++ b/src-tauri/core/src/lib.rs
@@ -1,4 +1,4 @@
-use std::{fmt, path::PathBuf};
+use std::{fmt, path::PathBuf, str::FromStr};
#[cfg(unix)]
use std::{
fs::{set_permissions, Permissions},
@@ -124,6 +124,14 @@ pub struct CommonConnectionInfo {
pub const DEFAULT_ROUTE_IPV4: &str = "0.0.0.0/0";
pub const DEFAULT_ROUTE_IPV6: &str = "::/0";
+#[must_use]
+pub fn contains_default_route(allowed_ips: &str) -> bool {
+ allowed_ips
+ .split(',')
+ .filter_map(|entry| defguard_wireguard_rs::net::IpAddrMask::from_str(entry.trim()).ok())
+ .any(|addr| addr.address.is_unspecified() && addr.cidr == 0)
+}
+
pub enum DateTimeAggregation {
Hour,
Second,
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index 0b925d16a..8004bdd97 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -7,7 +7,7 @@ use defguard_client_core::connection::daemon_client::DAEMON_CLIENT;
use defguard_client_core::{
connection::{
active_connections::{find_connection, get_connection_id_by_type, ACTIVE_CONNECTIONS},
- disconnect_interface,
+ disconnect_interface, ConnectionTarget,
},
enrollment::{self},
mfa,
@@ -81,6 +81,8 @@ pub enum ConnectError {
#[error("Service unavailable: {0}")]
ServiceUnavailable(String),
#[error("{0}")]
+ AllTrafficConflict(String),
+ #[error("{0}")]
Other(String),
}
@@ -89,6 +91,7 @@ impl From for ConnectError {
match error {
Error::PostureCheckFailed(message) => Self::PostureCheckFailed(message),
Error::ServiceUnavailable(message) => Self::ServiceUnavailable(message),
+ Error::AllTrafficConflict(message) => Self::AllTrafficConflict(message),
error => Self::Other(error.to_string()),
}
}
@@ -1602,6 +1605,11 @@ pub async fn mfa_start(
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "Location not found".to_string())?;
+ // FIXME: ugly struct
+ ConnectionTarget::Location(location.clone())
+ .ensure_single_all_traffic_connection(&DB_POOL, None)
+ .await
+ .map_err(|err| err.to_string())?;
let posture_data = if location.posture_check_required {
Some(
defguard_client_posture::get_posture_data()