From de9d1738ac2af5ef6a762da61c350dbfef6de178 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:56:54 +0100 Subject: [PATCH 01/18] issue certificates for gateway --- Cargo.lock | 2 + crates/defguard_certs/Cargo.toml | 2 + crates/defguard_certs/src/lib.rs | 73 +++- .../defguard_common/src/db/models/gateway.rs | 4 + .../defguard_core/src/grpc/gateway/handler.rs | 326 ++++++++++++------ crates/defguard_core/src/grpc/gateway/mod.rs | 79 +++-- ...4_gateway_certificates_management.down.sql | 2 + ...304_gateway_certificates_management.up.sql | 2 + 8 files changed, 358 insertions(+), 132 deletions(-) create mode 100644 migrations/20260113094304_gateway_certificates_management.down.sql create mode 100644 migrations/20260113094304_gateway_certificates_management.up.sql diff --git a/Cargo.lock b/Cargo.lock index ce1a729fa..eea5159b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,6 +1148,8 @@ dependencies = [ "serde", "sqlx", "thiserror 2.0.17", + "time", + "x509-parser 0.18.0", ] [[package]] diff --git a/crates/defguard_certs/Cargo.toml b/crates/defguard_certs/Cargo.toml index 8ea34cbeb..b769b0f9c 100644 --- a/crates/defguard_certs/Cargo.toml +++ b/crates/defguard_certs/Cargo.toml @@ -14,3 +14,5 @@ serde.workspace = true sqlx.workspace = true thiserror.workspace = true rustls-pki-types.workspace = true +time = "0.3" +x509-parser = "0.18" diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 574505b21..444e2dcf0 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -1,10 +1,12 @@ use base64::{Engine, prelude::BASE64_STANDARD}; use rcgen::{ - BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, IsCa, - Issuer, KeyPair, SigningKey, + BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, + ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SigningKey, }; use rustls_pki_types::{CertificateDer, CertificateSigningRequestDer, pem::PemObject}; use thiserror::Error; +use time::{Duration, OffsetDateTime}; +use x509_parser::parse_x509_certificate; const CA_NAME: &str = "Defguard CA"; const CA_ORG: &str = "Defguard"; @@ -59,7 +61,8 @@ impl CertificateAuthority<'_> { pub fn new() -> Result { let mut ca_params = CertificateParams::new(vec![CA_NAME.to_string()])?; - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + // path length 0 to avoid issuing further CAs + ca_params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0)); ca_params .distinguished_name .push(rcgen::DnType::OrganizationName, CA_ORG); @@ -73,8 +76,35 @@ impl CertificateAuthority<'_> { } pub fn sign_csr(&self, csr: &Csr) -> Result { - let csr = csr.params()?; - let cert = csr.signed_by(&self.issuer)?; + // TODO: make validity configurable? + self.sign_csr_with_validity(csr, 360) + } + + /// Sign CSR with explicit validity in days. + pub fn sign_csr_with_validity( + &self, + csr: &Csr, + days_valid: i64, + ) -> Result { + let mut csr_params = csr.params()?; + + let now = OffsetDateTime::now_utc(); + let not_before = now - Duration::minutes(5); + let not_after = now + Duration::days(days_valid); + + csr_params.params.not_before = not_before; + csr_params.params.not_after = not_after; + + csr_params.params.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::KeyEncipherment, + ]; + csr_params.params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ]; + + let cert = csr_params.signed_by(&self.issuer)?; Ok(cert) } @@ -93,6 +123,14 @@ impl CertificateAuthority<'_> { } } +/// Extract the expiry date (not_after) from a certificate. +pub fn get_certificate_expiry(cert: &Certificate) -> Result { + let (_, parsed) = parse_x509_certificate(cert.der()) + .map_err(|e| CertificateError::ParsingError(format!("Failed to parse certificate: {e}")))?; + + Ok(parsed.tbs_certificate.validity.not_after.to_datetime()) +} + pub struct Csr<'a> { csr: CertificateSigningRequestDer<'a>, } @@ -207,7 +245,7 @@ mod tests { #[test] fn test_sign_csr() { let ca = CertificateAuthority::new().unwrap(); - let cert_key_pair = KeyPair::generate().unwrap(); + let cert_key_pair = generate_key_pair().unwrap(); let csr = Csr::new( &cert_key_pair, &["example.com".to_string(), "www.example.com".to_string()], @@ -221,6 +259,29 @@ mod tests { assert!(signed_cert.pem().contains("BEGIN CERTIFICATE")); } + #[test] + fn test_sign_csr_with_validity() { + use x509_parser::parse_x509_certificate; + + let ca = CertificateAuthority::new().unwrap(); + let cert_key_pair = generate_key_pair().unwrap(); + let csr = Csr::new( + &cert_key_pair, + &["example.com".to_string()], + vec![(rcgen::DnType::CommonName, "example.com")], + ) + .unwrap(); + let signed_cert: Certificate = ca.sign_csr_with_validity(&csr, 90).unwrap(); + let der = signed_cert.der(); + let (_rem, parsed) = parse_x509_certificate(&der).unwrap(); + let validity = parsed.tbs_certificate.validity; + let not_before = validity.not_before.to_datetime(); + let not_after = validity.not_after.to_datetime(); + let days = (not_after - not_before).whole_days(); + assert!(days >= 89 && days <= 91, "expected 89-91 days, got {days}"); + assert!(not_after > not_before); + } + #[test] fn test_der_to_pem() { assert_eq!(PemLabel::Certificate.as_str(), "CERTIFICATE"); diff --git a/crates/defguard_common/src/db/models/gateway.rs b/crates/defguard_common/src/db/models/gateway.rs index bab5cff05..f7f25189d 100644 --- a/crates/defguard_common/src/db/models/gateway.rs +++ b/crates/defguard_common/src/db/models/gateway.rs @@ -15,6 +15,8 @@ pub struct Gateway { pub hostname: Option, pub connected_at: Option, pub disconnected_at: Option, + pub has_certificate: bool, + pub certificate_expiry: Option, } impl Gateway { @@ -39,6 +41,8 @@ impl Gateway { hostname: None, connected_at: None, disconnected_at: None, + has_certificate: false, + certificate_expiry: None, } } } diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 51995484f..747b161e0 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -8,22 +8,25 @@ use std::{ }; use chrono::{DateTime, TimeDelta, Utc}; +use defguard_certs::{Csr, der_to_pem}; use defguard_common::{ VERSION, auth::claims::Claims, db::{ Id, NoId, models::{ - Device, User, WireguardNetwork, gateway::Gateway, + Device, Settings, User, WireguardNetwork, gateway::Gateway, wireguard_peer_stats::WireguardPeerStats, }, }, }; use defguard_mail::Mail; use defguard_proto::gateway::{ - CoreResponse, PeerStats, core_request, core_response, gateway_client, + CoreResponse, DerPayload, InitialSetupInfo, PeerStats, core_request, core_response, + gateway_client, gateway_setup_client, }; use defguard_version::client::ClientVersionInterceptor; +use reqwest::Url; use semver::Version; use sqlx::PgPool; use tokio::{ @@ -36,7 +39,7 @@ use tokio::{ use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{ Code, Status, - transport::{ClientTlsConfig, Endpoint}, + transport::{Certificate, ClientTlsConfig, Endpoint}, }; use crate::{ @@ -44,11 +47,27 @@ use crate::{ enterprise::firewall::try_get_location_firewall_config, grpc::{ ClientMap, GrpcEvent, TEN_SECS, - gateway::{GrpcRequestContext, events::GatewayEvent, get_peers}, + gateway::{GatewayError, GrpcRequestContext, events::GatewayEvent, get_peers}, }, handlers::mail::send_gateway_disconnected_email, }; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Scheme { + Http, + Https, +} + +impl Scheme { + #[must_use] + pub const fn as_str(&self) -> &str { + match self { + Self::Http => "http", + Self::Https => "https", + } + } +} + fn peer_stats_from_proto(stats: PeerStats, network_id: Id, device_id: Id) -> WireguardPeerStats { let endpoint = match stats.endpoint { endpoint if endpoint.is_empty() => None, @@ -71,7 +90,8 @@ fn peer_stats_from_proto(stats: PeerStats, network_id: Id, device_id: Id) -> Wir /// One instance per connected Gateway. pub(crate) struct GatewayHandler { - endpoint: Endpoint, + // Gateway server endpoint URL. + url: Url, gateway: Gateway, message_id: AtomicU64, pool: PgPool, @@ -84,25 +104,21 @@ pub(crate) struct GatewayHandler { impl GatewayHandler { pub(crate) fn new( gateway: Gateway, - tls_config: Option, pool: PgPool, client_state: Arc>, events_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, - ) -> Result { - let endpoint = Endpoint::from_shared(gateway.url.clone())? - .http2_keep_alive_interval(TEN_SECS) - .tcp_keepalive(Some(TEN_SECS)) - .keep_alive_while_idle(true); - let endpoint = if let Some(tls) = tls_config { - endpoint.tls_config(tls)? - } else { - endpoint - }; + ) -> Result { + let url = Url::from_str(&gateway.url).map_err(|err| { + GatewayError::EndpointError(format!( + "Failed to parse Gateway URL {}: {}", + &gateway.url, err + )) + })?; Ok(Self { - endpoint, + url, gateway, message_id: AtomicU64::new(0), pool, @@ -113,33 +129,74 @@ impl GatewayHandler { }) } + pub const fn has_certificate(&self) -> bool { + self.gateway.has_certificate + } + + fn endpoint(&self, scheme: Scheme) -> Result { + let mut url = self.url.clone(); + + if let Err(err) = url.set_scheme(scheme.as_str()) { + return Err(GatewayError::EndpointError(format!( + "Failed to set scheme {} for Gateway URL {:?}", + scheme.as_str(), + self.url + ))); + } + + let endpoint = Endpoint::from_shared(url.to_string()) + .map_err(|err| { + GatewayError::EndpointError(format!( + "Failed to create endpoint for Gateway URL {:?}: {}", + url, err + )) + })? + .http2_keep_alive_interval(TEN_SECS) + .tcp_keepalive(Some(TEN_SECS)) + .keep_alive_while_idle(true); + + if scheme == Scheme::Https { + let settings = Settings::get_current_settings(); + let Some(ca_cert_der) = settings.ca_cert_der else { + return Err(GatewayError::EndpointError( + "Core CA is not setup, can't create a Gateway endpoint.".to_string(), + )); + }; + + let cert_pem = der_to_pem(&ca_cert_der, defguard_certs::PemLabel::Certificate) + .map_err(|err| { + GatewayError::EndpointError(format!( + "Failed to convert CA certificate DER to PEM for Gateway URL {:?}: {}", + url, err + )) + })?; + let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(&cert_pem)); + + Ok(endpoint.tls_config(tls).map_err(|err| { + GatewayError::EndpointError(format!( + "Failed to set TLS config for Gateway URL {:?}: {}", + url, err + )) + })?) + } else { + Ok(endpoint) + } + } + /// Send network and VPN configuration to Gateway. async fn send_configuration( &self, tx: &UnboundedSender, - ) -> Result, Status> { + ) -> Result, GatewayError> { debug!("Sending configuration to Gateway"); let network_id = self.gateway.network_id; - let mut conn = self.pool.acquire().await.map_err(|err| { - error!("Failed to acquire DB connection: {err}"); - Status::new( - Code::Internal, - "Failed to acquire database connection".to_string(), - ) - })?; + let mut conn = self.pool.acquire().await?; let mut network = WireguardNetwork::find_by_id(&mut *conn, network_id) - .await - .map_err(|err| { - error!("Network {network_id} not found"); - Status::new(Code::Internal, format!("Failed to retrieve network: {err}")) - })? + .await? .ok_or_else(|| { - Status::new( - Code::Internal, - format!("Network with id {network_id} not found"), - ) + GatewayError::NotFound(format!("Network with id {network_id} not found")) })?; debug!( @@ -153,23 +210,9 @@ impl GatewayHandler { ); } - let peers = get_peers(&network, &self.pool).await.map_err(|error| { - error!("Failed to fetch peers from the database for network {network_id}: {error}",); - Status::new( - Code::Internal, - format!("Failed to retrieve peers from the database for network: {network_id}"), - ) - })?; + let peers = get_peers(&network, &self.pool).await?; - let maybe_firewall_config = try_get_location_firewall_config(&network, &mut conn) - .await - .map_err(|err| { - error!("Failed to generate firewall config for network {network_id}: {err}"); - Status::new( - Code::Internal, - format!("Failed to generate firewall config for network: {network_id}"), - ) - })?; + let maybe_firewall_config = try_get_location_firewall_config(&network, &mut conn).await?; let payload = Some(core_response::Payload::Config(super::gen_config( &network, peers, @@ -184,10 +227,10 @@ impl GatewayHandler { } Err(err) => { error!("Failed to send configuration sent to {}", self.gateway); - Err(Status::new( - Code::Internal, - format!("Configuration not sent to {}, error {err}", self.gateway), - )) + Err(GatewayError::MessageChannelError(format!( + "Configuration not sent to {}, error {err}", + self.gateway + ))) } } } @@ -241,17 +284,11 @@ impl GatewayHandler { } /// Helper method to fetch `Device` info from DB by pubkey and return appropriate errors - async fn fetch_device_from_db(&self, public_key: &str) -> Result>, Status> { - let device = Device::find_by_pubkey(&self.pool, public_key) - .await - .map_err(|err| { - error!("Failed to retrieve device with public key {public_key}: {err}",); - Status::new( - Code::Internal, - format!("Failed to retrieve device with public key {public_key}: {err}",), - ) - })?; - + async fn fetch_device_from_db( + &self, + public_key: &str, + ) -> Result>, GatewayError> { + let device = Device::find_by_pubkey(&self.pool, public_key).await?; Ok(device) } @@ -259,48 +296,32 @@ impl GatewayHandler { async fn fetch_location_from_db( &self, location_id: Id, - ) -> Result, Status> { - let location = match WireguardNetwork::find_by_id(&self.pool, location_id).await { - Ok(Some(location)) => location, - Ok(None) => { + ) -> Result, GatewayError> { + let location = match WireguardNetwork::find_by_id(&self.pool, location_id).await? { + Some(location) => location, + None => { error!("Location {location_id} not found"); - return Err(Status::new( - Code::Internal, - format!("Location {location_id} not found"), - )); - } - Err(err) => { - error!("Failed to retrieve location {location_id}: {err}",); - return Err(Status::new( - Code::Internal, - format!("Failed to retrieve location {location_id}: {err}",), - )); + return Err(GatewayError::NotFound(format!( + "Location {location_id} not found" + ))); } }; Ok(location) } /// Helper method to fetch `User` info from DB and return appropriate errors - async fn fetch_user_from_db(&self, user_id: Id, public_key: &str) -> Result, Status> { - let user = match User::find_by_id(&self.pool, user_id).await { - Ok(Some(user)) => user, - Ok(None) => { + async fn fetch_user_from_db( + &self, + user_id: Id, + public_key: &str, + ) -> Result, GatewayError> { + let user = match User::find_by_id(&self.pool, user_id).await? { + Some(user) => user, + None => { error!("User {user_id} assigned to device with public key {public_key} not found"); - return Err(Status::new( - Code::Internal, - format!("User assigned to device with public key {public_key} not found"), - )); - } - Err(err) => { - error!( - "Failed to retrieve user {user_id} for device with public key {public_key}: {err}", - ); - return Err(Status::new( - Code::Internal, - format!( - "Failed to retrieve user for device with public key {public_key}: {err}", - ), - )); + return Err(GatewayError::NotFound(format!( + "User assigned to device with public key {public_key} not found" + ))); } }; @@ -313,14 +334,113 @@ impl GatewayHandler { } } + pub(crate) async fn handle_setup(&mut self) -> Result<(), GatewayError> { + debug!("Handling initial setup for Gateway {}", self.gateway); + let endpoint = self.endpoint(Scheme::Http)?; + let uri = endpoint.uri().to_string(); + + let hostname = self + .url + .host_str() + .ok_or_else(|| { + error!("Failed to get hostname from Gateway URL {}", self.url); + GatewayError::EndpointError(format!( + "Failed to get hostname from Gateway URL {}", + self.url + )) + })? + .to_string(); + + #[cfg(not(test))] + let channel = endpoint.connect_lazy(); + #[cfg(test)] + let channel = endpoint.connect_with_connector_lazy(tower::service_fn( + |_: tonic::transport::Uri| async { + Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new( + tokio::net::UnixStream::connect(super::TONIC_SOCKET).await?, + )) + }, + )); + + debug!("Connecting to Gateway {uri}"); + let interceptor = ClientVersionInterceptor::new( + Version::parse(VERSION).expect("failed to parse self version"), + ); + let mut client = + gateway_setup_client::GatewaySetupClient::with_interceptor(channel, interceptor); + + let request = InitialSetupInfo { + cert_hostname: hostname, + }; + + let response = client.start(request).await?; + let response = response.into_inner(); + + let csr = Csr::from_der(&response.der_data)?; + + let settings = Settings::get_current_settings(); + + let ca_cert_der = settings.ca_cert_der.ok_or_else(|| { + GatewayError::ConfigurationError( + "CA certificate DER not found in settings for Gateway setup".to_string(), + ) + })?; + let ca_key_pair = settings.ca_key_der.ok_or_else(|| { + GatewayError::ConfigurationError( + "CA key pairs DER not found in settings for Gateway setup".to_string(), + ) + })?; + + let ca = defguard_certs::CertificateAuthority::from_cert_der_key_pair( + &ca_cert_der, + &ca_key_pair, + )?; + + match ca.sign_csr(&csr) { + Ok(cert) => { + let req = DerPayload { + der_data: cert.der().to_vec(), + }; + + client.send_cert(req).await?; + + let expiry = defguard_certs::get_certificate_expiry(&cert)?; + + self.gateway.has_certificate = true; + self.gateway.certificate_expiry = Some( + chrono::DateTime::from_timestamp(expiry.unix_timestamp(), 0) + .ok_or_else(|| { + GatewayError::ConversionError(format!( + "Failed to convert certificate expiry timestamp {} to DateTime", + expiry.unix_timestamp() + )) + })? + .naive_utc(), + ); + self.gateway.save(&self.pool).await?; + } + Err(err) => { + error!("Failed to sign CSR: {err}"); + } + } + + debug!( + "Saving information about issued certificate to the database for Gateway {}", + self.gateway + ); + + Ok(()) + } + /// Connect to Gateway and handle its messages through gRPC. - pub(crate) async fn handle_connection(&mut self) -> ! { - let uri = self.endpoint.uri(); + pub(crate) async fn handle_connection(&mut self) -> Result<(), GatewayError> { + let endpoint = self.endpoint(Scheme::Https)?; + let uri = endpoint.uri().to_string(); loop { #[cfg(not(test))] - let channel = self.endpoint.connect_lazy(); + let channel = endpoint.connect_lazy(); #[cfg(test)] - let channel = self.endpoint.connect_with_connector_lazy(tower::service_fn( + let channel = endpoint.connect_with_connector_lazy(tower::service_fn( |_: tonic::transport::Uri| async { Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new( tokio::net::UnixStream::connect(super::TONIC_SOCKET).await?, diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index e9963f89d..670d1a2d3 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, net::IpAddr, sync::{Arc, Mutex}, + time::Duration, }; use defguard_common::{ @@ -28,7 +29,7 @@ use tokio::{ use tonic::{Code, Status}; use crate::{ - enterprise::is_enterprise_license_active, + enterprise::{firewall::FirewallError, is_enterprise_license_active}, events::{GrpcEvent, GrpcRequestContext}, grpc::gateway::{client_state::ClientMap, events::GatewayEvent, handler::GatewayHandler}, }; @@ -90,15 +91,34 @@ pub fn send_multiple_wireguard_events(events: Vec, wg_tx: &Sender< #[allow(clippy::large_enum_variant)] #[derive(Debug, Error)] -pub enum GatewayServerError { +pub enum GatewayError { #[error("Failed to acquire lock on VPN client state map")] ClientStateMutexError, #[error("gRPC event channel error: {0}")] GrpcEventChannelError(#[from] SendError), + #[error("Endpoint error: {0}")] + EndpointError(String), + #[error("gRPC communication error: {0}")] + GrpcCommunicationError(#[from] tonic::Status), + #[error(transparent)] + CertificateError(#[from] defguard_certs::CertificateError), + #[error("Configuration error: {0}")] + ConfigurationError(String), + #[error("Conversion error: {0}")] + ConversionError(String), + #[error(transparent)] + SqlxError(#[from] sqlx::Error), + #[error("Not found: {0}")] + NotFound(String), + // mpsc channel send/receive error + #[error("Message channel error: {0}")] + MessageChannelError(String), + #[error(transparent)] + FirewallError(#[from] FirewallError), } -impl From for Status { - fn from(value: GatewayServerError) -> Self { +impl From for Status { + fn from(value: GatewayError) -> Self { Self::new(Code::Internal, value.to_string()) } } @@ -198,8 +218,10 @@ fn gen_config( } const GATEWAY_TABLE_TRIGGER: &str = "gateway_change"; +const GATEWAY_SETUP_DELAY: Duration = Duration::from_secs(1); +const GATEWAY_RECONNECT_DELAY: Duration = Duration::from_secs(5); -/// Bi-directional gRPC stream for comminication with Defguard Gateway. +/// Bi-directional gRPC stream for communication with Defguard Gateway. pub async fn run_grpc_gateway_stream( pool: PgPool, client_state: Arc>, @@ -208,28 +230,39 @@ pub async fn run_grpc_gateway_stream( grpc_event_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { let config = server_config(); - let tls_config = config.grpc_client_tls_config()?; - let mut abort_handles = HashMap::new(); let mut tasks = JoinSet::new(); // Helper closure to launch `GatewayHandler`. - let mut launch_gateway_handler = - |gateway: Gateway| -> Result { - let mut gateway_handler = GatewayHandler::new( - gateway, - tls_config.clone(), - pool.clone(), - Arc::clone(&client_state), - events_tx.clone(), - mail_tx.clone(), - grpc_event_tx.clone(), - )?; - let abort_handle = tasks.spawn(async move { - gateway_handler.handle_connection().await; - }); - Ok(abort_handle) - }; + let mut launch_gateway_handler = |gateway: Gateway| -> Result { + let mut gateway_handler = GatewayHandler::new( + gateway, + pool.clone(), + Arc::clone(&client_state), + events_tx.clone(), + mail_tx.clone(), + grpc_event_tx.clone(), + )?; + let abort_handle = tasks.spawn(async move { + loop { + if gateway_handler.has_certificate() { + info!("Gateway has a valid certificate, proceeding to connection"); + } else { + info!("Gateway does not have a valid certificate, proceeding to setup"); + if let Err(err) = gateway_handler.handle_setup().await { + warn!("Gateway setup failed: {err}, will try to connect anyway..."); + } else { + tokio::time::sleep(GATEWAY_SETUP_DELAY).await; + } + } + if let Err(err) = gateway_handler.handle_connection().await { + error!("Gateway connection error: {err}, retrying in 5 seconds..."); + tokio::time::sleep(GATEWAY_RECONNECT_DELAY).await; + } + } + }); + Ok(abort_handle) + }; for gateway in Gateway::all(&pool).await? { let id = gateway.id; diff --git a/migrations/20260113094304_gateway_certificates_management.down.sql b/migrations/20260113094304_gateway_certificates_management.down.sql new file mode 100644 index 000000000..fa9a52941 --- /dev/null +++ b/migrations/20260113094304_gateway_certificates_management.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE gateway DROP COLUMN has_certificate; +ALTER TABLE gateway DROP COLUMN certificate_expiry; diff --git a/migrations/20260113094304_gateway_certificates_management.up.sql b/migrations/20260113094304_gateway_certificates_management.up.sql new file mode 100644 index 000000000..aa5825457 --- /dev/null +++ b/migrations/20260113094304_gateway_certificates_management.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE gateway ADD COLUMN has_certificate boolean NOT NULL DEFAULT false; +ALTER TABLE gateway ADD COLUMN certificate_expiry timestamp without time zone NULL; From 3382c67240e85a7b8987cd580aea96ce700ddb68 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:32:05 +0100 Subject: [PATCH 02/18] remove gw tokens, cleanup --- .../defguard_common/src/db/models/device.rs | 32 ++++++------- crates/defguard_common/src/db/models/group.rs | 2 +- .../defguard_common/src/db/models/mfa_info.rs | 5 +- .../src/db/models/oauth2authorizedapp.rs | 3 +- .../src/db/models/oauth2token.rs | 3 +- .../src/db/models/polling_token.rs | 7 +-- .../defguard_common/src/db/models/session.rs | 3 +- crates/defguard_common/src/db/models/user.rs | 24 +++++----- .../defguard_common/src/db/models/webauthn.rs | 3 +- .../src/db/models/wireguard.rs | 2 +- .../src/db/models/wireguard_peer_stats.rs | 3 +- .../defguard_common/src/db/models/yubikey.rs | 3 +- crates/defguard_common/src/types/user_info.rs | 9 ++-- .../src/enterprise/handlers/mod.rs | 1 - .../defguard_core/src/grpc/gateway/handler.rs | 47 ++----------------- crates/defguard_core/src/grpc/gateway/mod.rs | 1 - crates/defguard_core/src/wg_config.rs | 7 ++- .../defguard_proxy_manager/src/enrollment.rs | 27 +++++------ .../src/password_reset.rs | 17 ++++--- 19 files changed, 83 insertions(+), 116 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 2153bea86..a7dad283b 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -1,19 +1,5 @@ use std::{fmt, net::IpAddr}; -use crate::{ - KEY_LENGTH, - csv::AsCsv, - db::{ - Id, NoId, - models::{ - ModelError, WireguardNetwork, - user::User, - wireguard::{ - LocationMfaMode, NetworkAddressError, ServiceLocationMode, WIREGUARD_MAX_HANDSHAKE, - }, - }, - }, -}; use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::{NaiveDate, NaiveDateTime, Timelike, Utc}; use ipnetwork::IpNetwork; @@ -32,6 +18,21 @@ use thiserror::Error; use tracing::{debug, error, info}; use utoipa::ToSchema; +use crate::{ + KEY_LENGTH, + csv::AsCsv, + db::{ + Id, NoId, + models::{ + ModelError, WireguardNetwork, + user::User, + wireguard::{ + LocationMfaMode, NetworkAddressError, ServiceLocationMode, WIREGUARD_MAX_HANDSHAKE, + }, + }, + }, +}; + #[derive(Serialize, ToSchema)] pub struct DeviceConfig { pub network_id: Id, @@ -1005,9 +1006,8 @@ mod test { use claims::{assert_err, assert_ok}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - use crate::db::setup_pool; - use super::*; + use crate::db::setup_pool; impl Device { /// Create new device and assign IP in a given network diff --git a/crates/defguard_common/src/db/models/group.rs b/crates/defguard_common/src/db/models/group.rs index 8acd6cab6..e6f65e19f 100644 --- a/crates/defguard_common/src/db/models/group.rs +++ b/crates/defguard_common/src/db/models/group.rs @@ -160,10 +160,10 @@ impl Group { #[cfg(test)] mod test { - use crate::db::setup_pool; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; + use crate::db::setup_pool; #[sqlx::test] async fn test_group(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_common/src/db/models/mfa_info.rs b/crates/defguard_common/src/db/models/mfa_info.rs index b7925ce97..07eda6948 100644 --- a/crates/defguard_common/src/db/models/mfa_info.rs +++ b/crates/defguard_common/src/db/models/mfa_info.rs @@ -1,9 +1,10 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{Error as SqlxError, PgPool, query_as}; + use crate::db::{ Id, models::{MFAMethod, user::User}, }; -use serde::{Deserialize, Serialize}; -use sqlx::{Error as SqlxError, PgPool, query_as}; #[derive(Deserialize, Serialize)] pub struct MFAInfo { diff --git a/crates/defguard_common/src/db/models/oauth2authorizedapp.rs b/crates/defguard_common/src/db/models/oauth2authorizedapp.rs index e6f5119ab..421a93437 100644 --- a/crates/defguard_common/src/db/models/oauth2authorizedapp.rs +++ b/crates/defguard_common/src/db/models/oauth2authorizedapp.rs @@ -1,7 +1,8 @@ -use crate::db::{Id, NoId}; use model_derive::Model; use sqlx::{Error as SqlxError, PgPool, query_as}; +use crate::db::{Id, NoId}; + #[derive(Model)] pub struct OAuth2AuthorizedApp { pub id: I, diff --git a/crates/defguard_common/src/db/models/oauth2token.rs b/crates/defguard_common/src/db/models/oauth2token.rs index 468e83f64..c7bc50e52 100644 --- a/crates/defguard_common/src/db/models/oauth2token.rs +++ b/crates/defguard_common/src/db/models/oauth2token.rs @@ -1,7 +1,8 @@ -use crate::{config::server_config, db::Id, random::gen_alphanumeric}; use chrono::{TimeDelta, Utc}; use sqlx::{Error as SqlxError, PgPool, query, query_as}; +use crate::{config::server_config, db::Id, random::gen_alphanumeric}; + pub struct OAuth2Token { pub oauth2authorizedapp_id: Id, pub access_token: String, diff --git a/crates/defguard_common/src/db/models/polling_token.rs b/crates/defguard_common/src/db/models/polling_token.rs index f834402ff..750ec80a8 100644 --- a/crates/defguard_common/src/db/models/polling_token.rs +++ b/crates/defguard_common/src/db/models/polling_token.rs @@ -1,10 +1,11 @@ +use chrono::{NaiveDateTime, Utc}; +use model_derive::Model; +use sqlx::{PgExecutor, query_as}; + use crate::{ db::{Id, NoId}, random::gen_alphanumeric, }; -use chrono::{NaiveDateTime, Utc}; -use model_derive::Model; -use sqlx::{PgExecutor, query_as}; // Token used for polling requests. #[derive(Clone, Debug, Model)] diff --git a/crates/defguard_common/src/db/models/session.rs b/crates/defguard_common/src/db/models/session.rs index e1859844e..6a8de55ee 100644 --- a/crates/defguard_common/src/db/models/session.rs +++ b/crates/defguard_common/src/db/models/session.rs @@ -1,8 +1,9 @@ -use crate::{config::server_config, db::Id, random::gen_alphanumeric}; use chrono::{NaiveDateTime, TimeDelta, Utc}; use sqlx::{Error as SqlxError, PgExecutor, PgPool, Type, query, query_as}; use webauthn_rs::prelude::{PasskeyAuthentication, PasskeyRegistration}; +use crate::{config::server_config, db::Id, random::gen_alphanumeric}; + #[derive(Clone, PartialEq, Type)] #[repr(i16)] pub enum SessionState { diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs index 96c266dfe..7557aedd6 100644 --- a/crates/defguard_common/src/db/models/user.rs +++ b/crates/defguard_common/src/db/models/user.rs @@ -1,14 +1,5 @@ use std::{fmt, time::SystemTime}; -use crate::{ - config::server_config, - db::{ - Id, NoId, - models::{MFAInfo, Session, WebAuthn}, - }, - random::{gen_alphanumeric, gen_totp_secret}, - types::user_info::OAuth2AuthorizedAppInfo, -}; use argon2::{ Argon2, password_hash::{ @@ -36,6 +27,15 @@ use super::{ device::{Device, DeviceType, UserDevice}, group::{Group, Permission}, }; +use crate::{ + config::server_config, + db::{ + Id, NoId, + models::{MFAInfo, Session, WebAuthn}, + }, + random::{gen_alphanumeric, gen_totp_secret}, + types::user_info::OAuth2AuthorizedAppInfo, +}; const RECOVERY_CODES_COUNT: usize = 8; pub const TOTP_CODE_VALIDITY_PERIOD: u64 = 30; @@ -1221,13 +1221,13 @@ impl Distribution> for Standard { #[cfg(test)] mod test { + use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + + use super::*; use crate::{ config::{DefGuardConfig, SERVER_CONFIG}, db::{models::settings::initialize_current_settings, setup_pool}, }; - use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - - use super::*; #[sqlx::test] async fn test_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_common/src/db/models/webauthn.rs b/crates/defguard_common/src/db/models/webauthn.rs index 2861a13b1..2fc9730f6 100644 --- a/crates/defguard_common/src/db/models/webauthn.rs +++ b/crates/defguard_common/src/db/models/webauthn.rs @@ -1,8 +1,9 @@ -use crate::db::{Id, NoId, models::ModelError}; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query, query_as, query_scalar}; use webauthn_rs::prelude::Passkey; +use crate::db::{Id, NoId, models::ModelError}; + #[derive(Model, Clone, Debug, PartialEq)] pub struct WebAuthn { pub id: I, diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index ee6dd3292..d64c777d8 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -1182,12 +1182,12 @@ pub async fn networks_stats( mod test { use std::str::FromStr; - use crate::db::setup_pool; use chrono::{SubsecRound, TimeDelta, Utc}; use matches::assert_matches; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; + use crate::db::setup_pool; #[sqlx::test] async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_common/src/db/models/wireguard_peer_stats.rs b/crates/defguard_common/src/db/models/wireguard_peer_stats.rs index 902f20028..099f89229 100644 --- a/crates/defguard_common/src/db/models/wireguard_peer_stats.rs +++ b/crates/defguard_common/src/db/models/wireguard_peer_stats.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use crate::db::{Id, NoId}; use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc}; use humantime::format_duration; use ipnetwork::IpNetwork; @@ -9,6 +8,8 @@ use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, query, query_as, query_scalar}; use tracing::{debug, info}; +use crate::db::{Id, NoId}; + #[derive(Debug, Deserialize, Model, Serialize)] #[table(wireguard_peer_stats)] pub struct WireguardPeerStats { diff --git a/crates/defguard_common/src/db/models/yubikey.rs b/crates/defguard_common/src/db/models/yubikey.rs index 5eec85d52..171de03d8 100644 --- a/crates/defguard_common/src/db/models/yubikey.rs +++ b/crates/defguard_common/src/db/models/yubikey.rs @@ -1,8 +1,9 @@ -use crate::db::{Id, NoId}; use model_derive::Model; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, query, query_as}; +use crate::db::{Id, NoId}; + #[derive(Deserialize, Model, Serialize)] pub struct YubiKey { pub id: I, diff --git a/crates/defguard_common/src/types/user_info.rs b/crates/defguard_common/src/types/user_info.rs index 9609d5d00..6716d877a 100644 --- a/crates/defguard_common/src/types/user_info.rs +++ b/crates/defguard_common/src/types/user_info.rs @@ -1,3 +1,7 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{Error as SqlxError, PgConnection, PgPool}; +use utoipa::ToSchema; + use crate::{ db::{ Id, @@ -5,9 +9,6 @@ use crate::{ }, types::group_diff::GroupDiff, }; -use serde::{Deserialize, Serialize}; -use sqlx::{Error as SqlxError, PgConnection, PgPool}; -use utoipa::ToSchema; #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct OAuth2AuthorizedAppInfo { @@ -146,10 +147,10 @@ impl UserInfo { #[cfg(test)] mod test { - use crate::db::setup_pool; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; + use crate::db::setup_pool; #[sqlx::test] async fn test_user_info(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/enterprise/handlers/mod.rs b/crates/defguard_core/src/enterprise/handlers/mod.rs index 084b1a117..781eded1b 100644 --- a/crates/defguard_core/src/enterprise/handlers/mod.rs +++ b/crates/defguard_core/src/enterprise/handlers/mod.rs @@ -15,7 +15,6 @@ use axum::{ extract::{FromRef, FromRequestParts}, http::{StatusCode, request::Parts}, }; - use serde::Serialize; use super::{ diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 747b161e0..0afcfca7b 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -11,7 +11,6 @@ use chrono::{DateTime, TimeDelta, Utc}; use defguard_certs::{Csr, der_to_pem}; use defguard_common::{ VERSION, - auth::claims::Claims, db::{ Id, NoId, models::{ @@ -37,13 +36,9 @@ use tokio::{ time::sleep, }; use tokio_stream::wrappers::UnboundedReceiverStream; -use tonic::{ - Code, Status, - transport::{Certificate, ClientTlsConfig, Endpoint}, -}; +use tonic::transport::{Certificate, ClientTlsConfig, Endpoint}; use crate::{ - ClaimsType, enterprise::firewall::try_get_location_firewall_config, grpc::{ ClientMap, GrpcEvent, TEN_SECS, @@ -136,7 +131,7 @@ impl GatewayHandler { fn endpoint(&self, scheme: Scheme) -> Result { let mut url = self.url.clone(); - if let Err(err) = url.set_scheme(scheme.as_str()) { + if let Err(()) = url.set_scheme(scheme.as_str()) { return Err(GatewayError::EndpointError(format!( "Failed to set scheme {} for Gateway URL {:?}", scheme.as_str(), @@ -147,8 +142,7 @@ impl GatewayHandler { let endpoint = Endpoint::from_shared(url.to_string()) .map_err(|err| { GatewayError::EndpointError(format!( - "Failed to create endpoint for Gateway URL {:?}: {}", - url, err + "Failed to create endpoint for Gateway URL {url:?}: {err}", )) })? .http2_keep_alive_interval(TEN_SECS) @@ -166,16 +160,14 @@ impl GatewayHandler { let cert_pem = der_to_pem(&ca_cert_der, defguard_certs::PemLabel::Certificate) .map_err(|err| { GatewayError::EndpointError(format!( - "Failed to convert CA certificate DER to PEM for Gateway URL {:?}: {}", - url, err + "Failed to convert CA certificate DER to PEM for Gateway URL {url:?}: {err}", )) })?; let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(&cert_pem)); Ok(endpoint.tls_config(tls).map_err(|err| { GatewayError::EndpointError(format!( - "Failed to set TLS config for Gateway URL {:?}: {}", - url, err + "Failed to set TLS config for Gateway URL {url:?}: {err}", )) })?) } else { @@ -486,35 +478,6 @@ impl GatewayHandler { ); continue; } - // Validate authorization token. - if let Ok(claims) = Claims::from_jwt( - ClaimsType::Gateway, - &config_request.auth_token, - ) { - if let Ok(client_id) = Id::from_str(&claims.client_id) { - if client_id == self.gateway.network_id { - debug!( - "Authorization token is correct for {}", - self.gateway - ); - } else { - warn!( - "Authorization token received from {uri} has \ - `client_id` for a different network" - ); - continue; - } - } else { - warn!( - "Authorization token received from {uri} has incorrect \ - `client_id`" - ); - continue; - } - } else { - warn!("Invalid authorization token received from {uri}"); - continue; - } // Send network configuration to Gateway. match self.send_configuration(&tx).await { diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 670d1a2d3..d734941b8 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -229,7 +229,6 @@ pub async fn run_grpc_gateway_stream( mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { - let config = server_config(); let mut abort_handles = HashMap::new(); let mut tasks = JoinSet::new(); diff --git a/crates/defguard_core/src/wg_config.rs b/crates/defguard_core/src/wg_config.rs index 33de00fc8..429d841d5 100644 --- a/crates/defguard_core/src/wg_config.rs +++ b/crates/defguard_core/src/wg_config.rs @@ -1,10 +1,6 @@ use std::net::IpAddr; use base64::{DecodeError, Engine, prelude::BASE64_STANDARD}; -use ipnetwork::{IpNetwork, IpNetworkError}; -use thiserror::Error; -use x25519_dalek::{PublicKey, StaticSecret}; - use defguard_common::{ KEY_LENGTH, db::models::{ @@ -15,6 +11,9 @@ use defguard_common::{ }, }, }; +use ipnetwork::{IpNetwork, IpNetworkError}; +use thiserror::Error; +use x25519_dalek::{PublicKey, StaticSecret}; #[derive(Clone, Deserialize, Serialize)] pub struct ImportedDevice { diff --git a/crates/defguard_proxy_manager/src/enrollment.rs b/crates/defguard_proxy_manager/src/enrollment.rs index e0358ce9c..16aea9712 100644 --- a/crates/defguard_proxy_manager/src/enrollment.rs +++ b/crates/defguard_proxy_manager/src/enrollment.rs @@ -12,20 +12,6 @@ use defguard_common::{ }, }, }; -use defguard_mail::{Mail, templates::TemplateLocation}; -use defguard_proto::proxy::{ - ActivateUserRequest, AdminInfo, CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, - CodeMfaSetupStartRequest, CodeMfaSetupStartResponse, DeviceConfigResponse, - EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, InitialUserInfo, MfaMethod, - NewDevice, RegisterMobileAuthRequest, -}; -use sqlx::{PgPool, query_scalar}; -use tokio::sync::{ - broadcast::Sender, - mpsc::{UnboundedSender, error::SendError}, -}; -use tonic::Status; - use defguard_core::{ db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}, enterprise::{ @@ -50,6 +36,19 @@ use defguard_core::{ headers::get_device_info, is_valid_phone_number, }; +use defguard_mail::{Mail, templates::TemplateLocation}; +use defguard_proto::proxy::{ + ActivateUserRequest, AdminInfo, CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, + CodeMfaSetupStartRequest, CodeMfaSetupStartResponse, DeviceConfigResponse, + EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, InitialUserInfo, MfaMethod, + NewDevice, RegisterMobileAuthRequest, +}; +use sqlx::{PgPool, query_scalar}; +use tokio::sync::{ + broadcast::Sender, + mpsc::{UnboundedSender, error::SendError}, +}; +use tonic::Status; pub(super) struct EnrollmentServer { pool: PgPool, diff --git a/crates/defguard_proxy_manager/src/password_reset.rs b/crates/defguard_proxy_manager/src/password_reset.rs index 3c27dbd38..208b3e526 100644 --- a/crates/defguard_proxy_manager/src/password_reset.rs +++ b/crates/defguard_proxy_manager/src/password_reset.rs @@ -1,13 +1,4 @@ use defguard_common::{config::server_config, db::models::User}; -use defguard_mail::Mail; -use defguard_proto::proxy::{ - DeviceInfo, PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, - PasswordResetStartResponse, -}; -use sqlx::PgPool; -use tokio::sync::mpsc::{UnboundedSender, error::SendError}; -use tonic::Status; - use defguard_core::{ db::models::enrollment::{PASSWORD_RESET_TOKEN_TYPE, Token}, enterprise::ldap::utils::ldap_change_password, @@ -19,6 +10,14 @@ use defguard_core::{ }, headers::get_device_info, }; +use defguard_mail::Mail; +use defguard_proto::proxy::{ + DeviceInfo, PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, + PasswordResetStartResponse, +}; +use sqlx::PgPool; +use tokio::sync::mpsc::{UnboundedSender, error::SendError}; +use tonic::Status; pub(super) struct PasswordResetServer { pool: PgPool, From 5f51eb685cc46cab021fbfae1d82e6797a545a4c Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:53:49 +0100 Subject: [PATCH 03/18] cleanup --- crates/defguard_core/src/grpc/gateway/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index d734941b8..45bbe078e 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -5,12 +5,9 @@ use std::{ time::Duration, }; -use defguard_common::{ - config::server_config, - db::{ - ChangeNotification, Id, TriggerOperation, - models::{WireguardNetwork, gateway::Gateway, wireguard::ServiceLocationMode}, - }, +use defguard_common::db::{ + ChangeNotification, Id, TriggerOperation, + models::{WireguardNetwork, gateway::Gateway, wireguard::ServiceLocationMode}, }; use defguard_mail::Mail; use defguard_proto::{ From 2f8a147e27c52d04daab18c09dea9cfc02e1cd63 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:02:32 +0100 Subject: [PATCH 04/18] clippy --- crates/defguard_certs/src/lib.rs | 4 ++-- crates/defguard_core/src/grpc/gateway/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 444e2dcf0..bf0b00904 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -273,12 +273,12 @@ mod tests { .unwrap(); let signed_cert: Certificate = ca.sign_csr_with_validity(&csr, 90).unwrap(); let der = signed_cert.der(); - let (_rem, parsed) = parse_x509_certificate(&der).unwrap(); + let (_rem, parsed) = parse_x509_certificate(der).unwrap(); let validity = parsed.tbs_certificate.validity; let not_before = validity.not_before.to_datetime(); let not_after = validity.not_after.to_datetime(); let days = (not_after - not_before).whole_days(); - assert!(days >= 89 && days <= 91, "expected 89-91 days, got {days}"); + assert!((89..=91).contains(&days), "expected 89-91 days, got {days}"); assert!(not_after > not_before); } diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 45bbe078e..0f4f31753 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -242,7 +242,7 @@ pub async fn run_grpc_gateway_stream( let abort_handle = tasks.spawn(async move { loop { if gateway_handler.has_certificate() { - info!("Gateway has a valid certificate, proceeding to connection"); + info!("A certificate was already issued for Gateway, proceeding to connection"); } else { info!("Gateway does not have a valid certificate, proceeding to setup"); if let Err(err) = gateway_handler.handle_setup().await { From 85ab322bc76fbad59b9cd68c14a326f97d1a9774 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:07:25 +0100 Subject: [PATCH 05/18] sqlx prepare --- ...6f56f89123c84fc2351ca92ab1e17525c1097ef.json} | 6 ++++-- ...73b1033ff78a115ae0a3f4882c28b3becb3d0a5.json} | 6 ++++-- ...94ce3d23b6d3b80d820d022502916dd8adc0262.json} | 16 ++++++++++++++-- ...fa8c23bff66bc40eafba7400a7d7db49ab36e2e.json} | 16 ++++++++++++++-- ...bddf997807b66e0b532da747b146513c34e15c5c.json | 12 ++++++++++++ 5 files changed, 48 insertions(+), 8 deletions(-) rename .sqlx/{query-dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4.json => query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json} (66%) rename .sqlx/{query-5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e.json => query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json} (61%) rename .sqlx/{query-0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554.json => query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json} (67%) rename .sqlx/{query-0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d.json => query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json} (67%) diff --git a/.sqlx/query-dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4.json b/.sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json similarity index 66% rename from .sqlx/query-dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4.json rename to .sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json index f5f9307d7..b52e77094 100644 --- a/.sqlx/query-dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4.json +++ b/.sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"gateway\" SET \"network_id\" = $2,\"url\" = $3,\"hostname\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6 WHERE id = $1", + "query": "UPDATE \"gateway\" SET \"network_id\" = $2,\"url\" = $3,\"hostname\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6,\"has_certificate\" = $7,\"certificate_expiry\" = $8 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -10,10 +10,12 @@ "Text", "Text", "Timestamp", + "Timestamp", + "Bool", "Timestamp" ] }, "nullable": [] }, - "hash": "dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4" + "hash": "5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef" } diff --git a/.sqlx/query-5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e.json b/.sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json similarity index 61% rename from .sqlx/query-5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e.json rename to .sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json index 58a9bd507..b45febc00 100644 --- a/.sqlx/query-5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e.json +++ b/.sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\") VALUES ($1,$2,$3,$4,$5) RETURNING id", + "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id", "describe": { "columns": [ { @@ -15,6 +15,8 @@ "Text", "Text", "Timestamp", + "Timestamp", + "Bool", "Timestamp" ] }, @@ -22,5 +24,5 @@ false ] }, - "hash": "5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e" + "hash": "95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5" } diff --git a/.sqlx/query-0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554.json b/.sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json similarity index 67% rename from .sqlx/query-0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554.json rename to .sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json index 8afd59ce0..ed24b8d62 100644 --- a/.sqlx/query-0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554.json +++ b/.sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\" FROM \"gateway\" WHERE id = $1", + "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\" FROM \"gateway\" WHERE id = $1", "describe": { "columns": [ { @@ -32,6 +32,16 @@ "ordinal": 5, "name": "disconnected_at", "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "certificate_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -45,8 +55,10 @@ false, true, true, + true, + false, true ] }, - "hash": "0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554" + "hash": "9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262" } diff --git a/.sqlx/query-0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d.json b/.sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json similarity index 67% rename from .sqlx/query-0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d.json rename to .sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json index 9476d09d1..c321d2e9e 100644 --- a/.sqlx/query-0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d.json +++ b/.sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\" FROM \"gateway\"", + "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\" FROM \"gateway\"", "describe": { "columns": [ { @@ -32,6 +32,16 @@ "ordinal": 5, "name": "disconnected_at", "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "certificate_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -43,8 +53,10 @@ false, true, true, + true, + false, true ] }, - "hash": "0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d" + "hash": "a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e" } diff --git a/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json b/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json index 6fa095298..6b597e448 100644 --- a/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json +++ b/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json @@ -32,6 +32,16 @@ "ordinal": 5, "name": "disconnected_at", "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "certificate_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -45,6 +55,8 @@ false, true, true, + true, + false, true ] }, From e5e73ee31e71b9c8bce2de5c380952d0e7cfc772 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:16:56 +0100 Subject: [PATCH 06/18] consts --- crates/defguard_certs/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index bf0b00904..09ca0ea91 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -10,6 +10,8 @@ use x509_parser::parse_x509_certificate; const CA_NAME: &str = "Defguard CA"; const CA_ORG: &str = "Defguard"; +const NOT_BEFORE_OFFSET_SECS: Duration = Duration::minutes(5); +const DEFAULT_CERT_VALIDITY_DAYS: i64 = 365; #[derive(Debug, Error)] pub enum CertificateError { @@ -77,7 +79,7 @@ impl CertificateAuthority<'_> { pub fn sign_csr(&self, csr: &Csr) -> Result { // TODO: make validity configurable? - self.sign_csr_with_validity(csr, 360) + self.sign_csr_with_validity(csr, DEFAULT_CERT_VALIDITY_DAYS) } /// Sign CSR with explicit validity in days. @@ -89,7 +91,7 @@ impl CertificateAuthority<'_> { let mut csr_params = csr.params()?; let now = OffsetDateTime::now_utc(); - let not_before = now - Duration::minutes(5); + let not_before = now - NOT_BEFORE_OFFSET_SECS; let not_after = now + Duration::days(days_valid); csr_params.params.not_before = not_before; From f87c198a2ec81c9e03b206a19fc3cce102869db5 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:26:03 +0100 Subject: [PATCH 07/18] update protobufs --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index c48340f72..161c6c677 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit c48340f72b9de3a69cf71318c75ff1361ebd7897 +Subproject commit 161c6c677662130924e8bac0c16421b8ed085d33 From 4816598cc164fda7503990260103140080053302 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:38:46 +0100 Subject: [PATCH 08/18] proxy wizard backend --- Cargo.lock | 3 + Cargo.toml | 1 + crates/defguard/src/main.rs | 18 +- crates/defguard_certs/Cargo.toml | 1 + crates/defguard_certs/src/lib.rs | 125 +++- crates/defguard_common/src/db/models/proxy.rs | 36 ++ .../defguard_common/src/db/models/settings.rs | 8 +- crates/defguard_common/src/types/mod.rs | 1 + crates/defguard_common/src/types/proxy.rs | 7 + crates/defguard_core/Cargo.toml | 2 + crates/defguard_core/src/appstate.rs | 5 +- crates/defguard_core/src/error.rs | 3 + .../defguard_core/src/grpc/gateway/handler.rs | 14 +- crates/defguard_core/src/handlers/ca.rs | 43 ++ crates/defguard_core/src/handlers/mod.rs | 5 +- .../defguard_core/src/handlers/proxy_setup.rs | 542 ++++++++++++++++++ crates/defguard_core/src/lib.rs | 17 +- crates/defguard_core/src/version.rs | 2 +- crates/defguard_proxy_manager/src/lib.rs | 196 ++++--- ...095450_[2.0.0]_proxy_certificates.down.sql | 4 + ...16095450_[2.0.0]_proxy_certificates.up.sql | 4 + 21 files changed, 918 insertions(+), 119 deletions(-) create mode 100644 crates/defguard_common/src/types/proxy.rs create mode 100644 crates/defguard_core/src/handlers/ca.rs create mode 100644 crates/defguard_core/src/handlers/proxy_setup.rs create mode 100644 migrations/20260116095450_[2.0.0]_proxy_certificates.down.sql create mode 100644 migrations/20260116095450_[2.0.0]_proxy_certificates.up.sql diff --git a/Cargo.lock b/Cargo.lock index 535698a6e..9de36f0bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1145,6 +1145,7 @@ name = "defguard_certs" version = "0.0.0" dependencies = [ "base64 0.22.1", + "chrono", "rcgen", "rustls-pki-types", "serde", @@ -1197,6 +1198,7 @@ version = "0.0.0" dependencies = [ "ammonia", "anyhow", + "async-stream", "axum", "axum-client-ip", "axum-extra", @@ -1211,6 +1213,7 @@ dependencies = [ "defguard_proto", "defguard_version", "defguard_web_ui", + "futures", "humantime", "hyper-util", "ipnetwork", diff --git a/Cargo.toml b/Cargo.toml index 5d2ad055e..c19463c20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ chrono = { version = "0.4", default-features = false, features = [ ] } claims = "0.8" clap = { version = "4.5", features = ["derive", "env"] } +futures = "0.3" humantime = "2.1" # match version used by sqlx ipnetwork = "0.20" diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 2fdc57229..31d0c62b1 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -16,6 +16,7 @@ use defguard_common::{ // wireguard_peer_stats::WireguardPeerStats, }, }, + types::proxy::ProxyControlMessage, }; use defguard_core::{ auth::failed_login::FailedLoginMap, @@ -43,7 +44,10 @@ use defguard_mail::{Mail, run_mail_handler}; use defguard_proxy_manager::{ProxyManager, ProxyTxSet}; // use defguard_session_manager::run_session_manager; use secrecy::ExposeSecret; -use tokio::sync::{broadcast, mpsc::unbounded_channel}; +use tokio::sync::{ + broadcast, + mpsc::{channel, unbounded_channel}, +}; #[macro_use] extern crate tracing; @@ -134,7 +138,7 @@ async fn main() -> Result<(), anyhow::Error> { "No gRPC TLS certificate or key found in settings, generating self-signed certificate for gRPC server." ); - let ca = defguard_certs::CertificateAuthority::new()?; + let ca = defguard_certs::CertificateAuthority::new("Defguard", "", 10)?; let (cert_der, key_der) = (ca.cert_der().to_vec(), ca.key_pair_der().to_vec()); @@ -174,9 +178,14 @@ async fn main() -> Result<(), anyhow::Error> { } } + let (proxy_control_tx, proxy_control_rx) = channel::(100); let proxy_tx = ProxyTxSet::new(wireguard_tx.clone(), mail_tx.clone(), bidi_event_tx.clone()); - let proxy_manager = - ProxyManager::new(pool.clone(), proxy_tx, Arc::clone(&incompatible_components)); + let proxy_manager = ProxyManager::new( + pool.clone(), + proxy_tx, + Arc::clone(&incompatible_components), + proxy_control_rx, + ); // run services tokio::select! { @@ -205,6 +214,7 @@ async fn main() -> Result<(), anyhow::Error> { failed_logins, api_event_tx, incompatible_components, + proxy_control_tx ) => error!("Web server returned early: {res:?}"), res = run_mail_handler(mail_rx) => error!("Mail handler returned early: {res:?}"), res = run_periodic_peer_disconnect( diff --git a/crates/defguard_certs/Cargo.toml b/crates/defguard_certs/Cargo.toml index b769b0f9c..9207838d3 100644 --- a/crates/defguard_certs/Cargo.toml +++ b/crates/defguard_certs/Cargo.toml @@ -14,5 +14,6 @@ serde.workspace = true sqlx.workspace = true thiserror.workspace = true rustls-pki-types.workspace = true +chrono.workspace = true time = "0.3" x509-parser = "0.18" diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 09ca0ea91..6a4e6c9aa 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -1,15 +1,17 @@ +use std::str::FromStr; + use base64::{Engine, prelude::BASE64_STANDARD}; use rcgen::{ BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, - ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SigningKey, + ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SigningKey, string::Ia5String, }; use rustls_pki_types::{CertificateDer, CertificateSigningRequestDer, pem::PemObject}; +use sqlx::types::chrono::NaiveDateTime; use thiserror::Error; use time::{Duration, OffsetDateTime}; use x509_parser::parse_x509_certificate; const CA_NAME: &str = "Defguard CA"; -const CA_ORG: &str = "Defguard"; const NOT_BEFORE_OFFSET_SECS: Duration = Duration::minutes(5); const DEFAULT_CERT_VALIDITY_DAYS: i64 = 365; @@ -60,17 +62,27 @@ impl CertificateAuthority<'_> { Ok(CertificateAuthority { issuer, cert_der }) } - pub fn new() -> Result { + pub fn new( + common_name: &str, + email: &str, + valid_for_days: u32, + ) -> Result { let mut ca_params = CertificateParams::new(vec![CA_NAME.to_string()])?; // path length 0 to avoid issuing further CAs ca_params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0)); ca_params .distinguished_name - .push(rcgen::DnType::OrganizationName, CA_ORG); + .push(rcgen::DnType::CommonName, common_name); + + let email_string = Ia5String::from_str(email)?; ca_params - .distinguished_name - .push(rcgen::DnType::CommonName, CA_NAME); + .subject_alt_names + .push(rcgen::SanType::Rfc822Name(email_string)); + + let now = OffsetDateTime::now_utc(); + ca_params.not_before = now - NOT_BEFORE_OFFSET_SECS; + ca_params.not_after = now + Duration::days(i64::from(valid_for_days)); let ca_key_pair = KeyPair::generate()?; @@ -123,14 +135,26 @@ impl CertificateAuthority<'_> { pub fn key_pair_der(&self) -> &[u8] { self.issuer.key().serialized_der() } + + #[must_use] + pub fn expiry(&self) -> Result { + get_certificate_expiry(&self.cert_der) + } } /// Extract the expiry date (not_after) from a certificate. -pub fn get_certificate_expiry(cert: &Certificate) -> Result { - let (_, parsed) = parse_x509_certificate(cert.der()) +pub fn get_certificate_expiry(cert_der: &[u8]) -> Result { + let (_, parsed) = parse_x509_certificate(cert_der) .map_err(|e| CertificateError::ParsingError(format!("Failed to parse certificate: {e}")))?; - Ok(parsed.tbs_certificate.validity.not_after.to_datetime()) + let expiry = parsed.tbs_certificate.validity.not_after.to_datetime(); + Ok(chrono::DateTime::from_timestamp(expiry.unix_timestamp(), 0) + .ok_or_else(|| { + CertificateError::ParsingError(format!( + "Failed to convert certificate expiry {expiry} to NaiveDateTime", + )) + })? + .naive_utc()) } pub struct Csr<'a> { @@ -235,7 +259,7 @@ mod tests { #[test] fn test_ca_creation() { - let ca = CertificateAuthority::new().unwrap(); + let ca = CertificateAuthority::new("Defguard CA", "email@email.com", 10).unwrap(); let key = ca.issuer.key(); let der = &ca.cert_der; let pem_string = cert_der_to_pem(der.as_ref()).unwrap(); @@ -246,7 +270,7 @@ mod tests { #[test] fn test_sign_csr() { - let ca = CertificateAuthority::new().unwrap(); + let ca = CertificateAuthority::new("Defguard CA", "email@email.com", 10).unwrap(); let cert_key_pair = generate_key_pair().unwrap(); let csr = Csr::new( &cert_key_pair, @@ -265,7 +289,7 @@ mod tests { fn test_sign_csr_with_validity() { use x509_parser::parse_x509_certificate; - let ca = CertificateAuthority::new().unwrap(); + let ca = CertificateAuthority::new("Defguard CA", "email@email.com", 10).unwrap(); let cert_key_pair = generate_key_pair().unwrap(); let csr = Csr::new( &cert_key_pair, @@ -309,4 +333,81 @@ mod tests { } } } + + #[test] + fn test_ca_validity() { + use x509_parser::parse_x509_certificate; + + let valid_days = 365; + let ca = CertificateAuthority::new("Test CA", "test@example.com", valid_days).unwrap(); + + let (_rem, parsed) = parse_x509_certificate(ca.cert_der()).unwrap(); + let validity = parsed.tbs_certificate.validity; + let not_before = validity.not_before.to_datetime(); + let not_after = validity.not_after.to_datetime(); + + let days = (not_after - not_before).whole_days(); + + assert!( + (valid_days as i64 - 1..=valid_days as i64 + 1).contains(&days), + "expected validity of {valid_days} days (±1), got {days} days" + ); + assert!( + not_after > not_before, + "not_after should be after not_before" + ); + } + + #[test] + fn test_ca_common_name() { + use x509_parser::parse_x509_certificate; + + let expected_cn = "My Custom CA"; + let ca = CertificateAuthority::new(expected_cn, "admin@example.com", 365).unwrap(); + + let (_rem, parsed) = parse_x509_certificate(ca.cert_der()).unwrap(); + let subject = &parsed.tbs_certificate.subject; + + let cn = subject + .iter_common_name() + .next() + .expect("Common Name not found") + .as_str() + .expect("Failed to parse CN as string"); + + assert_eq!( + cn, expected_cn, + "Common Name should match the provided value" + ); + } + + #[test] + fn test_ca_email() { + use x509_parser::parse_x509_certificate; + + let expected_email = "contact@defguard.net"; + let ca = CertificateAuthority::new("Test CA", expected_email, 365).unwrap(); + + let (_rem, parsed) = parse_x509_certificate(ca.cert_der()).unwrap(); + + let san_ext = parsed + .tbs_certificate + .extensions() + .iter() + .find(|ext| ext.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME) + .expect("Subject Alternative Name extension not found"); + + let san_value = san_ext.value; + + let email_bytes = expected_email.as_bytes(); + let email_found = san_value + .windows(email_bytes.len()) + .any(|window| window == email_bytes); + + assert!( + email_found, + "Email '{}' should be present in Subject Alternative Names", + expected_email + ); + } } diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index b95e126a1..ed79f2639 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -1,5 +1,6 @@ use chrono::NaiveDateTime; use model_derive::Model; +use sqlx::PgPool; use crate::db::{Id, NoId}; @@ -12,4 +13,39 @@ pub struct Proxy { pub public_address: String, pub connected_at: Option, pub disconnected_at: Option, + pub has_certificate: bool, + pub certificate_expiry: Option, +} + +impl Proxy { + pub fn new>(name: S, address: S, port: i32, public_address: S) -> Self { + Self { + id: NoId, + name: name.into(), + address: address.into(), + port, + public_address: public_address.into(), + connected_at: None, + disconnected_at: None, + has_certificate: false, + certificate_expiry: None, + } + } +} + +impl Proxy { + pub async fn find_by_address_port( + pool: &PgPool, + address: &str, + port: i32, + ) -> sqlx::Result> { + sqlx::query_as!( + Proxy, + "SELECT * FROM proxy WHERE address = $1 AND port = $2", + address, + port + ) + .fetch_optional(pool) + .await + } } diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index c9d648172..4c7b93e1a 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, fmt}; +use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, Type, query, query_as}; use struct_patch::Patch; @@ -146,6 +147,7 @@ pub struct Settings { pub gateway_disconnect_notifications_reconnect_notification_enabled: bool, pub ca_key_der: Option>, pub ca_cert_der: Option>, + pub ca_expiry: Option, } // Implement manually to avoid exposing the license key. @@ -253,7 +255,7 @@ impl Settings { ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, \ ldap_user_rdn_attr, ldap_sync_groups, \ openid_username_handling \"openid_username_handling: OpenidUsernameHandling\", \ - ca_key_der, ca_cert_der \ + ca_key_der, ca_cert_der, ca_expiry \ FROM \"settings\" WHERE id = 1", ) .fetch_optional(executor) @@ -332,7 +334,8 @@ impl Settings { ldap_sync_groups = $47, \ openid_username_handling = $48, \ ca_key_der = $49, \ - ca_cert_der = $50 \ + ca_cert_der = $50, \ + ca_expiry = $51 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -384,6 +387,7 @@ impl Settings { &self.openid_username_handling as &OpenidUsernameHandling, &self.ca_key_der as &Option>, &self.ca_cert_der as &Option>, + &self.ca_expiry as &Option ) .execute(executor) .await?; diff --git a/crates/defguard_common/src/types/mod.rs b/crates/defguard_common/src/types/mod.rs index ff2bbead0..e207a5c56 100644 --- a/crates/defguard_common/src/types/mod.rs +++ b/crates/defguard_common/src/types/mod.rs @@ -1,2 +1,3 @@ pub mod group_diff; +pub mod proxy; pub mod user_info; diff --git a/crates/defguard_common/src/types/proxy.rs b/crates/defguard_common/src/types/proxy.rs new file mode 100644 index 000000000..0f508210b --- /dev/null +++ b/crates/defguard_common/src/types/proxy.rs @@ -0,0 +1,7 @@ +use crate::db::Id; + +// Used by the proxy manager to control proxies (start/shutdown). +pub enum ProxyControlMessage { + StartConnection(Id), + ShutdownConnection(Id), +} diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index a38bb6e89..83a470645 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -26,6 +26,7 @@ base32 = { workspace = true } base64 = { workspace = true } bytes = { workspace = true } chrono = { workspace = true } +futures = { workspace = true } humantime = { workspace = true } # match version used by sqlx ipnetwork = { workspace = true } @@ -82,6 +83,7 @@ ammonia = "4.1" regex = "1.10" tower = "0.5" uaparser = "0.6" +async-stream = "0.3" [dev-dependencies] claims.workspace = true diff --git a/crates/defguard_core/src/appstate.rs b/crates/defguard_core/src/appstate.rs index 10387ade9..27722d50a 100644 --- a/crates/defguard_core/src/appstate.rs +++ b/crates/defguard_core/src/appstate.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex, RwLock}; use axum::extract::FromRef; use axum_extra::extract::cookie::Key; -use defguard_common::config::server_config; +use defguard_common::{config::server_config, types::proxy::ProxyControlMessage}; use defguard_mail::Mail; use reqwest::Client; use secrecy::ExposeSecret; @@ -39,6 +39,7 @@ pub struct AppState { key: Key, pub event_tx: UnboundedSender, pub incompatible_components: Arc>, + pub proxy_control_tx: tokio::sync::mpsc::Sender, } impl AppState { @@ -116,6 +117,7 @@ impl AppState { failed_logins: Arc>, event_tx: UnboundedSender, incompatible_components: Arc>, + proxy_control_tx: tokio::sync::mpsc::Sender, ) -> Self { spawn(Self::handle_triggers(pool.clone(), rx)); @@ -146,6 +148,7 @@ impl AppState { key, event_tx, incompatible_components, + proxy_control_tx, } } } diff --git a/crates/defguard_core/src/error.rs b/crates/defguard_core/src/error.rs index d0dd38abf..bb2976c11 100644 --- a/crates/defguard_core/src/error.rs +++ b/crates/defguard_core/src/error.rs @@ -76,6 +76,9 @@ pub enum WebError { #[error("Activity log stream error: {0}")] #[schema(value_type=Object)] ActivityLogStreamError(#[from] ActivityLogStreamError), + #[error(transparent)] + #[schema(value_type=Object)] + CertificateError(#[from] defguard_certs::CertificateError), } impl From for WebError { diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 0afcfca7b..aefc97721 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -396,19 +396,9 @@ impl GatewayHandler { client.send_cert(req).await?; - let expiry = defguard_certs::get_certificate_expiry(&cert)?; - self.gateway.has_certificate = true; - self.gateway.certificate_expiry = Some( - chrono::DateTime::from_timestamp(expiry.unix_timestamp(), 0) - .ok_or_else(|| { - GatewayError::ConversionError(format!( - "Failed to convert certificate expiry timestamp {} to DateTime", - expiry.unix_timestamp() - )) - })? - .naive_utc(), - ); + self.gateway.certificate_expiry = + Some(defguard_certs::get_certificate_expiry(cert.der())?); self.gateway.save(&self.pool).await?; } Err(err) => { diff --git a/crates/defguard_core/src/handlers/ca.rs b/crates/defguard_core/src/handlers/ca.rs new file mode 100644 index 000000000..7e82f1b6a --- /dev/null +++ b/crates/defguard_core/src/handlers/ca.rs @@ -0,0 +1,43 @@ +use axum::{Json, extract::State}; +use defguard_common::db::models::{Settings, settings::update_current_settings}; +use reqwest::StatusCode; +use serde_json::json; + +use crate::{ + appstate::AppState, + auth::AdminRole, + handlers::{ApiResponse, ApiResult}, +}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct CreateCA { + common_name: String, + email: String, + validity_period_days: u32, +} + +pub async fn create_ca( + _role: AdminRole, + State(appstate): State, + Json(ca_info): Json, +) -> ApiResult { + let mut settings = Settings::get_current_settings(); + let ca = defguard_certs::CertificateAuthority::new( + &ca_info.common_name, + &ca_info.email, + ca_info.validity_period_days, + )?; + + let (cert_der, key_der) = (ca.cert_der().to_vec(), ca.key_pair_der().to_vec()); + + settings.ca_cert_der = Some(cert_der); + settings.ca_key_der = Some(key_der); + settings.ca_expiry = Some(ca.expiry()?); + + update_current_settings(&appstate.pool, settings).await?; + + Ok(ApiResponse { + json: json!({}), + status: StatusCode::CREATED, + }) +} diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index 1002d5b36..811ba9a6f 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -30,6 +30,7 @@ use crate::{ pub(crate) mod activity_log; pub(crate) mod app_info; pub(crate) mod auth; +pub mod ca; pub(crate) mod forward_auth; pub(crate) mod group; pub mod mail; @@ -37,6 +38,7 @@ pub mod network_devices; pub mod openid_clients; pub mod openid_flow; pub(crate) mod pagination; +pub(crate) mod proxy_setup; pub(crate) mod settings; pub(crate) mod ssh_authorized_keys; pub(crate) mod support; @@ -92,7 +94,8 @@ impl From for ApiResponse { | WebError::ClientIpError | WebError::FirewallError(_) | WebError::ApiEventChannelError(_) - | WebError::ActivityLogStreamError(_) => { + | WebError::ActivityLogStreamError(_) + | WebError::CertificateError(_) => { error!("{web_error}"); ApiResponse::new( json!({"msg": "Internal server error"}), diff --git a/crates/defguard_core/src/handlers/proxy_setup.rs b/crates/defguard_core/src/handlers/proxy_setup.rs new file mode 100644 index 000000000..b62d08557 --- /dev/null +++ b/crates/defguard_core/src/handlers/proxy_setup.rs @@ -0,0 +1,542 @@ +use std::{convert::Infallible, time::Duration}; + +use axum::{ + extract::{Query, State}, + response::sse::{Event, KeepAlive, Sse}, +}; +use defguard_certs::{der_to_pem, get_certificate_expiry}; +use defguard_common::{ + VERSION, + auth::claims::Claims, + db::models::{Settings, proxy::Proxy}, + types::proxy::ProxyControlMessage, +}; +use defguard_proto::proxy::{CertificateInfo, DerPayload, proxy_setup_client::ProxySetupClient}; +use defguard_version::{Version, client::ClientVersionInterceptor}; +use futures::Stream; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use tokio_stream::StreamExt; +use tonic::{ + Request, Status, + service::Interceptor, + transport::{Certificate, ClientTlsConfig, Endpoint}, +}; + +use crate::{AppState, auth::AdminRole, version::MIN_PROXY_VERSION}; + +const TOKEN_CLIENT_ID: &str = "Defguard Core"; +const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); + +/// Guard that aborts a tokio task when dropped +struct TaskGuard(tokio::task::JoinHandle<()>); + +impl Drop for TaskGuard { + fn drop(&mut self) { + self.0.abort(); + eprintln!("Log reader task aborted"); + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ProxySetupRequest { + pub ip_or_domain: String, + pub grpc_port: u16, + pub common_name: String, +} + +#[derive(Debug, Serialize, Copy, Clone)] +#[serde(tag = "step", content = "data")] +pub enum ProxySetupStep { + CheckingConfiguration, + CheckingAvailability, + CheckingVersion, + ObtainingCsr, + SigningCertificate, + ConfiguringTls, + Done, +} + +#[derive(Debug, Serialize)] +pub struct ProxySetupResponse { + #[serde(flatten)] + pub step: ProxySetupStep, + pub proxy_version: Option, + pub message: Option, + pub logs: Option>, + pub error: bool, +} + +#[derive(Clone)] +struct AuthInterceptor { + token: String, +} + +impl AuthInterceptor { + const fn new(token: String) -> Self { + Self { token } + } +} + +impl Interceptor for AuthInterceptor { + fn call(&mut self, mut request: Request<()>) -> Result, Status> { + request.metadata_mut().insert( + "authorization", + format!("Bearer {}", self.token).parse().unwrap(), + ); + Ok(request) + } +} + +fn fallback_message(err: &str, last_step: ProxySetupStep) -> String { + format!( + r#"{{"step":"{last_step:?}","message":"Failed to serialize error response: {err}","error":true}}"#, + ) +} + +fn error_message(message: &str, last_step: ProxySetupStep, logs: Option>) -> Event { + let response = ProxySetupResponse { + step: last_step, + proxy_version: None, + message: Some(message.to_string()), + logs, + error: true, + }; + + match serde_json::to_string(&response) { + Ok(body) => Event::default().data(body), + Err(e) => Event::default().data(fallback_message(&e.to_string(), last_step)), + } +} + +fn set_step_message(next_step: ProxySetupStep) -> Event { + let response = ProxySetupResponse { + step: next_step, + proxy_version: None, + message: None, + logs: None, + error: false, + }; + + match serde_json::to_string(&response) { + Ok(body) => Event::default().data(body), + Err(e) => Event::default().data(fallback_message(&e.to_string(), next_step)), + } +} + +struct SetupFlow { + last_step: ProxySetupStep, + log_rx: tokio::sync::mpsc::UnboundedReceiver, +} + +impl SetupFlow { + const fn new(log_rx: tokio::sync::mpsc::UnboundedReceiver) -> Self { + Self { + last_step: ProxySetupStep::CheckingConfiguration, + log_rx, + } + } + + fn step(&mut self, next_step: ProxySetupStep) -> Event { + self.last_step = next_step; + set_step_message(next_step) + } + + fn error(&mut self, message: &str) -> Event { + let mut collected_logs = Vec::new(); + while let Ok(log) = self.log_rx.try_recv() { + collected_logs.push(log); + } + let logs = if collected_logs.is_empty() { + None + } else { + Some(collected_logs) + }; + error_message(message, self.last_step, logs) + } +} + +/// This is the endpoint responsible for the whole edge proxy TLS setup flow. +/// It uses Server-Sent Events (SSE) to stream progress updates back to the frontend in real-time. +// This is a get request, since HTML's EventSource only supports GET +pub async fn setup_proxy_tls_stream( + _admin: AdminRole, + State(appstate): State, + Query(request): Query, +) -> Sse>> { + let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel::(); + + let stream = async_stream::stream! { + let mut flow = SetupFlow::new(log_rx); + + // Step 1: Check configuration + yield Ok( + flow.step(ProxySetupStep::CheckingConfiguration) + ); + + match Proxy::find_by_address_port(&appstate.pool, &request.ip_or_domain, i32::from(request.grpc_port)).await { + Ok(Some(proxy)) => { + yield Ok(flow.error(&format!("An edge Proxy with address {}:{} is already registered with name \"{}\".", request.ip_or_domain, request.grpc_port, proxy.name))); + return; + } + Ok(None) => { + debug!("Verified no existing proxy registration for {}:{}", request.ip_or_domain, request.grpc_port); + }, + Err(e) => { + yield Ok(flow.error(&format!("Failed to query existing proxy: {e}"))); + return; + } + } + + let url_str = format!("http://{}:{}", request.ip_or_domain, request.grpc_port); + + let url = match Url::parse(&url_str) { + Ok(u) => u, + Err(e) => { + yield Ok(flow.error(&format!("Invalid URL: {e}"))); + return; + } + }; + + debug!("Successfully validated proxy address: {}", url_str); + + let endpoint = match Endpoint::from_shared(url_str) { + Ok(e) => e, + Err(e) => { + yield Ok(flow.error(&format!("Failed to create endpoint: {e}"))); + return; + } + }; + + let endpoint = endpoint + .http2_keep_alive_interval(Duration::from_secs(5)) + .tcp_keepalive(Some(Duration::from_secs(5))) + .keep_alive_while_idle(true); + + debug!("Connection endpoint configured with keep-alive settings"); + + let settings = Settings::get_current_settings(); + let Some(ca_cert_der) = settings.ca_cert_der else { + yield Ok(flow.error("CA certificate not found in settings")); + return; + }; + + let cert_pem = match der_to_pem(&ca_cert_der, defguard_certs::PemLabel::Certificate) { + Ok(pem) => pem, + Err(e) => { + yield Ok(flow.error(&format!("Failed to convert CA cert DER to PEM: {e}"))); + return; + } + }; + let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(&cert_pem)); + + debug!("Loaded CA certificate for secure communication"); + + let endpoint = match endpoint.tls_config(tls) { + Ok(e) => e, + Err(e) => { + yield Ok(flow.error(&format!("Failed to configure TLS for endpoint: {e}"))); + return; + } + }; + + debug!("Prepared secure connection endpoint for proxy at {}:{}", request.ip_or_domain, request.grpc_port); + + let version = match Version::parse(VERSION) { + Ok(v) => v, + Err(e) => { + yield Ok(flow.error(&format!("Failed to parse version: {e}"))); + return; + } + }; + + // Step 2: Check availability + yield Ok( + flow.step(ProxySetupStep::CheckingAvailability) + ); + + + let version_clone = version.clone(); + + let token = match Claims::new( + defguard_common::auth::claims::ClaimsType::Gateway, + url.to_string(), + TOKEN_CLIENT_ID.to_string(), + u32::MAX.into(), + ) + .to_jwt() + { + Ok(token) => token, + Err(err) => { + yield Ok(flow.error(&format!("Failed to generate setup token: {err}"))); + return; + } + }; + + debug!("Generated secure setup token for proxy authentication"); + + let version_interceptor = ClientVersionInterceptor::new(version); + let auth_interceptor = AuthInterceptor::new(token); + + let mut client = ProxySetupClient::with_interceptor( + endpoint.connect_lazy(), + move |mut req: Request<()>| { + req = version_interceptor.clone().call(req)?; + auth_interceptor.clone().call(req) + } + ); + + debug!("Initiating connection to edge proxy at {}:{}", request.ip_or_domain, request.grpc_port); + + let response_with_metadata = match tokio::time::timeout( + CONNECTION_TIMEOUT, + client.start(()) + ).await { + Ok(Ok(r)) => r, + Ok(Err(e)) => { + match e.code() { + tonic::Code::Unavailable => { + let error_msg = e.to_string(); + if error_msg.contains("h2 protocol error") || error_msg.contains("http2 error") { + yield Ok(flow.error(&format!( + "Failed to connect to edge proxy at {}:{}: {}. This may indicate that the proxy is already configured with TLS. Please check if the proxy has already been set up.", + request.ip_or_domain, request.grpc_port, e + ))); + } else { + yield Ok(flow.error(&format!( + "Failed to connect to edge proxy at {}:{}. Please ensure the address and port are correct and that the edge component is running.", + request.ip_or_domain, request.grpc_port + ))); + } + } + _ => { + yield Ok(flow.error(&format!("Failed to connect to edge proxy: {e}"))); + } + } + return; + } + Err(_) => { + yield Ok(flow.error(&format!( + "Connection to edge proxy at {}:{} timed out after 10 seconds.", + request.ip_or_domain, request.grpc_port + ))); + return; + } + }; + + debug!("Successfully connected to edge proxy"); + + // Step 3: Check version + yield Ok( + flow.step(ProxySetupStep::CheckingVersion) + ); + + let proxy_version = response_with_metadata + .metadata() + .get(defguard_version::VERSION_HEADER) + .and_then(|v| v.to_str().ok()) + .map(defguard_version::Version::parse) + .transpose() + .unwrap_or(None); + + debug!("Proxy metadata: {:?}", response_with_metadata.metadata()); + debug!("Proxy version: {:?}", proxy_version); + + if let Some(proxy_version) = proxy_version { + if proxy_version < MIN_PROXY_VERSION { + yield Ok(flow.error(&format!( + "Edge proxy version {proxy_version} is older than core version {version_clone}. Please update the edge component.", + ))); + return; + } + + debug!("Edge proxy version {} is compatible with core version {}", proxy_version, version_clone); + + let response = ProxySetupResponse { + step: ProxySetupStep::CheckingVersion, + proxy_version: Some(proxy_version.to_string()), + message: None, + logs: None, + error: false, + }; + + match serde_json::to_string(&response) { + Ok(body) => { + yield Ok( + Event::default().data(body) + ); + }, + Err(e) => { + yield Ok(flow.error(&format!("Failed to serialize version response: {e}"))); + return; + } + } + } else { + yield Ok(flow.error("Failed to determine edge proxy version")); + return; + } + + let mut response = response_with_metadata.into_inner(); + + let log_reader_task = tokio::spawn(async move { + while let Some(log_entry) = response.next().await { + match log_entry { + Ok(entry) => { + let level = entry.level + .strip_prefix("Level(") + .and_then(|s| s.strip_suffix(")")) + .unwrap_or(&entry.level) + .to_uppercase(); + + + let formatted = format!( + "{} {} {}: message={}", + entry.timestamp, + level, + entry.target, + entry.message + ); + if log_tx.send(formatted).is_err() { + break; + } + } + Err(e) => { + let _ = log_tx.send(format!("Error reading log: {e}")); + break; + } + } + } + }); + + // Create guard to ensure task is aborted on all exit paths + let _log_task_guard = TaskGuard(log_reader_task); + + // Step 4: Obtain CSR + yield Ok(flow.step(ProxySetupStep::ObtainingCsr)); + + let Some(hostname) = url.host_str() else { + yield Ok(flow.error("URL does not have a valid host")); + return; + }; + + let csr_response = match client + .get_csr(CertificateInfo { + cert_hostname: hostname.to_string(), + }) + .await + { + Ok(r) => r.into_inner(), + Err(e) => { + yield Ok(flow.error(&format!("Failed to obtain CSR: {e}"))); + return; + } + }; + + let csr = match defguard_certs::Csr::from_der(&csr_response.der_data) { + Ok(c) => c, + Err(e) => { + yield Ok(flow.error(&format!("Failed to parse CSR: {e}"))); + return; + } + }; + + debug!("Received certificate signing request from edge proxy for hostname: {}", hostname); + + // Step 5: Sign certificate + yield Ok(flow.step(ProxySetupStep::SigningCertificate)); + + let settings = Settings::get_current_settings(); + + let Some(ca_cert_der) = settings.ca_cert_der else { + yield Ok(flow.error("CA certificate not found in settings")); + return; + }; + + let Some(ca_key_pair) = settings.ca_key_der else { + yield Ok(flow.error("CA key pair not found in settings")); + return; + }; + + let ca = match defguard_certs::CertificateAuthority::from_cert_der_key_pair( + &ca_cert_der, + &ca_key_pair, + ) { + Ok(c) => c, + Err(e) => { + yield Ok(flow.error(&format!("Failed to create CA: {e}"))); + return; + } + }; + + debug!("Certificate authority loaded and ready to sign certificates"); + + let cert = match ca.sign_csr(&csr) { + Ok(c) => c, + Err(e) => { + yield Ok(flow.error(&format!("Failed to sign CSR: {e}"))); + return; + } + }; + + debug!("Successfully signed certificate for edge proxy"); + + // Step 6: Configure TLS + yield Ok(flow.step(ProxySetupStep::ConfiguringTls)); + + let response = DerPayload { + der_data: cert.der().to_vec(), + }; + + if let Err(e) = client.send_cert(response).await { + yield Ok(flow.error(&format!("Failed to send certificate: {e}"))); + return; + } + + debug!("Certificate successfully delivered to edge proxy"); + + let expiry = match get_certificate_expiry(cert.der()) { + Ok(dt) => { + dt + }, + Err(err) => { + yield Ok(flow.error(&format!("Failed to get certificate expiry: {err}"))); + return; + } + }; + + debug!("Certificate expiry date determined: {}", expiry); + + let mut proxy = Proxy::new( + &request.common_name, + &request.ip_or_domain, + i32::from(request.grpc_port), + &request.ip_or_domain, + ); + + proxy.has_certificate = true; + proxy.certificate_expiry = Some(expiry); + + + let proxy = match proxy.save(&appstate.pool).await { + Ok(p) => p, + Err(err) => { + yield Ok(flow.error(&format!("Failed to save proxy to database: {err}"))); + return; + } + }; + + debug!("Edge proxy '{}' registered successfully with ID: {}", request.common_name, proxy.id); + debug!("Establishing connection to newly configured edge proxy"); + if let Err(err) = appstate.proxy_control_tx.send(ProxyControlMessage::StartConnection(proxy.id)).await { + yield Ok(flow.error(&format!("Failed send message to connect to proxy after setup: {err}"))); + return; + } + + debug!("Edge proxy setup completed successfully - proxy is now operational"); + + // Step 7: Done + yield Ok(flow.step(ProxySetupStep::Done)); + }; + + Sse::new(stream).keep_alive(KeepAlive::default()) +} diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index eb622875a..c375a4939 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -28,6 +28,7 @@ use defguard_common::{ }, }, }, + types::proxy::ProxyControlMessage, }; use defguard_mail::Mail; use defguard_version::server::DefguardVersionLayer; @@ -42,6 +43,7 @@ use handlers::{ find_available_ips, get_network_device, list_network_devices, modify_network_device, start_network_device_setup, start_network_device_setup_for_device, }, + proxy_setup::setup_proxy_tls_stream, ssh_authorized_keys::{ add_authentication_key, delete_authentication_key, fetch_authentication_keys, rename_authentication_key, @@ -149,7 +151,10 @@ use self::{ use crate::{ enterprise::handlers::openid_providers::list_openid_providers, grpc::gateway::events::GatewayEvent, - handlers::wireguard::{add_gateway, change_gateway}, + handlers::{ + ca::create_ca, + wireguard::{add_gateway, change_gateway}, + }, location_management::sync_location_allowed_devices, version::IncompatibleComponents, }; @@ -211,6 +216,7 @@ pub fn build_webapp( event_tx: UnboundedSender, version: Version, incompatible_components: Arc>, + proxy_control_tx: tokio::sync::mpsc::Sender, ) -> Router { let webapp: Router = Router::new() .route("/", get(index)) @@ -349,7 +355,11 @@ pub fn build_webapp( // ldap .route("/ldap/test", get(test_ldap_settings)) // activity log - .route("/activity_log", get(get_activity_log_events)), + .route("/activity_log", get(get_activity_log_events)) + // Certificate authority + .route("/ca", post(create_ca)) + // Proxy setup with SSE + .route("/proxy/setup/stream", get(setup_proxy_tls_stream)), ); // Enterprise features @@ -537,6 +547,7 @@ pub fn build_webapp( failed_logins, event_tx, incompatible_components, + proxy_control_tx, )) .layer( TraceLayer::new_for_http() @@ -564,6 +575,7 @@ pub async fn run_web_server( failed_logins: Arc>, event_tx: UnboundedSender, incompatible_components: Arc>, + proxy_control_tx: tokio::sync::mpsc::Sender, ) -> Result<(), anyhow::Error> { let webapp = build_webapp( webhook_tx, @@ -576,6 +588,7 @@ pub async fn run_web_server( event_tx, Version::parse(VERSION)?, incompatible_components, + proxy_control_tx, ); info!("Started web services"); let server_config = server_config(); diff --git a/crates/defguard_core/src/version.rs b/crates/defguard_core/src/version.rs index 6f37bd582..27c557aa3 100644 --- a/crates/defguard_core/src/version.rs +++ b/crates/defguard_core/src/version.rs @@ -9,7 +9,7 @@ use defguard_version::{ComponentInfo, Version, is_version_lower}; use serde::Serialize; use tonic::{Status, service::Interceptor}; -const MIN_PROXY_VERSION: Version = Version::new(1, 6, 0); +pub const MIN_PROXY_VERSION: Version = Version::new(1, 6, 0); pub const MIN_GATEWAY_VERSION: Version = Version::new(1, 6, 0); static OUTDATED_COMPONENT_LIFETIME: TimeDelta = TimeDelta::hours(1); diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index ea4ffef29..cf91c154a 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -14,6 +14,7 @@ use defguard_common::{ Id, models::{Settings, proxy::Proxy}, }, + types::proxy::ProxyControlMessage, }; use defguard_core::{ db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError}, @@ -34,9 +35,8 @@ use defguard_core::{ }; use defguard_mail::Mail; use defguard_proto::proxy::{ - AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, DerPayload, - InitialSetupInfo, core_request, core_response, proxy_client::ProxyClient, - proxy_setup_client::ProxySetupClient, + AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, core_request, + core_response, proxy_client::ProxyClient, }; use defguard_version::{ ComponentInfo, DefguardComponent, client::ClientVersionInterceptor, get_tracing_variables, @@ -48,9 +48,11 @@ use semver::Version; use sqlx::PgPool; use thiserror::Error; use tokio::{ + select, sync::{ + Mutex, broadcast::Sender, - mpsc::{self, UnboundedSender}, + mpsc::{self, Receiver, UnboundedSender}, }, task::JoinSet, time::sleep, @@ -71,7 +73,6 @@ pub(crate) mod password_reset; extern crate tracing; const TEN_SECS: Duration = Duration::from_secs(10); -const PROXY_AFTER_SETUP_CONNECT_DELAY: Duration = Duration::from_secs(1); static VERSION_ZERO: Version = Version::new(0, 0, 0); static COOKIE_KEY_HEADER: &str = "dg-cookie-key-bin"; @@ -184,6 +185,7 @@ pub struct ProxyManager { tx: ProxyTxSet, incompatible_components: Arc>, router: Arc>, + proxy_control: Receiver, } impl ProxyManager { @@ -191,12 +193,14 @@ impl ProxyManager { pool: PgPool, tx: ProxyTxSet, incompatible_components: Arc>, + proxy_control_rx: Receiver, ) -> Self { Self { pool, tx, incompatible_components, router: Arc::default(), + proxy_control: proxy_control_rx, } } @@ -204,18 +208,22 @@ impl ProxyManager { /// /// Each proxy runs in its own task and shares Core-side infrastructure /// such as routing state and compatibility tracking. - pub async fn run(self) -> Result<(), ProxyError> { + pub async fn run(mut self) -> Result<(), ProxyError> { debug!("ProxyManager starting"); // Retrieve proxies from DB. + let mut shutdown_channels = HashMap::new(); let mut proxies: Vec = Proxy::all(&self.pool) .await? .iter() .map(|proxy| { + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); + shutdown_channels.insert(proxy.id, shutdown_tx); ProxyServer::from_proxy( proxy, self.pool.clone(), &self.tx, Arc::clone(&self.router), + Arc::new(Mutex::new(Some(shutdown_rx))), ) }) .collect::>()?; @@ -225,8 +233,16 @@ impl ProxyManager { if let Some(ref url) = server_config().proxy_url { debug!("Adding proxy from cli arg: {url}"); let url = Url::from_str(url)?; - let proxy = - ProxyServer::new(self.pool.clone(), url, &self.tx, Arc::clone(&self.router)); + + let proxy = ProxyServer::new( + self.pool.clone(), + url, + &self.tx, + Arc::clone(&self.router), + // Currently we can't shutdown this proxy since it was started via CLI arguments (no ID in DB) + // This should be removed when we do a proper import of old proxies + Arc::new(Mutex::new(None)), + ); proxies.push(proxy); } @@ -236,18 +252,64 @@ impl ProxyManager { tokio::time::sleep(Duration::MAX).await; return Ok(()); } - // Connect to all proxies. let mut tasks = JoinSet::>::new(); for proxy in proxies { debug!("Spawning proxy task for proxy {}", proxy.url); tasks.spawn(proxy.run(self.tx.clone(), self.incompatible_components.clone())); } - while let Some(result) = tasks.join_next().await { - match result { - Ok(Ok(())) => error!("Proxy task returned prematurely"), - Ok(Err(err)) => error!("Proxy task returned with error: {err}"), - Err(err) => error!("Proxy task execution failed: {err}"), + + loop { + select! { + result = tasks.join_next() => { + match result { + Some(Ok(Ok(()))) => error!("Proxy task returned prematurely"), + Some(Ok(Err(err))) => error!("Proxy task returned with error: {err}"), + Some(Err(err)) => error!("Proxy task execution failed: {err}"), + None => { + debug!("All proxy tasks completed"); + break; + } + } + } + msg = self.proxy_control.recv() => { + match msg { + Some(ProxyControlMessage::StartConnection(id)) => { + debug!("Starting proxy with ID: {id}"); + if let Ok(Some(proxy_model)) = Proxy::find_by_id(&self.pool, id).await { + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); + shutdown_channels.insert(id, shutdown_tx); + match ProxyServer::from_proxy( + &proxy_model, + self.pool.clone(), + &self.tx, + Arc::clone(&self.router), + Arc::new(Mutex::new(Some(shutdown_rx))), + ) { + Ok(proxy) => { + debug!("Spawning proxy task for proxy {}", proxy.url); + tasks.spawn(proxy.run(self.tx.clone(), self.incompatible_components.clone())); + } + Err(err) => error!("Failed to create proxy server: {err}"), + } + } else { + error!("Failed to find proxy with ID: {id}"); + } + } + Some(ProxyControlMessage::ShutdownConnection(id)) => { + debug!("Shutting down proxy with ID: {id}"); + if let Some(shutdown_tx) = shutdown_channels.remove(&id) { + let _ = shutdown_tx.send(()); + } else { + warn!("No shutdown channel found for proxy ID: {id}"); + } + } + None => { + debug!("Proxy control channel closed"); + break; + } + } + } } } Ok(()) @@ -278,6 +340,8 @@ impl ProxyTxSet { } } +type ShutdownReceiver = tokio::sync::oneshot::Receiver<()>; + /// Represents a single Core - Proxy connection. /// /// A `Proxy` is responsible for establishing and maintaining a gRPC @@ -293,10 +357,17 @@ struct ProxyServer { router: Arc>, /// Proxy server gRPC URL url: Url, + shutdown_signal: Arc>>, } impl ProxyServer { - pub fn new(pool: PgPool, url: Url, tx: &ProxyTxSet, router: Arc>) -> Self { + pub fn new( + pool: PgPool, + url: Url, + tx: &ProxyTxSet, + router: Arc>, + shutdown_signal: Arc>>, + ) -> Self { // Instantiate gRPC servers. let services = ProxyServices::new(&pool, tx); @@ -305,6 +376,7 @@ impl ProxyServer { services, router, url, + shutdown_signal, } } @@ -313,9 +385,10 @@ impl ProxyServer { pool: PgPool, tx: &ProxyTxSet, router: Arc>, + shutdown_signal: Arc>>, ) -> Result { let url = Url::from_str(&format!("http://{}:{}", proxy.address, proxy.port))?; - Ok(Self::new(pool, url, tx, router)) + Ok(Self::new(pool, url, tx, router, shutdown_signal)) } fn endpoint(&self, scheme: Scheme) -> Result { @@ -360,16 +433,6 @@ impl ProxyServer { incompatible_components: Arc>, ) -> Result<(), ProxyError> { loop { - // TODO: When we will have proxy table, we should first check in DB if we already configured - // this proxy, and only perform initial setup if not. - if let Err(err) = self.perform_initial_setup().await { - warn!( - "Failed to perform initial Proxy setup: {err}. Will try to connect anyway as proxy may be already setup." - ); - } else { - sleep(PROXY_AFTER_SETUP_CONNECT_DELAY).await; - } - let endpoint = self.endpoint(Scheme::Https)?; debug!("Connecting to proxy at {}", endpoint.uri()); @@ -435,65 +498,30 @@ impl ProxyServer { info!("Connected to proxy at {}", endpoint.uri()); let mut resp_stream = response.into_inner(); - self.message_loop(tx, tx_set.wireguard.clone(), &mut resp_stream) - .await?; - } - } - - /// Attempt to perform an initial setup of the target proxy. - /// If the proxy doesn't have signed gRPC certificates by Core yet, - /// this step will perform the signing. Otherwise, the step will be skipped - /// by instantly sending the "Done" message by both parties. - pub async fn perform_initial_setup(&self) -> Result<(), ProxyError> { - let endpoint = self.endpoint(Scheme::Http)?; - - let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); - let mut client = ProxySetupClient::with_interceptor(endpoint.connect_lazy(), interceptor); - let Some(hostname) = self.url.host_str() else { - return Err(ProxyError::UrlError( - "Proxy URL missing hostname".to_string(), - )); - }; - - let csr = client - .start(InitialSetupInfo { - cert_hostname: hostname.to_string(), - }) - .await? - .into_inner(); - - let csr = defguard_certs::Csr::from_der(&csr.der_data)?; - - let settings = Settings::get_current_settings(); - let ca_cert_der = settings.ca_cert_der.ok_or_else(|| { - ProxyError::MissingConfiguration( - "CA certificate DER not found in settings for proxy gRPC bidi stream".to_string(), - ) - })?; - let ca_key_pair = settings.ca_key_der.ok_or_else(|| { - ProxyError::MissingConfiguration( - "CA key pairs DER not found in settings for proxy gRPC bidi stream".to_string(), - ) - })?; - - let ca = defguard_certs::CertificateAuthority::from_cert_der_key_pair( - &ca_cert_der, - &ca_key_pair, - )?; - - match ca.sign_csr(&csr) { - Ok(cert) => { - let response = DerPayload { - der_data: cert.der().to_vec(), - }; - client.send_cert(response).await?; - info!( - "Signed CSR received from proxy during initial setup and sent back the certificate" - ); - } - Err(err) => { - error!("Failed to sign CSR: {err}"); + let shutdown_signal = self.shutdown_signal.lock().await.take(); + if let Some(shutdown_signal) = shutdown_signal { + select! { + res = self.message_loop(tx, tx_set.wireguard.clone(), &mut resp_stream) => { + if let Err(err) = res { + error!("Proxy message loop ended with error: {err}, reconnecting in {TEN_SECS:?}",); + } else { + info!("Proxy message loop ended, reconnecting in {TEN_SECS:?}"); + } + sleep(TEN_SECS).await; + } + res = shutdown_signal => { + if let Err(err) = res { + error!("An error occurred when trying to wait for a shutdown signal for Proxy: {err}. Reconnecting to: {}", endpoint.uri()); + } else { + info!("Shutdown signal received, stopping proxy connection to {}", endpoint.uri()); + } + break; + } + } + } else { + self.message_loop(tx, tx_set.wireguard.clone(), &mut resp_stream) + .await?; } } diff --git a/migrations/20260116095450_[2.0.0]_proxy_certificates.down.sql b/migrations/20260116095450_[2.0.0]_proxy_certificates.down.sql new file mode 100644 index 000000000..01200febf --- /dev/null +++ b/migrations/20260116095450_[2.0.0]_proxy_certificates.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE proxy DROP COLUMN has_certificate; +ALTER TABLE proxy DROP COLUMN certificate_expiry; +ALTER TABLE proxy DROP CONSTRAINT unique_address_port; +ALTER TABLE settings DROP COLUMN ca_expiry; diff --git a/migrations/20260116095450_[2.0.0]_proxy_certificates.up.sql b/migrations/20260116095450_[2.0.0]_proxy_certificates.up.sql new file mode 100644 index 000000000..de514147d --- /dev/null +++ b/migrations/20260116095450_[2.0.0]_proxy_certificates.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE proxy ADD COLUMN has_certificate BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE proxy ADD COLUMN certificate_expiry TIMESTAMP WITHOUT TIME ZONE NULL; +ALTER TABLE proxy ADD CONSTRAINT unique_address_port UNIQUE (address, port); +ALTER TABLE settings ADD COLUMN ca_expiry TIMESTAMP WITHOUT TIME ZONE NULL; From ce7a13ee610a5af47d8e39fe596e2dba53c5d131 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:53:55 +0100 Subject: [PATCH 09/18] proxy wizard frontend --- web/messages/en/edge_wizard.json | 47 ++++ web/project.inlang/settings.json | 3 +- web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx | 107 ++++++++ .../pages/EdgeSetupPage/assets/add_more.svg | 76 ++++++ web/src/pages/EdgeSetupPage/assets/deploy.svg | 256 ++++++++++++++++++ .../pages/EdgeSetupPage/assets/file_icon.png | Bin 0 -> 8764 bytes .../EdgeSetupPage/assets/welcome_image.svg | 109 ++++++++ .../steps/SetupConfirmationStep.tsx | 48 ++++ .../steps/SetupEdgeAdaptationStep.tsx | 202 ++++++++++++++ .../steps/SetupEdgeComponentStep.tsx | 153 +++++++++++ web/src/pages/EdgeSetupPage/steps/style.scss | 11 + web/src/pages/EdgeSetupPage/steps/types.ts | 29 ++ .../EdgeSetupPage/steps/useSSEController.tsx | 62 +++++ web/src/pages/EdgeSetupPage/style.scss | 136 ++++++++++ web/src/pages/EdgeSetupPage/types.ts | 7 + .../EdgeSetupPage/useEdgeWizardStore.tsx | 93 +++++++ web/src/routeTree.gen.ts | 22 ++ .../_authorized/_wizard/edge-wizard.tsx | 6 + .../wizard/WizardPage/WizardPage.tsx | 43 +-- .../components/wizard/WizardPage/style.scss | 4 + .../WizardWelcomePage/WizardWelcomePage.tsx | 45 +++ .../WizardWelcomePage/assets/file_icon.png | Bin 0 -> 8764 bytes .../wizard/WizardWelcomePage/index.ts | 0 .../wizard/WizardWelcomePage/style.scss | 81 ++++++ web/src/shared/components/wizard/types.ts | 11 + 25 files changed, 1533 insertions(+), 18 deletions(-) create mode 100644 web/messages/en/edge_wizard.json create mode 100644 web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx create mode 100644 web/src/pages/EdgeSetupPage/assets/add_more.svg create mode 100644 web/src/pages/EdgeSetupPage/assets/deploy.svg create mode 100644 web/src/pages/EdgeSetupPage/assets/file_icon.png create mode 100644 web/src/pages/EdgeSetupPage/assets/welcome_image.svg create mode 100644 web/src/pages/EdgeSetupPage/steps/SetupConfirmationStep.tsx create mode 100644 web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx create mode 100644 web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx create mode 100644 web/src/pages/EdgeSetupPage/steps/style.scss create mode 100644 web/src/pages/EdgeSetupPage/steps/types.ts create mode 100644 web/src/pages/EdgeSetupPage/steps/useSSEController.tsx create mode 100644 web/src/pages/EdgeSetupPage/style.scss create mode 100644 web/src/pages/EdgeSetupPage/types.ts create mode 100644 web/src/pages/EdgeSetupPage/useEdgeWizardStore.tsx create mode 100644 web/src/routes/_authorized/_wizard/edge-wizard.tsx create mode 100644 web/src/shared/components/wizard/WizardWelcomePage/WizardWelcomePage.tsx create mode 100644 web/src/shared/components/wizard/WizardWelcomePage/assets/file_icon.png create mode 100644 web/src/shared/components/wizard/WizardWelcomePage/index.ts create mode 100644 web/src/shared/components/wizard/WizardWelcomePage/style.scss diff --git a/web/messages/en/edge_wizard.json b/web/messages/en/edge_wizard.json new file mode 100644 index 000000000..411c240de --- /dev/null +++ b/web/messages/en/edge_wizard.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "edge_setup_confirmation_title": "Edge component configured successfully.", + "edge_setup_confirmation_subtitle": "Your VPN location is now fully configured.", + "edge_setup_add_multiple_edge_components_title": "Add multiple Edge components", + "edge_setup_add_multiple_edge_components_subtitle": "Each location can include multiple Edge components. You may add another component at this stage or return and do it later.", + "edge_setup_controls_add_another_edge_component": "Add another edge component", + "edge_setup_controls_go_to_edge_components": "Go to edge components", + "edge_setup_page_title": "Configure Edge component", + "edge_setup_page_subtitle": "To activate localization, make sure at least one Edge component is connected.", + "edge_setup_welcome_title": "Deploy your Edge component", + "edge_setup_welcome_subtitle": "Welcome to the Edge component Setup Wizard. This guide will help you deploy the Edge required for secure and reliable VPN communication.", + "edge_setup_welcome_deploy_title": "Deploy Edge component first", + "edge_setup_welcome_deploy_subtitle": "Make sure your Edge Component is deployed. If it's already in place, click the button bellow to configure it in the following steps — otherwise deploy it first and return to this wizard.", + "edge_setup_welcome_docs_text": "Before installation, we recommend reading our documentation to understand the system architecture and core components.", + "edge_setup_welcome_image_alt": "Welcome to Edge component setup wizard", + "edge_setup_controls_configure": "Configure Edge component", + "edge_setup_step_edge_component_label": "Configure edge component", + "edge_setup_step_edge_component_description": "Set up your VPN proxy quickly and ensure secure, optimized traffic flow for your users.", + "edge_setup_step_edge_adaptation_label": "Edge Component Adaptation", + "edge_setup_step_edge_adaptation_description": "Review the system's checks and see if any issues need attention before deployment.", + "edge_setup_step_confirmation_label": "Confirmation", + "edge_setup_step_confirmation_description": "Your configuration was successful. You're all set.", + "edge_setup_component_label_common_name": "Common Name", + "edge_setup_component_label_ip_or_domain": "IP or Domain", + "edge_setup_component_label_grpc_port": "gRPC Port", + "edge_setup_component_label_public_domain": "Public Domain", + "edge_setup_component_error_common_name_required": "Common Name is required", + "edge_setup_component_error_ip_or_domain_required": "IP or Domain is required", + "edge_setup_component_error_grpc_port_required": "gRPC Port is required", + "edge_setup_component_error_grpc_port_max": "gRPC Port must be less than 65536", + "edge_setup_component_error_public_domain_required": "Public Domain is required", + "edge_setup_component_controls_back": "Back", + "edge_setup_component_controls_submit": "Adopt Edge component", + "edge_setup_adaptation_checking_configuration": "Checking provided Edge component configuration", + "edge_setup_adaptation_checking_availability": "Checking if Edge is available at: {ip_or_domain}:{grpc_port}", + "edge_setup_adaptation_checking_version": "Checking Edge proxy version", + "edge_setup_adaptation_checking_version_with_value": "Checking Edge proxy version: {proxyVersion}", + "edge_setup_adaptation_obtaining_csr": "Obtaining Certificate Signing Request", + "edge_setup_adaptation_signing_certificate": "Signing Certificate", + "edge_setup_adaptation_configuring_tls": "Configuring TLS communication", + "edge_setup_adaptation_error_default": "An error occurred during setup.", + "edge_setup_adaptation_error_log_title": "Error log", + "edge_setup_adaptation_controls_retry": "Retry", + "edge_setup_adaptation_controls_back": "Back", + "edge_setup_adaptation_controls_continue": "Continue" +} diff --git a/web/project.inlang/settings.json b/web/project.inlang/settings.json index 6206c1b15..42820221c 100644 --- a/web/project.inlang/settings.json +++ b/web/project.inlang/settings.json @@ -19,7 +19,8 @@ "./messages/{locale}/webhooks.json", "./messages/{locale}/groups.json", "./messages/{locale}/openid.json", - "./messages/{locale}/activity.json" + "./messages/{locale}/activity.json", + "./messages/{locale}/edge_wizard.json" ] } } diff --git a/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx b/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx new file mode 100644 index 000000000..7e355393b --- /dev/null +++ b/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx @@ -0,0 +1,107 @@ +import './style.scss'; +import { useNavigate } from '@tanstack/react-router'; +import { type ReactNode, useMemo } from 'react'; +import { m } from '../../paraglide/messages'; +import { ActionCard } from '../../shared/components/ActionCard/ActionCard'; +import { Controls } from '../../shared/components/Controls/Controls'; +import type { WizardPageStep } from '../../shared/components/wizard/types'; +import { WizardPage } from '../../shared/components/wizard/WizardPage/WizardPage'; +import { Button } from '../../shared/defguard-ui/components/Button/Button'; +import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; +import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../shared/defguard-ui/types'; +import deployImage from './assets/deploy.svg'; +import welcomeImage from './assets/welcome_image.svg'; +import { SetupConfirmationStep } from './steps/SetupConfirmationStep'; +import { SetupEdgeAdaptationStep } from './steps/SetupEdgeAdaptationStep'; +import { SetupEdgeComponentStep } from './steps/SetupEdgeComponentStep'; +import { EdgeSetupStep, type EdgeSetupStepValue } from './types'; +import { useEdgeWizardStore } from './useEdgeWizardStore'; + +export const EdgeSetupPage = () => { + const activeStep = useEdgeWizardStore((s) => s.activeStep); + const showWelcome = useEdgeWizardStore((s) => s.showWelcome); + const setShowWelcome = useEdgeWizardStore((s) => s.setShowWelcome); + const navigate = useNavigate(); + + const stepsConfig = useMemo( + (): Record => ({ + edgeComponent: { + id: EdgeSetupStep.EdgeComponent, + order: 1, + label: m.edge_setup_step_edge_component_label(), + description: m.edge_setup_step_edge_component_description(), + }, + edgeAdaptation: { + id: EdgeSetupStep.EdgeAdaptation, + order: 2, + label: m.edge_setup_step_edge_adaptation_label(), + description: m.edge_setup_step_edge_adaptation_description(), + }, + confirmation: { + id: EdgeSetupStep.Confirmation, + order: 3, + label: m.edge_setup_step_confirmation_label(), + description: m.edge_setup_step_confirmation_description(), + }, + }), + [], + ); + + const stepsComponents = useMemo( + (): Record => ({ + edgeComponent: , + edgeAdaptation: , + confirmation: , + }), + [], + ); + + const WelcomePageContent = () => ( + <> + +
+ + + +
+ + ); + + return ( + { + useEdgeWizardStore.getState().reset(); + navigate({ + to: '/settings', + replace: true, + }); + }} + subtitle={m.edge_setup_page_subtitle()} + title={m.edge_setup_page_title()} + steps={stepsConfig} + id="setup-wizard" + showWelcome={showWelcome} + welcomePageConfig={{ + title: m.edge_setup_welcome_title(), + subtitle: m.edge_setup_welcome_subtitle(), + content: , + docsLink: 'https://docs.defguard.net/edge-component/deployment', + docsText: m.edge_setup_welcome_docs_text(), + media: {m.edge_setup_welcome_image_alt()}, + }} + > + {stepsComponents[activeStep]} + + ); +}; diff --git a/web/src/pages/EdgeSetupPage/assets/add_more.svg b/web/src/pages/EdgeSetupPage/assets/add_more.svg new file mode 100644 index 000000000..36badeb38 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/assets/add_more.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/pages/EdgeSetupPage/assets/deploy.svg b/web/src/pages/EdgeSetupPage/assets/deploy.svg new file mode 100644 index 000000000..346353af4 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/assets/deploy.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/pages/EdgeSetupPage/assets/file_icon.png b/web/src/pages/EdgeSetupPage/assets/file_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f0cafdc06a39ecd1199900f0f60091d143e4c3ce GIT binary patch literal 8764 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 mlqJd28mQTH{ChujVEKPFZr7m)txG)s0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/pages/EdgeSetupPage/steps/SetupConfirmationStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupConfirmationStep.tsx new file mode 100644 index 000000000..0e976d4db --- /dev/null +++ b/web/src/pages/EdgeSetupPage/steps/SetupConfirmationStep.tsx @@ -0,0 +1,48 @@ +import { useNavigate } from '@tanstack/react-router'; +import { m } from '../../../paraglide/messages'; +import { ActionCard } from '../../../shared/components/ActionCard/ActionCard'; +import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; +import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; +import { ModalControls } from '../../../shared/defguard-ui/components/ModalControls/ModalControls'; +import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../../shared/defguard-ui/types'; +import addMoreImage from '../assets/add_more.svg'; +import { useEdgeWizardStore } from '../useEdgeWizardStore'; + +export const SetupConfirmationStep = () => { + const navigate = useNavigate(); + + const handleBack = () => { + useEdgeWizardStore.getState().reset(); + }; + + const handleFinish = () => { + useEdgeWizardStore.getState().reset(); + navigate({ to: '/vpn-overview' }); + }; + + return ( + +

