From ef0b63d20b7e42f0cba7dfa5e799d21b818754f4 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 15 May 2026 08:53:23 +0200 Subject: [PATCH 1/5] nix flake update --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 21b6ba82..ab254e25 100644 --- a/flake.lock +++ b/flake.lock @@ -44,11 +44,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771423170, - "narHash": "sha256-K7Dg9TQ0mOcAtWTO/FX/FaprtWQ8BmEXTpLIaNRhEwU=", + "lastModified": 1778762200, + "narHash": "sha256-NiOfW9nHPaDebfu3svLnJbljIMQgkEQnkm3orhePHPY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "bcc4a9d9533c033d806a46b37dc444f9b0da49dd", + "rev": "27198194e52129ccd8e845e7d5911911e7fea7d7", "type": "github" }, "original": { @@ -99,11 +99,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1771816254, - "narHash": "sha256-vkp3iTF6QmHMvL+34DI93IiMPjS2lqcMlA1fl7nXVsQ=", + "lastModified": 1778815121, + "narHash": "sha256-xlhD+1NVJbhrUUM2usRHW6iKWTXP2uw2Fo6sWJmLg8g=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "085bdbf5dde5477538e4c87d1684b6c6df56c0ad", + "rev": "017351829a9356423afd2cca0dde9b63346c8ab3", "type": "github" }, "original": { From 552c5f6afba832204e489054d8bdf43c2c774ace Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 15 May 2026 12:39:45 +0200 Subject: [PATCH 2/5] initial work on posture checks on old UI --- src-tauri/src/commands.rs | 2 ++ .../LocationCardConnectButton/LocationCardConnectButton.tsx | 4 ++++ src/pages/client/types.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index eaf5cdb0..3571fada 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -418,6 +418,7 @@ pub struct LocationInfo { pub pubkey: String, pub network_id: Id, pub location_mfa_mode: LocationMfaMode, + pub posture_check_required: bool, } impl LocationInfo { @@ -470,6 +471,7 @@ pub async fn all_locations(instance_id: Id) -> Result, Error> pubkey: location.pubkey, network_id: location.network_id, location_mfa_mode: location.location_mfa_mode, + posture_check_required: location.posture_check_required, }; location_info.push(info); } diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx index 1b416876..238787e1 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx @@ -40,6 +40,8 @@ export const LocationCardConnectButton = ({ location }: Props) => { const handleClick = async () => { setIsLoading(true); + console.log('location:', location); + console.log('location.posture_check_required:', location?.posture_check_required); try { if (location) { if (location?.active) { @@ -50,6 +52,8 @@ export const LocationCardConnectButton = ({ location }: Props) => { } else { if (mfaEnabled) { openMFAModal(location); + } else if (location.posture_check_required) { + console.log('location.posture_check_required'); } else { await connect({ locationId: location?.id, diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts index 8c0b1e66..9954bb26 100644 --- a/src/pages/client/types.ts +++ b/src/pages/client/types.ts @@ -75,7 +75,7 @@ export type CommonWireguardFields = { pubkey: string; instance_id: number; network_id: number; - // TODO: device posture data, if available + posture_check_required: boolean; }; export type SelectedInstance = { From 01c97f128fd68d76c517d8e10dd1a2c9fd9f3a4c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Sat, 16 May 2026 08:25:34 +0200 Subject: [PATCH 3/5] nix flake update --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index ab254e25..37bef692 100644 --- a/flake.lock +++ b/flake.lock @@ -44,11 +44,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1778762200, - "narHash": "sha256-NiOfW9nHPaDebfu3svLnJbljIMQgkEQnkm3orhePHPY=", + "lastModified": 1778794387, + "narHash": "sha256-BL04pOS9453Awkeb9f90XBJXBSkWxN+vB7HIgnL0iMM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "27198194e52129ccd8e845e7d5911911e7fea7d7", + "rev": "8a1b0127302ea51e05bf4ea5a291743fac442406", "type": "github" }, "original": { From 9bdd8cf36c60589d3d7094ece2e07b489519565d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Sat, 16 May 2026 09:18:00 +0200 Subject: [PATCH 4/5] posture check for non-mfa locations --- .typesafe-i18n.json | 2 +- src-tauri/src/bin/defguard-client.rs | 1 + src-tauri/src/commands.rs | 19 +++- src-tauri/src/enterprise/mod.rs | 1 + src-tauri/src/enterprise/posture.rs | 103 ++++++++++++++++++ src-tauri/src/error.rs | 4 + src/pages/client/clientAPI/clientApi.ts | 4 + src/pages/client/clientAPI/types.ts | 1 + .../LocationCardConnectButton.tsx | 9 +- 9 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 src-tauri/src/enterprise/posture.rs diff --git a/.typesafe-i18n.json b/.typesafe-i18n.json index b3224e1e..e5c75769 100644 --- a/.typesafe-i18n.json +++ b/.typesafe-i18n.json @@ -1,4 +1,4 @@ { "adapter": "react", - "$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json" + "$schema": "https://unpkg.com/typesafe-i18n@5.27.1/schema/typesafe-i18n.json" } \ No newline at end of file diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index a01e6f99..88fac9af 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -158,6 +158,7 @@ fn main() { save_device_config, all_instances, connect, + connect_with_posture, disconnect, update_instance, location_stats, diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 3571fada..18a97df1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -29,7 +29,10 @@ use crate::{ }, DB_POOL, }, - enterprise::{periodic::config::poll_instance, provisioning::ProvisioningConfig}, + enterprise::{ + periodic::config::poll_instance, posture::connect_with_posture_check, + provisioning::ProvisioningConfig, + }, error::Error, events::EventKey, log_watcher::{ @@ -1415,6 +1418,20 @@ pub fn get_platform_header() -> String { construct_platform_header() } +/// Connect to a location that requires a posture check. +/// +/// Collects device posture data, sends it to the proxy, and on success establishes +/// the WireGuard tunnel using the returned preshared key. +#[tauri::command(async)] +pub async fn connect_with_posture( + location_id: Id, + _connection_type: ConnectionType, + handle: AppHandle, +) -> Result<(), Error> { + debug!("Received a command to connect with posture check to location with ID {location_id}"); + connect_with_posture_check(location_id, &handle).await +} + #[cfg(test)] mod tests { use super::select_reported_app_version; diff --git a/src-tauri/src/enterprise/mod.rs b/src-tauri/src/enterprise/mod.rs index 6074c054..a76ac9a2 100644 --- a/src-tauri/src/enterprise/mod.rs +++ b/src-tauri/src/enterprise/mod.rs @@ -1,5 +1,6 @@ pub mod inspector; pub mod models; pub mod periodic; +pub mod posture; pub mod provisioning; pub mod service_locations; diff --git a/src-tauri/src/enterprise/posture.rs b/src-tauri/src/enterprise/posture.rs new file mode 100644 index 00000000..04055f31 --- /dev/null +++ b/src-tauri/src/enterprise/posture.rs @@ -0,0 +1,103 @@ +use std::time::Duration; + +use reqwest::{Client, StatusCode}; +use serde::Deserialize; +use tauri::AppHandle; + +use crate::{ + database::{ + models::{instance::Instance, location::Location, wireguard_keys::WireguardKeys}, + DB_POOL, + }, + error::Error, + proto::defguard::enterprise::posture::v2::{ + DevicePostureCheckRequest, DevicePostureCheckResponse, DevicePostureData, + }, + tray::{configure_tray_icon, reload_tray_menu}, + utils::{construct_platform_header, handle_connection_for_location}, + CLIENT_PLATFORM_HEADER, CLIENT_VERSION_HEADER, PKG_VERSION, +}; + +const HTTP_TIMEOUT: Duration = Duration::from_secs(10); +const POSTURE_ENDPOINT: &str = "/api/v1/posture/connect"; + +/// Collects device posture data, sends it to the proxy, and on success establishes +/// the WireGuard tunnel using the returned preshared key. +pub async fn connect_with_posture_check( + location_id: crate::database::models::Id, + handle: &AppHandle, +) -> Result<(), Error> { + let location = Location::find_by_id(&*DB_POOL, location_id) + .await? + .ok_or(Error::NotFound)?; + + let instance = Instance::find_by_id(&*DB_POOL, location.instance_id) + .await? + .ok_or(Error::NotFound)?; + + let keys = WireguardKeys::find_by_instance_id(&*DB_POOL, location.instance_id) + .await? + .ok_or_else(|| { + Error::ResourceNotFound(format!( + "WireGuard keys not found for instance {}", + location.instance_id + )) + })?; + + let posture_data = DevicePostureData::new(); + + let request = DevicePostureCheckRequest { + location_id: location_id as i64, + pubkey: keys.pubkey, + device_posture_data: Some(posture_data), + }; + + let proxy_url = tauri::Url::parse(&instance.proxy_url) + .map_err(|e| Error::InternalError(format!("Invalid proxy URL: {e}")))? + .join(POSTURE_ENDPOINT) + .map_err(|e| Error::InternalError(format!("Failed to build posture URL: {e}")))?; + + debug!("Sending posture check request to {proxy_url}"); + let response = Client::new() + .post(proxy_url) + .json(&request) + .header(CLIENT_VERSION_HEADER, PKG_VERSION) + .header(CLIENT_PLATFORM_HEADER, construct_platform_header()) + .timeout(HTTP_TIMEOUT) + .send() + .await + .map_err(|e| Error::HttpError(e.to_string()))?; + + match response.status() { + StatusCode::OK => { + let body: DevicePostureCheckResponse = response + .json() + .await + .map_err(|e| Error::HttpError(e.to_string()))?; + debug!("Posture check approved for location {location_id}, connecting..."); + handle_connection_for_location(&location, Some(body.preshared_key), handle).await?; + reload_tray_menu(handle).await; + configure_tray_icon(handle).await?; + info!("Connected to location {location} after posture check"); + Ok(()) + } + StatusCode::FORBIDDEN => { + #[derive(Deserialize)] + struct PostureRejection { + error: String, + } + let body: PostureRejection = response + .json() + .await + .map_err(|e| Error::HttpError(e.to_string()))?; + error!( + "Posture check rejected for location {location_id}: {}", + body.error + ); + Err(Error::PostureCheckFailed(body.error)) + } + status => Err(Error::HttpError(format!( + "Unexpected proxy response: {status}" + ))), + } +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 176710cb..28a6db94 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -46,6 +46,10 @@ pub enum Error { ConversionError(String), #[error("JSON error: {0}")] JsonError(#[from] serde_json::Error), + #[error("HTTP request error: {0}")] + HttpError(String), + #[error("Posture check failed: {0}")] + PostureCheckFailed(String), } // we must manually implement serde::Serialize diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index 3fab2b3c..5c7d6a44 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -68,6 +68,9 @@ const getLocations = async ( const connect = async (data: ConnectionRequest): Promise => invokeWrapper('connect', data); +const connectWithPosture = async (data: ConnectionRequest): Promise => + invokeWrapper('connect_with_posture', data); + const disconnect = async (data: ConnectionRequest): Promise => invokeWrapper('disconnect', data); @@ -152,6 +155,7 @@ export const clientApi = { getTunnels, getLocations, connect, + connectWithPosture, disconnect, getLocationStats, getLastConnection, diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 0b970e95..e4131d02 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -132,6 +132,7 @@ export type TauriCommandKey = | 'all_instances' | 'all_locations' | 'connect' + | 'connect_with_posture' | 'disconnect' | 'location_stats' | 'last_connection' diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx index 238787e1..14f78790 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx @@ -17,7 +17,7 @@ import { clientApi } from '../../../../../../clientAPI/clientApi'; import { type CommonWireguardFields, LocationMfaType } from '../../../../../../types'; import { useMFAModal } from '../../modals/MFAModal/useMFAModal'; -const { connect, disconnect } = clientApi; +const { connect, connectWithPosture, disconnect } = clientApi; type Props = { location?: CommonWireguardFields; @@ -40,8 +40,6 @@ export const LocationCardConnectButton = ({ location }: Props) => { const handleClick = async () => { setIsLoading(true); - console.log('location:', location); - console.log('location.posture_check_required:', location?.posture_check_required); try { if (location) { if (location?.active) { @@ -53,7 +51,10 @@ export const LocationCardConnectButton = ({ location }: Props) => { if (mfaEnabled) { openMFAModal(location); } else if (location.posture_check_required) { - console.log('location.posture_check_required'); + await connectWithPosture({ + locationId: location.id, + connectionType: location.connection_type, + }); } else { await connect({ locationId: location?.id, From a41bb1ebdfce5ecfb8602545cddbdaa1d6073e76 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Sat, 16 May 2026 11:02:02 +0200 Subject: [PATCH 5/5] posture checks during MFA connection flow --- src-tauri/src/bin/defguard-client.rs | 3 ++- src-tauri/src/commands.rs | 10 +++++++++- src/pages/client/clientAPI/clientApi.ts | 4 ++++ .../LocationsList/modals/MFAModal/MFAModal.tsx | 10 ++++++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 88fac9af..086a2718 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -181,7 +181,8 @@ fn main() { command_get_app_config, command_set_app_config, get_provisioning_config, - get_platform_header + get_platform_header, + get_posture_data, ]) .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 18a97df1..727fad94 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -39,7 +39,9 @@ use crate::{ global_log_watcher::{spawn_global_log_watcher_task, stop_global_log_watcher_task}, service_log_watcher::stop_log_watcher_task, }, - proto::defguard::client_types::DeviceConfigResponse, + proto::defguard::{ + client_types::DeviceConfigResponse, enterprise::posture::v2::DevicePostureData, + }, tray::{configure_tray_icon, reload_tray_menu}, utils::{ construct_platform_header, disconnect_interface, get_location_interface_details, @@ -1432,6 +1434,12 @@ pub async fn connect_with_posture( connect_with_posture_check(location_id, &handle).await } +#[tauri::command(async)] +pub async fn get_posture_data() -> Result { + debug!("Received a command to prepare posture report"); + Ok(DevicePostureData::new()) +} + #[cfg(test)] mod tests { use super::select_reported_app_version; diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index 5c7d6a44..172b3e53 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -133,6 +133,9 @@ const stopGlobalLogWatcher = async (): Promise => const getAppConfig = async (): Promise => invokeWrapper('command_get_app_config'); +const getPostureData = async (): Promise => + invokeWrapper('get_posture_data'); + const getProvisioningConfig = async (): Promise => invokeWrapper('get_provisioning_config'); @@ -152,6 +155,7 @@ export const clientApi = { getAppConfig, setAppConfig, getInstances, + getPostureData, getTunnels, getLocations, connect, 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 cc63098c..0cd1e900 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 @@ -31,7 +31,7 @@ import { MfaMobileApprove } from './components/MfaMobileApprove/MfaMobileApprove import { BrowserErrorIcon, BrowserPendingIcon, GoToBrowserIcon } from './Icons'; import { useMFAModal } from './useMFAModal'; -const { connect } = clientApi; +const { connect, getPostureData } = clientApi; const CODE_LENGTH = 6; const CLIENT_MFA_ENDPOINT = 'api/v1/client-mfa'; @@ -112,11 +112,17 @@ export const MFAModal = () => { setProxyUrl(selectedInstance.proxy_url); const mfaStartUrl = `${selectedInstance.proxy_url + CLIENT_MFA_ENDPOINT}/start`; + console.log("MFAMofal, location.posture_check_required:", location.posture_check_required); + const posture_data = location.posture_check_required + ? await getPostureData() + : undefined; const data = { method, pubkey: selectedInstance.pubkey, location_id: location.network_id, + posture_data, }; + console.log("MFAModal, data:", data); try { const response = await fetch(mfaStartUrl, { @@ -168,7 +174,7 @@ export const MFAModal = () => { if (errorData === 'selected MFA method not available') { toaster.error(localLL.errors.mfaNotConfigured()); } else { - toaster.error(localLL.errors.mfaStartGeneric()); + toaster.error(errorData); } return;