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/22] 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 ce1a729fa3..eea5159b42 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 8ea34cbeb3..b769b0f9ca 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 574505b213..444e2dcf0f 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 bab5cff05f..f7f25189d7 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 51995484f5..747b161e03 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 e9963f89dd..670d1a2d37 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 0000000000..fa9a529419 --- /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 0000000000..aa5825457d --- /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/22] 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 2153bea867..a7dad283ba 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 8acd6cab65..e6f65e19f3 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 b7925ce97a..07eda69483 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 e6f5119abd..421a93437b 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 468e83f64e..c7bc50e521 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 f834402ff2..750ec80a80 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 e1859844e8..6a8de55ee7 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 96c266dfef..7557aedd6e 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 2861a13b10..2fc9730f6a 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 ee6dd32924..d64c777d8a 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 902f20028a..099f89229f 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 5eec85d52a..171de03d81 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 9609d5d005..6716d877a0 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 084b1a117f..781eded1b3 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 747b161e03..0afcfca7bd 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 670d1a2d37..d734941b8d 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 33de00fc8f..429d841d53 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 e0358ce9cf..16aea97128 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 3c27dbd384..208b3e5260 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/22] 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 d734941b8d..45bbe078e0 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/22] 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 444e2dcf0f..bf0b009043 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 45bbe078e0..0f4f317536 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/22] 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 f5f9307d7d..b52e770941 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 58a9bd507f..b45febc008 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 8afd59ce08..ed24b8d62a 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 9476d09d17..c321d2e9e7 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 6fa0952987..6b597e4488 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/22] 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 bf0b009043..09ca0ea91a 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/22] update protobufs --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index c48340f72b..161c6c6776 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/22] 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 535698a6ec..9de36f0bc0 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 5d2ad055ea..c19463c20c 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 2fdc572297..31d0c62b17 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 b769b0f9ca..9207838d3c 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 09ca0ea91a..6a4e6c9aac 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 b95e126a1a..ed79f2639f 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 c9d6481721..4c7b93e1a6 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 ff2bbead02..e207a5c568 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 0000000000..0f508210be --- /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 a38bb6e892..83a4706456 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 10387ade90..27722d50a6 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 d0dd38abf5..bb2976c11c 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 0afcfca7bd..aefc977213 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 0000000000..7e82f1b6af --- /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 1002d5b363..811ba9a6f6 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 0000000000..b62d085573 --- /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 eb622875ab..c375a49393 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 6f37bd5821..27c557aa34 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 ea4ffef294..cf91c154a8 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 0000000000..01200febf8 --- /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 0000000000..de514147d4 --- /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/22] 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 0000000000..411c240de2 --- /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 6206c1b159..42820221ca 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 0000000000..7e355393b3 --- /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 0000000000..36badeb389 --- /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 0000000000..346353af49 --- /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 0000000000..0e976d4db4 --- /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 0000000000..0541e20964 --- /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 0000000000..c40c716871 --- /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 0000000000..10e13b988e --- /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 0000000000..b19078c105 --- /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 0000000000..da993a82bc --- /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 0000000000..a76d227e12 --- /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 0000000000..de35244910 --- /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 0000000000..adf2dc1992 --- /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 adaabe675f..18ed878a26 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 0000000000..38fdbce000 --- /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 91fecae8b3..72ad14fcc9 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 db7e173f5a..9b7ef34b5f 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 0000000000..d701461e65 --- /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/22] 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 b62d085573..9260fc4417 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/22] 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 3b54d54c4e..1549b665ac 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 a76d227e12..9ca97f6e8c 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/22] 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 9260fc4417..e858072683 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 1549b665ac..921930af93 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 0541e20964..12d603a692 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/22] 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 12d603a692..a513a390ee 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 e993c70a2ff469df02218f789e8780a9c7ef4305 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:47:04 +0100 Subject: [PATCH 14/22] gateway wizard 1 --- Cargo.lock | 1 + crates/defguard/Cargo.toml | 1 + crates/defguard/src/main.rs | 11 +- .../defguard_common/src/db/models/gateway.rs | 12 + .../defguard_core/src/grpc/gateway/handler.rs | 98 +--- crates/defguard_core/src/grpc/gateway/mod.rs | 11 - .../{proxy_setup.rs => component_setup.rs} | 435 +++++++++++++++++- crates/defguard_core/src/handlers/mod.rs | 2 +- crates/defguard_core/src/lib.rs | 7 +- crates/defguard_version/src/tracing.rs | 28 +- web/messages/en/gateway_wizard.json | 45 ++ web/project.inlang/settings.json | 3 +- .../steps => hooks}/useSSEController.tsx | 12 +- .../steps/SetupEdgeAdaptationStep.tsx | 6 +- web/src/pages/EdgeSetupPage/steps/style.scss | 5 + web/src/pages/EdgeSetupPage/steps/types.ts | 10 +- .../EdgeSetupPage/useEdgeWizardStore.tsx | 2 +- .../GatewaySetupPage/GatewaySetupPage.tsx | 98 ++++ .../GatewaySetupPage/assets/add_more.svg | 76 +++ .../pages/GatewaySetupPage/assets/deploy.svg | 256 +++++++++++ .../GatewaySetupPage/assets/file_icon.png | Bin 0 -> 8764 bytes .../GatewaySetupPage/assets/welcome_image.svg | 109 +++++ .../steps/SetupConfirmationStep.tsx | 48 ++ .../steps/SetupGatewayAdaptationStep.tsx | 197 ++++++++ .../steps/SetupGatewayComponentStep.tsx | 139 ++++++ .../pages/GatewaySetupPage/steps/style.scss | 16 + web/src/pages/GatewaySetupPage/steps/types.ts | 21 + web/src/pages/GatewaySetupPage/style.scss | 135 ++++++ web/src/pages/GatewaySetupPage/types.ts | 8 + .../useGatewayWizardStore.tsx | 93 ++++ .../LocationOverviewPage.tsx | 2 - .../LocationsOverviewPage.tsx | 2 - web/src/pages/LocationsPage/LocationsPage.tsx | 2 - .../components/LocationsTable.tsx | 8 +- web/src/routeTree.gen.ts | 22 + .../_authorized/_wizard/gateway-wizard.tsx | 6 + .../GatewaySetupModal/GatewaySetupModal.tsx | 140 ------ .../modals/GatewaySetupModal/style.scss | 9 - .../shared/hooks/modalControls/modalTypes.ts | 5 - web/src/shared/hooks/modalControls/types.ts | 5 - 40 files changed, 1755 insertions(+), 331 deletions(-) rename crates/defguard_core/src/handlers/{proxy_setup.rs => component_setup.rs} (53%) create mode 100644 web/messages/en/gateway_wizard.json rename web/src/{pages/EdgeSetupPage/steps => hooks}/useSSEController.tsx (82%) create mode 100644 web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx create mode 100644 web/src/pages/GatewaySetupPage/assets/add_more.svg create mode 100644 web/src/pages/GatewaySetupPage/assets/deploy.svg create mode 100644 web/src/pages/GatewaySetupPage/assets/file_icon.png create mode 100644 web/src/pages/GatewaySetupPage/assets/welcome_image.svg create mode 100644 web/src/pages/GatewaySetupPage/steps/SetupConfirmationStep.tsx create mode 100644 web/src/pages/GatewaySetupPage/steps/SetupGatewayAdaptationStep.tsx create mode 100644 web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx create mode 100644 web/src/pages/GatewaySetupPage/steps/style.scss create mode 100644 web/src/pages/GatewaySetupPage/steps/types.ts create mode 100644 web/src/pages/GatewaySetupPage/style.scss create mode 100644 web/src/pages/GatewaySetupPage/types.ts create mode 100644 web/src/pages/GatewaySetupPage/useGatewayWizardStore.tsx create mode 100644 web/src/routes/_authorized/_wizard/gateway-wizard.tsx delete mode 100644 web/src/shared/components/modals/GatewaySetupModal/GatewaySetupModal.tsx delete mode 100644 web/src/shared/components/modals/GatewaySetupModal/style.scss diff --git a/Cargo.lock b/Cargo.lock index 9184fa4c93..391b9dd301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1139,6 +1139,7 @@ dependencies = [ "secrecy", "tokio", "tracing", + "tracing-subscriber", ] [[package]] diff --git a/crates/defguard/Cargo.toml b/crates/defguard/Cargo.toml index e6dda2964a..777ec89353 100644 --- a/crates/defguard/Cargo.toml +++ b/crates/defguard/Cargo.toml @@ -26,3 +26,4 @@ dotenvy = "0.15" secrecy = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 244cb0bae0..2a7ba57684 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -47,6 +47,7 @@ use tokio::sync::{ broadcast, mpsc::{channel, unbounded_channel}, }; +use tracing_subscriber::util::SubscriberInitExt; #[macro_use] extern crate tracing; @@ -61,11 +62,13 @@ async fn main() -> Result<(), anyhow::Error> { .set(config.clone()) .expect("Failed to initialize server config."); - // initialize tracing with version formatter - defguard_version::tracing::init( - defguard_version::Version::parse(VERSION)?, + let subscriber = tracing_subscriber::registry(); + defguard_version::tracing::with_version_formatters( + &defguard_version::Version::parse(VERSION)?, &config.log_level, - )?; + subscriber, + ) + .init(); info!("Starting ... version v{VERSION}"); debug!("Using config: {config:?}"); diff --git a/crates/defguard_common/src/db/models/gateway.rs b/crates/defguard_common/src/db/models/gateway.rs index f7f25189d7..a5b78e3a89 100644 --- a/crates/defguard_common/src/db/models/gateway.rs +++ b/crates/defguard_common/src/db/models/gateway.rs @@ -118,6 +118,18 @@ impl Gateway { Ok(()) } + + // TODO: Split the URL into address and port fields just like in proxy + pub async fn find_by_url<'e, E>(executor: E, url: &str) -> Result, sqlx::Error> + where + E: PgExecutor<'e>, + { + let record = query_as!(Self, "SELECT * FROM gateway WHERE url = $1", url) + .fetch_optional(executor) + .await?; + + Ok(record) + } } impl fmt::Display for Gateway { diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index d08be9f3f1..e85f4c9be2 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -8,7 +8,7 @@ use std::{ }; use chrono::{DateTime, TimeDelta, Utc}; -use defguard_certs::{Csr, der_to_pem}; +use defguard_certs::der_to_pem; use defguard_common::{ VERSION, db::{ @@ -22,8 +22,7 @@ use defguard_common::{ }; use defguard_mail::Mail; use defguard_proto::gateway::{ - CoreResponse, DerPayload, InitialSetupInfo, PeerStats, core_request, core_response, - gateway_client, gateway_setup_client, + CoreResponse, PeerStats, core_request, core_response, gateway_client, }; use defguard_version::client::ClientVersionInterceptor; use reqwest::Url; @@ -51,6 +50,7 @@ use crate::{ #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Scheme { + #[allow(dead_code)] Http, Https, } @@ -129,10 +129,6 @@ 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(); @@ -331,94 +327,6 @@ 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?; - - self.gateway.has_certificate = true; - self.gateway.certificate_expiry = - Some(defguard_certs::get_certificate_expiry(cert.der())?); - 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) -> Result<(), GatewayError> { let endpoint = self.endpoint(Scheme::Https)?; diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 60bfc863aa..cf9e3602df 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -249,7 +249,6 @@ 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 communication with Defguard Gateway. @@ -277,16 +276,6 @@ pub async fn run_grpc_gateway_stream( )?; let abort_handle = tasks.spawn(async move { loop { - if gateway_handler.has_certificate() { - 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 { - 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; diff --git a/crates/defguard_core/src/handlers/proxy_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs similarity index 53% rename from crates/defguard_core/src/handlers/proxy_setup.rs rename to crates/defguard_core/src/handlers/component_setup.rs index e858072683..81968f0a28 100644 --- a/crates/defguard_core/src/handlers/proxy_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -8,10 +8,16 @@ use defguard_certs::{der_to_pem, get_certificate_expiry}; use defguard_common::{ VERSION, auth::claims::Claims, - db::models::{Settings, proxy::Proxy}, + db::{ + Id, + models::{Settings, gateway::Gateway, proxy::Proxy}, + }, types::proxy::ProxyControlMessage, }; -use defguard_proto::proxy::{CertificateInfo, DerPayload, proxy_setup_client::ProxySetupClient}; +use defguard_proto::{ + gateway::gateway_setup_client::GatewaySetupClient, + proxy::{CertificateInfo, DerPayload, proxy_setup_client::ProxySetupClient}, +}; use defguard_version::{Version, client::ClientVersionInterceptor}; use futures::Stream; use reqwest::Url; @@ -23,7 +29,11 @@ use tonic::{ transport::{Certificate, ClientTlsConfig, Endpoint}, }; -use crate::{AppState, auth::AdminRole, version::MIN_PROXY_VERSION}; +use crate::{ + AppState, + auth::AdminRole, + version::{MIN_GATEWAY_VERSION, MIN_PROXY_VERSION}, +}; const TOKEN_CLIENT_ID: &str = "Defguard Core"; const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); @@ -44,9 +54,16 @@ pub struct ProxySetupRequest { pub common_name: String, } +#[derive(Debug, Deserialize, Serialize)] +pub struct GatewaySetupRequest { + pub ip_or_domain: String, + pub grpc_port: u16, + pub network_id: Id, +} + #[derive(Debug, Serialize, Copy, Clone)] #[serde(tag = "step", content = "data")] -pub enum ProxySetupStep { +pub enum SetupStep { CheckingConfiguration, CheckingAvailability, CheckingVersion, @@ -57,9 +74,9 @@ pub enum ProxySetupStep { } #[derive(Debug, Serialize)] -pub struct ProxySetupResponse { +pub struct SetupResponse { #[serde(flatten)] - pub step: ProxySetupStep, + pub step: SetupStep, pub proxy_version: Option, pub message: Option, pub logs: Option>, @@ -87,14 +104,14 @@ impl Interceptor for AuthInterceptor { } } -fn fallback_message(err: &str, last_step: ProxySetupStep) -> String { +fn fallback_message(err: &str, last_step: SetupStep) -> 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 { +fn error_message(message: &str, last_step: SetupStep, logs: Option>) -> Event { + let response = SetupResponse { step: last_step, proxy_version: None, message: Some(message.to_string()), @@ -108,8 +125,8 @@ fn error_message(message: &str, last_step: ProxySetupStep, logs: Option Event { - let response = ProxySetupResponse { +fn set_step_message(next_step: SetupStep) -> Event { + let response = SetupResponse { step: next_step, proxy_version: None, message: None, @@ -124,19 +141,19 @@ fn set_step_message(next_step: ProxySetupStep) -> Event { } struct SetupFlow { - last_step: ProxySetupStep, + last_step: SetupStep, log_rx: tokio::sync::mpsc::UnboundedReceiver, } impl SetupFlow { const fn new(log_rx: tokio::sync::mpsc::UnboundedReceiver) -> Self { Self { - last_step: ProxySetupStep::CheckingConfiguration, + last_step: SetupStep::CheckingConfiguration, log_rx, } } - fn step(&mut self, next_step: ProxySetupStep) -> Event { + fn step(&mut self, next_step: SetupStep) -> Event { self.last_step = next_step; set_step_message(next_step) } @@ -170,7 +187,7 @@ pub async fn setup_proxy_tls_stream( // Step 1: Check configuration yield Ok( - flow.step(ProxySetupStep::CheckingConfiguration) + flow.step(SetupStep::CheckingConfiguration) ); match Proxy::find_by_address_port(&appstate.pool, &request.ip_or_domain, i32::from(request.grpc_port)).await { @@ -251,7 +268,7 @@ pub async fn setup_proxy_tls_stream( // Step 2: Check availability yield Ok( - flow.step(ProxySetupStep::CheckingAvailability) + flow.step(SetupStep::CheckingAvailability) ); @@ -327,7 +344,7 @@ pub async fn setup_proxy_tls_stream( // Step 3: Check version yield Ok( - flow.step(ProxySetupStep::CheckingVersion) + flow.step(SetupStep::CheckingVersion) ); let proxy_version = response_with_metadata @@ -351,8 +368,8 @@ pub async fn setup_proxy_tls_stream( debug!("Edge proxy version {} is compatible with core version {}", proxy_version, version_clone); - let response = ProxySetupResponse { - step: ProxySetupStep::CheckingVersion, + let response = SetupResponse { + step: SetupStep::CheckingVersion, proxy_version: Some(proxy_version.to_string()), message: None, logs: None, @@ -411,7 +428,7 @@ pub async fn setup_proxy_tls_stream( let _log_task_guard = TaskGuard(log_reader_task); // Step 4: Obtain CSR - yield Ok(flow.step(ProxySetupStep::ObtainingCsr)); + yield Ok(flow.step(SetupStep::ObtainingCsr)); let Some(hostname) = url.host_str() else { yield Ok(flow.error("URL does not have a valid host")); @@ -442,7 +459,7 @@ pub async fn setup_proxy_tls_stream( debug!("Received certificate signing request from edge proxy for hostname: {}", hostname); // Step 5: Sign certificate - yield Ok(flow.step(ProxySetupStep::SigningCertificate)); + yield Ok(flow.step(SetupStep::SigningCertificate)); let settings = Settings::get_current_settings(); @@ -480,7 +497,7 @@ pub async fn setup_proxy_tls_stream( debug!("Successfully signed certificate for edge proxy"); // Step 6: Configure TLS - yield Ok(flow.step(ProxySetupStep::ConfiguringTls)); + yield Ok(flow.step(SetupStep::ConfiguringTls)); let response = DerPayload { der_data: cert.der().to_vec(), @@ -534,7 +551,379 @@ pub async fn setup_proxy_tls_stream( debug!("Edge proxy setup completed successfully"); // Step 7: Done - yield Ok(flow.step(ProxySetupStep::Done)); + yield Ok(flow.step(SetupStep::Done)); + }; + + Sse::new(stream).keep_alive(KeepAlive::default()) +} + +/// This is the endpoint responsible for the whole gateway 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_gateway_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(SetupStep::CheckingConfiguration) + ); + + + let url_str = format!("http://{}:{}", request.ip_or_domain, request.grpc_port); + + match Gateway::find_by_url(&appstate.pool, &url_str).await { + Ok(Some(gateway)) => { + yield Ok(flow.error(&format!("A Gateway with url {} is already registered with hostname \"{:?}\".", url_str, gateway.hostname))); + return; + } + Ok(None) => { + debug!("Verified no existing Gateway registration for {}:{}", request.ip_or_domain, request.grpc_port); + }, + Err(e) => { + yield Ok(flow.error(&format!("Failed to query existing Gateway: {e}"))); + return; + } + } + + let url = match Url::parse(&url_str) { + Ok(u) => u, + Err(e) => { + yield Ok(flow.error(&format!("Invalid URL: {e}"))); + return; + } + }; + + debug!("Successfully validated Gateway address: {}", url_str); + + let endpoint = match Endpoint::from_shared(url.to_string()) { + 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 Gateway 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(SetupStep::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 Gateway authentication"); + + let version_interceptor = ClientVersionInterceptor::new(version); + let auth_interceptor = AuthInterceptor::new(token); + + let mut client = GatewaySetupClient::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 Gateway 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 Gateway at {}:{}: {}. This may indicate that the Gateway is already configured with TLS. Please check if the Gateway has already been set up.", + request.ip_or_domain, request.grpc_port, e + ))); + } else { + yield Ok(flow.error(&format!( + "Failed to connect to Gateway at {}:{}. Please ensure the address and port are correct and that the Gateway is running.", + request.ip_or_domain, request.grpc_port + ))); + } + } + _ => { + yield Ok(flow.error(&format!("Failed to connect to Gateway: {e}"))); + } + } + return; + } + Err(_) => { + yield Ok(flow.error(&format!( + "Connection to Gateway at {}:{} timed out after 10 seconds.", + request.ip_or_domain, request.grpc_port + ))); + return; + } + }; + + debug!("Successfully connected to Gateway"); + + // Step 3: Check version + yield Ok( + flow.step(SetupStep::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_GATEWAY_VERSION { + yield Ok(flow.error(&format!( + "Gateway version {proxy_version} is older than core version {version_clone}. Please update the edge component.", + ))); + return; + } + + debug!("Gateway version {} is compatible with core version {}", proxy_version, version_clone); + + let response = SetupResponse { + step: SetupStep::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 Gateway 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(SetupStep::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(defguard_proto::gateway::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 Gateway for hostname: {}", hostname); + + // Step 5: Sign certificate + yield Ok(flow.step(SetupStep::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 Gateway"); + + // Step 6: Configure TLS + yield Ok(flow.step(SetupStep::ConfiguringTls)); + + let response = defguard_proto::gateway::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 Gateway"); + + 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 gateway = Gateway::new( + request.network_id, + url_str, + ); + + gateway.has_certificate = true; + gateway.certificate_expiry = Some(expiry); + + if let Err(err) = gateway.save(&appstate.pool).await { + yield Ok(flow.error(&format!("Failed to save Gateway to database: {err}"))); + return; + }; + + debug!("Gateway setup completed successfully"); + + // Step 7: Done + yield Ok(flow.step(SetupStep::Done)); }; Sse::new(stream).keep_alive(KeepAlive::default()) diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index 811ba9a6f6..f9c1d197f7 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -31,6 +31,7 @@ pub(crate) mod activity_log; pub(crate) mod app_info; pub(crate) mod auth; pub mod ca; +pub(crate) mod component_setup; pub(crate) mod forward_auth; pub(crate) mod group; pub mod mail; @@ -38,7 +39,6 @@ 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; diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 7c7eff5661..c40d427d90 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -37,13 +37,13 @@ use events::ApiEvent; use handlers::{ activity_log::get_activity_log_events, auth::disable_user_mfa, + component_setup::setup_proxy_tls_stream, group::{bulk_assign_to_groups, list_groups_info}, network_devices::{ add_network_device, check_ip_availability, download_network_device_config, 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, @@ -115,6 +115,7 @@ use crate::{ webauthn_start, }, ca::create_ca, + component_setup::setup_gateway_tls_stream, forward_auth::forward_auth, group::{ add_group_member, create_group, delete_group, get_group, list_groups, modify_group, @@ -358,7 +359,9 @@ pub fn build_webapp( // Certificate authority .route("/ca", post(create_ca)) // Proxy setup with SSE - .route("/proxy/setup/stream", get(setup_proxy_tls_stream)), + .route("/proxy/setup/stream", get(setup_proxy_tls_stream)) + // Gateway setup with SSE + .route("/gateway/setup/stream", get(setup_gateway_tls_stream)), ); // Enterprise features diff --git a/crates/defguard_version/src/tracing.rs b/crates/defguard_version/src/tracing.rs index bcf33fecdf..69fd29a641 100644 --- a/crates/defguard_version/src/tracing.rs +++ b/crates/defguard_version/src/tracing.rs @@ -91,10 +91,9 @@ use tracing_subscriber::{ }, layer::{Context, SubscriberExt}, registry::LookupSpan, - util::SubscriberInitExt, }; -use crate::{ComponentInfo, DefguardComponent, DefguardVersionError, SystemInfo}; +use crate::{ComponentInfo, DefguardComponent, SystemInfo}; /// Container for version information extracted from tracing span hierarchy. /// @@ -404,7 +403,7 @@ impl tracing::field::Visit for FieldFilterVisitor<'_> { } } -/// Initializes tracing with custom formatter that conditionally displays version information. +/// Adds a custom formatter that conditionally displays version information to a given subscriber. /// /// The formatter will: /// - For ERROR level logs: display own and remote component version and system-info @@ -421,10 +420,22 @@ impl tracing::field::Visit for FieldFilterVisitor<'_> { /// /// # Examples /// ``` -/// defguard_version::tracing::init(defguard_version::Version::new(1, 5, 0), "info"); +/// let subscriber = tracing_subscriber::registry(); +/// defguard_version::tracing::with_version_formatter( +/// &defguard_version::Version::new(1, 5, 0), +/// "info", +/// subscriber, +/// ); /// ``` -pub fn init(own_version: crate::Version, log_level: &str) -> Result<(), DefguardVersionError> { - tracing_subscriber::registry() +pub fn with_version_formatters( + own_version: &crate::Version, + log_level: &str, + subscriber: S, +) -> impl tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a> + Send + Sync +where + S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a> + Send + Sync, +{ + subscriber .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| format!("{log_level},h2=info").into()), @@ -434,12 +445,9 @@ pub fn init(own_version: crate::Version, log_level: &str) -> Result<(), Defguard tracing_subscriber::fmt::layer() .with_ansi(true) .event_format(VersionSuffixFormat::new( - own_version, + crate::Version::new(own_version.major, own_version.minor, own_version.patch), Format::default().with_ansi(true), )) .fmt_fields(VersionFilteredFields), ) - .init(); - - Ok(()) } diff --git a/web/messages/en/gateway_wizard.json b/web/messages/en/gateway_wizard.json new file mode 100644 index 0000000000..7816574338 --- /dev/null +++ b/web/messages/en/gateway_wizard.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "gateway_setup_confirmation_title": "Gateway component configured successfully.", + "gateway_setup_confirmation_subtitle": "Your VPN location is now fully configured.", + "gateway_setup_add_multiple_gateways_title": "Add multiple Gateways", + "gateway_setup_add_multiple_gateways_subtitle": "Each location can include multiple Gateways. You may add another component at this stage or return and do it later.", + "gateway_setup_controls_add_another_gateway": "Add another Gateway", + "gateway_setup_controls_go_to_locations": "Go to locations", + "gateway_setup_page_title": "Configure Gateway component", + "gateway_setup_page_subtitle": "To activate localization, make sure at least one Gateway is connected.", + "gateway_setup_welcome_title": "Deploy your Gateway component", + "gateway_setup_welcome_subtitle": "Welcome to the Gateway component Setup Wizard. This guide will help you deploy the Gateway required for secure and reliable VPN communication.", + "gateway_setup_welcome_deploy_title": "Deploy Gateway component first", + "gateway_setup_welcome_deploy_subtitle": "Make sure your Gateway 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.", + "gateway_setup_welcome_docs_text": "Before installation, we recommend reading our documentation to understand the system architecture and core components.", + "gateway_setup_welcome_image_alt": "Welcome to Gateway component setup wizard", + "gateway_setup_controls_configure": "Configure gateway", + "gateway_setup_step_gateway_component_label": "Configure Gateway component", + "gateway_setup_step_gateway_component_description": "Set up your VPN proxy quickly and ensure secure, optimized traffic flow for your users.", + "gateway_setup_step_gateway_adaptation_label": "Gateway Adoption", + "gateway_setup_step_gateway_adaptation_description": "Review the system's checks and see if any issues need attention before deployment.", + "gateway_setup_step_confirmation_label": "Configuration confirmation", + "gateway_setup_step_confirmation_description": "Your configuration was successful. You're all set.", + "gateway_setup_component_label_common_name": "Common Name", + "gateway_setup_component_label_ip_or_domain": "IP or Domain", + "gateway_setup_component_label_grpc_port": "gRPC Port", + "gateway_setup_component_error_common_name_required": "Common Name is required", + "gateway_setup_component_error_ip_or_domain_required": "IP or Domain is required", + "gateway_setup_component_error_grpc_port_required": "gRPC Port is required", + "gateway_setup_component_error_grpc_port_max": "gRPC Port must be less than 65536", + "gateway_setup_component_controls_back": "Back", + "gateway_setup_component_controls_submit": "Adopt Gateway component", + "gateway_setup_adaptation_checking_configuration": "Checking provided Gateway component configuration", + "gateway_setup_adaptation_checking_availability": "Checking if Gateway is available at: {ip_or_domain}:{grpc_port}", + "gateway_setup_adaptation_checking_version": "Checking Gateway version", + "gateway_setup_adaptation_checking_version_with_value": "Checking Gateway version: {gatewayVersion}", + "gateway_setup_adaptation_obtaining_csr": "Obtaining Certificate Signing Request", + "gateway_setup_adaptation_signing_certificate": "Signing Certificate", + "gateway_setup_adaptation_configuring_tls": "Configuring TLS communication", + "gateway_setup_adaptation_error_default": "An error occurred during setup.", + "gateway_setup_adaptation_error_log_title": "Error log", + "gateway_setup_adaptation_controls_retry": "Retry", + "gateway_setup_adaptation_controls_back": "Back", + "gateway_setup_adaptation_controls_continue": "Continue" +} diff --git a/web/project.inlang/settings.json b/web/project.inlang/settings.json index ca115a53cf..a080a2894b 100644 --- a/web/project.inlang/settings.json +++ b/web/project.inlang/settings.json @@ -19,7 +19,8 @@ "./messages/{locale}/openid.json", "./messages/{locale}/activity.json", "./messages/{locale}/edge_wizard.json", - "./messages/{locale}/settings.json" + "./messages/{locale}/settings.json", + "./messages/{locale}/gateway_wizard.json" ] } } diff --git a/web/src/pages/EdgeSetupPage/steps/useSSEController.tsx b/web/src/hooks/useSSEController.tsx similarity index 82% rename from web/src/pages/EdgeSetupPage/steps/useSSEController.tsx rename to web/src/hooks/useSSEController.tsx index da993a82bc..a46fe8c861 100644 --- a/web/src/pages/EdgeSetupPage/steps/useSSEController.tsx +++ b/web/src/hooks/useSSEController.tsx @@ -1,11 +1,19 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import type { SSEHookOptions } from './types'; + +// 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; +} // 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, + params: Record, options: SSEHookOptions = {}, ) { const eventSourceRef = useRef(null); diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx index a513a390ee..2ddc07a0c2 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeAdaptationStep.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo } from 'react'; +import { useSSEController } from '../../../hooks/useSSEController'; import { m } from '../../../paraglide/messages'; import { Controls } from '../../../shared/components/Controls/Controls'; import { LoadingStep } from '../../../shared/components/LoadingStep/LoadingStep'; @@ -11,7 +12,6 @@ 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); @@ -26,7 +26,7 @@ export const SetupEdgeAdaptationStep = () => { currentStep: event.step, isComplete: event.step === 'Done', isProcessing: event.step !== 'Done' && !event.error, - proxyVersion: event.proxy_version ?? null, + proxyVersion: event.version ?? null, errorMessage: event.error ? event.message || m.edge_setup_adaptation_error_default() : null, @@ -127,6 +127,7 @@ export const SetupEdgeAdaptationStep = () => { [edgeAdaptationState.errorMessage, edgeAdaptationState.currentStep], ); + // biome-ignore lint/correctness/useExhaustiveDependencies: only run on mount 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 ( diff --git a/web/src/pages/EdgeSetupPage/steps/style.scss b/web/src/pages/EdgeSetupPage/steps/style.scss index 10e13b988e..44414ce342 100644 --- a/web/src/pages/EdgeSetupPage/steps/style.scss +++ b/web/src/pages/EdgeSetupPage/steps/style.scss @@ -8,4 +8,9 @@ .controls { padding-top: 0; } + + .code-card { + white-space: pre-wrap; + word-break: break-word; + } } diff --git a/web/src/pages/EdgeSetupPage/steps/types.ts b/web/src/pages/EdgeSetupPage/steps/types.ts index b19078c105..25f7b28fae 100644 --- a/web/src/pages/EdgeSetupPage/steps/types.ts +++ b/web/src/pages/EdgeSetupPage/steps/types.ts @@ -1,6 +1,6 @@ export type SetupEvent = { step: SetupStepId; - proxy_version?: string; + version?: string; message?: string; logs?: string[]; error: boolean; @@ -19,11 +19,3 @@ export type SetupStepId = | '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/useEdgeWizardStore.tsx b/web/src/pages/EdgeSetupPage/useEdgeWizardStore.tsx index adf2dc1992..4301ede973 100644 --- a/web/src/pages/EdgeSetupPage/useEdgeWizardStore.tsx +++ b/web/src/pages/EdgeSetupPage/useEdgeWizardStore.tsx @@ -76,7 +76,7 @@ export const useEdgeWizardStore = create()( })), }), { - name: 'setup-wizard-store', + name: 'edge-wizard-store', storage: createJSONStorage(() => sessionStorage), partialize: (state) => omit(state, [ diff --git a/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx b/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx new file mode 100644 index 0000000000..b15eeaef08 --- /dev/null +++ b/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx @@ -0,0 +1,98 @@ +import './style.scss'; +import { useNavigate } from '@tanstack/react-router'; +import { type ReactNode, useMemo } from 'react'; +import { m } from '../../paraglide/messages'; +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 { ThemeSpacing } from '../../shared/defguard-ui/types'; +import welcomeImage from './assets/welcome_image.svg'; +import { SetupConfirmationStep } from './steps/SetupConfirmationStep'; +import { SetupGatewayAdaptationStep } from './steps/SetupGatewayAdaptationStep'; +import { SetupGatewayComponentStep } from './steps/SetupGatewayComponentStep'; +import { GatewaySetupStep, type GatewaySetupStepValue } from './types'; +import { useGatewayWizardStore } from './useGatewayWizardStore'; + +export const GatewaySetupPage = () => { + const activeStep = useGatewayWizardStore((s) => s.activeStep); + const showWelcome = useGatewayWizardStore((s) => s.showWelcome); + const setShowWelcome = useGatewayWizardStore((s) => s.setShowWelcome); + const navigate = useNavigate(); + + const stepsConfig = useMemo( + (): Record => ({ + gatewayComponent: { + id: GatewaySetupStep.GatewayComponent, + order: 1, + label: m.gateway_setup_step_gateway_component_label(), + description: m.gateway_setup_step_gateway_component_description(), + }, + gatewayAdaptation: { + id: GatewaySetupStep.GatewayAdaptation, + order: 2, + label: m.gateway_setup_step_gateway_adaptation_label(), + description: m.gateway_setup_step_gateway_adaptation_description(), + }, + confirmation: { + id: GatewaySetupStep.Confirmation, + order: 3, + label: m.gateway_setup_step_confirmation_label(), + description: m.gateway_setup_step_confirmation_description(), + }, + }), + [], + ); + + const stepsComponents = useMemo( + (): Record => ({ + gatewayComponent: , + gatewayAdaptation: , + confirmation: , + }), + [], + ); + + const WelcomePageContent = () => ( + <> + +
+ +
+ + ); + + return ( + { + useGatewayWizardStore.getState().reset(); + navigate({ + to: '/settings', + replace: true, + }); + }} + subtitle={m.gateway_setup_page_subtitle()} + title={m.gateway_setup_page_title()} + steps={stepsConfig} + id="setup-wizard" + showWelcome={showWelcome} + welcomePageConfig={{ + title: m.gateway_setup_welcome_title(), + subtitle: m.gateway_setup_welcome_subtitle(), + content: , + docsLink: 'https://docs.defguard.net/edge-component/deployment', + docsText: m.gateway_setup_welcome_docs_text(), + media: {m.gateway_setup_welcome_image_alt()}, + }} + > + {stepsComponents[activeStep]} + + ); +}; diff --git a/web/src/pages/GatewaySetupPage/assets/add_more.svg b/web/src/pages/GatewaySetupPage/assets/add_more.svg new file mode 100644 index 0000000000..36badeb389 --- /dev/null +++ b/web/src/pages/GatewaySetupPage/assets/add_more.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/pages/GatewaySetupPage/assets/deploy.svg b/web/src/pages/GatewaySetupPage/assets/deploy.svg new file mode 100644 index 0000000000..346353af49 --- /dev/null +++ b/web/src/pages/GatewaySetupPage/assets/deploy.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/pages/GatewaySetupPage/assets/file_icon.png b/web/src/pages/GatewaySetupPage/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/GatewaySetupPage/steps/SetupConfirmationStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupConfirmationStep.tsx new file mode 100644 index 0000000000..62b109e6bc --- /dev/null +++ b/web/src/pages/GatewaySetupPage/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 { useGatewayWizardStore } from '../useGatewayWizardStore'; + +export const SetupConfirmationStep = () => { + const navigate = useNavigate(); + + const handleBack = () => { + useGatewayWizardStore.getState().reset(); + }; + + const handleFinish = () => { + useGatewayWizardStore.getState().reset(); + navigate({ to: '/locations' }); + }; + + return ( + +

{m.edge_setup_confirmation_title()}

+ +

{m.edge_setup_confirmation_subtitle()}

+ + + +
+ ); +}; diff --git a/web/src/pages/GatewaySetupPage/steps/SetupGatewayAdaptationStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupGatewayAdaptationStep.tsx new file mode 100644 index 0000000000..5b613b8c9d --- /dev/null +++ b/web/src/pages/GatewaySetupPage/steps/SetupGatewayAdaptationStep.tsx @@ -0,0 +1,197 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { useSSEController } from '../../../hooks/useSSEController'; +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 { GatewaySetupStep } from '../types'; +import { useGatewayWizardStore } from '../useGatewayWizardStore'; +import type { SetupEvent, SetupStep, SetupStepId } from './types'; + +export const SetupGatewayAdaptationStep = () => { + const setActiveStep = useGatewayWizardStore((s) => s.setActiveStep); + const gatewayComponentWizardStore = useGatewayWizardStore((s) => s); + const gatewayAdaptationState = useGatewayWizardStore((s) => s.gatewayAdaptationState); + const setGatewayAdaptationState = useGatewayWizardStore( + (s) => s.setGatewayAdaptationState, + ); + const resetGatewayAdaptationState = useGatewayWizardStore( + (s) => s.resetGatewayAdaptationState, + ); + + // take networkid from params of wizard start + + const handleEvent = useCallback( + (event: SetupEvent) => { + setGatewayAdaptationState({ + currentStep: event.step, + isComplete: event.step === 'Done', + isProcessing: event.step !== 'Done' && !event.error, + gatewayVersion: event.version ?? null, + errorMessage: event.error + ? event.message || m.edge_setup_adaptation_error_default() + : null, + gatewayLogs: event.logs && event.logs.length > 0 ? [...event.logs] : [], + }); + }, + [setGatewayAdaptationState], + ); + + const sse = useSSEController( + '/api/v1/gateway/setup/stream', + { + ip_or_domain: gatewayComponentWizardStore.ip_or_domain, + grpc_port: gatewayComponentWizardStore.grpc_port, + common_name: gatewayComponentWizardStore.common_name, + network_id: gatewayComponentWizardStore.network_id, + }, + { + onMessage: handleEvent, + }, + ); + + const handleBack = () => { + useGatewayWizardStore.getState().resetGatewayAdaptationState(); + setActiveStep(GatewaySetupStep.GatewayComponent); + }; + + const handleNext = () => { + setActiveStep(GatewaySetupStep.Confirmation); + }; + + const steps: SetupStep[] = useMemo( + () => [ + { + id: 'CheckingConfiguration', + title: m.gateway_setup_adaptation_checking_configuration(), + }, + { + id: 'CheckingAvailability', + title: `Checking if Gateway is available at: ${gatewayComponentWizardStore.ip_or_domain}:${gatewayComponentWizardStore.grpc_port}`, + }, + { + id: 'CheckingVersion', + title: gatewayAdaptationState.gatewayVersion + ? `Checking Gateway version: ${gatewayAdaptationState.gatewayVersion}` + : m.gateway_setup_adaptation_checking_version(), + }, + { + id: 'ObtainingCsr', + title: m.gateway_setup_adaptation_obtaining_csr(), + }, + { + id: 'SigningCertificate', + title: m.gateway_setup_adaptation_signing_certificate(), + }, + { + id: 'ConfiguringTls', + title: m.gateway_setup_adaptation_configuring_tls(), + }, + ], + [gatewayComponentWizardStore, gatewayAdaptationState.gatewayVersion], + ); + + const stepDone = useCallback( + (stepId: SetupStepId): boolean => { + const stepIndex = steps.findIndex((step) => step.id === stepId); + const currentStepIndex = gatewayAdaptationState.currentStep + ? steps.findIndex((step) => step.id === gatewayAdaptationState.currentStep) + : -1; + return stepIndex < currentStepIndex || gatewayAdaptationState.isComplete; + }, + [gatewayAdaptationState.isComplete, gatewayAdaptationState.currentStep, steps], + ); + + const stepLoading = useCallback( + (stepId: SetupStepId): boolean => { + return ( + gatewayAdaptationState.isProcessing && + gatewayAdaptationState.currentStep === stepId + ); + }, + [gatewayAdaptationState.isProcessing, gatewayAdaptationState.currentStep], + ); + + const stepError = useCallback( + (stepId: SetupStepId): string | null => { + if ( + gatewayAdaptationState.errorMessage && + gatewayAdaptationState.currentStep === stepId + ) { + return gatewayAdaptationState.errorMessage; + } + return null; + }, + [gatewayAdaptationState.errorMessage, gatewayAdaptationState.currentStep], + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: only run on mount + useEffect(() => { + resetGatewayAdaptationState(); + sse.start(); + + return () => { + sse.stop(); + }; + }, []); + + return ( + +
+ {steps.map((step, index) => ( + + {gatewayAdaptationState.gatewayLogs.length > 0 ? ( + <> + + + + ) : null} + +
+
+
+
+ ))} +
+ +
+ ); +}; diff --git a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx new file mode 100644 index 0000000000..d2d9d3b0ab --- /dev/null +++ b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx @@ -0,0 +1,139 @@ +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 { GatewaySetupStep } from '../types'; +import { useGatewayWizardStore } from '../useGatewayWizardStore'; +import './style.scss'; +import { validateIpOrDomain } from '../../../shared/validators'; + +type FormFields = StoreValues; + +type StoreValues = { + common_name: string; + ip_or_domain: string; + grpc_port: number; +}; + +export const SetupGatewayComponentStep = () => { + const setActiveStep = useGatewayWizardStore((s) => s.setActiveStep); + const navigate = useNavigate(); + const defaultValues = useGatewayWizardStore( + useShallow( + (s): FormFields => ({ + common_name: s.common_name, + ip_or_domain: s.ip_or_domain, + grpc_port: s.grpc_port, + }), + ), + ); + + const handleNext = () => { + form.handleSubmit(); + }; + + const handleBack = () => { + useGatewayWizardStore.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()), + }), + [], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: formSchema, + onChange: formSchema, + }, + onSubmit: ({ value }) => { + useGatewayWizardStore.setState({ + ...value, + }); + setActiveStep(GatewaySetupStep.GatewayAdaptation); + }, + }); + + return ( + +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + +
+ +
+ ); +}; diff --git a/web/src/pages/GatewaySetupPage/steps/style.scss b/web/src/pages/GatewaySetupPage/steps/style.scss new file mode 100644 index 0000000000..44414ce342 --- /dev/null +++ b/web/src/pages/GatewaySetupPage/steps/style.scss @@ -0,0 +1,16 @@ +.wizard-card { + .modal-controls > .buttons { + display: flex; + flex-grow: 1; + justify-content: space-between; + } + + .controls { + padding-top: 0; + } + + .code-card { + white-space: pre-wrap; + word-break: break-word; + } +} diff --git a/web/src/pages/GatewaySetupPage/steps/types.ts b/web/src/pages/GatewaySetupPage/steps/types.ts new file mode 100644 index 0000000000..25f7b28fae --- /dev/null +++ b/web/src/pages/GatewaySetupPage/steps/types.ts @@ -0,0 +1,21 @@ +export type SetupEvent = { + step: SetupStepId; + 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'; diff --git a/web/src/pages/GatewaySetupPage/style.scss b/web/src/pages/GatewaySetupPage/style.scss new file mode 100644 index 0000000000..9ca97f6e8c --- /dev/null +++ b/web/src/pages/GatewaySetupPage/style.scss @@ -0,0 +1,135 @@ +/* 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/GatewaySetupPage/types.ts b/web/src/pages/GatewaySetupPage/types.ts new file mode 100644 index 0000000000..f4ae6bcdf0 --- /dev/null +++ b/web/src/pages/GatewaySetupPage/types.ts @@ -0,0 +1,8 @@ +export const GatewaySetupStep = { + GatewayComponent: 'gatewayComponent', + GatewayAdaptation: 'gatewayAdaptation', + Confirmation: 'confirmation', +} as const; + +export type GatewaySetupStepValue = + (typeof GatewaySetupStep)[keyof typeof GatewaySetupStep]; diff --git a/web/src/pages/GatewaySetupPage/useGatewayWizardStore.tsx b/web/src/pages/GatewaySetupPage/useGatewayWizardStore.tsx new file mode 100644 index 0000000000..bac51baf1f --- /dev/null +++ b/web/src/pages/GatewaySetupPage/useGatewayWizardStore.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 { GatewaySetupStep, type GatewaySetupStepValue } from './types'; + +type GatewayAdaptationState = { + isProcessing: boolean; + isComplete: boolean; + currentStep: SetupStepId | null; + errorMessage: string | null; + gatewayVersion: string | null; + gatewayLogs: string[]; +}; + +type StoreValues = { + activeStep: GatewaySetupStepValue; + showWelcome: boolean; + common_name: string; + ip_or_domain: string; + grpc_port: number; + network_id: number | null; + gatewayAdaptationState: GatewayAdaptationState; +}; + +type StoreMethods = { + reset: () => void; + start: (values?: Partial) => void; + setActiveStep: (step: GatewaySetupStepValue) => void; + setShowWelcome: (show: boolean) => void; + updateValues: (values: Partial) => void; + resetGatewayAdaptationState: () => void; + setGatewayAdaptationState: (state: GatewayAdaptationState) => void; +}; + +const gatewayAdaptationStateDefaults: GatewayAdaptationState = { + isProcessing: false, + isComplete: false, + currentStep: null, + errorMessage: null, + gatewayVersion: null, + gatewayLogs: [], +}; + +const defaults: StoreValues = { + activeStep: GatewaySetupStep.GatewayComponent, + showWelcome: true, + common_name: '', + ip_or_domain: '', + grpc_port: 50066, + network_id: null, + gatewayAdaptationState: gatewayAdaptationStateDefaults, +}; + +export const useGatewayWizardStore = 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), + resetGatewayAdaptationState: () => + set(() => ({ + gatewayAdaptationState: { ...gatewayAdaptationStateDefaults }, + })), + setGatewayAdaptationState: (state: Partial) => + set((s) => ({ + gatewayAdaptationState: { ...s.gatewayAdaptationState, ...state }, + })), + }), + { + name: 'gateway-wizard-store', + storage: createJSONStorage(() => sessionStorage), + partialize: (state) => + omit(state, [ + 'reset', + 'start', + 'setActiveStep', + 'updateValues', + 'setShowWelcome', + 'resetEdgeAdaptationState', + 'setEdgeAdaptationState', + ]), + }, + ), +); diff --git a/web/src/pages/LocationOverviewPage/LocationOverviewPage.tsx b/web/src/pages/LocationOverviewPage/LocationOverviewPage.tsx index ed88e04936..8af444c4bd 100644 --- a/web/src/pages/LocationOverviewPage/LocationOverviewPage.tsx +++ b/web/src/pages/LocationOverviewPage/LocationOverviewPage.tsx @@ -6,7 +6,6 @@ import { useMemo, useState } from 'react'; import api from '../../shared/api/api'; import type { LocationDevicesStats } from '../../shared/api/types'; import { GatewaysStatusBadge } from '../../shared/components/GatewaysStatusBadge/GatewaysStatusBadge'; -import { GatewaySetupModal } from '../../shared/components/modals/GatewaySetupModal/GatewaySetupModal'; import { OverviewPeriodSelect } from '../../shared/components/OverviewPeriodSelect/OverviewPeriodSelect'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; @@ -91,7 +90,6 @@ export const LocationOverviewPage = () => { )} - ); }; diff --git a/web/src/pages/LocationsOverviewPage/LocationsOverviewPage.tsx b/web/src/pages/LocationsOverviewPage/LocationsOverviewPage.tsx index 6334691605..31c58a8d9a 100644 --- a/web/src/pages/LocationsOverviewPage/LocationsOverviewPage.tsx +++ b/web/src/pages/LocationsOverviewPage/LocationsOverviewPage.tsx @@ -2,7 +2,6 @@ import './style.scss'; import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useSearch } from '@tanstack/react-router'; import api from '../../shared/api/api'; -import { GatewaySetupModal } from '../../shared/components/modals/GatewaySetupModal/GatewaySetupModal'; import { OverviewPeriodSelect } from '../../shared/components/OverviewPeriodSelect/OverviewPeriodSelect'; import { Page } from '../../shared/components/Page/Page'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; @@ -82,7 +81,6 @@ export const LocationsOverviewPage = () => { ))} - ); }; diff --git a/web/src/pages/LocationsPage/LocationsPage.tsx b/web/src/pages/LocationsPage/LocationsPage.tsx index 4e8df36a73..983a75ccc8 100644 --- a/web/src/pages/LocationsPage/LocationsPage.tsx +++ b/web/src/pages/LocationsPage/LocationsPage.tsx @@ -4,7 +4,6 @@ import { LocationsTable } from './components/LocationsTable'; import './style.scss'; import { useEffect } from 'react'; import api from '../../shared/api/api'; -import { GatewaySetupModal } from '../../shared/components/modals/GatewaySetupModal/GatewaySetupModal'; import { isPresent } from '../../shared/defguard-ui/utils/isPresent'; import { openModal } from '../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../shared/hooks/modalControls/modalTypes'; @@ -39,7 +38,6 @@ export const LocationsPage = () => { - ); diff --git a/web/src/pages/LocationsPage/components/LocationsTable.tsx b/web/src/pages/LocationsPage/components/LocationsTable.tsx index 5c19b7a8e2..c07708f055 100644 --- a/web/src/pages/LocationsPage/components/LocationsTable.tsx +++ b/web/src/pages/LocationsPage/components/LocationsTable.tsx @@ -28,6 +28,7 @@ import { ThemeSpacing, ThemeVariable } from '../../../shared/defguard-ui/types'; import { openModal } from '../../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../../shared/hooks/modalControls/modalTypes'; import { tableSortingFns } from '../../../shared/utils/dateSortingFn'; +import { useGatewayWizardStore } from '../../GatewaySetupPage/useGatewayWizardStore'; type Props = { locations: NetworkLocation[]; @@ -201,10 +202,9 @@ export const LocationsTable = ({ locations }: Props) => { icon: 'network-settings', text: 'Gateway setup', onClick: async () => { - const { data } = await api.location.getGatewayToken(row.id); - openModal(ModalName.GatewaySetup, { - data: data, - networkId: row.id, + useGatewayWizardStore.getState().start({ network_id: row.id }); + navigate({ + to: '/gateway-wizard', }); }, }, diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 18ed878a26..9999e927cb 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 AuthorizedWizardGatewayWizardRouteImport } from './routes/_authorized/_wizard/gateway-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' @@ -136,6 +137,12 @@ const AuthorizedWizardSetupWizardRoute = path: '/setup-wizard', getParentRoute: () => AuthorizedRoute, } as any) +const AuthorizedWizardGatewayWizardRoute = + AuthorizedWizardGatewayWizardRouteImport.update({ + id: '/_wizard/gateway-wizard', + path: '/gateway-wizard', + getParentRoute: () => AuthorizedRoute, + } as any) const AuthorizedWizardEdgeWizardRoute = AuthorizedWizardEdgeWizardRouteImport.update({ id: '/_wizard/edge-wizard', @@ -304,6 +311,7 @@ export interface FileRoutesByFullPath { '/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/add-location': typeof AuthorizedWizardAddLocationRoute '/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute + '/gateway-wizard': typeof AuthorizedWizardGatewayWizardRoute '/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -345,6 +353,7 @@ export interface FileRoutesByTo { '/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/add-location': typeof AuthorizedWizardAddLocationRoute '/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute + '/gateway-wizard': typeof AuthorizedWizardGatewayWizardRoute '/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -390,6 +399,7 @@ export interface FileRoutesById { '/_authorized/_wizard/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/_authorized/_wizard/add-location': typeof AuthorizedWizardAddLocationRoute '/_authorized/_wizard/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute + '/_authorized/_wizard/gateway-wizard': typeof AuthorizedWizardGatewayWizardRoute '/_authorized/_wizard/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -434,6 +444,7 @@ export interface FileRouteTypes { | '/add-external-openid' | '/add-location' | '/edge-wizard' + | '/gateway-wizard' | '/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -475,6 +486,7 @@ export interface FileRouteTypes { | '/add-external-openid' | '/add-location' | '/edge-wizard' + | '/gateway-wizard' | '/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -519,6 +531,7 @@ export interface FileRouteTypes { | '/_authorized/_wizard/add-external-openid' | '/_authorized/_wizard/add-location' | '/_authorized/_wizard/edge-wizard' + | '/_authorized/_wizard/gateway-wizard' | '/_authorized/_wizard/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -672,6 +685,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthorizedWizardSetupWizardRouteImport parentRoute: typeof AuthorizedRoute } + '/_authorized/_wizard/gateway-wizard': { + id: '/_authorized/_wizard/gateway-wizard' + path: '/gateway-wizard' + fullPath: '/gateway-wizard' + preLoaderRoute: typeof AuthorizedWizardGatewayWizardRouteImport + parentRoute: typeof AuthorizedRoute + } '/_authorized/_wizard/edge-wizard': { id: '/_authorized/_wizard/edge-wizard' path: '/edge-wizard' @@ -914,6 +934,7 @@ interface AuthorizedRouteChildren { AuthorizedWizardAddExternalOpenidRoute: typeof AuthorizedWizardAddExternalOpenidRoute AuthorizedWizardAddLocationRoute: typeof AuthorizedWizardAddLocationRoute AuthorizedWizardEdgeWizardRoute: typeof AuthorizedWizardEdgeWizardRoute + AuthorizedWizardGatewayWizardRoute: typeof AuthorizedWizardGatewayWizardRoute AuthorizedWizardSetupWizardRoute: typeof AuthorizedWizardSetupWizardRoute } @@ -923,6 +944,7 @@ const AuthorizedRouteChildren: AuthorizedRouteChildren = { AuthorizedWizardAddExternalOpenidRoute, AuthorizedWizardAddLocationRoute: AuthorizedWizardAddLocationRoute, AuthorizedWizardEdgeWizardRoute: AuthorizedWizardEdgeWizardRoute, + AuthorizedWizardGatewayWizardRoute: AuthorizedWizardGatewayWizardRoute, AuthorizedWizardSetupWizardRoute: AuthorizedWizardSetupWizardRoute, } diff --git a/web/src/routes/_authorized/_wizard/gateway-wizard.tsx b/web/src/routes/_authorized/_wizard/gateway-wizard.tsx new file mode 100644 index 0000000000..dbe77b7a96 --- /dev/null +++ b/web/src/routes/_authorized/_wizard/gateway-wizard.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { GatewaySetupPage } from '../../../pages/GatewaySetupPage/GatewaySetupPage'; + +export const Route = createFileRoute('/_authorized/_wizard/gateway-wizard')({ + component: GatewaySetupPage, +}); diff --git a/web/src/shared/components/modals/GatewaySetupModal/GatewaySetupModal.tsx b/web/src/shared/components/modals/GatewaySetupModal/GatewaySetupModal.tsx deleted file mode 100644 index 226f55312d..0000000000 --- a/web/src/shared/components/modals/GatewaySetupModal/GatewaySetupModal.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { Modal } from '../../../defguard-ui/components/Modal/Modal'; -import { isPresent } from '../../../defguard-ui/utils/isPresent'; -import { - subscribeCloseModal, - subscribeOpenModal, -} from '../../../hooks/modalControls/modalsSubjects'; -import { ModalName } from '../../../hooks/modalControls/modalTypes'; -import type { OpenGatewaySetupModal } from '../../../hooks/modalControls/types'; -import './style.scss'; -import { useEffect, useMemo, useState } from 'react'; -import { m } from '../../../../paraglide/messages'; -import api from '../../../api/api'; -import { Badge } from '../../../defguard-ui/components/Badge/Badge'; -import { Button } from '../../../defguard-ui/components/Button/Button'; -import { CopyField } from '../../../defguard-ui/components/CopyField/CopyField'; -import { Divider } from '../../../defguard-ui/components/Divider/Divider'; -import { MarkedSection } from '../../../defguard-ui/components/MarkedSection/MarkedSection'; -import { SizedBox } from '../../../defguard-ui/components/SizedBox/SizedBox'; -import { ThemeSpacing } from '../../../defguard-ui/types'; -import { DescriptionBlock } from '../../DescriptionBlock/DescriptionBlock'; - -const modalNameValue = ModalName.GatewaySetup; - -type ModalData = OpenGatewaySetupModal & { - initialGw: string[]; -}; - -export const GatewaySetupModal = () => { - const [isOpen, setOpen] = useState(false); - const [modalData, setModalData] = useState(null); - - useEffect(() => { - const openSub = subscribeOpenModal(modalNameValue, async (data) => { - const { data: gwStatus } = await api.location.getLocationGatewaysStatus( - data.networkId, - ); - - setModalData({ - ...data, - initialGw: gwStatus.map((gw) => gw.uid), - }); - setOpen(true); - }); - const closeSub = subscribeCloseModal(modalNameValue, () => setOpen(false)); - return () => { - openSub.unsubscribe(); - closeSub.unsubscribe(); - }; - }, []); - - return ( - setOpen(false)} - afterClose={() => { - setModalData(null); - }} - > - {isPresent(modalData) && } - - ); -}; - -const ModalContent = ({ data, networkId, initialGw }: ModalData) => { - const { - data: gwStatus, - refetch: refetchGwStatus, - isRefetching, - } = useQuery({ - queryFn: () => api.location.getLocationGatewaysStatus(networkId), - queryKey: ['network', networkId, 'gateways'], - select: (resp) => resp.data, - refetchInterval: 60_000, - }); - - const isNewConnected = useMemo(() => { - if (gwStatus) { - const newGw = gwStatus.find((gw) => !initialGw.includes(gw.uid)); - return isPresent(newGw); - } - return false; - }, [gwStatus, initialGw]); - - return ( - <> - - -

Use the token below to authenticate and configure your gateway node.

-
- - - - - -
- - -

- Once everything is set up and your token is entered, check the connection - status below. If it still fails, review the gateway logs. -

-
- -
- {isNewConnected && ( - - )} - {!isNewConnected && ( - - )} -
-
- - ); -}; diff --git a/web/src/shared/components/modals/GatewaySetupModal/style.scss b/web/src/shared/components/modals/GatewaySetupModal/style.scss deleted file mode 100644 index fa92bdc678..0000000000 --- a/web/src/shared/components/modals/GatewaySetupModal/style.scss +++ /dev/null @@ -1,9 +0,0 @@ -#gateway-setup-modal .modal-content { - .connection-status { - display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: space-between; - column-gap: var(--spacing-2xl); - } -} diff --git a/web/src/shared/hooks/modalControls/modalTypes.ts b/web/src/shared/hooks/modalControls/modalTypes.ts index 5082d0db8e..7e5e0ddbe5 100644 --- a/web/src/shared/hooks/modalControls/modalTypes.ts +++ b/web/src/shared/hooks/modalControls/modalTypes.ts @@ -12,7 +12,6 @@ import type { OpenEditDeviceModal, OpenEditNetworkDeviceModal, OpenEditUserModal, - OpenGatewaySetupModal, OpenLicenseModal, OpenNetworkDeviceConfigModal, OpenNetworkDeviceTokenModal, @@ -136,10 +135,6 @@ const modalOpenArgsSchema = z.discriminatedUnion('name', [ name: z.literal(ModalName.DisplayList), data: z.custom(), }), - z.object({ - name: z.literal(ModalName.GatewaySetup), - data: z.custom(), - }), z.object({ name: z.literal(ModalName.AddLocation), }), diff --git a/web/src/shared/hooks/modalControls/types.ts b/web/src/shared/hooks/modalControls/types.ts index 6613abcab0..5b040fc8de 100644 --- a/web/src/shared/hooks/modalControls/types.ts +++ b/web/src/shared/hooks/modalControls/types.ts @@ -87,11 +87,6 @@ export interface OpenDisplayListModal { data: string[]; } -export interface OpenGatewaySetupModal { - networkId: number; - data: GatewayTokenResponse; -} - export interface OpenLicenseModal { license?: string | null; } From dbb2266df3777fd02d76dd0258f02c91e60199d5 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:48:24 +0100 Subject: [PATCH 15/22] fix domain validator --- web/src/shared/patterns.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts index b9c4d6cb7b..f769cb2090 100644 --- a/web/src/shared/patterns.ts +++ b/web/src/shared/patterns.ts @@ -66,8 +66,7 @@ export const patternValidUrl = new RegExp( export const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; export const ipv4WithPortPattern = /^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$/; export const ipv4WithCIDRPattern = /^(\d{1,3}\.){3}\d{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/; -export const domainPattern = - /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3}))$/; +export const domainPattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; export const domainWithPortPattern = /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9\-.]){1,61}(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3})):[0-9]{1,5}$/; From def828f88648391b8384fc25e3db73be2a65900f Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:58:49 +0100 Subject: [PATCH 16/22] fixes, gateway names, navigation --- .../defguard_common/src/db/models/gateway.rs | 4 +- .../src/handlers/component_setup.rs | 8 +- .../defguard_core/src/handlers/wireguard.rs | 21 ----- crates/defguard_core/src/lib.rs | 19 ++-- ...85637_[2.0.0]_gateway_common_name.down.sql | 1 + ...7085637_[2.0.0]_gateway_common_name.up.sql | 1 + .../steps/AddLocationFirewallStep.tsx | 20 ++-- web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx | 8 +- .../steps/SetupConfirmationStep.tsx | 7 +- .../steps/SetupEdgeComponentStep.tsx | 8 +- .../GatewaySetupPage/GatewaySetupPage.tsx | 8 +- .../steps/SetupConfirmationStep.tsx | 7 +- .../steps/SetupGatewayAdaptationStep.tsx | 13 ++- .../steps/SetupGatewayComponentStep.tsx | 8 +- .../LocationOverviewPage.tsx | 54 +++++------ .../LocationsOverviewPage.tsx | 94 +++++++++---------- web/src/shared/hooks/modalControls/types.ts | 1 - web/src/shared/patterns.ts | 3 +- 18 files changed, 139 insertions(+), 146 deletions(-) create mode 100644 migrations/20260127085637_[2.0.0]_gateway_common_name.down.sql create mode 100644 migrations/20260127085637_[2.0.0]_gateway_common_name.up.sql diff --git a/crates/defguard_common/src/db/models/gateway.rs b/crates/defguard_common/src/db/models/gateway.rs index a5b78e3a89..c05b7d8455 100644 --- a/crates/defguard_common/src/db/models/gateway.rs +++ b/crates/defguard_common/src/db/models/gateway.rs @@ -17,6 +17,7 @@ pub struct Gateway { pub disconnected_at: Option, pub has_certificate: bool, pub certificate_expiry: Option, + pub name: String, } impl Gateway { @@ -33,7 +34,7 @@ impl Gateway { impl Gateway { #[must_use] - pub fn new>(network_id: Id, url: S) -> Self { + pub fn new>(network_id: Id, url: S, name: S) -> Self { Self { id: NoId, network_id, @@ -43,6 +44,7 @@ impl Gateway { disconnected_at: None, has_certificate: false, certificate_expiry: None, + name: name.into(), } } } diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index 81968f0a28..ef5dc0cc9f 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -1,7 +1,7 @@ use std::{convert::Infallible, time::Duration}; use axum::{ - extract::{Query, State}, + extract::{Path, Query, State}, response::sse::{Event, KeepAlive, Sse}, }; use defguard_certs::{der_to_pem, get_certificate_expiry}; @@ -56,9 +56,9 @@ pub struct ProxySetupRequest { #[derive(Debug, Deserialize, Serialize)] pub struct GatewaySetupRequest { + pub common_name: String, pub ip_or_domain: String, pub grpc_port: u16, - pub network_id: Id, } #[derive(Debug, Serialize, Copy, Clone)] @@ -564,6 +564,7 @@ pub async fn setup_gateway_tls_stream( _admin: AdminRole, State(appstate): State, Query(request): Query, + Path(network_id): Path, ) -> Sse>> { let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel::(); @@ -908,8 +909,9 @@ pub async fn setup_gateway_tls_stream( debug!("Certificate expiry date determined: {}", expiry); let mut gateway = Gateway::new( - request.network_id, + network_id, url_str, + request.common_name, ); gateway.has_certificate = true; diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index fc9755c42d..01e7d2171e 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -598,27 +598,6 @@ pub(crate) struct GatewayData { url: String, } -/// Add gateway (POST). -pub(crate) async fn add_gateway( - Path(network_id): Path, - _role: AdminRole, - State(appstate): State, - Json(data): Json, -) -> ApiResult { - debug!("Adding gateway in network {network_id}"); - - let gateway = Gateway::new(network_id, data.url) - .save(&appstate.pool) - .await?; - - info!("Added gateway in network {network_id}"); - - Ok(ApiResponse { - json: json!(GatewayInfo::from(gateway)), - status: StatusCode::CREATED, - }) -} - /// Change gateway (PUT). pub(crate) async fn change_gateway( Path((network_id, gateway_id)): Path<(Id, Id)>, diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index c40d427d90..d19a112d83 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -147,11 +147,10 @@ use crate::{ add_webhook, change_enabled, change_webhook, delete_webhook, get_webhook, list_webhooks, }, wireguard::{ - add_device, add_gateway, add_user_devices, change_gateway, create_network, - create_network_token, delete_device, delete_network, devices_stats, download_config, - gateway_status, get_device, import_network, list_devices, list_networks, - list_user_devices, modify_device, modify_network, network_details, network_stats, - remove_gateway, + add_device, add_user_devices, change_gateway, create_network, create_network_token, + delete_device, delete_network, devices_stats, download_config, gateway_status, + get_device, import_network, list_devices, list_networks, list_user_devices, + modify_device, modify_network, network_details, network_stats, remove_gateway, }, worker::{create_job, create_worker_token, job_status, list_workers, remove_worker}, }, @@ -359,9 +358,7 @@ pub fn build_webapp( // Certificate authority .route("/ca", post(create_ca)) // Proxy setup with SSE - .route("/proxy/setup/stream", get(setup_proxy_tls_stream)) - // Gateway setup with SSE - .route("/gateway/setup/stream", get(setup_gateway_tls_stream)), + .route("/proxy/setup/stream", get(setup_proxy_tls_stream)), ); // Enterprise features @@ -504,8 +501,12 @@ pub fn build_webapp( .delete(delete_network) .get(network_details), ) + // Gateway adding (uses SSE) + .route( + "/network/{network_id}/gateways/setup", + get(setup_gateway_tls_stream), + ) .route("/network/{network_id}/gateways", get(gateway_status)) - .route("/network/{network_id}/gateways", post(add_gateway)) .route( "/network/{network_id}/gateways/{gateway_id}", put(change_gateway).delete(remove_gateway), diff --git a/migrations/20260127085637_[2.0.0]_gateway_common_name.down.sql b/migrations/20260127085637_[2.0.0]_gateway_common_name.down.sql new file mode 100644 index 0000000000..3cde267aba --- /dev/null +++ b/migrations/20260127085637_[2.0.0]_gateway_common_name.down.sql @@ -0,0 +1 @@ +ALTER TABLE gateway DROP COLUMN name; diff --git a/migrations/20260127085637_[2.0.0]_gateway_common_name.up.sql b/migrations/20260127085637_[2.0.0]_gateway_common_name.up.sql new file mode 100644 index 0000000000..9d1887dd3a --- /dev/null +++ b/migrations/20260127085637_[2.0.0]_gateway_common_name.up.sql @@ -0,0 +1 @@ +ALTER TABLE gateway ADD COLUMN name TEXT NOT NULL; diff --git a/web/src/pages/AddLocationPage/steps/AddLocationFirewallStep.tsx b/web/src/pages/AddLocationPage/steps/AddLocationFirewallStep.tsx index bb0d4b03b7..b7fd4b3244 100644 --- a/web/src/pages/AddLocationPage/steps/AddLocationFirewallStep.tsx +++ b/web/src/pages/AddLocationPage/steps/AddLocationFirewallStep.tsx @@ -13,7 +13,7 @@ import { ModalControls } from '../../../shared/defguard-ui/components/ModalContr import { Radio } from '../../../shared/defguard-ui/components/Radio/Radio'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; -import { useLocationsPageStore } from '../../LocationsPage/hooks/useLocationsPage'; +import { useGatewayWizardStore } from '../../GatewaySetupPage/useGatewayWizardStore'; import actionCardImage from '../assets/gateway-setup-action-card.png'; import { AddLocationPageStep } from '../types'; import { useAddLocationStore } from '../useAddLocationStore'; @@ -32,15 +32,19 @@ export const AddLocationFirewallStep = () => { }, onSuccess: ({ data }) => { if (showGateway) { - useLocationsPageStore.setState({ - networkGatewayStartup: data.id, + useGatewayWizardStore.getState().start({ network_id: data.id }); + navigate({ to: '/gateway-wizard', replace: true }).then(() => { + setTimeout(() => { + useAddLocationStore.getState().reset(); + }, 100); + }); + } else { + navigate({ to: '/locations', replace: true }).then(() => { + setTimeout(() => { + useAddLocationStore.getState().reset(); + }, 100); }); } - navigate({ to: '/locations', replace: true }).then(() => { - setTimeout(() => { - useAddLocationStore.getState().reset(); - }, 100); - }); }, }); diff --git a/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx b/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx index 7e355393b3..d8a8f7c48e 100644 --- a/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx +++ b/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx @@ -81,10 +81,10 @@ export const EdgeSetupPage = () => { { - useEdgeWizardStore.getState().reset(); - navigate({ - to: '/settings', - replace: true, + navigate({ to: '/vpn-overview', replace: true }).then(() => { + setTimeout(() => { + useEdgeWizardStore.getState().reset(); + }, 100); }); }} subtitle={m.edge_setup_page_subtitle()} diff --git a/web/src/pages/EdgeSetupPage/steps/SetupConfirmationStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupConfirmationStep.tsx index 0e976d4db4..dedc5ebe9b 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupConfirmationStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupConfirmationStep.tsx @@ -17,8 +17,11 @@ export const SetupConfirmationStep = () => { }; const handleFinish = () => { - useEdgeWizardStore.getState().reset(); - navigate({ to: '/vpn-overview' }); + navigate({ to: '/vpn-overview', replace: true }).then(() => { + setTimeout(() => { + useEdgeWizardStore.getState().reset(); + }, 100); + }); }; return ( diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx index c40c716871..a24762a5f9 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx @@ -42,10 +42,10 @@ export const SetupEdgeComponentStep = () => { }; const handleBack = () => { - useEdgeWizardStore.getState().reset(); - navigate({ - to: '/edge-wizard', - replace: true, + navigate({ to: '/edge-wizard', replace: true }).then(() => { + setTimeout(() => { + useEdgeWizardStore.getState().reset(); + }, 100); }); }; diff --git a/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx b/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx index b15eeaef08..769f21c1f2 100644 --- a/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx +++ b/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx @@ -72,10 +72,10 @@ export const GatewaySetupPage = () => { { - useGatewayWizardStore.getState().reset(); - navigate({ - to: '/settings', - replace: true, + navigate({ to: '/locations', replace: true }).then(() => { + setTimeout(() => { + useGatewayWizardStore.getState().reset(); + }, 100); }); }} subtitle={m.gateway_setup_page_subtitle()} diff --git a/web/src/pages/GatewaySetupPage/steps/SetupConfirmationStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupConfirmationStep.tsx index 62b109e6bc..30e4983335 100644 --- a/web/src/pages/GatewaySetupPage/steps/SetupConfirmationStep.tsx +++ b/web/src/pages/GatewaySetupPage/steps/SetupConfirmationStep.tsx @@ -17,8 +17,11 @@ export const SetupConfirmationStep = () => { }; const handleFinish = () => { - useGatewayWizardStore.getState().reset(); - navigate({ to: '/locations' }); + navigate({ to: '/locations', replace: true }).then(() => { + setTimeout(() => { + useGatewayWizardStore.getState().reset(); + }, 100); + }); }; return ( diff --git a/web/src/pages/GatewaySetupPage/steps/SetupGatewayAdaptationStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupGatewayAdaptationStep.tsx index 5b613b8c9d..222311b176 100644 --- a/web/src/pages/GatewaySetupPage/steps/SetupGatewayAdaptationStep.tsx +++ b/web/src/pages/GatewaySetupPage/steps/SetupGatewayAdaptationStep.tsx @@ -24,8 +24,6 @@ export const SetupGatewayAdaptationStep = () => { (s) => s.resetGatewayAdaptationState, ); - // take networkid from params of wizard start - const handleEvent = useCallback( (event: SetupEvent) => { setGatewayAdaptationState({ @@ -43,7 +41,7 @@ export const SetupGatewayAdaptationStep = () => { ); const sse = useSSEController( - '/api/v1/gateway/setup/stream', + `/api/v1/network/${gatewayComponentWizardStore.network_id}/gateways/setup`, { ip_or_domain: gatewayComponentWizardStore.ip_or_domain, grpc_port: gatewayComponentWizardStore.grpc_port, @@ -72,12 +70,17 @@ export const SetupGatewayAdaptationStep = () => { }, { id: 'CheckingAvailability', - title: `Checking if Gateway is available at: ${gatewayComponentWizardStore.ip_or_domain}:${gatewayComponentWizardStore.grpc_port}`, + title: m.gateway_setup_adaptation_checking_availability({ + ip_or_domain: gatewayComponentWizardStore.ip_or_domain, + grpc_port: String(gatewayComponentWizardStore.grpc_port), + }), }, { id: 'CheckingVersion', title: gatewayAdaptationState.gatewayVersion - ? `Checking Gateway version: ${gatewayAdaptationState.gatewayVersion}` + ? m.gateway_setup_adaptation_checking_version_with_value({ + gatewayVersion: gatewayAdaptationState.gatewayVersion, + }) : m.gateway_setup_adaptation_checking_version(), }, { diff --git a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx index d2d9d3b0ab..3674d57f01 100644 --- a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx +++ b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx @@ -40,10 +40,10 @@ export const SetupGatewayComponentStep = () => { }; const handleBack = () => { - useGatewayWizardStore.getState().reset(); - navigate({ - to: '/edge-wizard', - replace: true, + navigate({ to: '/locations', replace: true }).then(() => { + setTimeout(() => { + useGatewayWizardStore.getState().reset(); + }, 100); }); }; diff --git a/web/src/pages/LocationOverviewPage/LocationOverviewPage.tsx b/web/src/pages/LocationOverviewPage/LocationOverviewPage.tsx index 8af444c4bd..6e1dd4bfee 100644 --- a/web/src/pages/LocationOverviewPage/LocationOverviewPage.tsx +++ b/web/src/pages/LocationOverviewPage/LocationOverviewPage.tsx @@ -59,38 +59,34 @@ export const LocationOverviewPage = () => { }); return ( - <> - - -
-
-
-

{location.name}

- {isPresent(gateways) && } -
-
- { - navigate({ - search: { - period: value, - }, - }); - }} - /> -
+ + +
+
+
+