{m.edge_setup_confirmation_title()}

+ +

{m.edge_setup_confirmation_subtitle()}

+ + + +
+ ); +}; diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx new file mode 100644 index 000000000..0541e2096 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx @@ -0,0 +1,202 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { m } from '../../../paraglide/messages'; +import { Controls } from '../../../shared/components/Controls/Controls'; +import { LoadingStep } from '../../../shared/components/LoadingStep/LoadingStep'; +import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; +import { Button } from '../../../shared/defguard-ui/components/Button/Button'; +import { CodeCard } from '../../../shared/defguard-ui/components/CodeCard/CodeCard'; +import { ModalControls } from '../../../shared/defguard-ui/components/ModalControls/ModalControls'; +import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../../shared/defguard-ui/types'; +import { EdgeSetupStep } from '../types'; +import { useEdgeWizardStore } from '../useEdgeWizardStore'; +import type { SetupEvent, SetupStep, SetupStepId } from './types'; +import { useSSEController } from './useSSEController'; + +export const SetupEdgeAdaptationStep = () => { + const setActiveStep = useEdgeWizardStore((s) => s.setActiveStep); + const edgeComponentWizardStore = useEdgeWizardStore((s) => s); + const edgeAdaptationState = useEdgeWizardStore((s) => s.edgeAdaptationState); + const setEdgeAdaptationState = useEdgeWizardStore((s) => s.setEdgeAdaptationState); + const resetEdgeAdaptationState = useEdgeWizardStore((s) => s.resetEdgeAdaptationState); + + const handleEvent = useCallback( + (event: SetupEvent) => { + setEdgeAdaptationState({ + currentStep: event.step, + isComplete: event.step === 'Done', + isProcessing: event.step !== 'Done' && !event.error, + proxyVersion: event.proxy_version ?? null, + errorMessage: event.error + ? event.message || m.edge_setup_adaptation_error_default() + : null, + proxyLogs: event.logs && event.logs.length > 0 ? [...event.logs] : [], + }); + }, + [setEdgeAdaptationState], + ); + + const sse = useSSEController( + '/api/v1/proxy/setup/stream', + { + ip_or_domain: edgeComponentWizardStore.ip_or_domain, + grpc_port: edgeComponentWizardStore.grpc_port, + common_name: edgeComponentWizardStore.common_name, + }, + { + onOpen: () => + setEdgeAdaptationState({ + ...edgeAdaptationState, + isProcessing: true, + }), + onMessage: handleEvent, + onError: () => { + setEdgeAdaptationState({ + ...edgeAdaptationState, + isProcessing: false, + }); + }, + }, + ); + + const handleBack = () => { + useEdgeWizardStore.getState().resetEdgeAdaptationState(); + setActiveStep(EdgeSetupStep.EdgeComponent); + }; + + const handleNext = () => { + setActiveStep(EdgeSetupStep.Confirmation); + }; + + const steps: SetupStep[] = useMemo( + () => [ + { + id: 'CheckingConfiguration', + title: m.edge_setup_adaptation_checking_configuration(), + }, + { + id: 'CheckingAvailability', + title: m.edge_setup_adaptation_checking_availability({ + ip_or_domain: edgeComponentWizardStore.ip_or_domain, + grpc_port: edgeComponentWizardStore.grpc_port.toString(), + }), + }, + { + id: 'CheckingVersion', + title: edgeAdaptationState.proxyVersion + ? m.edge_setup_adaptation_checking_version_with_value({ + proxyVersion: edgeAdaptationState.proxyVersion, + }) + : m.edge_setup_adaptation_checking_version(), + }, + { + id: 'ObtainingCsr', + title: m.edge_setup_adaptation_obtaining_csr(), + }, + { + id: 'SigningCertificate', + title: m.edge_setup_adaptation_signing_certificate(), + }, + { + id: 'ConfiguringTls', + title: m.edge_setup_adaptation_configuring_tls(), + }, + ], + [edgeComponentWizardStore, edgeAdaptationState.proxyVersion], + ); + + const stepDone = useCallback( + (stepId: SetupStepId): boolean => { + const stepIndex = steps.findIndex((step) => step.id === stepId); + const currentStepIndex = edgeAdaptationState.currentStep + ? steps.findIndex((step) => step.id === edgeAdaptationState.currentStep) + : -1; + return stepIndex < currentStepIndex || edgeAdaptationState.isComplete; + }, + [edgeAdaptationState.isComplete, edgeAdaptationState.currentStep, steps], + ); + + const stepLoading = useCallback( + (stepId: SetupStepId): boolean => { + return ( + edgeAdaptationState.isProcessing && edgeAdaptationState.currentStep === stepId + ); + }, + [edgeAdaptationState.isProcessing, edgeAdaptationState.currentStep], + ); + + const stepError = useCallback( + (stepId: SetupStepId): string | null => { + if ( + edgeAdaptationState.errorMessage && + edgeAdaptationState.currentStep === stepId + ) { + return edgeAdaptationState.errorMessage; + } + return null; + }, + [edgeAdaptationState.errorMessage, edgeAdaptationState.currentStep], + ); + + useEffect(() => { + resetEdgeAdaptationState(); + sse.start(); + + return () => { + sse.stop(); + }; + }, [resetEdgeAdaptationState, sse.start, sse.stop]); + + return ( + +
+ {steps.map((step, index) => ( + + {edgeAdaptationState.proxyLogs.length > 0 ? ( + <> + + + + ) : null} + +
+
+
+
+ ))} +
+ +
+ ); +}; diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx new file mode 100644 index 000000000..c40c71687 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx @@ -0,0 +1,153 @@ +import { useNavigate } from '@tanstack/react-router'; +import { useMemo } from 'react'; +import z from 'zod'; +import { useShallow } from 'zustand/react/shallow'; +import { m } from '../../../paraglide/messages'; +import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; +import { ModalControls } from '../../../shared/defguard-ui/components/ModalControls/ModalControls'; +import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../../shared/defguard-ui/types'; +import { useAppForm } from '../../../shared/form'; +import { formChangeLogic } from '../../../shared/formLogic'; +import { EdgeSetupStep } from '../types'; +import { useEdgeWizardStore } from '../useEdgeWizardStore'; +import './style.scss'; +import { validateIpOrDomain } from '../../../shared/validators'; + +type FormFields = StoreValues; + +type StoreValues = { + common_name: string; + ip_or_domain: string; + grpc_port: number; + public_domain: string; +}; + +export const SetupEdgeComponentStep = () => { + const setActiveStep = useEdgeWizardStore((s) => s.setActiveStep); + const navigate = useNavigate(); + const defaultValues = useEdgeWizardStore( + useShallow( + (s): FormFields => ({ + common_name: s.common_name, + ip_or_domain: s.ip_or_domain, + grpc_port: s.grpc_port, + public_domain: s.public_domain, + }), + ), + ); + + const handleNext = () => { + form.handleSubmit(); + }; + + const handleBack = () => { + useEdgeWizardStore.getState().reset(); + navigate({ + to: '/edge-wizard', + replace: true, + }); + }; + + const formSchema = useMemo( + () => + z.object({ + common_name: z + .string() + .min(1, m.edge_setup_component_error_common_name_required()), + ip_or_domain: z + .string() + .min(1, m.edge_setup_component_error_ip_or_domain_required()) + .refine((val) => validateIpOrDomain(val, false, true)), + grpc_port: z + .number() + .min(1, m.edge_setup_component_error_grpc_port_required()) + .max(65535, m.edge_setup_component_error_grpc_port_max()), + public_domain: z + .string() + .min(1, m.edge_setup_component_error_public_domain_required()), + }), + [], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: formSchema, + onChange: formSchema, + }, + onSubmit: ({ value }) => { + useEdgeWizardStore.setState({ + ...value, + }); + setActiveStep(EdgeSetupStep.EdgeAdaptation); + }, + }); + + return ( + +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + +
+ +
+ ); +}; diff --git a/web/src/pages/EdgeSetupPage/steps/style.scss b/web/src/pages/EdgeSetupPage/steps/style.scss new file mode 100644 index 000000000..10e13b988 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/steps/style.scss @@ -0,0 +1,11 @@ +.wizard-card { + .modal-controls > .buttons { + display: flex; + flex-grow: 1; + justify-content: space-between; + } + + .controls { + padding-top: 0; + } +} diff --git a/web/src/pages/EdgeSetupPage/steps/types.ts b/web/src/pages/EdgeSetupPage/steps/types.ts new file mode 100644 index 000000000..b19078c10 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/steps/types.ts @@ -0,0 +1,29 @@ +export type SetupEvent = { + step: SetupStepId; + proxy_version?: string; + message?: string; + logs?: string[]; + error: boolean; +}; + +export type SetupStep = { + id: SetupStepId; + title: string; +}; + +export type SetupStepId = + | 'CheckingConfiguration' + | 'CheckingAvailability' + | 'CheckingVersion' + | 'ObtainingCsr' + | 'SigningCertificate' + | 'ConfiguringTls' + | 'Done'; +// biome-ignore lint/suspicious/noExplicitAny: SSE hook accepts various data types +export interface SSEHookOptions { + onMessage?: (data: T) => void; + onError?: (error: Event) => void; + onOpen?: () => void; + parseJSON?: boolean; + params?: Record; +} diff --git a/web/src/pages/EdgeSetupPage/steps/useSSEController.tsx b/web/src/pages/EdgeSetupPage/steps/useSSEController.tsx new file mode 100644 index 000000000..da993a82b --- /dev/null +++ b/web/src/pages/EdgeSetupPage/steps/useSSEController.tsx @@ -0,0 +1,62 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { SSEHookOptions } from './types'; + +// SSE (Server-Sent Events) controller hook for processing real-time events received from the backend. +// biome-ignore lint/suspicious/noExplicitAny: SSE hook accepts various data types +export function useSSEController( + url: string, + params: Record, + options: SSEHookOptions = {}, +) { + const eventSourceRef = useRef(null); + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(null); + + const buildUrl = useCallback(() => { + const qs = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { + if (v !== undefined && v !== null) qs.append(k, String(v)); + }); + return qs.toString() ? `${url}?${qs}` : url; + }, [url, params]); + + const stop = useCallback(() => { + eventSourceRef.current?.close(); + eventSourceRef.current = null; + setIsConnected(false); + }, []); + + const start = useCallback(() => { + if (eventSourceRef.current) return; + + const es = new EventSource(buildUrl()); + eventSourceRef.current = es; + + es.onopen = () => { + setIsConnected(true); + setError(null); + options.onOpen?.(); + }; + + es.onmessage = (e) => { + const data = options.parseJSON === false ? e.data : JSON.parse(e.data); + options.onMessage?.(data); + }; + + es.onerror = (e) => { + setError(e); + setIsConnected(false); + options.onError?.(e); + stop(); + }; + }, [buildUrl, options, stop]); + + const restart = useCallback(() => { + stop(); + start(); + }, [start, stop]); + + useEffect(() => stop, [stop]); + + return { start, stop, restart, isConnected, error }; +} diff --git a/web/src/pages/EdgeSetupPage/style.scss b/web/src/pages/EdgeSetupPage/style.scss new file mode 100644 index 000000000..a76d227e1 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/style.scss @@ -0,0 +1,136 @@ +/* stylelint-disable no-descending-specificity */ +#setup-page { + background-color: var(--bg-muted); + width: 100%; + min-height: 100dvh; +} + +#setup-page > .content-limiter { + display: flex; + flex-flow: row; + align-items: flex-start; + justify-content: center; +} + +#setup-page .page-grid { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-content: flex-start; + width: 100%; + max-width: 1120px; + box-sizing: border-box; + min-height: 100dvh; +} + +#setup-page header { + height: 60px; + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + padding-top: var(--spacing-2xl); + padding-bottom: var(--spacing-4xl); +} + +#setup-page #content-card { + display: grid; + grid-template-columns: 667fr 443fr; + border-radius: var(--radius-xxl); + background-color: var(--bg-default); + box-shadow: var(--menu-shadow); + overflow: hidden; + + & > .main-track { + box-sizing: border-box; + padding: var(--spacing-4xl); + + display: flex; + flex-direction: column; + justify-content: space-between; + } + + & > .image { + height: 100%; + width: 100%; + max-width: 100%; + position: relative; + overflow: hidden; + + video { + width: 100%; + min-height: 657px; + min-width: 443px; + object-fit: cover; + } + } +} + +#docs-card { + display: grid; + grid-template-columns: 54px 1fr; + background-color: transparent; + border: var(--border-1) solid var(--border-disabled); + border-radius: var(--radius-lg); + padding: var(--spacing-md); + column-gap: var(--spacing-2xl); + + .image-track { + width: 100%; + max-width: 100%; + overflow: hidden; + + img { + width: 100%; + } + } + + .content { + display: flex; + flex-flow: column; + row-gap: var(--spacing-sm); + + p { + font: var(--t-body-sm-400); + color: var(--fg-muted); + } + } +} + +#setup-page footer { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + width: 100%; + margin-top: auto; + padding-top: var(--spacing-4xl); + padding-bottom: var(--spacing-2xl); + + & > div:nth-child(2) { + margin-left: auto; + } + + div p { + font: var(--t-body-xs-400); + color: var(--fg-muted); + + span { + color: inherit; + font: inherit; + } + } + + div:nth-child(1) { + a { + color: inherit; + } + } + + div:nth-child(2) { + a { + color: var(--fg-action); + text-decoration: none; + } + } +} diff --git a/web/src/pages/EdgeSetupPage/types.ts b/web/src/pages/EdgeSetupPage/types.ts new file mode 100644 index 000000000..de3524491 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/types.ts @@ -0,0 +1,7 @@ +export const EdgeSetupStep = { + EdgeComponent: 'edgeComponent', + EdgeAdaptation: 'edgeAdaptation', + Confirmation: 'confirmation', +} as const; + +export type EdgeSetupStepValue = (typeof EdgeSetupStep)[keyof typeof EdgeSetupStep]; diff --git a/web/src/pages/EdgeSetupPage/useEdgeWizardStore.tsx b/web/src/pages/EdgeSetupPage/useEdgeWizardStore.tsx new file mode 100644 index 000000000..adf2dc199 --- /dev/null +++ b/web/src/pages/EdgeSetupPage/useEdgeWizardStore.tsx @@ -0,0 +1,93 @@ +import { omit } from 'lodash-es'; +import { create } from 'zustand'; +import { createJSONStorage, persist } from 'zustand/middleware'; +import type { SetupStepId } from './steps/types'; +import { EdgeSetupStep, type EdgeSetupStepValue } from './types'; + +type EdgeAdaptationState = { + isProcessing: boolean; + isComplete: boolean; + currentStep: SetupStepId | null; + errorMessage: string | null; + proxyVersion: string | null; + proxyLogs: string[]; +}; + +type StoreValues = { + activeStep: EdgeSetupStepValue; + showWelcome: boolean; + common_name: string; + ip_or_domain: string; + grpc_port: number; + public_domain: string; + edgeAdaptationState: EdgeAdaptationState; +}; + +type StoreMethods = { + reset: () => void; + start: (values?: Partial) => void; + setActiveStep: (step: EdgeSetupStepValue) => void; + setShowWelcome: (show: boolean) => void; + updateValues: (values: Partial) => void; + resetEdgeAdaptationState: () => void; + setEdgeAdaptationState: (state: EdgeAdaptationState) => void; +}; + +const edgeAdaptationStateDefaults: EdgeAdaptationState = { + isProcessing: false, + isComplete: false, + currentStep: null, + errorMessage: null, + proxyVersion: null, + proxyLogs: [], +}; + +const defaults: StoreValues = { + activeStep: EdgeSetupStep.EdgeComponent, + showWelcome: true, + common_name: '', + ip_or_domain: '', + grpc_port: 50051, + public_domain: '', + edgeAdaptationState: edgeAdaptationStateDefaults, +}; + +export const useEdgeWizardStore = create()( + persist( + (set) => ({ + ...defaults, + reset: () => set(defaults), + start: (initial) => { + set({ + ...defaults, + ...initial, + }); + }, + setActiveStep: (step) => set({ activeStep: step }), + setShowWelcome: (show) => set({ showWelcome: show }), + updateValues: (values) => set(values), + resetEdgeAdaptationState: () => + set(() => ({ + edgeAdaptationState: { ...edgeAdaptationStateDefaults }, + })), + setEdgeAdaptationState: (state: Partial) => + set((s) => ({ + edgeAdaptationState: { ...s.edgeAdaptationState, ...state }, + })), + }), + { + name: 'setup-wizard-store', + storage: createJSONStorage(() => sessionStorage), + partialize: (state) => + omit(state, [ + 'reset', + 'start', + 'setActiveStep', + 'updateValues', + 'setShowWelcome', + 'resetEdgeAdaptationState', + 'setEdgeAdaptationState', + ]), + }, + ), +); diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index adaabe675..18ed878a2 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -26,6 +26,7 @@ import { Route as AuthMfaTotpRouteImport } from './routes/auth/mfa/totp' import { Route as AuthMfaRecoveryRouteImport } from './routes/auth/mfa/recovery' import { Route as AuthMfaEmailRouteImport } from './routes/auth/mfa/email' import { Route as AuthorizedWizardSetupWizardRouteImport } from './routes/_authorized/_wizard/setup-wizard' +import { Route as AuthorizedWizardEdgeWizardRouteImport } from './routes/_authorized/_wizard/edge-wizard' import { Route as AuthorizedWizardAddLocationRouteImport } from './routes/_authorized/_wizard/add-location' import { Route as AuthorizedWizardAddExternalOpenidRouteImport } from './routes/_authorized/_wizard/add-external-openid' import { Route as AuthorizedDefaultWebhooksRouteImport } from './routes/_authorized/_default/webhooks' @@ -135,6 +136,12 @@ const AuthorizedWizardSetupWizardRoute = path: '/setup-wizard', getParentRoute: () => AuthorizedRoute, } as any) +const AuthorizedWizardEdgeWizardRoute = + AuthorizedWizardEdgeWizardRouteImport.update({ + id: '/_wizard/edge-wizard', + path: '/edge-wizard', + getParentRoute: () => AuthorizedRoute, + } as any) const AuthorizedWizardAddLocationRoute = AuthorizedWizardAddLocationRouteImport.update({ id: '/_wizard/add-location', @@ -296,6 +303,7 @@ export interface FileRoutesByFullPath { '/webhooks': typeof AuthorizedDefaultWebhooksRoute '/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/add-location': typeof AuthorizedWizardAddLocationRoute + '/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute '/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -336,6 +344,7 @@ export interface FileRoutesByTo { '/webhooks': typeof AuthorizedDefaultWebhooksRoute '/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/add-location': typeof AuthorizedWizardAddLocationRoute + '/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute '/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -380,6 +389,7 @@ export interface FileRoutesById { '/_authorized/_default/webhooks': typeof AuthorizedDefaultWebhooksRoute '/_authorized/_wizard/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/_authorized/_wizard/add-location': typeof AuthorizedWizardAddLocationRoute + '/_authorized/_wizard/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute '/_authorized/_wizard/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -423,6 +433,7 @@ export interface FileRouteTypes { | '/webhooks' | '/add-external-openid' | '/add-location' + | '/edge-wizard' | '/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -463,6 +474,7 @@ export interface FileRouteTypes { | '/webhooks' | '/add-external-openid' | '/add-location' + | '/edge-wizard' | '/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -506,6 +518,7 @@ export interface FileRouteTypes { | '/_authorized/_default/webhooks' | '/_authorized/_wizard/add-external-openid' | '/_authorized/_wizard/add-location' + | '/_authorized/_wizard/edge-wizard' | '/_authorized/_wizard/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -659,6 +672,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthorizedWizardSetupWizardRouteImport parentRoute: typeof AuthorizedRoute } + '/_authorized/_wizard/edge-wizard': { + id: '/_authorized/_wizard/edge-wizard' + path: '/edge-wizard' + fullPath: '/edge-wizard' + preLoaderRoute: typeof AuthorizedWizardEdgeWizardRouteImport + parentRoute: typeof AuthorizedRoute + } '/_authorized/_wizard/add-location': { id: '/_authorized/_wizard/add-location' path: '/add-location' @@ -893,6 +913,7 @@ interface AuthorizedRouteChildren { AuthorizedDefaultRoute: typeof AuthorizedDefaultRouteWithChildren AuthorizedWizardAddExternalOpenidRoute: typeof AuthorizedWizardAddExternalOpenidRoute AuthorizedWizardAddLocationRoute: typeof AuthorizedWizardAddLocationRoute + AuthorizedWizardEdgeWizardRoute: typeof AuthorizedWizardEdgeWizardRoute AuthorizedWizardSetupWizardRoute: typeof AuthorizedWizardSetupWizardRoute } @@ -901,6 +922,7 @@ const AuthorizedRouteChildren: AuthorizedRouteChildren = { AuthorizedWizardAddExternalOpenidRoute: AuthorizedWizardAddExternalOpenidRoute, AuthorizedWizardAddLocationRoute: AuthorizedWizardAddLocationRoute, + AuthorizedWizardEdgeWizardRoute: AuthorizedWizardEdgeWizardRoute, AuthorizedWizardSetupWizardRoute: AuthorizedWizardSetupWizardRoute, } diff --git a/web/src/routes/_authorized/_wizard/edge-wizard.tsx b/web/src/routes/_authorized/_wizard/edge-wizard.tsx new file mode 100644 index 000000000..38fdbce00 --- /dev/null +++ b/web/src/routes/_authorized/_wizard/edge-wizard.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { EdgeSetupPage } from '../../../pages/EdgeSetupPage/EdgeSetupPage'; + +export const Route = createFileRoute('/_authorized/_wizard/edge-wizard')({ + component: EdgeSetupPage, +}); diff --git a/web/src/shared/components/wizard/WizardPage/WizardPage.tsx b/web/src/shared/components/wizard/WizardPage/WizardPage.tsx index 91fecae8b..72ad14fcc 100644 --- a/web/src/shared/components/wizard/WizardPage/WizardPage.tsx +++ b/web/src/shared/components/wizard/WizardPage/WizardPage.tsx @@ -10,6 +10,7 @@ import { LayoutGrid } from '../../LayoutGrid/LayoutGrid'; import type { WizardPageConfig } from '../types'; import { WizardStepsCard } from '../WizardStepsCard/WizardStepsCard'; import { WizardTop } from '../WizardTop/WizardTop'; +import { WizardWelcomePage } from '../WizardWelcomePage/WizardWelcomePage'; type Props = HTMLProps & PropsWithChildren & @@ -25,6 +26,8 @@ export const WizardPage = ({ title, children, onClose, + welcomePageConfig, + showWelcome, ...containerProps }: Props) => { const activeStep = steps[activeStepId]; @@ -61,23 +64,29 @@ export const WizardPage = ({
-
-

