From 19285d3993a0f91e3a2065840b338de8a31e0efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 11 Mar 2026 08:30:09 +0100 Subject: [PATCH 1/3] expand ACL backfill logic to migrate legacy rules --- .../20251218140442_[2.0.0]_initial.down.sql | 10 +- .../20251218140442_[2.0.0]_initial.up.sql | 99 +++++++++++++++++-- 2 files changed, 96 insertions(+), 13 deletions(-) diff --git a/migrations/20251218140442_[2.0.0]_initial.down.sql b/migrations/20251218140442_[2.0.0]_initial.down.sql index 3f366a7432..4c4f7f4b72 100644 --- a/migrations/20251218140442_[2.0.0]_initial.down.sql +++ b/migrations/20251218140442_[2.0.0]_initial.down.sql @@ -18,6 +18,11 @@ DROP FUNCTION row_change(); DROP TABLE gateway; -- Restore ACL naming and flags from before 2.0.0. +ALTER TABLE aclrule RENAME COLUMN addresses TO destination; +ALTER TABLE aclrule RENAME COLUMN all_locations TO all_networks; + +ALTER TABLE aclalias RENAME COLUMN addresses TO destination; + ALTER TABLE aclrule DROP COLUMN any_address, DROP COLUMN any_port, @@ -26,16 +31,11 @@ ALTER TABLE aclrule DROP COLUMN allow_all_groups, DROP COLUMN deny_all_groups; -ALTER TABLE aclrule RENAME COLUMN addresses TO destination; -ALTER TABLE aclrule RENAME COLUMN all_locations TO all_networks; - ALTER TABLE aclalias DROP COLUMN any_address, DROP COLUMN any_port, DROP COLUMN any_protocol; -ALTER TABLE aclalias RENAME COLUMN addresses TO destination; - -- Remove 2.0.0 OpenID provider extensions. ALTER TABLE openidprovider DROP COLUMN kind; DROP TYPE openid_provider_kind; diff --git a/migrations/20251218140442_[2.0.0]_initial.up.sql b/migrations/20251218140442_[2.0.0]_initial.up.sql index 353d8a8603..16227efb8a 100644 --- a/migrations/20251218140442_[2.0.0]_initial.up.sql +++ b/migrations/20251218140442_[2.0.0]_initial.up.sql @@ -48,11 +48,31 @@ ALTER TABLE aclalias ADD COLUMN any_port boolean NOT NULL DEFAULT false, ADD COLUMN any_protocol boolean NOT NULL DEFAULT false; -UPDATE aclalias +-- Backfill explicit any_* flags so migrated 1.6 aliases keep treating empty +-- destination, port, and protocol inputs as "match any". +WITH alias_destination_input_state AS ( + SELECT + alias.id, + COALESCE(cardinality(alias.destination), 0) = 0 AS has_no_destination_addresses, + NOT EXISTS ( + SELECT 1 + FROM aclaliasdestinationrange AS alias_range + WHERE alias_range.alias_id = alias.id + ) AS has_no_destination_ranges, + COALESCE(cardinality(alias.ports), 0) = 0 AS has_no_ports, + COALESCE(cardinality(alias.protocols), 0) = 0 AS has_no_protocols + FROM aclalias AS alias +) +UPDATE aclalias AS alias SET - any_address = array_length(destination, 1) IS NULL, - any_port = array_length(ports, 1) IS NULL, - any_protocol = array_length(protocols, 1) IS NULL; + any_address = ( + state.has_no_destination_addresses + AND state.has_no_destination_ranges + ), + any_port = state.has_no_ports, + any_protocol = state.has_no_protocols +FROM alias_destination_input_state AS state +WHERE state.id = alias.id; ALTER TABLE aclalias RENAME COLUMN destination TO addresses; @@ -64,12 +84,75 @@ ALTER TABLE aclrule ADD COLUMN allow_all_groups boolean NOT NULL DEFAULT false, ADD COLUMN deny_all_groups boolean NOT NULL DEFAULT false; -UPDATE aclrule +-- Preserve migrated 1.6 rule behavior by separating destination aliases from +-- component aliases: destination aliases define alias-driven destinations, +-- while component aliases only count when they provide concrete inputs. +WITH rule_alias_destination_input_state AS ( + SELECT + rule_alias.rule_id, + BOOL_OR(alias.kind = 'destination') AS has_destination_aliases, + BOOL_OR(alias.kind = 'component' AND NOT alias.any_address) AS has_component_alias_addresses, + BOOL_OR(alias.kind = 'component' AND NOT alias.any_port) AS has_component_alias_ports, + BOOL_OR(alias.kind = 'component' AND NOT alias.any_protocol) AS has_component_alias_protocols + FROM aclrulealias AS rule_alias + JOIN aclalias AS alias ON alias.id = rule_alias.alias_id + GROUP BY rule_alias.rule_id +), +-- Rule-local destination inputs must still be checked after alias detection so +-- legacy rules keep manual settings whenever they stored addresses, ranges, +-- ports, or protocols directly on the rule. +rule_destination_input_state AS ( + SELECT + rule.id, + COALESCE(cardinality(rule.destination), 0) = 0 AS has_no_destination_addresses, + NOT EXISTS ( + SELECT 1 + FROM aclruledestinationrange AS rule_range + WHERE rule_range.rule_id = rule.id + ) AS has_no_destination_ranges, + COALESCE(cardinality(rule.ports), 0) = 0 AS has_no_ports, + COALESCE(cardinality(rule.protocols), 0) = 0 AS has_no_protocols, + COALESCE(alias_state.has_destination_aliases, false) AS has_destination_aliases, + NOT COALESCE(alias_state.has_component_alias_addresses, false) AS has_no_component_alias_addresses, + NOT COALESCE(alias_state.has_component_alias_ports, false) AS has_no_component_alias_ports, + NOT COALESCE(alias_state.has_component_alias_protocols, false) AS has_no_component_alias_protocols + FROM aclrule AS rule + LEFT JOIN rule_alias_destination_input_state AS alias_state ON alias_state.rule_id = rule.id +) +UPDATE aclrule AS rule SET - any_address = array_length(destination, 1) IS NULL, - any_port = array_length(ports, 1) IS NULL, - any_protocol = array_length(protocols, 1) IS NULL; + any_address = ( + state.has_no_destination_addresses + AND state.has_no_destination_ranges + AND state.has_no_component_alias_addresses + ), + any_port = ( + state.has_no_ports + AND state.has_no_component_alias_ports + ), + any_protocol = ( + state.has_no_protocols + AND state.has_no_component_alias_protocols + ), + -- Only switch migrated 1.6 rules away from manual destination settings + -- when destination aliases were the sole source of destination inputs. + use_manual_destination_settings = NOT ( + state.has_no_destination_addresses + AND state.has_no_destination_ranges + AND state.has_no_ports + AND state.has_no_protocols + AND state.has_no_component_alias_addresses + AND state.has_no_component_alias_ports + AND state.has_no_component_alias_protocols + AND state.has_destination_aliases + ), + allow_all_groups = false, + deny_all_groups = false +FROM rule_destination_input_state AS state +WHERE state.id = rule.id; +-- Rename after backfills because these queries must read the legacy 1.6 column +-- names while deriving the new flags and destination-mode settings. ALTER TABLE aclrule RENAME COLUMN destination TO addresses; ALTER TABLE aclrule RENAME COLUMN all_networks TO all_locations; From fc76d812ce60570dead210181b98f1a2cedbe51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 11 Mar 2026 12:57:33 +0100 Subject: [PATCH 2/3] add helper command for debugging gateway config --- crates/defguard/src/main.rs | 63 ++++++++++++------- crates/defguard_common/src/config.rs | 8 +++ crates/defguard_core/src/lib.rs | 48 +++++++++++++- .../defguard_gateway_manager/src/handler.rs | 19 +----- crates/defguard_proto/src/lib.rs | 26 +++++++- 5 files changed, 121 insertions(+), 43 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 0971412a5e..62fafa9c27 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -23,6 +23,7 @@ use defguard_core::{ limits::update_counts, }, events::{ApiEvent, BidiStreamEvent}, + gateway_config, grpc::{GatewayEvent, WorkerState, run_grpc_server}, init_dev_env, init_vpn_location, run_web_server, utility_thread::run_utility_thread, @@ -75,6 +76,29 @@ async fn main() -> Result<(), anyhow::Error> { ) .await; + if config.openid_signing_key.is_some() { + info!("Using RSA OpenID signing key"); + } else { + info!("Using HMAC OpenID signing key"); + } + + // initialize global settings struct + initialize_current_settings(&pool).await?; + + debug!("Checking enterprise license status"); + match License::load_or_renew(&pool).await { + Ok(license) => { + set_cached_license(license); + } + Err(err) => { + warn!( + "There was an error while loading the license, error: {err}. The enterprise \ + features will be disabled." + ); + set_cached_license(None); + } + } + // handle optional subcommands if let Some(command) = &config.cmd { match command { @@ -85,21 +109,16 @@ async fn main() -> Result<(), anyhow::Error> { let token = init_vpn_location(&pool, args).await?; println!("{token}"); } + Command::GatewayConfig(args) => { + let config = gateway_config(&pool, args).await?; + println!("{config:#?}"); + } } // return early return Ok(()); } - if config.openid_signing_key.is_some() { - info!("Using RSA OpenID signing key"); - } else { - info!("Using HMAC OpenID signing key"); - } - - // initialize global settings struct - initialize_current_settings(&pool).await?; - let has_auto_adopt_flags = config.adopt_edge.is_some() || config.adopt_gateway.is_some(); let wizard = Wizard::init(&pool, has_auto_adopt_flags).await?; let mut ini_server_config = true; @@ -199,19 +218,19 @@ async fn main() -> Result<(), anyhow::Error> { update_counts(&pool).await?; - debug!("Checking enterprise license status"); - match License::load_or_renew(&pool).await { - Ok(license) => { - set_cached_license(license); - } - Err(err) => { - warn!( - "There was an error while loading the license, error: {err}. The enterprise \ - features will be disabled." - ); - set_cached_license(None); - } - } + // debug!("Checking enterprise license status"); + // match License::load_or_renew(&pool).await { + // Ok(license) => { + // set_cached_license(license); + // } + // Err(err) => { + // warn!( + // "There was an error while loading the license, error: {err}. The enterprise \ + // features will be disabled." + // ); + // set_cached_license(None); + // } + // } let (proxy_control_tx, proxy_control_rx) = channel::(100); let proxy_secret_key = settings.secret_key_required()?.to_string(); diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index fdd1d779c2..cdfb214b43 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -197,6 +197,8 @@ pub enum Command { about = "Add a new VPN location and return a gateway token. Used for automated setup." )] InitVpnLocation(InitVpnLocationArgs), + #[command(about = "Output the gateway gRPC configuration payload for a VPN location by ID.")] + GatewayConfig(GatewayConfigArgs), } #[derive(Args, Debug, Clone)] @@ -221,6 +223,12 @@ pub struct InitVpnLocationArgs { pub id: Option, } +#[derive(Args, Debug, Clone)] +pub struct GatewayConfigArgs { + #[arg(long)] + pub location_id: i64, +} + impl DefGuardConfig { #[must_use] pub fn new() -> Self { diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 753c01040f..ab8d605c83 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -16,7 +16,7 @@ use defguard_certs::CertificateAuthority; use defguard_common::{ VERSION, auth::claims::{Claims, ClaimsType}, - config::{DefGuardConfig, InitVpnLocationArgs, server_config}, + config::{DefGuardConfig, GatewayConfigArgs, InitVpnLocationArgs, server_config}, db::{ init_db, models::{ @@ -31,6 +31,7 @@ use defguard_common::{ }, types::proxy::ProxyControlMessage, }; +use defguard_proto::gateway::Configuration; use defguard_version::server::DefguardVersionLayer; use defguard_web_ui::{index, svg, web_asset}; use events::ApiEvent; @@ -79,6 +80,7 @@ use crate::{ auth::failed_login::FailedLoginMap, db::AppEvent, enterprise::{ + firewall::try_get_location_firewall_config, handlers::{ acl::{ alias::{ @@ -167,7 +169,9 @@ use crate::{ }, worker::{create_job, create_worker_token, job_status, list_workers, remove_worker}, }, - location_management::sync_location_allowed_devices, + location_management::{ + allowed_peers::get_location_allowed_peers, sync_location_allowed_devices, + }, version::IncompatibleComponents, }; @@ -931,6 +935,46 @@ pub async fn init_vpn_location( Ok(token) } +pub async fn gateway_config( + pool: &PgPool, + args: &GatewayConfigArgs, +) -> Result { + let location_id = args.location_id; + + let mut conn = pool.acquire().await?; + + // fetch specified location + let location = match WireguardNetwork::find_by_id(&mut *conn, location_id).await { + Ok(Some(network)) => network, + Ok(None) => return Err(anyhow!("Location {location_id} not found")), + Err(err) => { + return Err(anyhow!( + "Failed to rerieve location {location_id} with error: {err}" + )); + } + }; + + // get peers + let peers = get_location_allowed_peers(&location, &mut *conn) + .await + .map_err(|err| anyhow!("Failed to get peers for location {location} with error: {err}"))?; + + // prepare firewall config + let maybe_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .map_err(|err| { + anyhow!("Failed to prepare firewall config for location {location} with error: {err}") + })?; + + // generate config + let mut config = Configuration::new(&location, peers, maybe_firewall_config); + + // overwrite private key just in case + config.prvkey = "REDACTED".into(); + + Ok(config) +} + pub fn is_valid_phone_number(number: &str) -> bool { PHONE_NUMBER_REGEX.is_match(number) } diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index ce705fdb5c..3983ab1c0b 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -143,7 +143,7 @@ impl GatewayHandler { let peers = get_location_allowed_peers(&network, &self.pool).await?; let maybe_firewall_config = try_get_location_firewall_config(&network, &mut conn).await?; - let payload = Some(core_response::Payload::Config(gen_config( + let payload = Some(core_response::Payload::Config(Configuration::new( &network, peers, maybe_firewall_config, @@ -790,20 +790,3 @@ fn try_protos_into_stats_message( latest_handshake, )) } - -fn gen_config( - network: &WireguardNetwork, - peers: Vec, - maybe_firewall_config: Option, -) -> Configuration { - Configuration { - name: network.name.clone(), - port: network.port.cast_unsigned(), - prvkey: network.prvkey.clone(), - addresses: network.address.iter().map(ToString::to_string).collect(), - peers, - firewall_config: maybe_firewall_config, - mtu: network.mtu.cast_unsigned(), - fwmark: network.fwmark as u32, - } -} diff --git a/crates/defguard_proto/src/lib.rs b/crates/defguard_proto/src/lib.rs index 84e78505c1..5b21cc2a6e 100644 --- a/crates/defguard_proto/src/lib.rs +++ b/crates/defguard_proto/src/lib.rs @@ -23,7 +23,7 @@ use defguard_common::{ db::{ Id, models::{ - Device, DeviceConfig, User, + Device, DeviceConfig, User, WireguardNetwork, vpn_client_session::VpnClientMfaMethod, wireguard::{LocationMfaMode, ServiceLocationMode}, }, @@ -33,6 +33,11 @@ use proxy::{CoreError, MfaMethod}; use serde::Serialize; use tonic::Status; +use crate::{ + enterprise::firewall::FirewallConfig, + gateway::{Configuration, Peer}, +}; + // Client MFA methods impl fmt::Display for MfaMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -159,3 +164,22 @@ impl From for proxy::ServiceLocationMode { } } } + +impl Configuration { + pub fn new( + location: &WireguardNetwork, + peers: Vec, + maybe_firewall_config: Option, + ) -> Self { + Self { + name: location.name.clone(), + port: location.port.cast_unsigned(), + prvkey: location.prvkey.clone(), + addresses: location.address.iter().map(ToString::to_string).collect(), + peers, + firewall_config: maybe_firewall_config, + mtu: location.mtu.cast_unsigned(), + fwmark: location.fwmark as u32, + } + } +} From 0dd3bf4b3ae84497dc4ebdea7d28a3a64e601ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 11 Mar 2026 14:07:55 +0100 Subject: [PATCH 3/3] remove commented out code --- crates/defguard/src/main.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 7068c051cc..deeb7989f4 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -226,20 +226,6 @@ async fn main() -> Result<(), anyhow::Error> { update_counts(&pool).await?; - // debug!("Checking enterprise license status"); - // match License::load_or_renew(&pool).await { - // Ok(license) => { - // set_cached_license(license); - // } - // Err(err) => { - // warn!( - // "There was an error while loading the license, error: {err}. The enterprise \ - // features will be disabled." - // ); - // set_cached_license(None); - // } - // } - let (proxy_control_tx, proxy_control_rx) = channel::(100); let proxy_secret_key = settings.secret_key_required()?; let proxy_manager = ProxyManager::new(