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
11 changes: 10 additions & 1 deletion crates/defguard_core/src/grpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ use crate::{
#[cfg(feature = "worker")]
use crate::{auth::ClaimsType, db::GatewayEvent};

static VERSION_ZERO: Version = Version::new(0, 0, 0);

mod auth;
pub(crate) mod client_mfa;
pub mod enrollment;
Expand Down Expand Up @@ -637,12 +639,19 @@ pub async fn run_grpc_bidi_stream(
let _guard = span.enter();
if !proxy_is_supported {
// Store incompatible proxy
let data = IncompatibleProxyData::new(version);
let maybe_version = if version == VERSION_ZERO {
None
} else {
Some(version)
};
let data = IncompatibleProxyData::new(maybe_version);
data.insert(&incompatible_components);

// Sleep before trying to reconnect
sleep(TEN_SECS).await;
continue;
} else {
IncompatibleComponents::remove_proxy(&incompatible_components);
}

info!("Connected to proxy at {}", endpoint.uri());
Expand Down
2 changes: 2 additions & 0 deletions crates/defguard_core/src/handlers/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{
appstate::AppState,
auth::{AdminRole, SessionInfo},
updates::get_update,
version::IncompatibleComponents,
};

pub(crate) async fn check_new_version(_admin: AdminRole, session: SessionInfo) -> ApiResult {
Expand All @@ -32,6 +33,7 @@ pub(crate) async fn outdated_components(
_admin: AdminRole,
State(appstate): State<AppState>,
) -> ApiResult {
IncompatibleComponents::remove_expired(&appstate.incompatible_components);
let incompatible_components = (*appstate
.incompatible_components
.read()
Expand Down
152 changes: 138 additions & 14 deletions crates/defguard_core/src/version.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use std::{
collections::HashSet,
hash::{Hash, Hasher},
sync::{Arc, RwLock},
};

use chrono::{NaiveDateTime, TimeDelta, Utc};
use serde::Serialize;
use tonic::{Status, service::Interceptor};

use defguard_version::{ComponentInfo, Version, is_version_lower};

const MIN_PROXY_VERSION: Version = Version::new(1, 5, 0);
pub const MIN_GATEWAY_VERSION: Version = Version::new(1, 5, 0);
static OUTDATED_COMPONENT_LIFETIME: TimeDelta = TimeDelta::hours(1);

/// Checks if Defguard Proxy version meets minimum version requirements.
pub(crate) fn is_proxy_version_supported(version: Option<&Version>) -> bool {
Expand Down Expand Up @@ -81,18 +84,20 @@ impl Interceptor for GatewayVersionInterceptor {
fn call(&mut self, request: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
let maybe_info = ComponentInfo::from_metadata(request.metadata());
let version = maybe_info.as_ref().map(|info| &info.version);
if !self.is_version_supported(version) {
let maybe_hostname = request
.metadata()
.get("hostname")
.and_then(|v| v.to_str().ok())
.map(String::from);
if self.is_version_supported(version) {
IncompatibleComponents::remove_gateway(&self.incompatible_components, &maybe_hostname);
} else {
let data = IncompatibleGatewayData::new(version.cloned(), maybe_hostname);
data.insert(&self.incompatible_components);
let msg = match version {
Some(version) => format!("Version {version} not supported"),
None => "Missing version headers".to_string(),
};
let maybe_hostname = request
.metadata()
.get("hostname")
.and_then(|v| v.to_str().ok())
.map(String::from);
let data = IncompatibleGatewayData::new(version.cloned(), maybe_hostname);
data.insert(&self.incompatible_components);
return Err(Status::failed_precondition(msg));
}

Expand All @@ -106,15 +111,124 @@ pub struct IncompatibleComponents {
pub proxy: Option<IncompatibleProxyData>,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
impl IncompatibleComponents {
/// Clears proxy metadata while avoiding write-locking the structure unnecessarily.
pub fn remove_proxy(components: &Arc<RwLock<Self>>) -> bool {
if components
.read()
.expect("Failed to read-lock IncompatibleComponents")
.proxy
.is_none()
{
return false;
}
components
.write()
.expect("Failed to write-lock IncompatibleComponents")
.proxy = None;

true
}

/// Removes metadata from the HashSet while avoiding write-locking the structure unnecessarily.
pub fn remove_gateway(components: &Arc<RwLock<Self>>, maybe_hostname: &Option<String>) -> bool {
if !components
.read()
.expect("Failed to read-lock IncompatibleComponents")
.gateways
.iter()
.any(|gw| &gw.hostname == maybe_hostname)
{
return false;
}
components
.write()
.expect("Failed to write-lock IncompatibleComponents")
.gateways
.retain(|gw| &gw.hostname != maybe_hostname);

true
}

/// Removes expired (older than `OUTDATED_COMPONENT_LIFETIME`) components.
/// Avoids unnecessary write-locks.
pub fn remove_expired(components: &Arc<RwLock<Self>>) -> bool {
let now = Utc::now().naive_utc();
if !Self::contains_expired(components, now) {
return false;
}

let mut components = components
.write()
.expect("Failed to write-lock IncompatibleComponents");
components.proxy = components
.proxy
.take()
.filter(|proxy| (now - proxy.created) <= OUTDATED_COMPONENT_LIFETIME);
components
.gateways
.retain(|gateway| (now - gateway.created) <= OUTDATED_COMPONENT_LIFETIME);

true
}

/// Returns true if expired (older than `OUTDATED_COMPONENT_LIFETIME`) components exist.
fn contains_expired(components: &Arc<RwLock<Self>>, now: NaiveDateTime) -> bool {
if components
.read()
.expect("Failed to read-lock IncompatibleComponents")
.proxy
.as_ref()
.filter(|proxy| (now - proxy.created) > OUTDATED_COMPONENT_LIFETIME)
.is_some()
{
return true;
}

if components
.read()
.expect("Failed to read-lock IncompatibleComponents")
.gateways
.iter()
.any(|gateway| (now - gateway.created) > OUTDATED_COMPONENT_LIFETIME)
{
return true;
}

false
}
}

#[derive(Clone, Debug, Serialize)]
pub struct IncompatibleGatewayData {
pub version: Option<Version>,
pub hostname: Option<String>,
created: NaiveDateTime,
}

impl PartialEq for IncompatibleGatewayData {
fn eq(&self, other: &Self) -> bool {
self.version == other.version && self.hostname == other.hostname
}
}

impl Eq for IncompatibleGatewayData {}

impl Hash for IncompatibleGatewayData {
fn hash<H: Hasher>(&self, state: &mut H) {
self.version.hash(state);
self.hostname.hash(state);
}
}

impl IncompatibleGatewayData {
pub fn new(version: Option<Version>, hostname: Option<String>) -> Self {
Self { version, hostname }
let created = Utc::now().naive_utc();
Self {
version,
hostname,
created,
}
}

/// Inserts metadata into the HashSet while avoiding write-locking the structure unnecessarily.
Expand All @@ -135,14 +249,24 @@ impl IncompatibleGatewayData {
}
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[derive(Clone, Debug, Serialize)]
pub struct IncompatibleProxyData {
pub version: Version,
pub version: Option<Version>,
created: NaiveDateTime,
}

impl PartialEq for IncompatibleProxyData {
fn eq(&self, other: &Self) -> bool {
self.version == other.version
}
}

impl Eq for IncompatibleProxyData {}

impl IncompatibleProxyData {
pub fn new(version: Version) -> Self {
Self { version }
pub fn new(version: Option<Version>) -> Self {
let created = Utc::now().naive_utc();
Self { version, created }
}

/// Inserts metadata while avoiding write-locking the structure unnecessarily.
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@tanstack/react-virtual": "3.13.12",
"@tanstack/virtual-core": "3.13.12",
"@use-gesture/react": "^10.3.1",
"axios": "^1.11.0",
"axios": "^1.12.0",
"byte-size": "^9.0.1",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
Expand Down
21 changes: 15 additions & 6 deletions web/pnpm-lock.yaml

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

Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ type ProxyItemProps = {
};

const ProxyListItem = ({ data }: ProxyItemProps) => {
const { LL } = useI18nContext();
const localLL = LL.modals.outdatedComponentsModal.content;
return (
<li>
<div>
Proxy
<span>-</span>
<span className="version">{data.version}</span>
<span className="version">{data.version || localLL.unknownVersion()}</span>
</div>
</li>
);
Expand Down
2 changes: 1 addition & 1 deletion web/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
import type { UpdateInfo } from './hooks/store/useUpdatesStore';

export type OutdatedProxy = {
version: string;
version?: string;
};

export type OutdatedGateway = {
Expand Down