Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .typesafe-i18n.json
Original file line number Diff line number Diff line change
@@ -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"
}
12 changes: 6 additions & 6 deletions flake.lock

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

4 changes: 3 additions & 1 deletion src-tauri/src/bin/defguard-client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ fn main() {
save_device_config,
all_instances,
connect,
connect_with_posture,
disconnect,
update_instance,
location_stats,
Expand All @@ -180,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 {
Expand Down
31 changes: 29 additions & 2 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ 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::{
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,
Expand Down Expand Up @@ -418,6 +423,7 @@ pub struct LocationInfo {
pub pubkey: String,
pub network_id: Id,
pub location_mfa_mode: LocationMfaMode,
pub posture_check_required: bool,
}

impl LocationInfo {
Expand Down Expand Up @@ -470,6 +476,7 @@ pub async fn all_locations(instance_id: Id) -> Result<Vec<LocationInfo>, 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);
}
Expand Down Expand Up @@ -1413,6 +1420,26 @@ 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
}

#[tauri::command(async)]
pub async fn get_posture_data() -> Result<DevicePostureData, Error> {
debug!("Received a command to prepare posture report");
Ok(DevicePostureData::new())
}

#[cfg(test)]
mod tests {
use super::select_reported_app_version;
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/enterprise/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod inspector;
pub mod models;
pub mod periodic;
pub mod posture;
pub mod provisioning;
pub mod service_locations;
103 changes: 103 additions & 0 deletions src-tauri/src/enterprise/posture.rs
Original file line number Diff line number Diff line change
@@ -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}"
))),
}
}
4 changes: 4 additions & 0 deletions src-tauri/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/pages/client/clientAPI/clientApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
const connect = async (data: ConnectionRequest): Promise<void> =>
invokeWrapper('connect', data);

const connectWithPosture = async (data: ConnectionRequest): Promise<void> =>
invokeWrapper('connect_with_posture', data);

const disconnect = async (data: ConnectionRequest): Promise<void> =>
invokeWrapper('disconnect', data);

Expand Down Expand Up @@ -130,6 +133,9 @@
const getAppConfig = async (): Promise<AppConfig> =>
invokeWrapper('command_get_app_config');

const getPostureData = async (): Promise<DevicePostureData> =>

Check failure on line 136 in src/pages/client/clientAPI/clientApi.ts

View workflow job for this annotation

GitHub Actions / lint-web

Cannot find name 'DevicePostureData'.
invokeWrapper('get_posture_data');

Check failure on line 137 in src/pages/client/clientAPI/clientApi.ts

View workflow job for this annotation

GitHub Actions / lint-web

Argument of type '"get_posture_data"' is not assignable to parameter of type 'TauriCommandKey'.

const getProvisioningConfig = async (): Promise<ProvisioningConfig | null> =>
invokeWrapper('get_provisioning_config');

Expand All @@ -149,9 +155,11 @@
getAppConfig,
setAppConfig,
getInstances,
getPostureData,
getTunnels,
getLocations,
connect,
connectWithPosture,
disconnect,
getLocationStats,
getLastConnection,
Expand Down
1 change: 1 addition & 0 deletions src/pages/client/clientAPI/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export type TauriCommandKey =
| 'all_instances'
| 'all_locations'
| 'connect'
| 'connect_with_posture'
| 'disconnect'
| 'location_stats'
| 'last_connection'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,6 +50,11 @@ export const LocationCardConnectButton = ({ location }: Props) => {
} else {
if (mfaEnabled) {
openMFAModal(location);
} else if (location.posture_check_required) {
await connectWithPosture({
locationId: location.id,
connectionType: location.connection_type,
});
} else {
await connect({
locationId: location?.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading