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
14 changes: 7 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ on:
branches:
- main
- dev
- "release/**"
- 'release/**'
paths-ignore:
- "*.md"
- "LICENSE"
- '*.md'
- 'LICENSE'
pull_request:
branches:
- main
- dev
- "release/**"
- 'release/**'
paths-ignore:
- "*.md"
- "LICENSE"
- '*.md'
- 'LICENSE'

env:
CARGO_TERM_COLOR: always
Expand All @@ -27,7 +27,7 @@ jobs:
- codebuild-defguard-client-runner-${{ github.run_id }}-${{ github.run_attempt }}

container:
image: rust:1
image: public.ecr.aws/docker/library/rust:1
options: --user root

defaults:
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

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

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ x25519-dalek = { version = "2", features = [
] }
tauri-plugin-opener = "2.5.0"
tauri-plugin-os = "2.3.1"
semver = "1.0.26"

[target.'cfg(unix)'.dependencies]
tokio-stream = "0.1"
Expand Down
143 changes: 142 additions & 1 deletion src-tauri/src/enterprise/periodic/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
use std::{str::FromStr, time::Duration};
use std::{
cmp::Ordering,
collections::HashSet,
str::FromStr,
sync::{LazyLock, Mutex},
time::Duration,
};

use reqwest::{Client, StatusCode};
use serde::Serialize;
use sqlx::{Sqlite, Transaction};
use tauri::{AppHandle, Emitter, Url};
use tokio::time::sleep;
Expand All @@ -15,6 +22,7 @@ use crate::{
error::Error,
events::EventKey,
proto::{DeviceConfigResponse, InstanceInfoRequest, InstanceInfoResponse},
MIN_CORE_VERSION, MIN_PROXY_VERSION,
};