{location.name}

+ {isPresent(gateways) && } +
+
+ { + navigate({ + search: { + period: value, + }, + }); + }} + />
- {isPresent(locationStats) && ( - - )}
- - {isPresent(locationDevicesStats) && ( - + {isPresent(locationStats) && ( + )} - - +
+ + {isPresent(locationDevicesStats) && } +
); }; diff --git a/web/src/pages/LocationsOverviewPage/LocationsOverviewPage.tsx b/web/src/pages/LocationsOverviewPage/LocationsOverviewPage.tsx index 31c58a8d9a..6c4060834d 100644 --- a/web/src/pages/LocationsOverviewPage/LocationsOverviewPage.tsx +++ b/web/src/pages/LocationsOverviewPage/LocationsOverviewPage.tsx @@ -33,54 +33,52 @@ export const LocationsOverviewPage = () => { }); return ( - <> - - -
-

Dashboard

-
- { - navigate({ - from: '/vpn-overview', - search: { - period: value, - }, - }); - }} - period={period} - /> -
+ + +
+

Dashboard

+
+ { + navigate({ + from: '/vpn-overview', + search: { + period: value, + }, + }); + }} + period={period} + />
- -
    - {isPresent(allStats) && ( -
  • - -
    -

    All locations summary

    -
    -
    -
  • - )} - {locations.map((location) => ( -
  • - -
  • - ))} -
