From e64dc347a6c5504bcd4aaaef5a64640442cfa518 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:56:27 +0100 Subject: [PATCH 1/7] client version checking 1 --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/bin/defguard-client.rs | 3 +- src-tauri/src/commands.rs | 12 +++++-- src-tauri/src/enterprise/periodic/config.rs | 6 +++- src-tauri/src/lib.rs | 5 ++- src-tauri/src/service/mod.rs | 1 - src-tauri/src/service/named_pipe.rs | 3 +- src-tauri/src/utils.rs | 32 +++++++++++++++++++ src/components/App/App.tsx | 31 ++++++++++++++---- src/pages/client/clientAPI/clientApi.ts | 4 +++ src/pages/client/clientAPI/types.ts | 8 ++++- src/pages/client/hooks/useClientStore.tsx | 7 +++- .../AddInstanceDeviceForm.tsx | 4 +++ .../modals/MFAModal/MFAModal.tsx | 13 ++++++-- .../components/UpdateInstanceModalForm.tsx | 5 +-- src/pages/client/query.ts | 1 + .../enrollment/hooks/useEnrollmentApi.tsx | 16 ++++++++++ src/shared/constants.ts | 4 +++ 19 files changed, 137 insertions(+), 20 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 16dbf24a..d716ed8c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1406,6 +1406,7 @@ dependencies = [ "known-folders", "log", "nix", + "os_info", "prost", "regex", "reqwest", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c148408f..a9ec85c4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -121,6 +121,7 @@ x25519-dalek = { version = "2", features = [ "serde", "static_secrets", ] } +os_info = "3.12.0" [target.'cfg(target_os = "macos")'.dependencies] swift-rs = "1.0" diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index d133701e..3d5fabe8 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -132,7 +132,8 @@ fn main() { stop_global_logwatcher, command_get_app_config, command_set_app_config, - get_provisioning_config + get_provisioning_config, + get_platform_header ]) .on_window_event(|window, event| { if let WindowEvent::CloseRequested { api, .. } = event { diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d7372597..c4ae8199 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -46,9 +46,9 @@ use crate::{ }, tray::{configure_tray_icon, reload_tray_menu}, utils::{ - 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, execute_command, + 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, @@ -1274,3 +1274,9 @@ pub fn get_provisioning_config( trace!("Returning config: {res:?}"); Ok(res) } + +#[tauri::command] +#[must_use] +pub fn get_platform_header() -> String { + construct_platform_header() +} diff --git a/src-tauri/src/enterprise/periodic/config.rs b/src-tauri/src/enterprise/periodic/config.rs index 46b87acf..894c3211 100644 --- a/src-tauri/src/enterprise/periodic/config.rs +++ b/src-tauri/src/enterprise/periodic/config.rs @@ -22,7 +22,9 @@ use crate::{ error::Error, events::EventKey, proto::{DeviceConfigResponse, InstanceInfoRequest, InstanceInfoResponse}, - MIN_CORE_VERSION, MIN_PROXY_VERSION, + utils::construct_platform_header, + CLIENT_PLATFORM_HEADER, CLIENT_VERSION_HEADER, MIN_CORE_VERSION, MIN_PROXY_VERSION, + PKG_VERSION, }; const INTERVAL_SECONDS: Duration = Duration::from_secs(30); @@ -137,6 +139,8 @@ pub async fn poll_instance( let response = Client::new() .post(url) .json(&request) + .header(CLIENT_VERSION_HEADER, PKG_VERSION) + .header(CLIENT_PLATFORM_HEADER, construct_platform_header()) .timeout(HTTP_REQ_TIMEOUT) .send() .await; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 16692304..f623567e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -36,9 +36,12 @@ pub mod wg_config; 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); +pub const CLIENT_VERSION_HEADER: &str = "defguard-client-version"; +pub const CLIENT_PLATFORM_HEADER: &str = "defguard-client-platform"; +pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); // 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. +// Returns the path to the user's data directory. #[must_use] pub fn app_data_dir() -> Option { dirs_next::data_dir().map(|dir| dir.join(BUNDLE_IDENTIFIER)) diff --git a/src-tauri/src/service/mod.rs b/src-tauri/src/service/mod.rs index 3a1d8c05..b81a70c1 100644 --- a/src-tauri/src/service/mod.rs +++ b/src-tauri/src/service/mod.rs @@ -54,7 +54,6 @@ use super::VERSION; use crate::enterprise::service_locations::ServiceLocationManager; #[cfg(windows)] use crate::service::named_pipe::{get_named_pipe_server_stream, PIPE_NAME}; - use crate::{ enterprise::service_locations::ServiceLocationError, service::proto::{DeleteServiceLocationsRequest, SaveServiceLocationsRequest}, diff --git a/src-tauri/src/service/named_pipe.rs b/src-tauri/src/service/named_pipe.rs index 2d6e3e14..321d76cc 100644 --- a/src-tauri/src/service/named_pipe.rs +++ b/src-tauri/src/service/named_pipe.rs @@ -1,6 +1,7 @@ +use std::{os::windows::io::RawHandle, pin::Pin}; + use async_stream::stream; use futures_core::stream::Stream; -use std::{os::windows::io::RawHandle, pin::Pin}; use tokio::{ io::{self, AsyncRead, AsyncWrite}, net::windows::named_pipe::NamedPipeServer, diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 9132437c..85993aaf 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -958,3 +958,35 @@ pub async fn sync_connections(app_handle: &AppHandle) -> Result<(), Error> { Ok(()) } + +/// Get generic OS name (linux, windows, macos, etc.) +#[must_use] +const fn get_os_family() -> &'static str { + #[cfg(target_os = "linux")] + return "linux"; + + #[cfg(target_os = "windows")] + return "windows"; + + #[cfg(target_os = "macos")] + return "macos"; + + #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] + return "unknown"; +} + +#[must_use] +pub(crate) fn construct_platform_header() -> String { + let os = os_info::get(); + format!( + "os_family={}; os_type={}; version={}; edition={}; codename={}; bitness={}; architecture={}", + get_os_family(), + // OS type may be more specific, e.g. "Ubuntu", "Debian" but also may not: "Windows", "Mac OS" + os.os_type(), + os.version(), + os.edition().unwrap_or(""), + os.codename().unwrap_or(""), + os.bitness(), + os.architecture().unwrap_or("") + ) +} diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index df7132cf..8b70f22c 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -4,8 +4,10 @@ import '../../shared/scss/index.scss'; import { QueryClient } from '@tanstack/query-core'; import { QueryClientProvider } from '@tanstack/react-query'; +import { getVersion } from '@tauri-apps/api/app'; import { debug, info } from '@tauri-apps/plugin-log'; import { openUrl } from '@tauri-apps/plugin-opener'; +import { exit } from '@tauri-apps/plugin-process'; import dayjs from 'dayjs'; import customParseData from 'dayjs/plugin/customParseFormat'; import duration from 'dayjs/plugin/duration'; @@ -15,6 +17,7 @@ import timezone from 'dayjs/plugin/timezone'; import updateLocale from 'dayjs/plugin/updateLocale'; import utc from 'dayjs/plugin/utc'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; import { localStorageDetector } from 'typesafe-i18n/detectors'; import TypesafeI18n from '../../i18n/i18n-react'; @@ -22,6 +25,7 @@ import { detectLocale } from '../../i18n/i18n-util'; import { loadLocaleAsync } from '../../i18n/i18n-util.async'; import { ClientPage } from '../../pages/client/ClientPage'; import { clientApi } from '../../pages/client/clientAPI/clientApi'; +import type { PlatformInfo } from '../../pages/client/clientAPI/types'; import { useClientStore } from '../../pages/client/hooks/useClientStore'; import { CarouselPage } from '../../pages/client/pages/CarouselPage/CarouselPage'; import { ClientAddedPage } from '../../pages/client/pages/ClientAddedPage/ClientAddedPage'; @@ -38,8 +42,6 @@ import { useTheme } from '../../shared/defguard-ui/hooks/theme/useTheme'; import { ThemeProvider } from '../../shared/providers/ThemeProvider/ThemeProvider'; import { routes } from '../../shared/routes'; import { ApplicationUpdateManager } from '../ApplicationUpdateManager/ApplicationUpdateManager'; -import { exit } from '@tauri-apps/plugin-process'; -import { useHotkeys } from 'react-hotkeys-hook'; dayjs.extend(duration); dayjs.extend(utc); @@ -127,12 +129,13 @@ export const App = () => { const localeLoadRef = useRef(false); const [localeLoaded, setWasLoaded] = useState(false); const [settingsLoaded, setSettingsLoaded] = useState(false); + const [platformInfoLoaded, setPlatformInfoLoaded] = useState(false); const setClientState = useClientStore((state) => state.setState); const { changeTheme } = useTheme(); const appLoaded = useMemo( - () => localeLoaded && settingsLoaded, - [localeLoaded, settingsLoaded], + () => localeLoaded && settingsLoaded && platformInfoLoaded, + [localeLoaded, settingsLoaded, platformInfoLoaded], ); // load locales @@ -190,10 +193,26 @@ export const App = () => { // register ctrl+q keyboard shortcut useHotkeys('ctrl+q', () => { - info("Ctrl-Q pressed, exiting."); - exit(0); + info('Ctrl-Q pressed, exiting.'); + exit(0); }); + useEffect(() => { + const loadPlatformInfo = async () => { + debug('Loading platform info from Tauri'); + const version = await getVersion().catch(() => 'unknown'); + const platformHeader = await clientApi.getPlatformHeader(); + const platformInfo: PlatformInfo = { + client_version: `${version}`, + platform_info: platformHeader, + }; + setClientState({ platformInfo }); + debug('Platform info loaded from Tauri'); + setPlatformInfoLoaded(true); + }; + void loadPlatformInfo(); + }, [setClientState]); + if (!appLoaded) return null; return ( diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index 4ca1506a..855416a8 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -133,6 +133,9 @@ const getAppConfig = async (): Promise => const getProvisioningConfig = async (): Promise => invokeWrapper('get_provisioning_config'); +const getPlatformHeader = async (): Promise => + invokeWrapper('get_platform_header'); + const setAppConfig = async ( appConfig: Partial, emitEvent: boolean, @@ -169,4 +172,5 @@ export const clientApi = { startGlobalLogWatcher, stopGlobalLogWatcher, getProvisioningConfig, + getPlatformHeader, }; diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 95ed9f93..9bb6dce6 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -82,6 +82,11 @@ export type AppConfig = { peer_alive_period: number; }; +export type PlatformInfo = { + client_version: string; + platform_info: string; +}; + export type ProvisioningConfig = { enrollment_token: string; enrollment_url: string; @@ -148,4 +153,5 @@ export type TauriCommandKey = | 'stop_global_logwatcher' | 'command_get_app_config' | 'command_set_app_config' - | 'get_provisioning_config'; + | 'get_provisioning_config' + | 'get_platform_header'; diff --git a/src/pages/client/hooks/useClientStore.tsx b/src/pages/client/hooks/useClientStore.tsx index d77b509f..52d16890 100644 --- a/src/pages/client/hooks/useClientStore.tsx +++ b/src/pages/client/hooks/useClientStore.tsx @@ -3,7 +3,7 @@ import { createJSONStorage, persist } from 'zustand/middleware'; import { createWithEqualityFn } from 'zustand/traditional'; import { clientApi } from '../clientAPI/clientApi'; -import type { AppConfig, ClientView } from '../clientAPI/types'; +import type { AppConfig, ClientView, PlatformInfo } from '../clientAPI/types'; import { ClientConnectionType, type CommonWireguardFields, @@ -36,6 +36,10 @@ const defaultValues: StoreValues = { check_for_updates: true, peer_alive_period: 300, }, + platformInfo: { + client_version: '', + platform_info: '', + }, }; export const useClientStore = createWithEqualityFn()( @@ -110,6 +114,7 @@ type StoreValues = { listChecked: boolean; selectedView: ClientView; appConfig: AppConfig; + platformInfo: PlatformInfo; }; type StoreMethods = { diff --git a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx index b900d693..f90527fe 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx @@ -41,6 +41,7 @@ export const AddInstanceDeviceForm = () => { const localLL = LL.pages.client.pages.addInstancePage.forms.device; const toaster = useToaster(); const setClientStore = useClientStore((state) => state.setState); + const platformInfo = useClientStore((state) => state.platformInfo); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const response = useAddInstanceStore((s) => s.response as AddInstanceInitResponse); @@ -91,9 +92,12 @@ export const AddInstanceDeviceForm = () => { name: values.name, pubkey: publicKey, }; + const headers = { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, }; try { await fetch(`${proxyUrl}/enrollment/create_device`, { diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx index 2302090b..4915788e 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx @@ -11,7 +11,6 @@ import { type SubmitHandler, useForm } from 'react-hook-form'; import ReactMarkdown from 'react-markdown'; import { z } from 'zod'; import { shallow } from 'zustand/shallow'; - import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import { Button } from '../../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; import { @@ -81,6 +80,7 @@ export const MFAModal = () => { return instances.find((i) => i.id === instanceId); } }, [location, instances]); + const platformInfo = useClientStore((state) => state.platformInfo); const resetState = () => { reset(); @@ -121,6 +121,8 @@ export const MFAModal = () => { method: 'POST', headers: { 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, }, body: JSON.stringify(data), }); @@ -176,6 +178,7 @@ export const MFAModal = () => { location, selectedInstance, toaster.error, + platformInfo, ], ); @@ -433,6 +436,7 @@ const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps const location = useMFAModal((state) => state.instance); const closeModal = useMFAModal((state) => state.close); const [errorMessage, setErrorMessage] = useState(null); + const platformInfo = useClientStore((state) => state.platformInfo); useEffect(() => { const TIMEOUT_DURATION = 5 * 1000 * 60; // 5 minutes timeout @@ -450,6 +454,8 @@ const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps method: 'POST', headers: { 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, }, body: JSON.stringify(body_token), }); @@ -503,7 +509,7 @@ const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps clearInterval(interval); clearTimeout(timeoutId); }; - }, [proxyUrl, token, location, closeModal, localLL.errors, toaster]); + }, [proxyUrl, token, location, closeModal, localLL.errors, toaster, platformInfo]); return (
@@ -552,6 +558,7 @@ const MFACodeForm = ({ description, token, proxyUrl, resetState }: MFACodeForm) const [mfaError, setMFAError] = useState(''); const localLL = LL.modals.mfa.authentication; + const platformInfo = useClientStore((state) => state.platformInfo); const schema = useMemo( () => @@ -570,6 +577,8 @@ const MFACodeForm = ({ description, token, proxyUrl, resetState }: MFACodeForm) method: 'POST', headers: { 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, }, body: JSON.stringify(data), }); diff --git a/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx b/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx index 48ea334a..1b684680 100644 --- a/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx +++ b/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx @@ -5,7 +5,6 @@ import { useMemo } from 'react'; import { type SubmitHandler, useForm } from 'react-hook-form'; import { z } from 'zod'; import { shallow } from 'zustand/shallow'; - import { useI18nContext } from '../../../../../../../i18n/i18n-react'; import { FormInput } from '../../../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { Button } from '../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; @@ -42,6 +41,7 @@ export const UpdateInstanceModalForm = () => { const toaster = useToaster(); const queryClient = useQueryClient(); const setClientState = useClientStore((s) => s.setState, shallow); + const platformInfo = useClientStore((state) => state.platformInfo); const defaultValues = useMemo( (): FormFields => ({ @@ -85,9 +85,10 @@ export const UpdateInstanceModalForm = () => { }; const endpointUrl = url(); - const headers: Record = { 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, }; const data = { diff --git a/src/pages/client/query.ts b/src/pages/client/query.ts index a19ec385..e7931262 100644 --- a/src/pages/client/query.ts +++ b/src/pages/client/query.ts @@ -9,4 +9,5 @@ export const clientQueryKeys = { getTunnels: 'GET_TUNNELS', getApplicationConfig: 'GET_APPLICATION_CONFIG', getProvisioningConfig: 'GET_PROVISIONING_CONFIG', + getPlatformHeader: 'GET_PLATFORM_HEADER', }; diff --git a/src/pages/enrollment/hooks/useEnrollmentApi.tsx b/src/pages/enrollment/hooks/useEnrollmentApi.tsx index 1557d1f8..14c93561 100644 --- a/src/pages/enrollment/hooks/useEnrollmentApi.tsx +++ b/src/pages/enrollment/hooks/useEnrollmentApi.tsx @@ -2,12 +2,14 @@ import { fetch } from '@tauri-apps/plugin-http'; import { useEnrollmentStore } from '../../../pages/enrollment/hooks/store/useEnrollmentStore'; import type { UseApi } from '../../../shared/hooks/api/types'; +import { useClientStore } from '../../client/hooks/useClientStore'; export const useEnrollmentApi = (): UseApi => { const [proxyUrl, cookie] = useEnrollmentStore((state) => [ state.proxy_url, state.cookie, ]); + const platformInfo = useClientStore((state) => state.platformInfo); const networkInfo: UseApi['enrollment']['networkInfo'] = async ( data, @@ -19,6 +21,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: overrideCookie ?? cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify(data), }); @@ -33,6 +37,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify({ method: method.valueOf(), @@ -49,6 +55,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify(data), }); @@ -62,6 +70,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify({ token: data.token, @@ -76,6 +86,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify(data), }); @@ -89,6 +101,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify(data), }); @@ -102,6 +116,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, }); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index fb750bca..25182bef 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -12,3 +12,7 @@ export const mastodonUrl = export const githubUrl = 'https://github.com/Defguard/defguard'; export const matrixUrl = 'https://matrix.to/#/#defguard:teonite.com'; + +export const CLIENT_VERSION_HEADER = 'defguard-client-version'; + +export const CLIENT_PLATFORM_HEADER = 'defguard-client-platform'; From 36d652cec238be4a0d433f1a88435ba798a23733 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:05:53 +0100 Subject: [PATCH 2/7] osinfo --- src-tauri/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a9ec85c4..48a71fd5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -121,7 +121,7 @@ x25519-dalek = { version = "2", features = [ "serde", "static_secrets", ] } -os_info = "3.12.0" +os_info = "3.12" [target.'cfg(target_os = "macos")'.dependencies] swift-rs = "1.0" From 0f6a1956dc48715989374db0cbbb9f17fc3bf10c Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:50:04 +0100 Subject: [PATCH 3/7] change way of detecting os family --- src-tauri/src/utils.rs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 85993aaf..91916386 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -959,28 +959,13 @@ pub async fn sync_connections(app_handle: &AppHandle) -> Result<(), Error> { Ok(()) } -/// Get generic OS name (linux, windows, macos, etc.) -#[must_use] -const fn get_os_family() -> &'static str { - #[cfg(target_os = "linux")] - return "linux"; - - #[cfg(target_os = "windows")] - return "windows"; - - #[cfg(target_os = "macos")] - return "macos"; - - #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] - return "unknown"; -} - #[must_use] pub(crate) fn construct_platform_header() -> String { let os = os_info::get(); format!( "os_family={}; os_type={}; version={}; edition={}; codename={}; bitness={}; architecture={}", - get_os_family(), + // OS family is more generic, e.g. "linux", "windows", "macos" + std::env::consts::OS, // OS type may be more specific, e.g. "Ubuntu", "Debian" but also may not: "Windows", "Mac OS" os.os_type(), os.version(), From cafc45d996c8f08ae377fbdfe72d1d274f9e2657 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:16:52 +0100 Subject: [PATCH 4/7] bump min client version --- src-tauri/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f623567e..e62a8b93 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -34,8 +34,8 @@ pub mod utils; pub mod wg_config; 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); +pub const MIN_CORE_VERSION: Version = Version::new(1, 6, 0); +pub const MIN_PROXY_VERSION: Version = Version::new(1, 6, 0); pub const CLIENT_VERSION_HEADER: &str = "defguard-client-version"; pub const CLIENT_PLATFORM_HEADER: &str = "defguard-client-platform"; pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); From 457b833172561cf6f8a3c55d2c93cadd3edcb124 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:36:57 +0100 Subject: [PATCH 5/7] remove all service locations if there are none in polling --- src-tauri/src/commands.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index c4ae8199..7577b4a9 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -660,7 +660,25 @@ pub(crate) async fn do_update_instance( if service_locations.is_empty() { debug!( - "No service locations to process for instance {}({})", + "No service locations for instance {}({}), removing all existing service locations connections if there are any.", + instance.name, instance.id + ); + let delete_request = DeleteServiceLocationsRequest { + instance_id: instance.uuid.clone(), + }; + DAEMON_CLIENT + .clone() + .delete_service_locations(delete_request) + .await + .map_err(|err| { + error!( + "Error while deleting service locations from the daemon for instance {}({}): {err}", + instance.name, instance.id, + ); + Error::InternalError(err.to_string()) + })?; + debug!( + "Successfully removed all service locations from daemon for instance {}({})", instance.name, instance.id ); } else { From 578049f940ea930c9f1155d497ada3c44c219d6a Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:00:11 +0100 Subject: [PATCH 6/7] encode proto --- src-tauri/src/utils.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 91916386..d518655c 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -2,8 +2,10 @@ use std::time::Duration; use std::{env, path::Path, process::Command, str::FromStr}; +use base64::{prelude::BASE64_STANDARD, Engine}; use common::{find_free_tcp_port, get_interface_name}; use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration}; +use prost::Message; use sqlx::query; #[cfg(target_os = "macos")] use swift_rs::SRString; @@ -40,6 +42,7 @@ use crate::{ error::Error, events::EventKey, log_watcher::service_log_watcher::spawn_log_watcher_task, + proto::ClientPlatformInfo, service::{ proto::{CreateInterfaceRequest, ReadInterfaceDataRequest, RemoveInterfaceRequest}, utils::DAEMON_CLIENT, @@ -962,16 +965,20 @@ pub async fn sync_connections(app_handle: &AppHandle) -> Result<(), Error> { #[must_use] pub(crate) fn construct_platform_header() -> String { let os = os_info::get(); - format!( - "os_family={}; os_type={}; version={}; edition={}; codename={}; bitness={}; architecture={}", - // OS family is more generic, e.g. "linux", "windows", "macos" - std::env::consts::OS, - // OS type may be more specific, e.g. "Ubuntu", "Debian" but also may not: "Windows", "Mac OS" - os.os_type(), - os.version(), - os.edition().unwrap_or(""), - os.codename().unwrap_or(""), - os.bitness(), - os.architecture().unwrap_or("") - ) + + let platform_info = ClientPlatformInfo { + os_family: std::env::consts::OS.to_string(), + os_type: os.os_type().to_string(), + version: os.version().to_string(), + edition: os.edition().map(str::to_string), + codename: os.codename().map(str::to_string), + bitness: Some(os.bitness().to_string()), + architecture: os.architecture().map(str::to_string), + }; + + debug!("Constructed platform info header: {platform_info:?}"); + + let buffer = platform_info.encode_to_vec(); + + BASE64_STANDARD.encode(buffer) } From 062b10ad26325ce85a3cd0262d64aae4d75fe746 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:09:49 +0100 Subject: [PATCH 7/7] Update proto --- src-tauri/proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/proto b/src-tauri/proto index 764ba6e5..96249ebd 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit 764ba6e516781f5e13719d5ea6c5e7fcc7307c53 +Subproject commit 96249ebde0556f4ae8c47eebc6015efb04ed0104