{title}

- -

{subtitle}

- - -
-
- - -

{activeStep.label}

- {isPresent(activeStep.description) && ( -

{activeStep.description}

- )} - - {children} -
+ {welcomePageConfig && showWelcome ? ( + + ) : ( + <> +
+

{title}

+ +

{subtitle}

+ + +
+
+ + +

{activeStep.label}

+ {isPresent(activeStep.description) && ( +

{activeStep.description}

+ )} + + {children} +
+ + )}
diff --git a/web/src/shared/components/wizard/WizardPage/style.scss b/web/src/shared/components/wizard/WizardPage/style.scss index db7e173f5..9b7ef34b5 100644 --- a/web/src/shared/components/wizard/WizardPage/style.scss +++ b/web/src/shared/components/wizard/WizardPage/style.scss @@ -49,4 +49,8 @@ color: var(--fg-muted); } } + + .layout-grid > .wizard-welcome-page { + grid-column: 1 / 13; + } } diff --git a/web/src/shared/components/wizard/WizardWelcomePage/WizardWelcomePage.tsx b/web/src/shared/components/wizard/WizardWelcomePage/WizardWelcomePage.tsx new file mode 100644 index 000000000..d701461e6 --- /dev/null +++ b/web/src/shared/components/wizard/WizardWelcomePage/WizardWelcomePage.tsx @@ -0,0 +1,45 @@ +import './style.scss'; +import { AppText } from '../../../defguard-ui/components/AppText/AppText'; +import { ExternalLink } from '../../../defguard-ui/components/ExternalLink/ExternalLink'; +import { SizedBox } from '../../../defguard-ui/components/SizedBox/SizedBox'; +import { TextStyle, ThemeSpacing, ThemeVariable } from '../../../defguard-ui/types'; +import type { WizardWelcomePageConfig } from '../types'; +import fileIcon from './assets/file_icon.png'; + +type Props = WizardWelcomePageConfig; + +export const WizardWelcomePage = ({ + title, + subtitle, + content, + media, + docsLink = 'https://docs.defguard.net/', + docsText = 'Before installation, we recommend reading our documentation to understand the system architecture and core components.', +}: Props) => { + return ( +
+
+
+

{title}

+ + + {subtitle} + +
{content}
+
+
+
+ Documentation +
+
+

{docsText}

+
+ {`Read documentation`} +
+
+
+
+
{media}
+
+ ); +}; diff --git a/web/src/shared/components/wizard/WizardWelcomePage/assets/file_icon.png b/web/src/shared/components/wizard/WizardWelcomePage/assets/file_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f0cafdc06a39ecd1199900f0f60091d143e4c3ce GIT binary patch literal 8764 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 mlqJd28mQTH{ChujVEKPFZr7m)txG)s0000; From 8876f377aff62258bfd1e2474c463b8e02f3e7ad Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:18:48 +0100 Subject: [PATCH 10/18] remove eprintln --- crates/defguard_core/src/handlers/proxy_setup.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/defguard_core/src/handlers/proxy_setup.rs b/crates/defguard_core/src/handlers/proxy_setup.rs index b62d08557..9260fc441 100644 --- a/crates/defguard_core/src/handlers/proxy_setup.rs +++ b/crates/defguard_core/src/handlers/proxy_setup.rs @@ -34,7 +34,6 @@ struct TaskGuard(tokio::task::JoinHandle<()>); impl Drop for TaskGuard { fn drop(&mut self) { self.0.abort(); - eprintln!("Log reader task aborted"); } } From 4231d191cb6ce7f80632caa31069c99b1eff61ad Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:38:56 +0100 Subject: [PATCH 11/18] fix lint --- crates/defguard_proxy_manager/src/lib.rs | 5 +++-- web/src/pages/EdgeSetupPage/style.scss | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 3b54d54c4..1549b665a 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -35,8 +35,8 @@ use defguard_core::{ }; use defguard_mail::Mail; use defguard_proto::proxy::{ - AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, DerPayload, - InitialInfo, InitialSetupInfo, core_request, core_response, proxy_client::ProxyClient, + AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, InitialInfo, + core_request, core_response, proxy_client::ProxyClient, }; use defguard_version::{ ComponentInfo, DefguardComponent, client::ClientVersionInterceptor, get_tracing_variables, @@ -76,6 +76,7 @@ static VERSION_ZERO: Version = Version::new(0, 0, 0); #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(crate) enum Scheme { + #[allow(dead_code)] Http, Https, } diff --git a/web/src/pages/EdgeSetupPage/style.scss b/web/src/pages/EdgeSetupPage/style.scss index a76d227e1..9ca97f6e8 100644 --- a/web/src/pages/EdgeSetupPage/style.scss +++ b/web/src/pages/EdgeSetupPage/style.scss @@ -44,7 +44,6 @@ & > .main-track { box-sizing: border-box; padding: var(--spacing-4xl); - display: flex; flex-direction: column; justify-content: space-between; From c563a7db890f4835b24d3e998c1c8d74cf0a6dcf Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:24:46 +0100 Subject: [PATCH 12/18] fix --- crates/defguard_core/src/handlers/proxy_setup.rs | 2 +- crates/defguard_proxy_manager/src/lib.rs | 9 +-------- .../EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx | 3 ++- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/defguard_core/src/handlers/proxy_setup.rs b/crates/defguard_core/src/handlers/proxy_setup.rs index 9260fc441..e85807268 100644 --- a/crates/defguard_core/src/handlers/proxy_setup.rs +++ b/crates/defguard_core/src/handlers/proxy_setup.rs @@ -531,7 +531,7 @@ pub async fn setup_proxy_tls_stream( return; } - debug!("Edge proxy setup completed successfully - proxy is now operational"); + debug!("Edge proxy setup completed successfully"); // Step 7: Done yield Ok(flow.step(ProxySetupStep::Done)); diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 1549b665a..921930af9 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -245,12 +245,6 @@ impl ProxyManager { proxies.push(proxy); } - // TODO setup a channel to allow dynamic proxy connections - if proxies.is_empty() { - debug!("No proxies to connect to, waiting for changes"); - tokio::time::sleep(Duration::MAX).await; - return Ok(()); - } // Connect to all proxies. let mut tasks = JoinSet::>::new(); for proxy in proxies { @@ -260,14 +254,13 @@ impl ProxyManager { loop { select! { - result = tasks.join_next() => { + result = tasks.join_next(), if !tasks.is_empty() => { match result { Some(Ok(Ok(()))) => error!("Proxy task returned prematurely"), Some(Ok(Err(err))) => error!("Proxy task returned with error: {err}"), Some(Err(err)) => error!("Proxy task execution failed: {err}"), None => { debug!("All proxy tasks completed"); - break; } } } diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx index 0541e2096..12d603a69 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx @@ -145,7 +145,8 @@ export const SetupEdgeAdaptationStep = () => { return () => { sse.stop(); }; - }, [resetEdgeAdaptationState, sse.start, sse.stop]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( From b27f047a71863264f658551a99d067daac411b8f Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:45:03 +0100 Subject: [PATCH 13/18] fix 2 --- .../EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx index 12d603a69..a513a390e 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx @@ -44,18 +44,7 @@ export const SetupEdgeAdaptationStep = () => { common_name: edgeComponentWizardStore.common_name, }, { - onOpen: () => - setEdgeAdaptationState({ - ...edgeAdaptationState, - isProcessing: true, - }), onMessage: handleEvent, - onError: () => { - setEdgeAdaptationState({ - ...edgeAdaptationState, - isProcessing: false, - }); - }, }, ); @@ -145,7 +134,7 @@ export const SetupEdgeAdaptationStep = () => { return () => { sse.stop(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + // biome-ignore lint/react-hooks/exhaustive-deps: only run on mount }, []); return ( From a789f50bdd411b79e45766fa0f2d144745f3465c Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:21:38 +0100 Subject: [PATCH 14/18] update proto --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 906412eb5..413435816 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 906412eb50ac605f4904a355c2f325f3645c117e +Subproject commit 4134358160e4f819515c9a6e5c014434cfb46d74 From c8ac9ed341c5066dba88e4683a840de6ca6f97e4 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:26:06 +0100 Subject: [PATCH 15/18] fix it again --- web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx index a513a390e..408bab60f 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx @@ -127,6 +127,7 @@ export const SetupEdgeAdaptationStep = () => { [edgeAdaptationState.errorMessage, edgeAdaptationState.currentStep], ); + // biome-ignore lint/correctness/useExhaustiveDependencies: mount only useEffect(() => { resetEdgeAdaptationState(); sse.start(); @@ -134,7 +135,6 @@ export const SetupEdgeAdaptationStep = () => { return () => { sse.stop(); }; - // biome-ignore lint/react-hooks/exhaustive-deps: only run on mount }, []); return ( From 776a0a89761c0b87633887a6794a5a62bcb7a816 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:27:46 +0100 Subject: [PATCH 16/18] remove must use --- crates/defguard_certs/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 6a4e6c9aa..1b46c25b6 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -136,7 +136,6 @@ impl CertificateAuthority<'_> { self.issuer.key().serialized_der() } - #[must_use] pub fn expiry(&self) -> Result { get_certificate_expiry(&self.cert_der) } From 432d24502a39a242bab33106fdb12a75300970e5 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:43:53 +0100 Subject: [PATCH 17/18] sqlx prepare --- ...0d948c232c5817c395bbf44444ffd2261075.json} | 7 +- ...d82a34d3bfa4364cbe9b8368e71445bc20877.json | 71 +++++++++++++++++++ ...692e787fd1187d3dd2b8c128134c3d83cf92.json} | 16 ++++- ...967087f477add7724e08b45ca60baabd2705.json} | 6 +- ...24f23803f5a9d400d81f39d8d6eacadb7565.json} | 6 +- ...5327ebeecbefa97c0af2273f7147dab098be.json} | 16 ++++- ...2c5de2e76b4aba0b969b28c624d335a21cec.json} | 10 ++- 7 files changed, 119 insertions(+), 13 deletions(-) rename .sqlx/{query-bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a.json => query-34fed9b4c2195113c5548f5107c70d948c232c5817c395bbf44444ffd2261075.json} (93%) create mode 100644 .sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json rename .sqlx/{query-57d87e1e6c73c6f630153227c3220a0bef9f3e0ec3ca5697130e3f506cfc2e0a.json => query-b48534fac2ea138d617f3653168c692e787fd1187d3dd2b8c128134c3d83cf92.json} (69%) rename .sqlx/{query-a3e132169196b632eca55d7b0421fd4acdbd1fec82a836c8e25162fe7d86b5b8.json => query-cef21a0fb292e51a461fc1bc44ec967087f477add7724e08b45ca60baabd2705.json} (61%) rename .sqlx/{query-7b09429adcf009bc19f24d95905e7e6bdba8432097e2e04ca06b036cacf38257.json => query-d890aeacd36cbc31d5042a66cf8724f23803f5a9d400d81f39d8d6eacadb7565.json} (65%) rename .sqlx/{query-acfc047027db4967051af1f6404e6e503a54e4dd6144dacd824d5e8a829f2c04.json => query-ee66d9ce46f283c78402efcd27385327ebeecbefa97c0af2273f7147dab098be.json} (69%) rename .sqlx/{query-23d55f2b3d7f82c0bb6afdd4e88e64b9920b263b3d668704ecbe66d80aa7c586.json => query-f2eb1e5e54fd2d0ede1941eadaf62c5de2e76b4aba0b969b28c624d335a21cec.json} (97%) diff --git a/.sqlx/query-bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a.json b/.sqlx/query-34fed9b4c2195113c5548f5107c70d948c232c5817c395bbf44444ffd2261075.json similarity index 93% rename from .sqlx/query-bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a.json rename to .sqlx/query-34fed9b4c2195113c5548f5107c70d948c232c5817c395bbf44444ffd2261075.json index 4a1c1ceec..5bb4e8f78 100644 --- a/.sqlx/query-bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a.json +++ b/.sqlx/query-34fed9b4c2195113c5548f5107c70d948c232c5817c395bbf44444ffd2261075.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48, ca_key_der = $49, ca_cert_der = $50 WHERE id = 1", + "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48, ca_key_der = $49, ca_cert_der = $50, ca_expiry = $51 WHERE id = 1", "describe": { "columns": [], "parameters": { @@ -86,10 +86,11 @@ } }, "Bytea", - "Bytea" + "Bytea", + "Timestamp" ] }, "nullable": [] }, - "hash": "bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a" + "hash": "34fed9b4c2195113c5548f5107c70d948c232c5817c395bbf44444ffd2261075" } diff --git a/.sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json b/.sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json new file mode 100644 index 000000000..c6ce1e21b --- /dev/null +++ b/.sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json @@ -0,0 +1,71 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM proxy WHERE address = $1 AND port = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "address", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "public_address", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "certificate_expiry", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + true + ] + }, + "hash": "a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877" +} diff --git a/.sqlx/query-57d87e1e6c73c6f630153227c3220a0bef9f3e0ec3ca5697130e3f506cfc2e0a.json b/.sqlx/query-b48534fac2ea138d617f3653168c692e787fd1187d3dd2b8c128134c3d83cf92.json similarity index 69% rename from .sqlx/query-57d87e1e6c73c6f630153227c3220a0bef9f3e0ec3ca5697130e3f506cfc2e0a.json rename to .sqlx/query-b48534fac2ea138d617f3653168c692e787fd1187d3dd2b8c128134c3d83cf92.json index 1727fc222..15e64e3af 100644 --- a/.sqlx/query-57d87e1e6c73c6f630153227c3220a0bef9f3e0ec3ca5697130e3f506cfc2e0a.json +++ b/.sqlx/query-b48534fac2ea138d617f3653168c692e787fd1187d3dd2b8c128134c3d83cf92.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"address\",\"port\",\"public_address\",\"connected_at\",\"disconnected_at\" FROM \"proxy\"", + "query": "SELECT id, \"name\",\"address\",\"port\",\"public_address\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\" FROM \"proxy\"", "describe": { "columns": [ { @@ -37,6 +37,16 @@ "ordinal": 6, "name": "disconnected_at", "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "certificate_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -49,8 +59,10 @@ false, false, true, + true, + false, true ] }, - "hash": "57d87e1e6c73c6f630153227c3220a0bef9f3e0ec3ca5697130e3f506cfc2e0a" + "hash": "b48534fac2ea138d617f3653168c692e787fd1187d3dd2b8c128134c3d83cf92" } diff --git a/.sqlx/query-a3e132169196b632eca55d7b0421fd4acdbd1fec82a836c8e25162fe7d86b5b8.json b/.sqlx/query-cef21a0fb292e51a461fc1bc44ec967087f477add7724e08b45ca60baabd2705.json similarity index 61% rename from .sqlx/query-a3e132169196b632eca55d7b0421fd4acdbd1fec82a836c8e25162fe7d86b5b8.json rename to .sqlx/query-cef21a0fb292e51a461fc1bc44ec967087f477add7724e08b45ca60baabd2705.json index e73615683..e3f41ec87 100644 --- a/.sqlx/query-a3e132169196b632eca55d7b0421fd4acdbd1fec82a836c8e25162fe7d86b5b8.json +++ b/.sqlx/query-cef21a0fb292e51a461fc1bc44ec967087f477add7724e08b45ca60baabd2705.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"proxy\" (\"name\",\"address\",\"port\",\"public_address\",\"connected_at\",\"disconnected_at\") VALUES ($1,$2,$3,$4,$5,$6) RETURNING id", + "query": "INSERT INTO \"proxy\" (\"name\",\"address\",\"port\",\"public_address\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", "describe": { "columns": [ { @@ -16,6 +16,8 @@ "Int4", "Text", "Timestamp", + "Timestamp", + "Bool", "Timestamp" ] }, @@ -23,5 +25,5 @@ false ] }, - "hash": "a3e132169196b632eca55d7b0421fd4acdbd1fec82a836c8e25162fe7d86b5b8" + "hash": "cef21a0fb292e51a461fc1bc44ec967087f477add7724e08b45ca60baabd2705" } diff --git a/.sqlx/query-7b09429adcf009bc19f24d95905e7e6bdba8432097e2e04ca06b036cacf38257.json b/.sqlx/query-d890aeacd36cbc31d5042a66cf8724f23803f5a9d400d81f39d8d6eacadb7565.json similarity index 65% rename from .sqlx/query-7b09429adcf009bc19f24d95905e7e6bdba8432097e2e04ca06b036cacf38257.json rename to .sqlx/query-d890aeacd36cbc31d5042a66cf8724f23803f5a9d400d81f39d8d6eacadb7565.json index 108c81e56..475daea81 100644 --- a/.sqlx/query-7b09429adcf009bc19f24d95905e7e6bdba8432097e2e04ca06b036cacf38257.json +++ b/.sqlx/query-d890aeacd36cbc31d5042a66cf8724f23803f5a9d400d81f39d8d6eacadb7565.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"proxy\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"public_address\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7 WHERE id = $1", + "query": "UPDATE \"proxy\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"public_address\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"has_certificate\" = $8,\"certificate_expiry\" = $9 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -11,10 +11,12 @@ "Int4", "Text", "Timestamp", + "Timestamp", + "Bool", "Timestamp" ] }, "nullable": [] }, - "hash": "7b09429adcf009bc19f24d95905e7e6bdba8432097e2e04ca06b036cacf38257" + "hash": "d890aeacd36cbc31d5042a66cf8724f23803f5a9d400d81f39d8d6eacadb7565" } diff --git a/.sqlx/query-acfc047027db4967051af1f6404e6e503a54e4dd6144dacd824d5e8a829f2c04.json b/.sqlx/query-ee66d9ce46f283c78402efcd27385327ebeecbefa97c0af2273f7147dab098be.json similarity index 69% rename from .sqlx/query-acfc047027db4967051af1f6404e6e503a54e4dd6144dacd824d5e8a829f2c04.json rename to .sqlx/query-ee66d9ce46f283c78402efcd27385327ebeecbefa97c0af2273f7147dab098be.json index 305f3f71f..5f78596bc 100644 --- a/.sqlx/query-acfc047027db4967051af1f6404e6e503a54e4dd6144dacd824d5e8a829f2c04.json +++ b/.sqlx/query-ee66d9ce46f283c78402efcd27385327ebeecbefa97c0af2273f7147dab098be.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"address\",\"port\",\"public_address\",\"connected_at\",\"disconnected_at\" FROM \"proxy\" WHERE id = $1", + "query": "SELECT id, \"name\",\"address\",\"port\",\"public_address\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\" FROM \"proxy\" WHERE id = $1", "describe": { "columns": [ { @@ -37,6 +37,16 @@ "ordinal": 6, "name": "disconnected_at", "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "certificate_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -51,8 +61,10 @@ false, false, true, + true, + false, true ] }, - "hash": "acfc047027db4967051af1f6404e6e503a54e4dd6144dacd824d5e8a829f2c04" + "hash": "ee66d9ce46f283c78402efcd27385327ebeecbefa97c0af2273f7147dab098be" } diff --git a/.sqlx/query-23d55f2b3d7f82c0bb6afdd4e88e64b9920b263b3d668704ecbe66d80aa7c586.json b/.sqlx/query-f2eb1e5e54fd2d0ede1941eadaf62c5de2e76b4aba0b969b28c624d335a21cec.json similarity index 97% rename from .sqlx/query-23d55f2b3d7f82c0bb6afdd4e88e64b9920b263b3d668704ecbe66d80aa7c586.json rename to .sqlx/query-f2eb1e5e54fd2d0ede1941eadaf62c5de2e76b4aba0b969b28c624d335a21cec.json index b6b62477c..7aac0a869 100644 --- a/.sqlx/query-23d55f2b3d7f82c0bb6afdd4e88e64b9920b263b3d668704ecbe66d80aa7c586.json +++ b/.sqlx/query-f2eb1e5e54fd2d0ede1941eadaf62c5de2e76b4aba0b969b28c624d335a21cec.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", ca_key_der, ca_cert_der FROM \"settings\" WHERE id = 1", + "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", ca_key_der, ca_cert_der, ca_expiry FROM \"settings\" WHERE id = 1", "describe": { "columns": [ { @@ -284,6 +284,11 @@ "ordinal": 49, "name": "ca_cert_der", "type_info": "Bytea" + }, + { + "ordinal": 50, + "name": "ca_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -339,8 +344,9 @@ false, false, true, + true, true ] }, - "hash": "23d55f2b3d7f82c0bb6afdd4e88e64b9920b263b3d668704ecbe66d80aa7c586" + "hash": "f2eb1e5e54fd2d0ede1941eadaf62c5de2e76b4aba0b969b28c624d335a21cec" } From 40f2a2648833a96dde96e12f72862111be1dda78 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:45:24 +0100 Subject: [PATCH 18/18] fix tests --- crates/defguard_core/tests/integration/api/common/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index 225eeaf1d..7447bf2d3 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -33,7 +33,7 @@ use tokio::{ net::TcpListener, sync::{ broadcast::{self, Receiver}, - mpsc::{UnboundedReceiver, unbounded_channel}, + mpsc::{UnboundedReceiver, channel, unbounded_channel}, }, }; @@ -117,6 +117,8 @@ pub(crate) async fn make_base_client( config.clone(), ); + let (proxy_control_tx, _proxy_control_rx) = channel(10); + // Uncomment this to enable tracing in tests. // It only works for running a single test, so leave it commented out for running all tests. // use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -139,6 +141,7 @@ pub(crate) async fn make_base_client( api_event_tx, Version::parse(VERSION).unwrap(), Arc::default(), + proxy_control_tx, ); (