- - +
+ +
    + {isPresent(allStats) && ( +
  • + +
    +

    All locations summary

    +
    +
    +
  • + )} + {locations.map((location) => ( +
  • + +
  • + ))} +
+
); }; diff --git a/web/src/shared/hooks/modalControls/types.ts b/web/src/shared/hooks/modalControls/types.ts index 5b040fc8de..e01e845d85 100644 --- a/web/src/shared/hooks/modalControls/types.ts +++ b/web/src/shared/hooks/modalControls/types.ts @@ -1,7 +1,6 @@ import type { AvailableLocationIpResponse, Device, - GatewayTokenResponse, GroupInfo, NetworkDevice, NetworkLocation, diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts index f769cb2090..b955d40f44 100644 --- a/web/src/shared/patterns.ts +++ b/web/src/shared/patterns.ts @@ -66,7 +66,8 @@ export const patternValidUrl = new RegExp( export const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; export const ipv4WithPortPattern = /^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$/; export const ipv4WithCIDRPattern = /^(\d{1,3}\.){3}\d{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/; -export const domainPattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; +export const domainPattern = + /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; export const domainWithPortPattern = /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9\-.]){1,61}(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3})):[0-9]{1,5}$/; From 62fb4ed2222b7d340ce1e37bb10bbe61a196fe41 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:06:42 +0100 Subject: [PATCH 17/22] routetree --- web/src/routeTree.gen.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 18ed878a26..9999e927cb 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 AuthorizedWizardGatewayWizardRouteImport } from './routes/_authorized/_wizard/gateway-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' @@ -136,6 +137,12 @@ const AuthorizedWizardSetupWizardRoute = path: '/setup-wizard', getParentRoute: () => AuthorizedRoute, } as any) +const AuthorizedWizardGatewayWizardRoute = + AuthorizedWizardGatewayWizardRouteImport.update({ + id: '/_wizard/gateway-wizard', + path: '/gateway-wizard', + getParentRoute: () => AuthorizedRoute, + } as any) const AuthorizedWizardEdgeWizardRoute = AuthorizedWizardEdgeWizardRouteImport.update({ id: '/_wizard/edge-wizard', @@ -304,6 +311,7 @@ export interface FileRoutesByFullPath { '/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/add-location': typeof AuthorizedWizardAddLocationRoute '/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute + '/gateway-wizard': typeof AuthorizedWizardGatewayWizardRoute '/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -345,6 +353,7 @@ export interface FileRoutesByTo { '/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/add-location': typeof AuthorizedWizardAddLocationRoute '/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute + '/gateway-wizard': typeof AuthorizedWizardGatewayWizardRoute '/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -390,6 +399,7 @@ export interface FileRoutesById { '/_authorized/_wizard/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute '/_authorized/_wizard/add-location': typeof AuthorizedWizardAddLocationRoute '/_authorized/_wizard/edge-wizard': typeof AuthorizedWizardEdgeWizardRoute + '/_authorized/_wizard/gateway-wizard': typeof AuthorizedWizardGatewayWizardRoute '/_authorized/_wizard/setup-wizard': typeof AuthorizedWizardSetupWizardRoute '/auth/mfa/email': typeof AuthMfaEmailRoute '/auth/mfa/recovery': typeof AuthMfaRecoveryRoute @@ -434,6 +444,7 @@ export interface FileRouteTypes { | '/add-external-openid' | '/add-location' | '/edge-wizard' + | '/gateway-wizard' | '/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -475,6 +486,7 @@ export interface FileRouteTypes { | '/add-external-openid' | '/add-location' | '/edge-wizard' + | '/gateway-wizard' | '/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -519,6 +531,7 @@ export interface FileRouteTypes { | '/_authorized/_wizard/add-external-openid' | '/_authorized/_wizard/add-location' | '/_authorized/_wizard/edge-wizard' + | '/_authorized/_wizard/gateway-wizard' | '/_authorized/_wizard/setup-wizard' | '/auth/mfa/email' | '/auth/mfa/recovery' @@ -672,6 +685,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthorizedWizardSetupWizardRouteImport parentRoute: typeof AuthorizedRoute } + '/_authorized/_wizard/gateway-wizard': { + id: '/_authorized/_wizard/gateway-wizard' + path: '/gateway-wizard' + fullPath: '/gateway-wizard' + preLoaderRoute: typeof AuthorizedWizardGatewayWizardRouteImport + parentRoute: typeof AuthorizedRoute + } '/_authorized/_wizard/edge-wizard': { id: '/_authorized/_wizard/edge-wizard' path: '/edge-wizard' @@ -914,6 +934,7 @@ interface AuthorizedRouteChildren { AuthorizedWizardAddExternalOpenidRoute: typeof AuthorizedWizardAddExternalOpenidRoute AuthorizedWizardAddLocationRoute: typeof AuthorizedWizardAddLocationRoute AuthorizedWizardEdgeWizardRoute: typeof AuthorizedWizardEdgeWizardRoute + AuthorizedWizardGatewayWizardRoute: typeof AuthorizedWizardGatewayWizardRoute AuthorizedWizardSetupWizardRoute: typeof AuthorizedWizardSetupWizardRoute } @@ -923,6 +944,7 @@ const AuthorizedRouteChildren: AuthorizedRouteChildren = { AuthorizedWizardAddExternalOpenidRoute, AuthorizedWizardAddLocationRoute: AuthorizedWizardAddLocationRoute, AuthorizedWizardEdgeWizardRoute: AuthorizedWizardEdgeWizardRoute, + AuthorizedWizardGatewayWizardRoute: AuthorizedWizardGatewayWizardRoute, AuthorizedWizardSetupWizardRoute: AuthorizedWizardSetupWizardRoute, } From 41fe958ea801173f26ddb32d5ed99ff9704581fe Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:12:18 +0100 Subject: [PATCH 18/22] fix frontend --- .../EdgeSetupPage/steps/useSSEController.tsx | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 web/src/pages/EdgeSetupPage/steps/useSSEController.tsx diff --git a/web/src/pages/EdgeSetupPage/steps/useSSEController.tsx b/web/src/pages/EdgeSetupPage/steps/useSSEController.tsx deleted file mode 100644 index da993a82bc..0000000000 --- a/web/src/pages/EdgeSetupPage/steps/useSSEController.tsx +++ /dev/null @@ -1,62 +0,0 @@ -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 }; -} From 34939708608bd80c2c780f9195c6007115907550 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:15:17 +0100 Subject: [PATCH 19/22] after merge fix --- .../defguard_core/src/grpc/gateway/handler.rs | 44 +- crates/defguard_core/src/handlers/mod.rs | 1 - .../defguard_core/src/handlers/proxy_setup.rs | 541 ------------------ crates/defguard_core/src/lib.rs | 1 - crates/defguard_proxy_manager/src/lib.rs | 1 + 5 files changed, 2 insertions(+), 586 deletions(-) delete mode 100644 crates/defguard_core/src/handlers/proxy_setup.rs diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 462bec909c..55699488a8 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -3,7 +3,7 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, }; -use chrono::{DateTime, TimeDelta, Utc}; +use chrono::{DateTime, Utc}; use defguard_certs::der_to_pem; use defguard_common::{ VERSION, @@ -274,48 +274,6 @@ impl GatewayHandler { Ok(device) } - /// Helper method to fetch `WireguardNetwork` info from DB and return appropriate errors - async fn fetch_location_from_db( - &self, - location_id: Id, - ) -> 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(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, 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(GatewayError::NotFound(format!( - "User assigned to device with public key {public_key} not found" - ))); - } - }; - - Ok(user) - } - - fn emit_event(&self, event: GrpcEvent) { - if self.grpc_event_tx.send(event).is_err() { - warn!("Failed to send gRPC event"); - } - } - /// Connect to Gateway and handle its messages through gRPC. pub(crate) async fn handle_connection(&mut self) -> Result<(), GatewayError> { let endpoint = self.endpoint(Scheme::Https)?; diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index a8a53c66ea..f9c1d197f7 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -39,7 +39,6 @@ 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; diff --git a/crates/defguard_core/src/handlers/proxy_setup.rs b/crates/defguard_core/src/handlers/proxy_setup.rs deleted file mode 100644 index e858072683..0000000000 --- a/crates/defguard_core/src/handlers/proxy_setup.rs +++ /dev/null @@ -1,541 +0,0 @@ -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(); - } -} - -#[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"); - - // 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 da2e470e5b..d19a112d83 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -44,7 +44,6 @@ 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, diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 1ad4a95d41..ddb680e1dd 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -56,6 +56,7 @@ use tokio::{ Mutex, broadcast::Sender, mpsc::{self, Receiver, UnboundedSender}, + oneshot, }, task::JoinSet, time::sleep, From 36d2f1b377a7b249fce8c2f6b6c99d6437a72ba9 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:18:04 +0100 Subject: [PATCH 20/22] proto update --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 0b982922c4..fdbe98caa9 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 0b982922c4dab3304a8cb01aed1d8cee806600b7 +Subproject commit fdbe98caa9413b626833da210b5b588b287bb146 From 2e97e1f5ac1cdc779ddda04902b15ea0e727076a Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:27:49 +0100 Subject: [PATCH 21/22] fix linters --- ...129b087ca31d1d20d55137b9e66cf5474d91.json} | 7 +- ...93cdb37980c4e24ce6ac77abebd626e8ef83.json} | 12 +++- ...d1a91d77d98584e1cdc95f2a3c9f71439515.json} | 12 +++- ...8e49e0e43dfa0169cb8d1f4c009c108cb124.json} | 7 +- ...f997807b66e0b532da747b146513c34e15c5c.json | 8 ++- ...36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json | 70 +++++++++++++++++++ web/src/pages/EdgeSetupPage/steps/style.scss | 2 +- .../pages/GatewaySetupPage/steps/style.scss | 2 +- 8 files changed, 105 insertions(+), 15 deletions(-) rename .sqlx/{query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json => query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json} (66%) rename .sqlx/{query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json => query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json} (83%) rename .sqlx/{query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json => query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json} (82%) rename .sqlx/{query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json => query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json} (72%) create mode 100644 .sqlx/query-e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json diff --git a/.sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json b/.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json similarity index 66% rename from .sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json rename to .sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json index b52e770941..bf9ee301ea 100644 --- a/.sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json +++ b/.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.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,\"has_certificate\" = $7,\"certificate_expiry\" = $8 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,\"name\" = $9 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -12,10 +12,11 @@ "Timestamp", "Timestamp", "Bool", - "Timestamp" + "Timestamp", + "Text" ] }, "nullable": [] }, - "hash": "5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef" + "hash": "6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91" } diff --git a/.sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json b/.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json similarity index 83% rename from .sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json rename to .sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json index c321d2e9e7..c34a9feac0 100644 --- a/.sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json +++ b/.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\" FROM \"gateway\"", + "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\" FROM \"gateway\"", "describe": { "columns": [ { @@ -42,6 +42,11 @@ "ordinal": 7, "name": "certificate_expiry", "type_info": "Timestamp" + }, + { + "ordinal": 8, + "name": "name", + "type_info": "Text" } ], "parameters": { @@ -55,8 +60,9 @@ true, true, false, - true + true, + false ] }, - "hash": "a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e" + "hash": "74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83" } diff --git a/.sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json b/.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json similarity index 82% rename from .sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json rename to .sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json index ed24b8d62a..23412a03a9 100644 --- a/.sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json +++ b/.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\" FROM \"gateway\" WHERE id = $1", + "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\" FROM \"gateway\" WHERE id = $1", "describe": { "columns": [ { @@ -42,6 +42,11 @@ "ordinal": 7, "name": "certificate_expiry", "type_info": "Timestamp" + }, + { + "ordinal": 8, + "name": "name", + "type_info": "Text" } ], "parameters": { @@ -57,8 +62,9 @@ true, true, false, - true + true, + false ] }, - "hash": "9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262" + "hash": "992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515" } diff --git a/.sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json b/.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json similarity index 72% rename from .sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json rename to .sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json index b45febc008..d93d1303e4 100644 --- a/.sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json +++ b/.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "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", + "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", "describe": { "columns": [ { @@ -17,12 +17,13 @@ "Timestamp", "Timestamp", "Bool", - "Timestamp" + "Timestamp", + "Text" ] }, "nullable": [ false ] }, - "hash": "95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5" + "hash": "b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124" } diff --git a/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json b/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json index 6b597e4488..3759b875c3 100644 --- a/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json +++ b/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json @@ -42,6 +42,11 @@ "ordinal": 7, "name": "certificate_expiry", "type_info": "Timestamp" + }, + { + "ordinal": 8, + "name": "name", + "type_info": "Text" } ], "parameters": { @@ -57,7 +62,8 @@ true, true, false, - true + true, + false ] }, "hash": "d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c" diff --git a/.sqlx/query-e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json b/.sqlx/query-e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json new file mode 100644 index 0000000000..6e3f98f4ea --- /dev/null +++ b/.sqlx/query-e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM gateway WHERE url = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "network_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "url", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "hostname", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "certificate_expiry", + "type_info": "Timestamp" + }, + { + "ordinal": 8, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + false, + true, + false + ] + }, + "hash": "e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a" +} diff --git a/web/src/pages/EdgeSetupPage/steps/style.scss b/web/src/pages/EdgeSetupPage/steps/style.scss index 44414ce342..f8331ae599 100644 --- a/web/src/pages/EdgeSetupPage/steps/style.scss +++ b/web/src/pages/EdgeSetupPage/steps/style.scss @@ -11,6 +11,6 @@ .code-card { white-space: pre-wrap; - word-break: break-word; + overflow-wrap: anywhere; } } diff --git a/web/src/pages/GatewaySetupPage/steps/style.scss b/web/src/pages/GatewaySetupPage/steps/style.scss index 44414ce342..f8331ae599 100644 --- a/web/src/pages/GatewaySetupPage/steps/style.scss +++ b/web/src/pages/GatewaySetupPage/steps/style.scss @@ -11,6 +11,6 @@ .code-card { white-space: pre-wrap; - word-break: break-word; + overflow-wrap: anywhere; } } From 6d4f6b0fcebee8623eaa8f269ada913f4c43ec47 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:59:06 +0100 Subject: [PATCH 22/22] fix sqlx fixtures --- ...e4aee1d1b2098133ff953c1188346c7cb252e.json | 30 ------- ...0b4a7afcbee1b63e34932c143f4697a0bc2cc.json | 30 +++++++ ...b129b087ca31d1d20d55137b9e66cf5474d91.json | 30 ------- ...293cdb37980c4e24ce6ac77abebd626e8ef83.json | 83 ------------------ ...fd1a91d77d98584e1cdc95f2a3c9f71439515.json | 85 ------------------- ...7b7f32e22adefc9b9185a9b3cd80c169a6e2.json} | 25 ++---- ...88e49e0e43dfa0169cb8d1f4c009c108cb124.json | 37 -------- ...c38d3e2deb43d5ca3458724deb3ead69389a.json} | 25 ++---- ...612388c1e322dbc7ce93d92251b3a82caabef.json | 37 -------- ...f997807b66e0b532da747b146513c34e15c5c.json | 22 ++++- ...36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json | 8 +- ...953a7d9ef0becedf668c1d2225a1673003c77.json | 23 +++++ 12 files changed, 96 insertions(+), 339 deletions(-) delete mode 100644 .sqlx/query-16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e.json create mode 100644 .sqlx/query-5af0fbf61295a5a23149c6248ea0b4a7afcbee1b63e34932c143f4697a0bc2cc.json delete mode 100644 .sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json delete mode 100644 .sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json delete mode 100644 .sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json rename .sqlx/{query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json => query-ae3e3cef524f2a911808bf72e7c57b7f32e22adefc9b9185a9b3cd80c169a6e2.json} (54%) delete mode 100644 .sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json rename .sqlx/{query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json => query-b43694450d7abe3b93ea88fa7c95c38d3e2deb43d5ca3458724deb3ead69389a.json} (53%) delete mode 100644 .sqlx/query-cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef.json create mode 100644 .sqlx/query-ed3266f5f0d7b1613ad8745c9be953a7d9ef0becedf668c1d2225a1673003c77.json diff --git a/.sqlx/query-16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e.json b/.sqlx/query-16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e.json deleted file mode 100644 index bf3ce2dadf..0000000000 --- a/.sqlx/query-16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "db_name": "PostgreSQL", -<<<<<<<< HEAD:.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json - "query": "UPDATE \"gateway\" SET \"network_id\" = $2,\"url\" = $3,\"hostname\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6,\"has_certificate\" = $7,\"certificate_expiry\" = $8,\"name\" = $9 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,\"version\" = $9 WHERE id = $1", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e.json - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "Text", - "Timestamp", - "Timestamp", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [] - }, -<<<<<<<< HEAD:.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json - "hash": "6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91" -======== - "hash": "16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e" ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e.json -} diff --git a/.sqlx/query-5af0fbf61295a5a23149c6248ea0b4a7afcbee1b63e34932c143f4697a0bc2cc.json b/.sqlx/query-5af0fbf61295a5a23149c6248ea0b4a7afcbee1b63e34932c143f4697a0bc2cc.json new file mode 100644 index 0000000000..0ee0514bda --- /dev/null +++ b/.sqlx/query-5af0fbf61295a5a23149c6248ea0b4a7afcbee1b63e34932c143f4697a0bc2cc.json @@ -0,0 +1,30 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\",\"name\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Timestamp", + "Timestamp", + "Bool", + "Timestamp", + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "5af0fbf61295a5a23149c6248ea0b4a7afcbee1b63e34932c143f4697a0bc2cc" +} diff --git a/.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json b/.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json deleted file mode 100644 index bf3ce2dadf..0000000000 --- a/.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "db_name": "PostgreSQL", -<<<<<<<< HEAD:.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json - "query": "UPDATE \"gateway\" SET \"network_id\" = $2,\"url\" = $3,\"hostname\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6,\"has_certificate\" = $7,\"certificate_expiry\" = $8,\"name\" = $9 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,\"version\" = $9 WHERE id = $1", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e.json - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "Text", - "Timestamp", - "Timestamp", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [] - }, -<<<<<<<< HEAD:.sqlx/query-6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91.json - "hash": "6f38f3e01696f21fb78d9dba9aeb129b087ca31d1d20d55137b9e66cf5474d91" -======== - "hash": "16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e" ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-16802280f46083a640c0f084479e4aee1d1b2098133ff953c1188346c7cb252e.json -} diff --git a/.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json b/.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json deleted file mode 100644 index 1d214394a0..0000000000 --- a/.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "db_name": "PostgreSQL", -<<<<<<<< HEAD:.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\" FROM \"gateway\"", -======== - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\" FROM \"gateway\"", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "network_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "url", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "hostname", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "connected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "disconnected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 6, - "name": "has_certificate", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "certificate_expiry", - "type_info": "Timestamp" - }, - { - "ordinal": 8, -<<<<<<<< HEAD:.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json - "name": "name", -======== - "name": "version", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, -<<<<<<<< HEAD:.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json - false - ] - }, - "hash": "74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83" -======== - true - ] - }, - "hash": "9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db" ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json -} diff --git a/.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json b/.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json deleted file mode 100644 index c29541fd04..0000000000 --- a/.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "db_name": "PostgreSQL", -<<<<<<<< HEAD:.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\" FROM \"gateway\" WHERE id = $1", -======== - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\" FROM \"gateway\" WHERE id = $1", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "network_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "url", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "hostname", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "connected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "disconnected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 6, - "name": "has_certificate", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "certificate_expiry", - "type_info": "Timestamp" - }, - { - "ordinal": 8, -<<<<<<<< HEAD:.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json - "name": "name", -======== - "name": "version", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, -<<<<<<<< HEAD:.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json - false - ] - }, - "hash": "992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515" -======== - true - ] - }, - "hash": "671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e" ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json -} diff --git a/.sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json b/.sqlx/query-ae3e3cef524f2a911808bf72e7c57b7f32e22adefc9b9185a9b3cd80c169a6e2.json similarity index 54% rename from .sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json rename to .sqlx/query-ae3e3cef524f2a911808bf72e7c57b7f32e22adefc9b9185a9b3cd80c169a6e2.json index 1d214394a0..77722daf50 100644 --- a/.sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json +++ b/.sqlx/query-ae3e3cef524f2a911808bf72e7c57b7f32e22adefc9b9185a9b3cd80c169a6e2.json @@ -1,10 +1,6 @@ { "db_name": "PostgreSQL", -<<<<<<<< HEAD:.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\" FROM \"gateway\"", -======== - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\" FROM \"gateway\"", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json + "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\",\"name\" FROM \"gateway\"", "describe": { "columns": [ { @@ -49,11 +45,12 @@ }, { "ordinal": 8, -<<<<<<<< HEAD:.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json - "name": "name", -======== "name": "version", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "name", "type_info": "Text" } ], @@ -69,15 +66,9 @@ true, false, true, -<<<<<<<< HEAD:.sqlx/query-74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83.json + true, false ] }, - "hash": "74328fde1221a6fbdd9e924428a293cdb37980c4e24ce6ac77abebd626e8ef83" -======== - true - ] - }, - "hash": "9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db" ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-9655c2e6a8f749fe1ea966dcdc0ea2080a4c27137278b2fb761d3b279b69f9db.json + "hash": "ae3e3cef524f2a911808bf72e7c57b7f32e22adefc9b9185a9b3cd80c169a6e2" } diff --git a/.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json b/.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json deleted file mode 100644 index 767226eefb..0000000000 --- a/.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", -<<<<<<<< HEAD:.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json - "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", -======== - "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef.json - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Text", - "Text", - "Timestamp", - "Timestamp", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [ - false - ] - }, -<<<<<<<< HEAD:.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json - "hash": "b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124" -======== - "hash": "cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef" ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef.json -} diff --git a/.sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json b/.sqlx/query-b43694450d7abe3b93ea88fa7c95c38d3e2deb43d5ca3458724deb3ead69389a.json similarity index 53% rename from .sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json rename to .sqlx/query-b43694450d7abe3b93ea88fa7c95c38d3e2deb43d5ca3458724deb3ead69389a.json index c29541fd04..10051d513f 100644 --- a/.sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json +++ b/.sqlx/query-b43694450d7abe3b93ea88fa7c95c38d3e2deb43d5ca3458724deb3ead69389a.json @@ -1,10 +1,6 @@ { "db_name": "PostgreSQL", -<<<<<<<< HEAD:.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\" FROM \"gateway\" WHERE id = $1", -======== - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\" FROM \"gateway\" WHERE id = $1", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json + "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\",\"name\" FROM \"gateway\" WHERE id = $1", "describe": { "columns": [ { @@ -49,11 +45,12 @@ }, { "ordinal": 8, -<<<<<<<< HEAD:.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json - "name": "name", -======== "name": "version", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "name", "type_info": "Text" } ], @@ -71,15 +68,9 @@ true, false, true, -<<<<<<<< HEAD:.sqlx/query-992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515.json + true, false ] }, - "hash": "992d8cd450771193260a05f5004fd1a91d77d98584e1cdc95f2a3c9f71439515" -======== - true - ] - }, - "hash": "671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e" ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-671c3ef062fa5c901d26adb86c75b7ccad57d9cc9caa41502cbe85c6eafb087e.json + "hash": "b43694450d7abe3b93ea88fa7c95c38d3e2deb43d5ca3458724deb3ead69389a" } diff --git a/.sqlx/query-cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef.json b/.sqlx/query-cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef.json deleted file mode 100644 index 767226eefb..0000000000 --- a/.sqlx/query-cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", -<<<<<<<< HEAD:.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json - "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"name\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", -======== - "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\",\"version\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef.json - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Text", - "Text", - "Timestamp", - "Timestamp", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [ - false - ] - }, -<<<<<<<< HEAD:.sqlx/query-b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124.json - "hash": "b1148a35914e10954de757cb3b988e49e0e43dfa0169cb8d1f4c009c108cb124" -======== - "hash": "cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef" ->>>>>>>> d5d856dc3ec48ae984d1c1a403df9cdaf71face2:.sqlx/query-cd6c70e9b46c5cdce053d6f0772612388c1e322dbc7ce93d92251b3a82caabef.json -} diff --git a/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json b/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json index a7afe26653..b843b3c06c 100644 --- a/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json +++ b/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json @@ -45,14 +45,32 @@ }, { "ordinal": 8, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 9, "name": "version", "type_info": "Text" } ], "parameters": { - "Left": ["Int8"] + "Left": [ + "Int8" + ] }, - "nullable": [false, false, false, true, true, true, false, true, true] + "nullable": [ + false, + false, + false, + true, + true, + true, + false, + true, + false, + true + ] }, "hash": "d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c" } diff --git a/.sqlx/query-e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json b/.sqlx/query-e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json index 6e3f98f4ea..d3ffd878ef 100644 --- a/.sqlx/query-e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json +++ b/.sqlx/query-e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a.json @@ -47,6 +47,11 @@ "ordinal": 8, "name": "name", "type_info": "Text" + }, + { + "ordinal": 9, + "name": "version", + "type_info": "Text" } ], "parameters": { @@ -63,7 +68,8 @@ true, false, true, - false + false, + true ] }, "hash": "e9ca71b61f7a3736ca335d90aca36ab5a93dc8a00ad622267f13b3cd4cdb4a5a" diff --git a/.sqlx/query-ed3266f5f0d7b1613ad8745c9be953a7d9ef0becedf668c1d2225a1673003c77.json b/.sqlx/query-ed3266f5f0d7b1613ad8745c9be953a7d9ef0becedf668c1d2225a1673003c77.json new file mode 100644 index 0000000000..48849d4d38 --- /dev/null +++ b/.sqlx/query-ed3266f5f0d7b1613ad8745c9be953a7d9ef0becedf668c1d2225a1673003c77.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"gateway\" SET \"network_id\" = $2,\"url\" = $3,\"hostname\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6,\"has_certificate\" = $7,\"certificate_expiry\" = $8,\"version\" = $9,\"name\" = $10 WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text", + "Text", + "Timestamp", + "Timestamp", + "Bool", + "Timestamp", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "ed3266f5f0d7b1613ad8745c9be953a7d9ef0becedf668c1d2225a1673003c77" +}