const INTERVAL_SECONDS: Duration = Duration::from_secs(30);
Expand All @@ -26,6 +34,8 @@ static POLLING_ENDPOINT: &str = "/api/v1/poll";
/// otherwise event is emmited and UI message is displayed.
pub async fn poll_config(handle: AppHandle) {
debug!("Starting the configuration polling loop.");
// Polling starts sooner than app's frontend may load in dev builds, causing events (toasts) to be lost,
// you may want to wait here before starting if you want to debug it.
loop {
let Ok(mut transaction) = DB_POOL.begin().await else {
error!(
Expand Down Expand Up @@ -141,6 +151,8 @@ pub async fn poll_instance(
instance.name
);

check_min_version(&response, instance, handle)?;

// Return early if the enterprise features are disabled in the core
if response.status() == StatusCode::PAYMENT_REQUIRED {
debug!(
Expand Down Expand Up @@ -256,3 +268,132 @@ fn build_request(instance: &Instance<Id>) -> Result<InstanceInfoRequest, Error>
token: (*token).to_string(),
})
}

/// Tracks instance IDs that for which we already sent notification about version mismatches
/// to prevent duplicate notifications in the app's lifetime.
static NOTIFIED_INSTANCES: LazyLock<Mutex<HashSet<Id>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));

const CORE_VERSION_HEADER: &str = "defguard-core-version";
const PROXY_VERSION_HEADER: &str = "defguard-component-version";

#[derive(Clone, Serialize)]
struct VersionMismatchPayload {
instance_name: String,
instance_id: Id,
core_version: String,
proxy_version: String,
core_required_version: String,
proxy_required_version: String,
core_compatible: bool,
proxy_compatible: bool,
}

fn check_min_version(
response: &reqwest::Response,
instance: &Instance<Id>,
handle: &AppHandle,
) -> Result<(), Error> {
let mut notified_instances = NOTIFIED_INSTANCES.lock().unwrap();
if notified_instances.contains(&instance.id) {
debug!(
"Instance {}({}) already notified about version mismatch, skipping",
instance.name, instance.id
);
return Ok(());
}

let detected_core_version: String;
let detected_proxy_version: String;

let core_compatible = if let Some(core_version) = response.headers().get(CORE_VERSION_HEADER) {
if let Ok(core_version) = core_version.to_str() {
if let Ok(core_version) = semver::Version::from_str(core_version) {
detected_core_version = core_version.to_string();
core_version.cmp_precedence(&MIN_CORE_VERSION) != Ordering::Less
} else {
warn!(
"Core version header not a valid semver string in response for instance {}({}): \
'{core_version}'",
instance.name, instance.id
);
detected_core_version = core_version.to_string();
false
}
} else {
warn!(
"Core version header not a valid string in response for instance {}({}): \
'{core_version:?}'",
instance.name, instance.id
);
detected_core_version = "unknown".to_string();
false
}
} else {
warn!(
"Core version header not present in response for instance {}({})",
instance.name, instance.id
);
detected_core_version = "unknown".to_string();
false
};

let proxy_compatible = if let Some(proxy_version) = response.headers().get(PROXY_VERSION_HEADER)
{
if let Ok(proxy_version) = proxy_version.to_str() {
if let Ok(proxy_version) = semver::Version::from_str(proxy_version) {
detected_proxy_version = proxy_version.to_string();
proxy_version.cmp_precedence(&MIN_PROXY_VERSION) != Ordering::Less
} else {
warn!(
"Proxy version header not a valid semver string in response for instance {}({}): \
'{proxy_version}'",
instance.name, instance.id
);
detected_proxy_version = proxy_version.to_string();
false
}
} else {
warn!(
"Proxy version header not a valid string in response for instance {}({}): \
'{proxy_version:?}'",
instance.name, instance.id
);
detected_proxy_version = "unknown".to_string();
false
}
} else {
warn!(
"Proxy version header not present in response for instance {}({})",
instance.name, instance.id
);
detected_proxy_version = "unknown".to_string();
false
};

if !core_compatible || !proxy_compatible {
warn!(
"Instance {} is running incompatible versions: core {detected_core_version}, proxy {detected_proxy_version}. Required \
versions: core >= {MIN_CORE_VERSION}, proxy >= {MIN_PROXY_VERSION}",
instance.name,
);

let payload = VersionMismatchPayload {
instance_name: instance.name.clone(),
instance_id: instance.id,
core_version: detected_core_version,
proxy_version: detected_proxy_version,
core_required_version: MIN_CORE_VERSION.to_string(),
proxy_required_version: MIN_PROXY_VERSION.to_string(),
core_compatible,
proxy_compatible,
};
if let Err(err) = handle.emit(EventKey::VersionMismatch.into(), payload) {
error!("Failed to emit version mismatch event to the frontend: {err}");
} else {
notified_instances.insert(instance.id);
}
}

Ok(())
}
2 changes: 2 additions & 0 deletions src-tauri/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum EventKey {
ApplicationConfigChanged,
AddInstance,
MfaTrigger,
VersionMismatch,
}

impl From<EventKey> for &'static str {
Expand All @@ -32,6 +33,7 @@ impl From<EventKey> for &'static str {
EventKey::ApplicationConfigChanged => "application-config-changed",
EventKey::AddInstance => "add-instance",
EventKey::MfaTrigger => "mfa-trigger",
EventKey::VersionMismatch => "version-mismatch",
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::{fmt, path::PathBuf};

use chrono::NaiveDateTime;
use semver::Version;
use serde::{Deserialize, Serialize};

use self::database::models::{Id, NoId};
Expand Down Expand Up @@ -66,6 +67,8 @@ pub mod proto {
}

pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA"));
pub const MIN_CORE_VERSION: Version = Version::new(1, 5, 0);
pub const MIN_PROXY_VERSION: Version = Version::new(1, 5, 0);
// This must match tauri.bundle.identifier from tauri.conf.json.
const BUNDLE_IDENTIFIER: &str = "net.defguard";
// Returns the path to the user’s data directory.
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ const en = {
error: 'Clipboard is not accessible.',
success: 'Content copied to clipboard.',
},
versionMismatch:
'Your Defguard instance "{instance_name: string}" version is not supported by your Defguard Client version. \
Defguard Core version: {core_version: string} (required: {core_required_version: string}), Defguard Proxy version: {proxy_version: string} (required: {proxy_required_version: string}). \
Please contact your administrator.',
},
},
components: {
Expand Down
13 changes: 13 additions & 0 deletions src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,15 @@ type RootTranslation = {
*/
success: string
}
/**
* Y​o​u​r​ ​D​e​f​g​u​a​r​d​ ​i​n​s​t​a​n​c​e​ ​"​{​i​n​s​t​a​n​c​e​_​n​a​m​e​}​"​ ​v​e​r​s​i​o​n​ ​i​s​ ​n​o​t​ ​s​u​p​p​o​r​t​e​d​ ​b​y​ ​y​o​u​r​ ​D​e​f​g​u​a​r​d​ ​C​l​i​e​n​t​ ​v​e​r​s​i​o​n​.​ ​ ​ ​ ​ ​ ​ ​ ​ ​D​e​f​g​u​a​r​d​ ​C​o​r​e​ ​v​e​r​s​i​o​n​:​ ​{​c​o​r​e​_​v​e​r​s​i​o​n​}​ ​(​r​e​q​u​i​r​e​d​:​ ​{​c​o​r​e​_​r​e​q​u​i​r​e​d​_​v​e​r​s​i​o​n​}​)​,​ ​D​e​f​g​u​a​r​d​ ​P​r​o​x​y​ ​v​e​r​s​i​o​n​:​ ​{​p​r​o​x​y​_​v​e​r​s​i​o​n​}​ ​(​r​e​q​u​i​r​e​d​:​ ​{​p​r​o​x​y​_​r​e​q​u​i​r​e​d​_​v​e​r​s​i​o​n​}​)​.​ ​ ​ ​ ​ ​ ​ ​ ​ ​P​l​e​a​s​e​ ​c​o​n​t​a​c​t​ ​y​o​u​r​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​.
* @param {string} core_required_version
* @param {string} core_version
* @param {string} instance_name
* @param {string} proxy_required_version
* @param {string} proxy_version
*/
versionMismatch: RequiredParams<'core_required_version' | 'core_version' | 'instance_name' | 'proxy_required_version' | 'proxy_version'>
}
}
components: {
Expand Down Expand Up @@ -1871,6 +1880,10 @@ export type TranslationFunctions = {
*/
success: () => LocalizedString
}
/**
* Your Defguard instance "{instance_name}" version is not supported by your Defguard Client version. Defguard Core version: {core_version} (required: {core_required_version}), Defguard Proxy version: {proxy_version} (required: {proxy_required_version}). Please contact your administrator.
*/
versionMismatch: (arg: { core_required_version: string, core_version: string, instance_name: string, proxy_required_version: string, proxy_version: string }) => LocalizedString
}
}
components: {
Expand Down
24 changes: 24 additions & 0 deletions src/pages/client/ClientPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,29 @@ export const ClientPage = () => {
});
});

const verionMismatch = listen(TauriEventKey.VERSION_MISMATCH, (data) => {
const payload = data.payload as {
instance_name: string;
instance_id: number;
core_version: string;
proxy_version: string;
core_required_version: string;
proxy_required_version: string;
core_compatible: boolean;
proxy_compatible: boolean;
};
toaster.error(
LL.common.messages.versionMismatch({
instance_name: payload.instance_name,
core_version: payload.core_version,
proxy_version: payload.proxy_version,
core_required_version: payload.core_required_version,
proxy_required_version: payload.proxy_required_version,
}),
{ lifetime: -1 },
);
});

const locationUpdate = listen(TauriEventKey.LOCATION_UPDATE, () => {
const invalidate = [clientQueryKeys.getLocations, clientQueryKeys.getTunnels];
invalidate.forEach((key) => {
Expand Down Expand Up @@ -156,6 +179,7 @@ export const ClientPage = () => {
locationUpdate.then((cleanup) => cleanup());
appConfigChanged.then((cleanup) => cleanup());
mfaTrigger.then((cleanup) => cleanup());
verionMismatch.then((cleanup) => cleanup());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down
1 change: 1 addition & 0 deletions src/pages/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,5 @@ export enum TauriEventKey {
DEAD_CONNECTION_RECONNECTED = 'dead-connection-reconnected',
APPLICATION_CONFIG_CHANGED = 'application-config-changed',
MFA_TRIGGER = 'mfa-trigger',
VERSION_MISMATCH = 'version-mismatch',
}