From 886d512ea5e18ff6d321baf8688319f7905a55d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Feb 2026 16:24:26 +0100 Subject: [PATCH 01/25] extract source IP calculation logic --- .../src/enterprise/firewall/mod.rs | 110 +++++++++++------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index f298d957dd..f3c703e65a 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -41,6 +41,9 @@ pub enum FirewallError { /// - ALLOW which determines which devices can access a destination /// - DENY which stops all other traffic to a given destination /// +/// Additionally a separate set of rules is created for each pre-defined `Destination` used +/// as part of the rule. +/// /// In the resulting list all ALLOW rules are placed first and then DENY rules are added to the /// end. This way we can avoid conflicts when some ACLs are overlapping. pub async fn generate_firewall_rules_from_acls( @@ -61,48 +64,9 @@ pub async fn generate_firewall_rules_from_acls( // convert each ACL into a corresponding `FirewallRule`s for acl in acl_rules { debug!("Processing ACL rule: {acl:?}"); - // fetch allowed users - let allowed_users = acl.get_all_allowed_users(&mut *conn).await?; - - // fetch denied users - let denied_users = acl.get_all_denied_users(&mut *conn).await?; - - // get relevant users for determining source IPs - let users = get_source_users(allowed_users, &denied_users); - // prepare a list of user IDs - let user_ids: Vec = users.iter().map(|user| user.id).collect(); - - // get network IPs for devices belonging to those users - let user_device_ips = get_user_device_ips(&user_ids, location_id, &mut *conn).await?; - // separate IPv4 and IPv6 user-device addresses - let user_device_ips = user_device_ips - .iter() - .flatten() - .partition(|ip| ip.is_ipv4()); - - // fetch allowed network devices - let allowed_network_devices = acl.get_all_allowed_devices(&mut *conn, location_id).await?; - - // fetch denied network devices - let denied_network_devices = acl.get_all_denied_devices(&mut *conn, location_id).await?; - - // get network device IPs for rule source - let network_devices = - get_source_network_devices(allowed_network_devices, &denied_network_devices); - let network_device_ips = - get_network_device_ips(&network_devices, location_id, &mut *conn).await?; - - // separate IPv4 and IPv6 network-device addresses - let network_device_ips = network_device_ips - .iter() - .flatten() - .partition(|ip| ip.is_ipv4()); - - // convert device IPs into source addresses for a firewall rule - let ipv4_source_addrs = - get_source_addrs(user_device_ips.0, network_device_ips.0, IpVersion::Ipv4); - let ipv6_source_addrs = - get_source_addrs(user_device_ips.1, network_device_ips.1, IpVersion::Ipv6); + // prepare source IPs + let (ipv4_source_addrs, ipv6_source_addrs) = + get_source_ips(&mut *conn, location_id, &acl).await?; // extract destination parameters from ACL rule let AclRuleInfo { @@ -262,6 +226,68 @@ pub async fn generate_firewall_rules_from_acls( Ok(allow_rules.into_iter().chain(deny_rules).collect()) } +/// Prepare two lists of source IPs split between IPv4 and IPv6. +/// +/// This is achieved on first determining allowed users and network devices +/// and then getting assigned IP addresses of their devices. +async fn get_source_ips( + conn: &mut PgConnection, + location_id: Id, + acl: &AclRuleInfo, +) -> Result<(Vec, Vec), FirewallError> { + // fetch allowed users + let allowed_users = acl.get_all_allowed_users(&mut *conn).await?; + + // fetch denied users + let denied_users = acl.get_all_denied_users(&mut *conn).await?; + + // get relevant users for determining source IPs + let source_users = get_source_users(allowed_users, &denied_users); + + // prepare a list of user IDs + let source_user_ids: Vec = source_users.iter().map(|user| user.id).collect(); + + // get network IPs for devices belonging to those users + let source_user_device_ips = + get_user_device_ips(&source_user_ids, location_id, &mut *conn).await?; + // separate IPv4 and IPv6 user-device addresses + let source_user_device_ips = source_user_device_ips + .iter() + .flatten() + .partition(|ip| ip.is_ipv4()); + + // fetch allowed network devices + let allowed_network_devices = acl.get_all_allowed_devices(&mut *conn, location_id).await?; + + // fetch denied network devices + let denied_network_devices = acl.get_all_denied_devices(&mut *conn, location_id).await?; + + // get network device IPs for rule source + let source_network_devices = + get_source_network_devices(allowed_network_devices, &denied_network_devices); + let source_network_device_ips = + get_network_device_ips(&source_network_devices, location_id, &mut *conn).await?; + + // separate IPv4 and IPv6 network-device addresses + let source_network_device_ips = source_network_device_ips + .iter() + .flatten() + .partition(|ip| ip.is_ipv4()); + + // convert device IPs into source addresses for a firewall rule + let ipv4_source_addrs = get_source_addrs( + source_user_device_ips.0, + source_network_device_ips.0, + IpVersion::Ipv4, + ); + let ipv6_source_addrs = get_source_addrs( + source_user_device_ips.1, + source_network_device_ips.1, + IpVersion::Ipv6, + ); + Ok((ipv4_source_addrs, ipv6_source_addrs)) +} + /// Creates ALLOW and DENY rules for given set of source, destination /// addresses, ports and protocols. The DENY rule should block all /// remaining traffic to the destination from sources other than specified. From 8e7ec88deaea78515460f07dedb411a1fb5be097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Feb 2026 17:18:29 +0100 Subject: [PATCH 02/25] WIP: extract logic for generating rules for manual destination --- .../src/enterprise/db/models/acl.rs | 4 +- .../src/enterprise/firewall/mod.rs | 202 +++++++++++------- .../src/enterprise/firewall/tests.rs | 4 + 3 files changed, 128 insertions(+), 82 deletions(-) diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index d9b635c5aa..a39c4c0982 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -187,7 +187,7 @@ pub struct AclRuleInfo { pub any_destination: bool, pub any_port: bool, pub any_protocol: bool, - pub manual_settings: bool, + pub manual_destination_settings: bool, } impl AclRuleInfo { @@ -1134,7 +1134,7 @@ impl AclRule { any_destination: self.any_destination, any_port: self.any_port, any_protocol: self.any_protocol, - manual_settings: false, + manual_destination_settings: self.manual_settings, }) } } diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index f3c703e65a..e65d94fcf9 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -23,7 +23,10 @@ use super::{ utils::merge_ranges, }; use crate::enterprise::{ - db::models::{acl::AliasKind, snat::UserSnatBinding}, + db::models::{ + acl::{AclAlias, AliasKind}, + snat::UserSnatBinding, + }, is_business_license_active, }; @@ -71,99 +74,55 @@ pub async fn generate_firewall_rules_from_acls( // extract destination parameters from ACL rule let AclRuleInfo { id, - mut destination, + name, + destination, destination_ranges, - mut ports, - mut protocols, + ports, + protocols, aliases, + any_destination, + any_port, + any_protocol, + manual_destination_settings, .. } = acl; // split aliases into types - let (destination_aliases, component_aliases): (Vec<_>, Vec<_>) = aliases + let (destinations, aliases): (Vec<_>, Vec<_>) = aliases .into_iter() .partition(|alias| alias.kind == AliasKind::Destination); - // store alias ranges separately since they use a different struct - let mut alias_destination_ranges = Vec::new(); - - // process component aliases by appending destination parameters from each of them to - // existing lists - for alias in component_aliases { - // fetch destination ranges for a given alias - alias_destination_ranges.extend(alias.get_destination_ranges(&mut *conn).await?); - - // extend existing parameter lists - destination.extend(alias.destination); - ports.extend(alias.ports.into_iter().map(Into::into).collect::>()); - protocols.extend(alias.protocols); - } - - // prepare destination addresses - let (dest_addrs_v4, dest_addrs_v6) = - process_destination_addrs(&destination, &destination_ranges); - - // prepare destination ports - let destination_ports = merge_port_ranges(ports); - - // remove duplicate protocol entries - protocols.sort_unstable(); - protocols.dedup(); - - // skip creating default firewall rules if given ACL includes only destination aliases and no manual destination config - // at this point component aliases have been added to the manual config so they don't need to be handled separately - let has_no_manual_destination = dest_addrs_v4.is_empty() - && dest_addrs_v6.is_empty() - && destination_ports.is_empty() - && protocols.is_empty(); - let has_destination_aliases = !destination_aliases.is_empty(); - let is_destination_alias_only_rule = has_destination_aliases && has_no_manual_destination; - - if !is_destination_alias_only_rule { - let comment = format!("ACL {} - {}", acl.id, acl.name); - if has_ipv4_addresses { - // create IPv4 rules - let ipv4_rules = create_rules( - acl.id, - IpVersion::Ipv4, - &ipv4_source_addrs, - &dest_addrs_v4, - &destination_ports, - &protocols, - &comment, - ); - if let Some(rule) = ipv4_rules.0 { - allow_rules.push(rule); - } - deny_rules.push(ipv4_rules.1); - } - - if has_ipv6_addresses { - // create IPv6 rules - let ipv6_rules = create_rules( - acl.id, - IpVersion::Ipv6, - &ipv6_source_addrs, - &dest_addrs_v6, - &destination_ports, - &protocols, - &comment, - ); - if let Some(rule) = ipv6_rules.0 { - allow_rules.push(rule); - } - deny_rules.push(ipv6_rules.1); - } - } + // check if we need to add rules for manually defined destination + if manual_destination_settings { + let (manual_destination_allow_rules, manual_destination_deny_rules) = + get_manual_destination_rules( + &mut *conn, + id, + &name, + (&ipv4_source_addrs, &ipv6_source_addrs), + aliases, + destination, + destination_ranges, + ports, + protocols, + ) + .await?; + println!("{manual_destination_allow_rules:#?}"); + println!("{manual_destination_deny_rules:#?}"); + + // append generated rules to output + allow_rules.extend(manual_destination_allow_rules); + deny_rules.extend(manual_destination_deny_rules); + }; // process destination aliases by creating a dedicated set of rules for each of them - if !destination_aliases.is_empty() { + if !destinations.is_empty() { debug!( "Generating firewall rules for {} aliases used in ACL rule {id:?}", - destination_aliases.len() + destinations.len() ); } - for alias in destination_aliases { + for alias in destinations { debug!("Processing ACL alias: {alias:?}"); // fetch destination ranges for a given alias @@ -184,7 +143,7 @@ pub async fn generate_firewall_rules_from_acls( let comment = format!( "ACL {} - {}, ALIAS {} - {}", - acl.id, acl.name, alias.id, alias.name + acl.id, name, alias.id, alias.name ); if has_ipv4_addresses { // create IPv4 rules @@ -288,6 +247,89 @@ async fn get_source_ips( Ok((ipv4_source_addrs, ipv6_source_addrs)) } +/// Generates firewall rules for destination manually specified in ACL rule. +async fn get_manual_destination_rules( + conn: &mut PgConnection, + rule_id: Id, + rule_name: &str, + source_addrs: (&[IpAddress], &[IpAddress]), + aliases: Vec>, + mut destination: Vec, + destination_ranges: Vec>, + mut ports: Vec, + mut protocols: Vec, +) -> Result<(Vec, Vec), FirewallError> { + debug!("Generating firewall rules for manually configured destination in ACL rule {rule_id}"); + // store alias ranges separately since they use a different struct + let mut alias_destination_ranges = Vec::new(); + // process component aliases by appending destination parameters from each of them to + // existing lists + for alias in aliases { + // fetch destination ranges for a given alias + alias_destination_ranges.extend(alias.get_destination_ranges(&mut *conn).await?); + + // extend existing parameter lists + destination.extend(alias.destination); + ports.extend(alias.ports.into_iter().map(Into::into).collect::>()); + protocols.extend(alias.protocols); + } + + // prepare destination addresses + let (dest_addrs_v4, dest_addrs_v6) = + process_destination_addrs(&destination, &destination_ranges); + + // prepare destination ports + let destination_ports = merge_port_ranges(ports); + + // remove duplicate protocol entries + protocols.sort_unstable(); + protocols.dedup(); + println!("source addrs: {source_addrs:#?}"); + + let (ipv4_source_addrs, ipv6_source_addrs) = source_addrs; + let has_ipv4_addresses = !ipv4_source_addrs.is_empty(); + let has_ipv6_addresses = !ipv6_source_addrs.is_empty(); + + let comment = format!("ACL {} - {}", rule_id, rule_name); + let mut allow_rules = Vec::new(); + let mut deny_rules = Vec::new(); + if has_ipv4_addresses { + // create IPv4 rules + let ipv4_rules = create_rules( + rule_id, + IpVersion::Ipv4, + &ipv4_source_addrs, + &dest_addrs_v4, + &destination_ports, + &protocols, + &comment, + ); + if let Some(rule) = ipv4_rules.0 { + allow_rules.push(rule); + } + deny_rules.push(ipv4_rules.1); + } + + if has_ipv6_addresses { + // create IPv6 rules + let ipv6_rules = create_rules( + rule_id, + IpVersion::Ipv6, + &ipv6_source_addrs, + &dest_addrs_v6, + &destination_ports, + &protocols, + &comment, + ); + if let Some(rule) = ipv6_rules.0 { + allow_rules.push(rule); + } + deny_rules.push(ipv6_rules.1); + } + + Ok((allow_rules, deny_rules)) +} + /// Creates ALLOW and DENY rules for given set of source, destination /// addresses, ports and protocols. The DENY rule should block all /// remaining traffic to the destination from sources other than specified. diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index 3f0d480b67..bca9469a31 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -3113,6 +3113,7 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO enabled: true, state: RuleState::Applied, destination: vec!["192.168.1.0/24".parse().unwrap()], + manual_settings: true, ..Default::default() } .save(&pool) @@ -3125,6 +3126,7 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO enabled: true, all_networks: true, state: RuleState::Applied, + manual_settings: false, ..Default::default() } .save(&pool) @@ -3138,6 +3140,7 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO all_networks: true, allow_all_users: true, state: RuleState::Applied, + manual_settings: false, ..Default::default() } .save(&pool) @@ -3439,6 +3442,7 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P "192.168.1.0/24".parse().unwrap(), "fc00::0/112".parse().unwrap(), ], + manual_settings: true, ..Default::default() } .save(&pool) From 990cf6279891289207ac25f708f8c7200476c4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 4 Feb 2026 08:28:50 +0100 Subject: [PATCH 03/25] fix existing all locations test --- .../src/enterprise/firewall/mod.rs | 3 --- .../src/enterprise/firewall/tests.rs | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index e65d94fcf9..d2543ff26a 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -107,8 +107,6 @@ pub async fn generate_firewall_rules_from_acls( protocols, ) .await?; - println!("{manual_destination_allow_rules:#?}"); - println!("{manual_destination_deny_rules:#?}"); // append generated rules to output allow_rules.extend(manual_destination_allow_rules); @@ -284,7 +282,6 @@ async fn get_manual_destination_rules( // remove duplicate protocol entries protocols.sort_unstable(); protocols.dedup(); - println!("source addrs: {source_addrs:#?}"); let (ipv4_source_addrs, ipv6_source_addrs) = source_addrs; let has_ipv4_addresses = !ipv4_source_addrs.is_empty(); diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index bca9469a31..f182e8e3bf 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -3437,12 +3437,13 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::Applied, + manual_settings: true, destination: vec![ "192.168.1.0/24".parse().unwrap(), "fc00::0/112".parse().unwrap(), ], - manual_settings: true, ..Default::default() } .save(&pool) @@ -3454,7 +3455,13 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P expires: None, enabled: true, all_networks: true, + allow_all_users: true, state: RuleState::Applied, + manual_settings: true, + destination: vec![ + "192.168.2.0/24".parse().unwrap(), + "fb00::0/112".parse().unwrap(), + ], ..Default::default() } .save(&pool) @@ -3468,6 +3475,11 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P all_networks: true, allow_all_users: true, state: RuleState::Applied, + manual_settings: true, + destination: vec![ + "192.168.3.0/24".parse().unwrap(), + "fa00::0/112".parse().unwrap(), + ], ..Default::default() } .save(&pool) @@ -3495,8 +3507,8 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P .unwrap() .rules; - // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 8); + // all rules were used to this location + assert_eq!(generated_firewall_rules.len(), 12); let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) .await @@ -3504,8 +3516,8 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P .unwrap() .rules; - // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 6); + // rule with `all_networks` enabled was also used for this location + assert_eq!(generated_firewall_rules.len(), 8); } #[sqlx::test] From b3317c096c244d01c4bb31bd94c6fd13912d4286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 4 Feb 2026 12:41:35 +0100 Subject: [PATCH 04/25] update dependencies --- Cargo.lock | 36 +- .../firewall/tests/all_locations.rs | 502 +++ .../firewall/tests/expired_rules.rs | 202 + .../src/enterprise/firewall/tests/mod.rs | 3430 +++++++++++++++++ flake.lock | 12 +- 5 files changed, 4158 insertions(+), 24 deletions(-) create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 778215c22b..bf497f50eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,9 +568,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -691,9 +691,9 @@ checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" [[package]] name = "clap" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -701,9 +701,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -4370,9 +4370,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -4382,9 +4382,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4393,9 +4393,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" @@ -6555,9 +6555,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -6994,18 +6994,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.37" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.37" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", diff --git a/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs new file mode 100644 index 0000000000..c3ad11a368 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs @@ -0,0 +1,502 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_common::db::{ + NoId, + models::{Device, DeviceType, User, WireguardNetwork, device::WireguardNetworkDevice}, + setup_pool, +}; +use ipnetwork::IpNetwork; +use rand::{Rng, thread_rng}; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use crate::enterprise::{ + db::models::acl::{AclRule, AclRuleNetwork, RuleState}, + firewall::try_get_location_firewall_config, +}; + +#[sqlx::test] +async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + // Setup some test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_1.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_2.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 10, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + destination: vec!["192.168.1.0/24".parse().unwrap()], + manual_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + state: RuleState::Applied, + manual_settings: false, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + allow_all_users: true, + state: RuleState::Applied, + manual_settings: false, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location_1.id) + .save(&pool) + .await + .unwrap(); + } + for rule in [&acl_rule_2] { + AclRuleNetwork::new(rule.id, location_2.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were assigned to this location + assert_eq!(generated_firewall_rules.len(), 4); + + let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was used for this location + assert_eq!(generated_firewall_rules.len(), 3); +} + +#[sqlx::test] +async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + + // Setup some test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_1.id, + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_2.id, + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 10, + 10, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + destination: vec!["fc00::0/112".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + allow_all_users: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location_1.id) + .save(&pool) + .await + .unwrap(); + } + for rule in [&acl_rule_2] { + AclRuleNetwork::new(rule.id, location_2.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were assigned to this location + assert_eq!(generated_firewall_rules.len(), 4); + + let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was used for this location + assert_eq!(generated_firewall_rules.len(), 3); +} + +#[sqlx::test] +async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + // Setup some test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_1.id, + wireguard_ips: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_2.id, + wireguard_ips: vec![ + IpAddr::V4(Ipv4Addr::new(10, 10, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 10, + 10, + user.id as u16, + device_num as u16, + )), + ], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::Applied, + manual_settings: true, + destination: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::0/112".parse().unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + allow_all_users: true, + state: RuleState::Applied, + manual_settings: true, + destination: vec![ + "192.168.2.0/24".parse().unwrap(), + "fb00::0/112".parse().unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + allow_all_users: true, + state: RuleState::Applied, + manual_settings: true, + destination: vec![ + "192.168.3.0/24".parse().unwrap(), + "fa00::0/112".parse().unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location_1.id) + .save(&pool) + .await + .unwrap(); + } + for rule in [&acl_rule_2] { + AclRuleNetwork::new(rule.id, location_2.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // all rules were used to this location + assert_eq!(generated_firewall_rules.len(), 12); + + let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was also used for this location + assert_eq!(generated_firewall_rules.len(), 8); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs new file mode 100644 index 0000000000..ddd681df1a --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs @@ -0,0 +1,202 @@ +#[sqlx::test] +async fn test_expired_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_expired_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_expired_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs new file mode 100644 index 0000000000..85bcaacb67 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -0,0 +1,3430 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use chrono::{DateTime, NaiveDateTime}; +use defguard_common::db::{ + Id, NoId, + models::{ + Device, DeviceType, WireguardNetwork, device::WireguardNetworkDevice, group::Group, + user::User, + }, + setup_pool, +}; +use defguard_proto::enterprise::firewall::{ + FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, + ip_address::Address, port::Port as PortInner, +}; +use ipnetwork::{IpNetwork, Ipv6Network}; +use rand::{Rng, thread_rng}; +use sqlx::{ + PgPool, + postgres::{PgConnectOptions, PgPoolOptions}, + query, +}; + +use super::{ + find_largest_subnet_in_range, get_last_ip_in_v6_subnet, get_source_users, merge_addrs, + merge_port_ranges, process_destination_addrs, +}; +use crate::enterprise::{ + db::models::acl::{ + AclAlias, AclRule, AclRuleAlias, AclRuleDestinationRange, AclRuleDevice, AclRuleGroup, + AclRuleInfo, AclRuleNetwork, AclRuleUser, AliasKind, PortRange, RuleState, + }, + firewall::{get_source_addrs, get_source_network_devices, try_get_location_firewall_config}, +}; + +mod all_locations; +mod expired_rules; + +impl Default for AclRuleDestinationRange { + fn default() -> Self { + Self { + id: Id::default(), + rule_id: Id::default(), + start: IpAddr::V4(Ipv4Addr::UNSPECIFIED), + end: IpAddr::V4(Ipv4Addr::UNSPECIFIED), + } + } +} + +fn random_user_with_id(rng: &mut R, id: Id) -> User { + let mut user: User = rng.r#gen(); + user.id = id; + user +} + +fn random_network_device_with_id(rng: &mut R, id: Id) -> Device { + let device: Device = rng.r#gen(); + let mut device = device.with_id(id); + device.device_type = DeviceType::Network; + device +} + +#[test] +fn test_get_relevant_users() { + let mut rng = thread_rng(); + + // prepare allowed and denied users lists with shared elements + let user_1 = random_user_with_id(&mut rng, 1); + let user_2 = random_user_with_id(&mut rng, 2); + let user_3 = random_user_with_id(&mut rng, 3); + let user_4 = random_user_with_id(&mut rng, 4); + let user_5 = random_user_with_id(&mut rng, 5); + let allowed_users = vec![user_1.clone(), user_2.clone(), user_4.clone()]; + let denied_users = vec![user_3.clone(), user_4, user_5.clone()]; + + let users = get_source_users(allowed_users, &denied_users); + assert_eq!(users, vec![user_1, user_2]); +} + +#[test] +fn test_get_relevant_network_devices() { + let mut rng = thread_rng(); + + // prepare allowed and denied network devices lists with shared elements + let device_1 = random_network_device_with_id(&mut rng, 1); + let device_2 = random_network_device_with_id(&mut rng, 2); + let device_3 = random_network_device_with_id(&mut rng, 3); + let device_4 = random_network_device_with_id(&mut rng, 4); + let device_5 = random_network_device_with_id(&mut rng, 5); + let allowed_devices = vec![ + device_1.clone(), + device_3.clone(), + device_4.clone(), + device_5.clone(), + ]; + let denied_devices = vec![device_2.clone(), device_4, device_5.clone()]; + + let devices = get_source_network_devices(allowed_devices, &denied_devices); + assert_eq!(devices, vec![device_1, device_3]); +} + +#[test] +fn test_process_source_addrs_v4() { + // Test data with mixed IPv4 and IPv6 addresses + let user_device_ips = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 5)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), + ]; + + let network_device_ips = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 3)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 4)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), // Should be filtered out + IpAddr::V4(Ipv4Addr::new(172, 16, 1, 1)), + ]; + + let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv4); + + // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges + assert_eq!( + source_addrs, + [ + IpAddress { + address: Some(Address::Ip("10.0.1.1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.2/31".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.4/31".to_string())) + }, + IpAddress { + address: Some(Address::Ip("172.16.1.1".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.1.100".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv4); + assert!(empty_addrs.is_empty()); + + // Test with only IPv6 addresses - should return empty result for IPv4 + let ipv6_only = get_source_addrs( + vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))], + vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2))], + IpVersion::Ipv4, + ); + assert!(ipv6_only.is_empty()); +} + +#[test] +fn test_process_source_addrs_v6() { + // Test data with mixed IPv4 and IPv6 addresses + let user_device_ips = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 5)), + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), // Should be filtered out + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 1)), + ]; + + let network_device_ips = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 3)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 4)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), // Should be filtered out + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 2, 0, 0, 0, 1)), + ]; + + let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv6); + + // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges + assert_eq!( + source_addrs, + [ + IpAddress { + address: Some(Address::Ip("2001:db8::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::2/127".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::4/127".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:0:1::1".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8:0:2::1".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv6); + assert!(empty_addrs.is_empty()); + + // Test with only IPv4 addresses - should return empty result for IPv6 + let ipv4_only = get_source_addrs( + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))], + IpVersion::Ipv6, + ); + assert!(ipv4_only.is_empty()); +} + +#[test] +fn test_process_destination_addrs_v4() { + // Test data with mixed IPv4 and IPv6 networks + let destination_ips = [ + "10.0.1.0/24".parse().unwrap(), + "10.0.2.0/24".parse().unwrap(), + "2001:db8::/64".parse().unwrap(), // Should be filtered out + "192.168.1.0/24".parse().unwrap(), + ]; + + let destination_ranges = [ + AclRuleDestinationRange { + start: IpAddr::V4(Ipv4Addr::new(10, 0, 3, 255)), + end: IpAddr::V4(Ipv4Addr::new(10, 0, 4, 0)), + ..Default::default() + }, + AclRuleDestinationRange { + start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out + end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 100)), + ..Default::default() + }, + ]; + + let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + + assert_eq!( + destination_addrs.0, + [ + IpAddress { + address: Some(Address::IpSubnet("10.0.1.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.3.255".to_string(), + end: "10.0.4.0".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = process_destination_addrs(&[], &[]); + assert!(empty_addrs.0.is_empty()); + + // Test with only IPv6 addresses - should return empty result for IPv4 + let ipv6_only = process_destination_addrs(&["2001:db8::/64".parse().unwrap()], &[]); + assert!(ipv6_only.0.is_empty()); +} + +#[test] +fn test_process_destination_addrs_v6() { + // Test data with mixed IPv4 and IPv6 networks + let destination_ips = vec![ + "2001:db8:1::/64".parse().unwrap(), + "2001:db8:2::/64".parse().unwrap(), + "10.0.1.0/24".parse().unwrap(), // Should be filtered out + "2001:db8:3::/64".parse().unwrap(), + ]; + + let destination_ranges = vec![ + AclRuleDestinationRange { + start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 1)), + end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 3)), + ..Default::default() + }, + AclRuleDestinationRange { + start: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), // Should be filtered out + end: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), + ..Default::default() + }, + ]; + + let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + + assert_eq!( + destination_addrs.1, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::/64".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:2::/64".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:3::/64".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8:4::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:4::2/127".to_string())) + } + ] + ); + + // Test with empty input + let empty_addrs = process_destination_addrs(&[], &[]); + assert!(empty_addrs.1.is_empty()); + + // Test with only IPv4 addresses - should return empty result for IPv6 + let ipv4_only = process_destination_addrs(&["192.168.1.0/24".parse().unwrap()], &[]); + assert!(ipv4_only.1.is_empty()); +} + +#[test] +fn test_merge_v4_addrs() { + let addr_ranges = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 60, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 60, 25)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 22)), + IpAddr::V4(Ipv4Addr::new(10, 0, 8, 127))..=IpAddr::V4(Ipv4Addr::new(10, 0, 9, 12)), + IpAddr::V4(Ipv4Addr::new(10, 0, 9, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 12)), + IpAddr::V4(Ipv4Addr::new(10, 0, 9, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 31)), + IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20))..=IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20)), + IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::Ip("10.0.8.127".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.8.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.9.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.10.0/27".to_string())), + }, + IpAddress { + address: Some(Address::Ip("10.0.20.20".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.60.20/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.60.24/31".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.0.20".to_string())), + }, + ] + ); + + // merge single IPs into a range + let addr_ranges = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::IpSubnet("10.0.10.0/30".to_string())), + }, + IpAddress { + address: Some(Address::Ip("10.0.10.20".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_v6_addrs() { + let addr_ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x5)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x3)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x8)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x3)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::Ip("2001:db8:1::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::2/127".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::4/126".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:1::8".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:2::1".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:3::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:3::2/127".to_string())) + } + ] + ); +} + +#[test] +fn test_merge_addrs_extracts_ipv4_subnets() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.2.0/24".to_string())) + }, + ] + ); +} + +#[test] +fn test_merge_addrs_extracts_ipv6_subnets() { + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db9::ffff".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8::/32".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db9::/112".to_string())) + }, + ] + ); +} + +#[test] +fn test_merge_addrs_falls_back_to_range_when_no_subnet_fits() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 0)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::IpRange(IpRange { + start: "192.168.1.255".to_string(), + end: "192.168.2.0".to_string(), + })), + },] + ); + + let start = "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff" + .parse::() + .unwrap(); + let end = "2001:db9::".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::IpRange(IpRange { + start: "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff".to_string(), + end: "2001:db9::".to_string(), + })), + },] + ); +} + +#[test] +fn test_merge_addrs_handles_single_ip() { + // Test case: single IP should remain as IP + let ranges = + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::Ip("192.168.1.1".to_string())), + },] + ); + + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db8::".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::Ip("2001:db8::".to_string())), + },] + ); +} + +#[test] +fn test_find_largest_ipv4_subnet_perfect_match() { + // Test /24 subnet + let start = Ipv4Addr::new(192, 168, 1, 0); + let end = Ipv4Addr::new(192, 168, 1, 255); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "192.168.1.0/24"); + + // Test /28 subnet (16 addresses) + let start = Ipv4Addr::new(192, 168, 1, 0); + let end = Ipv4Addr::new(192, 168, 1, 15); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "192.168.1.0/28"); +} + +#[test] +fn test_find_largest_ipv6_subnet_perfect_match() { + // Test /112 subnet + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db8::ffff".parse::().unwrap(); + + let result = find_largest_subnet_in_range(IpAddr::V6(start), IpAddr::V6(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "2001:db8::/112"); +} + +#[test] +fn test_find_largest_subnet_mixed_ip_versions() { + // Test mixed IP versions should return None + let start = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)); + let end = IpAddr::V6("2001:db8::1".parse().unwrap()); + + let result = find_largest_subnet_in_range(start, end); + + assert!(result.is_none()); +} + +#[test] +fn test_find_largest_subnet_invalid_range() { + // Test invalid range (start > end) should return None + let start = Ipv4Addr::new(192, 168, 1, 10); + let end = Ipv4Addr::new(192, 168, 1, 5); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_none()); +} + +#[test] +fn test_merge_addrs_subnet_at_start_of_range() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 64)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/26".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.1.64".to_string())), + }, + ] + ); + + let ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x40)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8::/122".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8::40".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_addrs_subnet_at_end_of_range() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 15))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 31)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::Ip("192.168.1.15".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.1.16/28".to_string())), + }, + ] + ); + + let ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x0f)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1f)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::Ip("2001:db8::f".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::10/124".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_port_ranges() { + // single port + let input_ranges = vec![PortRange::new(100, 100)]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::SinglePort(100)) + }] + ); + + // overlapping ranges + let input_ranges = vec![ + PortRange::new(100, 200), + PortRange::new(150, 220), + PortRange::new(210, 300), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 300 + })) + }] + ); + + // duplicate ranges + let input_ranges = vec![ + PortRange::new(100, 200), + PortRange::new(100, 200), + PortRange::new(150, 220), + PortRange::new(150, 220), + PortRange::new(210, 300), + PortRange::new(210, 300), + PortRange::new(350, 400), + PortRange::new(350, 400), + PortRange::new(350, 400), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [ + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 300 + })) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 350, + end: 400 + })) + } + ] + ); + + // non-consecutive ranges + let input_ranges = vec![ + PortRange::new(501, 699), + PortRange::new(151, 220), + PortRange::new(210, 300), + PortRange::new(800, 800), + PortRange::new(200, 210), + PortRange::new(50, 50), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [ + Port { + port: Some(PortInner::SinglePort(50)) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 151, + end: 300 + })) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 501, + end: 699 + })) + }, + Port { + port: Some(PortInner::SinglePort(800)) + } + ] + ); + + // fully contained range + let input_ranges = vec![PortRange::new(100, 200), PortRange::new(120, 180)]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 200 + })) + }] + ); +} + +#[test] +fn test_last_ip_in_v6_subnet() { + let subnet: Ipv6Network = "2001:db8:85a3::8a2e:370:7334/64".parse().unwrap(); + let last_ip = get_last_ip_in_v6_subnet(&subnet); + assert_eq!( + last_ip, + IpAddr::V6(Ipv6Addr::new( + 0x2001, 0x0db8, 0x85a3, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff + )) + ); + + let subnet: Ipv6Network = "280b:47f8:c9d7:634c:cb35:11f3:14e1:5016/119" + .parse() + .unwrap(); + let last_ip = get_last_ip_in_v6_subnet(&subnet); + assert_eq!( + last_ip, + IpAddr::V6(Ipv6Addr::new( + 0x280b, 0x47f8, 0xc9d7, 0x634c, 0xcb35, 0x11f3, 0x14e1, 0x51ff + )) + ); +} + +async fn create_acl_rule( + pool: &PgPool, + rule: AclRule, + locations: Vec, + allowed_users: Vec, + denied_users: Vec, + allowed_groups: Vec, + denied_groups: Vec, + allowed_network_devices: Vec, + denied_network_devices: Vec, + destination_ranges: Vec<(IpAddr, IpAddr)>, + aliases: Vec, +) -> AclRuleInfo { + let mut conn = pool.acquire().await.unwrap(); + + // create base rule + let rule = rule.save(&mut *conn).await.unwrap(); + let rule_id = rule.id; + + // create related objects + // locations + for location_id in locations { + AclRuleNetwork::new(rule_id, location_id) + .save(&mut *conn) + .await + .unwrap(); + } + + // allowed users + for user_id in allowed_users { + AclRuleUser::new(rule_id, user_id, true) + .save(&mut *conn) + .await + .unwrap(); + } + + // denied users + for user_id in denied_users { + AclRuleUser::new(rule_id, user_id, false) + .save(&mut *conn) + .await + .unwrap(); + } + + // allowed groups + for group_id in allowed_groups { + AclRuleGroup::new(rule_id, group_id, true) + .save(&mut *conn) + .await + .unwrap(); + } + + // denied groups + for group_id in denied_groups { + AclRuleGroup::new(rule_id, group_id, false) + .save(&mut *conn) + .await + .unwrap(); + } + + // allowed devices + for device_id in allowed_network_devices { + AclRuleDevice::new(rule_id, device_id, true) + .save(&mut *conn) + .await + .unwrap(); + } + + // denied devices + for device_id in denied_network_devices { + AclRuleDevice::new(rule_id, device_id, false) + .save(&mut *conn) + .await + .unwrap(); + } + + // destination ranges + for range in destination_ranges { + AclRuleDestinationRange { + id: NoId, + rule_id, + start: range.0, + end: range.1, + } + .save(&mut *conn) + .await + .unwrap(); + } + + // aliases + for alias_id in aliases { + AclRuleAlias::new(rule_id, alias_id) + .save(&mut *conn) + .await + .unwrap(); + } + + // convert to output format + rule.to_info(&mut conn).await.unwrap() +} + +#[sqlx::test] +async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); + + // Setup test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.r#gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.r#gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.r#gen(); + let user_5 = user_5.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // Setup test groups + let group_1 = Group { + id: NoId, + name: "group_1".into(), + ..Default::default() + }; + let group_1 = group_1.save(&pool).await.unwrap(); + let group_2 = Group { + id: NoId, + name: "group_2".into(), + ..Default::default() + }; + let group_2 = group_2.save(&pool).await.unwrap(); + + // Assign users to groups: + // Group 1: users 1,2 + // Group 2: users 3,4 + let group_assignments = vec![ + (&group_1, vec![&user_1, &user_2]), + (&group_2, vec![&user_3, &user_4]), + ]; + + for (group, users) in group_assignments { + for user in users { + query!( + "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", + user.id, + group.id + ) + .execute(&pool) + .await + .unwrap(); + } + } + + // Create some network devices + let network_device_1 = Device { + id: NoId, + name: "network-device-1".into(), + user_id: user_1.id, // Owned by user 1 + device_type: DeviceType::Network, + description: Some("Test network device 1".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_1 = network_device_1.save(&pool).await.unwrap(); + + let network_device_2 = Device { + id: NoId, + name: "network-device-2".into(), + user_id: user_2.id, // Owned by user 2 + device_type: DeviceType::Network, + description: Some("Test network device 2".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_2 = network_device_2.save(&pool).await.unwrap(); + + let network_device_3 = Device { + id: NoId, + name: "network-device-3".into(), + user_id: user_3.id, // Owned by user 3 + device_type: DeviceType::Network, + description: Some("Test network device 3".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_3 = network_device_3.save(&pool).await.unwrap(); + + // Add network devices to location's VPN network + let network_devices = vec![ + ( + network_device_1.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + ), + ( + network_device_2.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + ), + ( + network_device_3.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + ), + ]; + + for (device_id, ip) in network_devices { + let network_device = WireguardNetworkDevice { + device_id, + wireguard_network_id: location.id, + wireguard_ips: vec![ip], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + + // Create first ACL rule - Web access + let acl_rule_1 = AclRule { + id: NoId, + name: "Web Access".into(), + all_networks: false, + expires: None, + allow_all_users: false, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec!["192.168.1.0/24".parse().unwrap()], + ports: vec![ + PortRange::new(80, 80).into(), + PortRange::new(443, 443).into(), + ], + protocols: vec![Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + }; + let locations = vec![location.id]; + let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web + let denied_users = vec![user_3.id]; // Third user explicitly denied + let allowed_groups = vec![group_1.id]; // First group allowed + let denied_groups = Vec::new(); + let allowed_devices = vec![network_device_1.id]; + let denied_devices = vec![network_device_2.id, network_device_3.id]; + let destination_ranges = Vec::new(); + let aliases = Vec::new(); + + let _acl_rule_1 = create_acl_rule( + &pool, + acl_rule_1, + locations, + allowed_users, + denied_users, + allowed_groups, + denied_groups, + allowed_devices, + denied_devices, + destination_ranges, + aliases, + ) + .await; + + // Create second ACL rule - DNS access + let acl_rule_2 = AclRule { + id: NoId, + name: "DNS Access".into(), + all_networks: false, + expires: None, + allow_all_users: true, // Allow all users + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: Vec::new(), // Will use destination ranges instead + ports: vec![PortRange::new(53, 53).into()], + protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + }; + let locations_2 = vec![location.id]; + let allowed_users_2 = Vec::new(); + let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS + let allowed_groups_2 = Vec::new(); + let denied_groups_2 = vec![group_2.id]; + let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed + let denied_devices_2 = vec![network_device_3.id]; // Third network device denied + let destination_ranges_2 = vec![ + ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), + ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), + ]; + let aliases_2 = Vec::new(); + + let _acl_rule_2 = create_acl_rule( + &pool, + acl_rule_2, + locations_2, + allowed_users_2, + denied_users_2, + allowed_groups_2, + denied_groups_2, + allowed_devices_2, + denied_devices_2, + destination_ranges_2, + aliases_2, + ) + .await; + + let mut conn = pool.acquire().await.unwrap(); + + // try to generate firewall config with ACL disabled + location.acl_enabled = false; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap(); + assert!(generated_firewall_config.is_none()); + + // generate firewall config with default policy Allow + location.acl_enabled = true; + location.acl_default_allow = true; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + assert_eq!( + generated_firewall_config.default_policy, + i32::from(FirewallPolicy::Allow) + ); + + let generated_firewall_rules = generated_firewall_config.rules; + + assert_eq!(generated_firewall_rules.len(), 4); + + // First ACL - Web Access ALLOW + let web_allow_rule = &generated_firewall_rules[0]; + assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); + assert_eq!( + web_allow_rule.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); + assert_eq!( + web_allow_rule.destination_ports, + [ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::Ip("10.0.100.1".to_string())), + }, + ] + ); + + // First ACL - Web Access DENY + let web_deny_rule = &generated_firewall_rules[2]; + assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule.protocols.is_empty()); + assert!(web_deny_rule.destination_ports.is_empty()); + assert!(web_deny_rule.source_addrs.is_empty()); + assert_eq!( + web_deny_rule.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); + + // Second ACL - DNS Access ALLOW + let dns_allow_rule = &generated_firewall_rules[1]; + assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!( + dns_allow_rule.protocols, + [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule.destination_ports, + [Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.100.1".to_string(), + end: "10.0.100.2".to_string(), + })), + }, + ] + ); + + let expected_destination_addrs = vec![ + IpAddress { + address: Some(Address::Ip("10.0.1.13".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), + }, + ]; + + assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); + + // Second ACL - DNS Access DENY + let dns_deny_rule = &generated_firewall_rules[3]; + assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule.protocols.is_empty(),); + assert!(dns_deny_rule.destination_ports.is_empty(),); + assert!(dns_deny_rule.source_addrs.is_empty(),); + assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); +} + +#[sqlx::test] +async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); + + // Setup test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.r#gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.r#gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.r#gen(); + let user_5 = user_5.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // Setup test groups + let group_1 = Group { + id: NoId, + name: "group_1".into(), + ..Default::default() + }; + let group_1 = group_1.save(&pool).await.unwrap(); + let group_2 = Group { + id: NoId, + name: "group_2".into(), + ..Default::default() + }; + let group_2 = group_2.save(&pool).await.unwrap(); + + // Assign users to groups: + // Group 1: users 1,2 + // Group 2: users 3,4 + let group_assignments = vec![ + (&group_1, vec![&user_1, &user_2]), + (&group_2, vec![&user_3, &user_4]), + ]; + + for (group, users) in group_assignments { + for user in users { + query!( + "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", + user.id, + group.id + ) + .execute(&pool) + .await + .unwrap(); + } + } + + // Create some network devices + let network_device_1 = Device { + id: NoId, + name: "network-device-1".into(), + user_id: user_1.id, // Owned by user 1 + device_type: DeviceType::Network, + description: Some("Test network device 1".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_1 = network_device_1.save(&pool).await.unwrap(); + + let network_device_2 = Device { + id: NoId, + name: "network-device-2".into(), + user_id: user_2.id, // Owned by user 2 + device_type: DeviceType::Network, + description: Some("Test network device 2".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_2 = network_device_2.save(&pool).await.unwrap(); + + let network_device_3 = Device { + id: NoId, + name: "network-device-3".into(), + user_id: user_3.id, // Owned by user 3 + device_type: DeviceType::Network, + description: Some("Test network device 3".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_3 = network_device_3.save(&pool).await.unwrap(); + + // Add network devices to location's VPN network + let network_devices = vec![ + ( + network_device_1.id, + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), + ), + ( + network_device_2.id, + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), + ), + ( + network_device_3.id, + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), + ), + ]; + + for (device_id, ip) in network_devices { + let network_device = WireguardNetworkDevice { + device_id, + wireguard_network_id: location.id, + wireguard_ips: vec![ip], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + + // Create first ACL rule - Web access + let acl_rule_1 = AclRule { + id: NoId, + name: "Web Access".into(), + all_networks: false, + expires: None, + allow_all_users: false, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec!["fc00::0/112".parse().unwrap()], + ports: vec![ + PortRange::new(80, 80).into(), + PortRange::new(443, 443).into(), + ], + protocols: vec![Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + }; + let locations = vec![location.id]; + let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web + let denied_users = vec![user_3.id]; // Third user explicitly denied + let allowed_groups = vec![group_1.id]; // First group allowed + let denied_groups = Vec::new(); + let allowed_devices = vec![network_device_1.id]; + let denied_devices = vec![network_device_2.id, network_device_3.id]; + let destination_ranges = Vec::new(); + let aliases = Vec::new(); + + let _acl_rule_1 = create_acl_rule( + &pool, + acl_rule_1, + locations, + allowed_users, + denied_users, + allowed_groups, + denied_groups, + allowed_devices, + denied_devices, + destination_ranges, + aliases, + ) + .await; + + // Create second ACL rule - DNS access + let acl_rule_2 = AclRule { + id: NoId, + name: "DNS Access".into(), + all_networks: false, + expires: None, + allow_all_users: true, // Allow all users + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: Vec::new(), // Will use destination ranges instead + ports: vec![PortRange::new(53, 53).into()], + protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + }; + let locations_2 = vec![location.id]; + let allowed_users_2 = Vec::new(); + let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS + let allowed_groups_2 = Vec::new(); + let denied_groups_2 = vec![group_2.id]; + let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed + let denied_devices_2 = vec![network_device_3.id]; // Third network device denied + let destination_ranges_2 = vec![ + ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), + ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), + ]; + let aliases_2 = Vec::new(); + + let _acl_rule_2 = create_acl_rule( + &pool, + acl_rule_2, + locations_2, + allowed_users_2, + denied_users_2, + allowed_groups_2, + denied_groups_2, + allowed_devices_2, + denied_devices_2, + destination_ranges_2, + aliases_2, + ) + .await; + + let mut conn = pool.acquire().await.unwrap(); + + // try to generate firewall config with ACL disabled + location.acl_enabled = false; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap(); + assert!(generated_firewall_config.is_none()); + + // generate firewall config with default policy Allow + location.acl_enabled = true; + location.acl_default_allow = true; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + assert_eq!( + generated_firewall_config.default_policy, + i32::from(FirewallPolicy::Allow) + ); + + let generated_firewall_rules = generated_firewall_config.rules; + + assert_eq!(generated_firewall_rules.len(), 4); + + // First ACL - Web Access ALLOW + let web_allow_rule = &generated_firewall_rules[0]; + assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); + assert_eq!( + web_allow_rule.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("fc00::/112".to_string())), + }] + ); + assert_eq!( + web_allow_rule.destination_ports, + [ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::Ip("ff00::100:1".to_string())), + }, + ] + ); + + // First ACL - Web Access DENY + let web_deny_rule = &generated_firewall_rules[2]; + assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule.protocols.is_empty()); + assert!(web_deny_rule.destination_ports.is_empty()); + assert!(web_deny_rule.source_addrs.is_empty()); + assert_eq!( + web_deny_rule.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("fc00::/112".to_string())), + }] + ); + + // Second ACL - DNS Access ALLOW + let dns_allow_rule = &generated_firewall_rules[1]; + assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!( + dns_allow_rule.protocols, + [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule.destination_ports, + [Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + + let expected_destination_addrs = vec![ + IpAddress { + address: Some(Address::Ip("fc00::1:13".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:54/126".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:58/125".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:60/123".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:80/121".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:100/120".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:200/119".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:400/118".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:800/117".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:1000/116".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:2000/115".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:4000/114".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:8000/113".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::2:0/122".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::2:40/126".to_string())), + }, + ]; + + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::100:1".to_string(), + end: "ff00::100:2".to_string(), + })), + }, + ] + ); + assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); + + // Second ACL - DNS Access DENY + let dns_deny_rule = &generated_firewall_rules[3]; + assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule.protocols.is_empty(),); + assert!(dns_deny_rule.destination_ports.is_empty(),); + assert!(dns_deny_rule.source_addrs.is_empty(),); + assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); +} + +#[sqlx::test] +async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); + + // Setup test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.r#gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.r#gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.r#gen(); + let user_5 = user_5.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // Setup test groups + let group_1 = Group { + id: NoId, + name: "group_1".into(), + ..Default::default() + }; + let group_1 = group_1.save(&pool).await.unwrap(); + let group_2 = Group { + id: NoId, + name: "group_2".into(), + ..Default::default() + }; + let group_2 = group_2.save(&pool).await.unwrap(); + + // Assign users to groups: + // Group 1: users 1,2 + // Group 2: users 3,4 + let group_assignments = vec![ + (&group_1, vec![&user_1, &user_2]), + (&group_2, vec![&user_3, &user_4]), + ]; + + for (group, users) in group_assignments { + for user in users { + query!( + "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", + user.id, + group.id + ) + .execute(&pool) + .await + .unwrap(); + } + } + + // Create some network devices + let network_device_1 = Device { + id: NoId, + name: "network-device-1".into(), + user_id: user_1.id, // Owned by user 1 + device_type: DeviceType::Network, + description: Some("Test network device 1".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_1 = network_device_1.save(&pool).await.unwrap(); + + let network_device_2 = Device { + id: NoId, + name: "network-device-2".into(), + user_id: user_2.id, // Owned by user 2 + device_type: DeviceType::Network, + description: Some("Test network device 2".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_2 = network_device_2.save(&pool).await.unwrap(); + + let network_device_3 = Device { + id: NoId, + name: "network-device-3".into(), + user_id: user_3.id, // Owned by user 3 + device_type: DeviceType::Network, + description: Some("Test network device 3".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_3 = network_device_3.save(&pool).await.unwrap(); + + // Add network devices to location's VPN network + let network_devices = vec![ + ( + network_device_1.id, + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), + ], + ), + ( + network_device_2.id, + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), + ], + ), + ( + network_device_3.id, + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), + ], + ), + ]; + + for (device_id, ips) in network_devices { + let network_device = WireguardNetworkDevice { + device_id, + wireguard_network_id: location.id, + wireguard_ips: ips, + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + + // Create first ACL rule - Web access + let acl_rule_1 = AclRule { + id: NoId, + name: "Web Access".into(), + all_networks: false, + expires: None, + allow_all_users: false, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::0/112".parse().unwrap(), + ], + ports: vec![ + PortRange::new(80, 80).into(), + PortRange::new(443, 443).into(), + ], + protocols: vec![Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + }; + let locations = vec![location.id]; + let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web + let denied_users = vec![user_3.id]; // Third user explicitly denied + let allowed_groups = vec![group_1.id]; // First group allowed + let denied_groups = Vec::new(); + let allowed_devices = vec![network_device_1.id]; + let denied_devices = vec![network_device_2.id, network_device_3.id]; + let destination_ranges = Vec::new(); + let aliases = Vec::new(); + + let _acl_rule_1 = create_acl_rule( + &pool, + acl_rule_1, + locations, + allowed_users, + denied_users, + allowed_groups, + denied_groups, + allowed_devices, + denied_devices, + destination_ranges, + aliases, + ) + .await; + + // Create second ACL rule - DNS access + let acl_rule_2 = AclRule { + id: NoId, + name: "DNS Access".into(), + all_networks: false, + expires: None, + allow_all_users: true, // Allow all users + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: Vec::new(), // Will use destination ranges instead + ports: vec![PortRange::new(53, 53).into()], + protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + }; + let locations_2 = vec![location.id]; + let allowed_users_2 = Vec::new(); + let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS + let allowed_groups_2 = Vec::new(); + let denied_groups_2 = vec![group_2.id]; + let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed + let denied_devices_2 = vec![network_device_3.id]; // Third network device denied + let destination_ranges_2 = vec![ + ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), + ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), + ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), + ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), + ]; + let aliases_2 = Vec::new(); + + let _acl_rule_2 = create_acl_rule( + &pool, + acl_rule_2, + locations_2, + allowed_users_2, + denied_users_2, + allowed_groups_2, + denied_groups_2, + allowed_devices_2, + denied_devices_2, + destination_ranges_2, + aliases_2, + ) + .await; + + let mut conn = pool.acquire().await.unwrap(); + + // try to generate firewall config with ACL disabled + location.acl_enabled = false; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap(); + assert!(generated_firewall_config.is_none()); + + // generate firewall config with default policy Allow + location.acl_enabled = true; + location.acl_default_allow = true; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + assert_eq!( + generated_firewall_config.default_policy, + i32::from(FirewallPolicy::Allow) + ); + + let generated_firewall_rules = generated_firewall_config.rules; + + assert_eq!(generated_firewall_rules.len(), 8); + + // First ACL - Web Access ALLOW + let web_allow_rule_ipv4 = &generated_firewall_rules[0]; + assert_eq!( + web_allow_rule_ipv4.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + web_allow_rule_ipv4.protocols, + vec![i32::from(Protocol::Tcp)] + ); + assert_eq!( + web_allow_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); + assert_eq!( + web_allow_rule_ipv4.destination_ports, + vec![ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule_ipv4.source_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::Ip("10.0.100.1".to_string())), + }, + ] + ); + + let web_allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!( + web_allow_rule_ipv6.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!(web_allow_rule_ipv6.protocols, [i32::from(Protocol::Tcp)]); + assert_eq!( + web_allow_rule_ipv6.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("fc00::/112".to_string())), + }] + ); + assert_eq!( + web_allow_rule_ipv6.destination_ports, + [ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule_ipv6.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::Ip("ff00::100:1".to_string())), + }, + ] + ); + + // First ACL - Web Access DENY + let web_deny_rule_ipv4 = &generated_firewall_rules[4]; + assert_eq!(web_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule_ipv4.protocols.is_empty()); + assert!(web_deny_rule_ipv4.destination_ports.is_empty()); + assert!(web_deny_rule_ipv4.source_addrs.is_empty()); + assert_eq!( + web_deny_rule_ipv4.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); + + let web_deny_rule_ipv6 = &generated_firewall_rules[5]; + assert_eq!(web_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule_ipv6.protocols.is_empty()); + assert!(web_deny_rule_ipv6.destination_ports.is_empty()); + assert!(web_deny_rule_ipv6.source_addrs.is_empty()); + assert_eq!( + web_deny_rule_ipv6.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("fc00::/112".to_string())), + }] + ); + + // Second ACL - DNS Access ALLOW + let dns_allow_rule_ipv4 = &generated_firewall_rules[2]; + assert_eq!( + dns_allow_rule_ipv4.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + dns_allow_rule_ipv4.protocols, + [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule_ipv4.destination_ports, + [Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule_ipv4.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.100.1".to_string(), + end: "10.0.100.2".to_string(), + })), + }, + ] + ); + + let expected_destination_addrs_v4 = vec![ + IpAddress { + address: Some(Address::Ip("10.0.1.13".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), + }, + ]; + + assert_eq!( + dns_allow_rule_ipv4.destination_addrs, + expected_destination_addrs_v4 + ); + + let dns_allow_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!( + dns_allow_rule_ipv6.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + dns_allow_rule_ipv6.protocols, + [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule_ipv6.destination_ports, + [Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule_ipv6.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::100:1".to_string(), + end: "ff00::100:2".to_string(), + })), + }, + ] + ); + + let expected_destination_addrs_v6 = vec![ + IpAddress { + address: Some(Address::Ip("fc00::1:13".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:54/126".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:58/125".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:60/123".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:80/121".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:100/120".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:200/119".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:400/118".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:800/117".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:1000/116".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:2000/115".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:4000/114".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:8000/113".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::2:0/122".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::2:40/126".to_string())), + }, + ]; + + assert_eq!( + dns_allow_rule_ipv6.destination_addrs, + expected_destination_addrs_v6 + ); + + // Second ACL - DNS Access DENY + let dns_deny_rule_ipv4 = &generated_firewall_rules[6]; + assert_eq!(dns_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule_ipv4.protocols.is_empty(),); + assert!(dns_deny_rule_ipv4.destination_ports.is_empty(),); + assert!(dns_deny_rule_ipv4.source_addrs.is_empty(),); + assert_eq!( + dns_deny_rule_ipv4.destination_addrs, + expected_destination_addrs_v4 + ); + + let dns_deny_rule_ipv6 = &generated_firewall_rules[7]; + assert_eq!(dns_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule_ipv6.protocols.is_empty(),); + assert!(dns_deny_rule_ipv6.destination_ports.is_empty(),); + assert!(dns_deny_rule_ipv6.source_addrs.is_empty(),); + assert_eq!( + dns_deny_rule_ipv6.destination_addrs, + expected_destination_addrs_v6 + ); +} + +#[sqlx::test] +async fn test_expired_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_expired_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_expired_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // Setup some test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rule + let acl_rule = AclRule { + id: NoId, + name: "test rule".to_string(), + expires: None, + enabled: true, + state: RuleState::Applied, + destination: vec!["192.168.1.0/24".parse().unwrap()], + allow_all_users: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // create different kinds of aliases and add them to the rule + let destination_alias = AclAlias { + id: NoId, + name: "destination alias".to_string(), + kind: AliasKind::Destination, + ports: vec![PortRange::new(100, 200).into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let component_alias = AclAlias { + id: NoId, + kind: AliasKind::Component, + destination: vec!["10.0.2.3".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + for alias in [&destination_alias, &component_alias] { + AclRuleAlias::new(acl_rule.id, alias.id) + .save(&pool) + .await + .unwrap(); + } + + // assign rule to location + AclRuleNetwork::new(acl_rule.id, location.id) + .save(&pool) + .await + .unwrap(); + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // check generated rules + assert_eq!(generated_firewall_rules.len(), 4); + let expected_source_addrs = [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + ]; + let expected_destination_addrs = [ + IpAddress { + address: Some(Address::Ip("10.0.2.3".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }, + ]; + + let allow_rule = &generated_firewall_rules[0]; + assert_eq!(allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule.source_addrs, expected_source_addrs); + assert_eq!(allow_rule.destination_addrs, expected_destination_addrs); + assert!(allow_rule.destination_ports.is_empty()); + assert!(allow_rule.protocols.is_empty()); + assert_eq!( + allow_rule.comment, + Some("ACL 1 - test rule ALLOW".to_string()) + ); + + let alias_allow_rule = &generated_firewall_rules[1]; + assert_eq!(alias_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(alias_allow_rule.source_addrs, expected_source_addrs); + assert!(alias_allow_rule.destination_addrs.is_empty()); + assert_eq!( + alias_allow_rule.destination_ports, + vec![Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 200, + })) + }] + ); + assert!(alias_allow_rule.protocols.is_empty()); + assert_eq!( + alias_allow_rule.comment, + Some("ACL 1 - test rule, ALIAS 1 - destination alias ALLOW".to_string()) + ); + + let deny_rule = &generated_firewall_rules[2]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule.source_addrs.is_empty()); + assert_eq!(deny_rule.destination_addrs, expected_destination_addrs); + assert!(deny_rule.destination_ports.is_empty()); + assert!(deny_rule.protocols.is_empty()); + assert_eq!( + deny_rule.comment, + Some("ACL 1 - test rule DENY".to_string()) + ); + + let alias_deny_rule = &generated_firewall_rules[3]; + assert_eq!(alias_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(alias_deny_rule.source_addrs.is_empty()); + assert!(alias_deny_rule.destination_addrs.is_empty()); + assert!(alias_deny_rule.destination_ports.is_empty()); + assert!(alias_deny_rule.protocols.is_empty()); + assert_eq!( + alias_deny_rule.comment, + Some("ACL 1 - test rule, ALIAS 1 - destination alias DENY".to_string()) + ); +} + +#[sqlx::test] +async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // Setup some test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{device_num}", user.id), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rule without manually configured destination + let acl_rule = AclRule { + id: NoId, + name: "test rule".to_string(), + expires: None, + enabled: true, + state: RuleState::Applied, + destination: Vec::new(), + allow_all_users: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // create different kinds of aliases and add them to the rule + let destination_alias_1 = AclAlias { + id: NoId, + name: "postgres".to_string(), + kind: AliasKind::Destination, + destination: vec!["10.0.2.3".parse().unwrap()], + ports: vec![PortRange::new(5432, 5432).into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let destination_alias_2 = AclAlias { + id: NoId, + name: "redis".to_string(), + kind: AliasKind::Destination, + destination: vec!["10.0.2.4".parse().unwrap()], + ports: vec![PortRange::new(6379, 6379).into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + for alias in [&destination_alias_1, &destination_alias_2] { + AclRuleAlias::new(acl_rule.id, alias.id) + .save(&pool) + .await + .unwrap(); + } + + // assign rule to location + AclRuleNetwork::new(acl_rule.id, location.id) + .save(&pool) + .await + .unwrap(); + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // check generated rules + assert_eq!(generated_firewall_rules.len(), 4); + let expected_source_addrs = vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + ]; + + let alias_allow_rule_1 = &generated_firewall_rules[0]; + assert_eq!(alias_allow_rule_1.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(alias_allow_rule_1.source_addrs, expected_source_addrs); + assert_eq!( + alias_allow_rule_1.destination_addrs, + vec![IpAddress { + address: Some(Address::Ip("10.0.2.3".to_string())), + },] + ); + assert_eq!( + alias_allow_rule_1.destination_ports, + vec![Port { + port: Some(PortInner::SinglePort(5432)) + }] + ); + assert!(alias_allow_rule_1.protocols.is_empty()); + assert_eq!( + alias_allow_rule_1.comment, + Some("ACL 1 - test rule, ALIAS 1 - postgres ALLOW".to_string()) + ); + + let alias_allow_rule_2 = &generated_firewall_rules[1]; + assert_eq!(alias_allow_rule_2.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(alias_allow_rule_2.source_addrs, expected_source_addrs); + assert_eq!( + alias_allow_rule_2.destination_addrs, + vec![IpAddress { + address: Some(Address::Ip("10.0.2.4".to_string())), + },] + ); + assert_eq!( + alias_allow_rule_2.destination_ports, + vec![Port { + port: Some(PortInner::SinglePort(6379)) + }] + ); + assert!(alias_allow_rule_2.protocols.is_empty()); + assert_eq!( + alias_allow_rule_2.comment, + Some("ACL 1 - test rule, ALIAS 2 - redis ALLOW".to_string()) + ); + + let alias_deny_rule_1 = &generated_firewall_rules[2]; + assert_eq!(alias_deny_rule_1.verdict, i32::from(FirewallPolicy::Deny)); + assert!(alias_deny_rule_1.source_addrs.is_empty()); + assert_eq!( + alias_deny_rule_1.destination_addrs, + vec![IpAddress { + address: Some(Address::Ip("10.0.2.3".to_string())), + },] + ); + assert!(alias_deny_rule_1.destination_ports.is_empty()); + assert!(alias_deny_rule_1.protocols.is_empty()); + assert_eq!( + alias_deny_rule_1.comment, + Some("ACL 1 - test rule, ALIAS 1 - postgres DENY".to_string()) + ); + + let alias_deny_rule_2 = &generated_firewall_rules[3]; + assert_eq!(alias_deny_rule_2.verdict, i32::from(FirewallPolicy::Deny)); + assert!(alias_deny_rule_2.source_addrs.is_empty()); + assert_eq!( + alias_deny_rule_2.destination_addrs, + vec![IpAddress { + address: Some(Address::Ip("10.0.2.4".to_string())), + },] + ); + assert!(alias_deny_rule_2.destination_ports.is_empty()); + assert!(alias_deny_rule_2.protocols.is_empty()); + assert_eq!( + alias_deny_rule_2.comment, + Some("ACL 1 - test rule, ALIAS 2 - redis DENY".to_string()) + ); +} diff --git a/flake.lock b/flake.lock index 49cfdf332e..fd5c915947 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770019141, - "narHash": "sha256-VKS4ZLNx4PNrABoB0L8KUpc1fE7CLpQXQs985tGfaCU=", + "lastModified": 1770115704, + "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cb369ef2efd432b3cdf8622b0ffc0a97a02f3137", + "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1770088046, - "narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=", + "lastModified": 1770174315, + "narHash": "sha256-GUaMxDmJB1UULsIYpHtfblskVC6zymAaQ/Zqfo+13jc=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "71f9daa4e05e49c434d08627e755495ae222bc34", + "rev": "095c394bb91342882f27f6c73f64064fb9de9f2a", "type": "github" }, "original": { From 6eaf02b0ea66cdc3dab9ce762c1afc5eaeee1120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 4 Feb 2026 12:41:49 +0100 Subject: [PATCH 05/25] split firewall test module --- .../src/enterprise/firewall/tests.rs | 3914 ----------------- .../enterprise/firewall/tests/destination.rs | 117 + .../firewall/tests/disabled_rules.rs | 213 + .../firewall/tests/expired_rules.rs | 12 + .../firewall/tests/ip_address_handling.rs | 500 +++ .../src/enterprise/firewall/tests/mod.rs | 3064 ++++--------- .../src/enterprise/firewall/tests/source.rs | 158 + .../firewall/tests/unapplied_rules.rs | 213 + 8 files changed, 2068 insertions(+), 6123 deletions(-) delete mode 100644 crates/defguard_core/src/enterprise/firewall/tests.rs create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/destination.rs create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/ip_address_handling.rs create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/source.rs create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs deleted file mode 100644 index f182e8e3bf..0000000000 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ /dev/null @@ -1,3914 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - -use chrono::{DateTime, NaiveDateTime}; -use defguard_common::db::{ - Id, NoId, - models::{ - Device, DeviceType, WireguardNetwork, device::WireguardNetworkDevice, group::Group, - user::User, - }, - setup_pool, -}; -use defguard_proto::enterprise::firewall::{ - FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, - ip_address::Address, port::Port as PortInner, -}; -use ipnetwork::{IpNetwork, Ipv6Network}; -use rand::{Rng, thread_rng}; -use sqlx::{ - PgPool, - postgres::{PgConnectOptions, PgPoolOptions}, - query, -}; - -use super::{ - find_largest_subnet_in_range, get_last_ip_in_v6_subnet, get_source_users, merge_addrs, - merge_port_ranges, process_destination_addrs, -}; -use crate::enterprise::{ - db::models::acl::{ - AclAlias, AclRule, AclRuleAlias, AclRuleDestinationRange, AclRuleDevice, AclRuleGroup, - AclRuleInfo, AclRuleNetwork, AclRuleUser, AliasKind, PortRange, RuleState, - }, - firewall::{get_source_addrs, get_source_network_devices, try_get_location_firewall_config}, -}; - -impl Default for AclRuleDestinationRange { - fn default() -> Self { - Self { - id: Id::default(), - rule_id: Id::default(), - start: IpAddr::V4(Ipv4Addr::UNSPECIFIED), - end: IpAddr::V4(Ipv4Addr::UNSPECIFIED), - } - } -} - -fn random_user_with_id(rng: &mut R, id: Id) -> User { - let mut user: User = rng.r#gen(); - user.id = id; - user -} - -fn random_network_device_with_id(rng: &mut R, id: Id) -> Device { - let device: Device = rng.r#gen(); - let mut device = device.with_id(id); - device.device_type = DeviceType::Network; - device -} - -#[test] -fn test_get_relevant_users() { - let mut rng = thread_rng(); - - // prepare allowed and denied users lists with shared elements - let user_1 = random_user_with_id(&mut rng, 1); - let user_2 = random_user_with_id(&mut rng, 2); - let user_3 = random_user_with_id(&mut rng, 3); - let user_4 = random_user_with_id(&mut rng, 4); - let user_5 = random_user_with_id(&mut rng, 5); - let allowed_users = vec![user_1.clone(), user_2.clone(), user_4.clone()]; - let denied_users = vec![user_3.clone(), user_4, user_5.clone()]; - - let users = get_source_users(allowed_users, &denied_users); - assert_eq!(users, vec![user_1, user_2]); -} - -#[test] -fn test_get_relevant_network_devices() { - let mut rng = thread_rng(); - - // prepare allowed and denied network devices lists with shared elements - let device_1 = random_network_device_with_id(&mut rng, 1); - let device_2 = random_network_device_with_id(&mut rng, 2); - let device_3 = random_network_device_with_id(&mut rng, 3); - let device_4 = random_network_device_with_id(&mut rng, 4); - let device_5 = random_network_device_with_id(&mut rng, 5); - let allowed_devices = vec![ - device_1.clone(), - device_3.clone(), - device_4.clone(), - device_5.clone(), - ]; - let denied_devices = vec![device_2.clone(), device_4, device_5.clone()]; - - let devices = get_source_network_devices(allowed_devices, &denied_devices); - assert_eq!(devices, vec![device_1, device_3]); -} - -#[test] -fn test_process_source_addrs_v4() { - // Test data with mixed IPv4 and IPv6 addresses - let user_device_ips = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 2)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 5)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), - ]; - - let network_device_ips = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 3)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 4)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), // Should be filtered out - IpAddr::V4(Ipv4Addr::new(172, 16, 1, 1)), - ]; - - let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv4); - - // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges - assert_eq!( - source_addrs, - [ - IpAddress { - address: Some(Address::Ip("10.0.1.1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.2/31".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.4/31".to_string())) - }, - IpAddress { - address: Some(Address::Ip("172.16.1.1".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.1.100".to_string())), - }, - ] - ); - - // Test with empty input - let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv4); - assert!(empty_addrs.is_empty()); - - // Test with only IPv6 addresses - should return empty result for IPv4 - let ipv6_only = get_source_addrs( - vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))], - vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2))], - IpVersion::Ipv4, - ); - assert!(ipv6_only.is_empty()); -} - -#[test] -fn test_process_source_addrs_v6() { - // Test data with mixed IPv4 and IPv6 addresses - let user_device_ips = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 5)), - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), // Should be filtered out - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 1)), - ]; - - let network_device_ips = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 3)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 4)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), // Should be filtered out - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 2, 0, 0, 0, 1)), - ]; - - let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv6); - - // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges - assert_eq!( - source_addrs, - [ - IpAddress { - address: Some(Address::Ip("2001:db8::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::2/127".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::4/127".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:0:1::1".to_string())), - }, - IpAddress { - address: Some(Address::Ip("2001:db8:0:2::1".to_string())), - }, - ] - ); - - // Test with empty input - let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv6); - assert!(empty_addrs.is_empty()); - - // Test with only IPv4 addresses - should return empty result for IPv6 - let ipv4_only = get_source_addrs( - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))], - IpVersion::Ipv6, - ); - assert!(ipv4_only.is_empty()); -} - -#[test] -fn test_process_destination_addrs_v4() { - // Test data with mixed IPv4 and IPv6 networks - let destination_ips = [ - "10.0.1.0/24".parse().unwrap(), - "10.0.2.0/24".parse().unwrap(), - "2001:db8::/64".parse().unwrap(), // Should be filtered out - "192.168.1.0/24".parse().unwrap(), - ]; - - let destination_ranges = [ - AclRuleDestinationRange { - start: IpAddr::V4(Ipv4Addr::new(10, 0, 3, 255)), - end: IpAddr::V4(Ipv4Addr::new(10, 0, 4, 0)), - ..Default::default() - }, - AclRuleDestinationRange { - start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out - end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 100)), - ..Default::default() - }, - ]; - - let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); - - assert_eq!( - destination_addrs.0, - [ - IpAddress { - address: Some(Address::IpSubnet("10.0.1.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.3.255".to_string(), - end: "10.0.4.0".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }, - ] - ); - - // Test with empty input - let empty_addrs = process_destination_addrs(&[], &[]); - assert!(empty_addrs.0.is_empty()); - - // Test with only IPv6 addresses - should return empty result for IPv4 - let ipv6_only = process_destination_addrs(&["2001:db8::/64".parse().unwrap()], &[]); - assert!(ipv6_only.0.is_empty()); -} - -#[test] -fn test_process_destination_addrs_v6() { - // Test data with mixed IPv4 and IPv6 networks - let destination_ips = vec![ - "2001:db8:1::/64".parse().unwrap(), - "2001:db8:2::/64".parse().unwrap(), - "10.0.1.0/24".parse().unwrap(), // Should be filtered out - "2001:db8:3::/64".parse().unwrap(), - ]; - - let destination_ranges = vec![ - AclRuleDestinationRange { - start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 1)), - end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 3)), - ..Default::default() - }, - AclRuleDestinationRange { - start: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), // Should be filtered out - end: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), - ..Default::default() - }, - ]; - - let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); - - assert_eq!( - destination_addrs.1, - [ - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::/64".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:2::/64".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:3::/64".to_string())), - }, - IpAddress { - address: Some(Address::Ip("2001:db8:4::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:4::2/127".to_string())) - } - ] - ); - - // Test with empty input - let empty_addrs = process_destination_addrs(&[], &[]); - assert!(empty_addrs.1.is_empty()); - - // Test with only IPv4 addresses - should return empty result for IPv6 - let ipv4_only = process_destination_addrs(&["192.168.1.0/24".parse().unwrap()], &[]); - assert!(ipv4_only.1.is_empty()); -} - -#[test] -fn test_merge_v4_addrs() { - let addr_ranges = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 60, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 60, 25)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 22)), - IpAddr::V4(Ipv4Addr::new(10, 0, 8, 127))..=IpAddr::V4(Ipv4Addr::new(10, 0, 9, 12)), - IpAddr::V4(Ipv4Addr::new(10, 0, 9, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 12)), - IpAddr::V4(Ipv4Addr::new(10, 0, 9, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 31)), - IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20))..=IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20)), - IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20)), - ]; - - let merged_addrs = merge_addrs(addr_ranges); - - assert_eq!( - merged_addrs, - [ - IpAddress { - address: Some(Address::Ip("10.0.8.127".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.8.128/25".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.9.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.10.0/27".to_string())), - }, - IpAddress { - address: Some(Address::Ip("10.0.20.20".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.60.20/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.60.24/31".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.0.20".to_string())), - }, - ] - ); - - // merge single IPs into a range - let addr_ranges = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20)), - ]; - - let merged_addrs = merge_addrs(addr_ranges); - assert_eq!( - merged_addrs, - [ - IpAddress { - address: Some(Address::IpSubnet("10.0.10.0/30".to_string())), - }, - IpAddress { - address: Some(Address::Ip("10.0.10.20".to_string())), - }, - ] - ); -} - -#[test] -fn test_merge_v6_addrs() { - let addr_ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x5)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x3)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x8)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x3)), - ]; - - let merged_addrs = merge_addrs(addr_ranges); - assert_eq!( - merged_addrs, - [ - IpAddress { - address: Some(Address::Ip("2001:db8:1::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::2/127".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::4/126".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:1::8".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:2::1".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:3::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:3::2/127".to_string())) - } - ] - ); -} - -#[test] -fn test_merge_addrs_extracts_ipv4_subnets() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.2.0/24".to_string())) - }, - ] - ); -} - -#[test] -fn test_merge_addrs_extracts_ipv6_subnets() { - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db9::ffff".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::IpSubnet("2001:db8::/32".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db9::/112".to_string())) - }, - ] - ); -} - -#[test] -fn test_merge_addrs_falls_back_to_range_when_no_subnet_fits() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 0)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [IpAddress { - address: Some(Address::IpRange(IpRange { - start: "192.168.1.255".to_string(), - end: "192.168.2.0".to_string(), - })), - },] - ); - - let start = "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff" - .parse::() - .unwrap(); - let end = "2001:db9::".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [IpAddress { - address: Some(Address::IpRange(IpRange { - start: "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff".to_string(), - end: "2001:db9::".to_string(), - })), - },] - ); -} - -#[test] -fn test_merge_addrs_handles_single_ip() { - // Test case: single IP should remain as IP - let ranges = - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [IpAddress { - address: Some(Address::Ip("192.168.1.1".to_string())), - },] - ); - - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db8::".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [IpAddress { - address: Some(Address::Ip("2001:db8::".to_string())), - },] - ); -} - -#[test] -fn test_find_largest_ipv4_subnet_perfect_match() { - // Test /24 subnet - let start = Ipv4Addr::new(192, 168, 1, 0); - let end = Ipv4Addr::new(192, 168, 1, 255); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "192.168.1.0/24"); - - // Test /28 subnet (16 addresses) - let start = Ipv4Addr::new(192, 168, 1, 0); - let end = Ipv4Addr::new(192, 168, 1, 15); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "192.168.1.0/28"); -} - -#[test] -fn test_find_largest_ipv6_subnet_perfect_match() { - // Test /112 subnet - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db8::ffff".parse::().unwrap(); - - let result = find_largest_subnet_in_range(IpAddr::V6(start), IpAddr::V6(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "2001:db8::/112"); -} - -#[test] -fn test_find_largest_subnet_mixed_ip_versions() { - // Test mixed IP versions should return None - let start = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)); - let end = IpAddr::V6("2001:db8::1".parse().unwrap()); - - let result = find_largest_subnet_in_range(start, end); - - assert!(result.is_none()); -} - -#[test] -fn test_find_largest_subnet_invalid_range() { - // Test invalid range (start > end) should return None - let start = Ipv4Addr::new(192, 168, 1, 10); - let end = Ipv4Addr::new(192, 168, 1, 5); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_none()); -} - -#[test] -fn test_merge_addrs_subnet_at_start_of_range() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 64)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/26".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.1.64".to_string())), - }, - ] - ); - - let ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x40)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::IpSubnet("2001:db8::/122".to_string())), - }, - IpAddress { - address: Some(Address::Ip("2001:db8::40".to_string())), - }, - ] - ); -} - -#[test] -fn test_merge_addrs_subnet_at_end_of_range() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 15))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 31)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::Ip("192.168.1.15".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.1.16/28".to_string())), - }, - ] - ); - - let ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x0f)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1f)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::Ip("2001:db8::f".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::10/124".to_string())), - }, - ] - ); -} - -#[test] -fn test_merge_port_ranges() { - // single port - let input_ranges = vec![PortRange::new(100, 100)]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::SinglePort(100)) - }] - ); - - // overlapping ranges - let input_ranges = vec![ - PortRange::new(100, 200), - PortRange::new(150, 220), - PortRange::new(210, 300), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 300 - })) - }] - ); - - // duplicate ranges - let input_ranges = vec![ - PortRange::new(100, 200), - PortRange::new(100, 200), - PortRange::new(150, 220), - PortRange::new(150, 220), - PortRange::new(210, 300), - PortRange::new(210, 300), - PortRange::new(350, 400), - PortRange::new(350, 400), - PortRange::new(350, 400), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [ - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 300 - })) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 350, - end: 400 - })) - } - ] - ); - - // non-consecutive ranges - let input_ranges = vec![ - PortRange::new(501, 699), - PortRange::new(151, 220), - PortRange::new(210, 300), - PortRange::new(800, 800), - PortRange::new(200, 210), - PortRange::new(50, 50), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [ - Port { - port: Some(PortInner::SinglePort(50)) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 151, - end: 300 - })) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 501, - end: 699 - })) - }, - Port { - port: Some(PortInner::SinglePort(800)) - } - ] - ); - - // fully contained range - let input_ranges = vec![PortRange::new(100, 200), PortRange::new(120, 180)]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 200 - })) - }] - ); -} - -#[test] -fn test_last_ip_in_v6_subnet() { - let subnet: Ipv6Network = "2001:db8:85a3::8a2e:370:7334/64".parse().unwrap(); - let last_ip = get_last_ip_in_v6_subnet(&subnet); - assert_eq!( - last_ip, - IpAddr::V6(Ipv6Addr::new( - 0x2001, 0x0db8, 0x85a3, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff - )) - ); - - let subnet: Ipv6Network = "280b:47f8:c9d7:634c:cb35:11f3:14e1:5016/119" - .parse() - .unwrap(); - let last_ip = get_last_ip_in_v6_subnet(&subnet); - assert_eq!( - last_ip, - IpAddr::V6(Ipv6Addr::new( - 0x280b, 0x47f8, 0xc9d7, 0x634c, 0xcb35, 0x11f3, 0x14e1, 0x51ff - )) - ); -} - -async fn create_acl_rule( - pool: &PgPool, - rule: AclRule, - locations: Vec, - allowed_users: Vec, - denied_users: Vec, - allowed_groups: Vec, - denied_groups: Vec, - allowed_network_devices: Vec, - denied_network_devices: Vec, - destination_ranges: Vec<(IpAddr, IpAddr)>, - aliases: Vec, -) -> AclRuleInfo { - let mut conn = pool.acquire().await.unwrap(); - - // create base rule - let rule = rule.save(&mut *conn).await.unwrap(); - let rule_id = rule.id; - - // create related objects - // locations - for location_id in locations { - AclRuleNetwork::new(rule_id, location_id) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed users - for user_id in allowed_users { - AclRuleUser::new(rule_id, user_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied users - for user_id in denied_users { - AclRuleUser::new(rule_id, user_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed groups - for group_id in allowed_groups { - AclRuleGroup::new(rule_id, group_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied groups - for group_id in denied_groups { - AclRuleGroup::new(rule_id, group_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed devices - for device_id in allowed_network_devices { - AclRuleDevice::new(rule_id, device_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied devices - for device_id in denied_network_devices { - AclRuleDevice::new(rule_id, device_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // destination ranges - for range in destination_ranges { - AclRuleDestinationRange { - id: NoId, - rule_id, - start: range.0, - end: range.1, - } - .save(&mut *conn) - .await - .unwrap(); - } - - // aliases - for alias_id in aliases { - AclRuleAlias::new(rule_id, alias_id) - .save(&mut *conn) - .await - .unwrap(); - } - - // convert to output format - rule.to_info(&mut conn).await.unwrap() -} - -#[sqlx::test] -async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: false, - ..Default::default() - }; - let mut location = location.save(&pool).await.unwrap(); - - // Setup test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - let user_3: User = rng.r#gen(); - let user_3 = user_3.save(&pool).await.unwrap(); - let user_4: User = rng.r#gen(); - let user_4 = user_4.save(&pool).await.unwrap(); - let user_5: User = rng.r#gen(); - let user_5 = user_5.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // Setup test groups - let group_1 = Group { - id: NoId, - name: "group_1".into(), - ..Default::default() - }; - let group_1 = group_1.save(&pool).await.unwrap(); - let group_2 = Group { - id: NoId, - name: "group_2".into(), - ..Default::default() - }; - let group_2 = group_2.save(&pool).await.unwrap(); - - // Assign users to groups: - // Group 1: users 1,2 - // Group 2: users 3,4 - let group_assignments = vec![ - (&group_1, vec![&user_1, &user_2]), - (&group_2, vec![&user_3, &user_4]), - ]; - - for (group, users) in group_assignments { - for user in users { - query!( - "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", - user.id, - group.id - ) - .execute(&pool) - .await - .unwrap(); - } - } - - // Create some network devices - let network_device_1 = Device { - id: NoId, - name: "network-device-1".into(), - user_id: user_1.id, // Owned by user 1 - device_type: DeviceType::Network, - description: Some("Test network device 1".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_1 = network_device_1.save(&pool).await.unwrap(); - - let network_device_2 = Device { - id: NoId, - name: "network-device-2".into(), - user_id: user_2.id, // Owned by user 2 - device_type: DeviceType::Network, - description: Some("Test network device 2".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_2 = network_device_2.save(&pool).await.unwrap(); - - let network_device_3 = Device { - id: NoId, - name: "network-device-3".into(), - user_id: user_3.id, // Owned by user 3 - device_type: DeviceType::Network, - description: Some("Test network device 3".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_3 = network_device_3.save(&pool).await.unwrap(); - - // Add network devices to location's VPN network - let network_devices = vec![ - ( - network_device_1.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), - ), - ( - network_device_2.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), - ), - ( - network_device_3.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), - ), - ]; - - for (device_id, ip) in network_devices { - let network_device = WireguardNetworkDevice { - device_id, - wireguard_network_id: location.id, - wireguard_ips: vec![ip], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - - // Create first ACL rule - Web access - let acl_rule_1 = AclRule { - id: NoId, - name: "Web Access".into(), - all_networks: false, - expires: None, - allow_all_users: false, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: vec!["192.168.1.0/24".parse().unwrap()], - ports: vec![ - PortRange::new(80, 80).into(), - PortRange::new(443, 443).into(), - ], - protocols: vec![Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations = vec![location.id]; - let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web - let denied_users = vec![user_3.id]; // Third user explicitly denied - let allowed_groups = vec![group_1.id]; // First group allowed - let denied_groups = Vec::new(); - let allowed_devices = vec![network_device_1.id]; - let denied_devices = vec![network_device_2.id, network_device_3.id]; - let destination_ranges = Vec::new(); - let aliases = Vec::new(); - - let _acl_rule_1 = create_acl_rule( - &pool, - acl_rule_1, - locations, - allowed_users, - denied_users, - allowed_groups, - denied_groups, - allowed_devices, - denied_devices, - destination_ranges, - aliases, - ) - .await; - - // Create second ACL rule - DNS access - let acl_rule_2 = AclRule { - id: NoId, - name: "DNS Access".into(), - all_networks: false, - expires: None, - allow_all_users: true, // Allow all users - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead - ports: vec![PortRange::new(53, 53).into()], - protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations_2 = vec![location.id]; - let allowed_users_2 = Vec::new(); - let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS - let allowed_groups_2 = Vec::new(); - let denied_groups_2 = vec![group_2.id]; - let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed - let denied_devices_2 = vec![network_device_3.id]; // Third network device denied - let destination_ranges_2 = vec![ - ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), - ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), - ]; - let aliases_2 = Vec::new(); - - let _acl_rule_2 = create_acl_rule( - &pool, - acl_rule_2, - locations_2, - allowed_users_2, - denied_users_2, - allowed_groups_2, - denied_groups_2, - allowed_devices_2, - denied_devices_2, - destination_ranges_2, - aliases_2, - ) - .await; - - let mut conn = pool.acquire().await.unwrap(); - - // try to generate firewall config with ACL disabled - location.acl_enabled = false; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap(); - assert!(generated_firewall_config.is_none()); - - // generate firewall config with default policy Allow - location.acl_enabled = true; - location.acl_default_allow = true; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap(); - assert_eq!( - generated_firewall_config.default_policy, - i32::from(FirewallPolicy::Allow) - ); - - let generated_firewall_rules = generated_firewall_config.rules; - - assert_eq!(generated_firewall_rules.len(), 4); - - // First ACL - Web Access ALLOW - let web_allow_rule = &generated_firewall_rules[0]; - assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); - assert_eq!( - web_allow_rule.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }] - ); - assert_eq!( - web_allow_rule.destination_ports, - [ - Port { - port: Some(PortInner::SinglePort(80)) - }, - Port { - port: Some(PortInner::SinglePort(443)) - } - ] - ); - // Source addresses should include devices of users 1,2 and network_device_1 - assert_eq!( - web_allow_rule.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::Ip("10.0.100.1".to_string())), - }, - ] - ); - - // First ACL - Web Access DENY - let web_deny_rule = &generated_firewall_rules[2]; - assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule.protocols.is_empty()); - assert!(web_deny_rule.destination_ports.is_empty()); - assert!(web_deny_rule.source_addrs.is_empty()); - assert_eq!( - web_deny_rule.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }] - ); - - // Second ACL - DNS Access ALLOW - let dns_allow_rule = &generated_firewall_rules[1]; - assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!( - dns_allow_rule.protocols, - [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] - ); - assert_eq!( - dns_allow_rule.destination_ports, - [Port { - port: Some(PortInner::SinglePort(53)) - }] - ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.100.1".to_string(), - end: "10.0.100.2".to_string(), - })), - }, - ] - ); - - let expected_destination_addrs = vec![ - IpAddress { - address: Some(Address::Ip("10.0.1.13".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), - }, - ]; - - assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); - - // Second ACL - DNS Access DENY - let dns_deny_rule = &generated_firewall_rules[3]; - assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule.protocols.is_empty(),); - assert!(dns_deny_rule.destination_ports.is_empty(),); - assert!(dns_deny_rule.source_addrs.is_empty(),); - assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); -} - -#[sqlx::test] -async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: false, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let mut location = location.save(&pool).await.unwrap(); - - // Setup test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - let user_3: User = rng.r#gen(); - let user_3 = user_3.save(&pool).await.unwrap(); - let user_4: User = rng.r#gen(); - let user_4 = user_4.save(&pool).await.unwrap(); - let user_5: User = rng.r#gen(); - let user_5 = user_5.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // Setup test groups - let group_1 = Group { - id: NoId, - name: "group_1".into(), - ..Default::default() - }; - let group_1 = group_1.save(&pool).await.unwrap(); - let group_2 = Group { - id: NoId, - name: "group_2".into(), - ..Default::default() - }; - let group_2 = group_2.save(&pool).await.unwrap(); - - // Assign users to groups: - // Group 1: users 1,2 - // Group 2: users 3,4 - let group_assignments = vec![ - (&group_1, vec![&user_1, &user_2]), - (&group_2, vec![&user_3, &user_4]), - ]; - - for (group, users) in group_assignments { - for user in users { - query!( - "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", - user.id, - group.id - ) - .execute(&pool) - .await - .unwrap(); - } - } - - // Create some network devices - let network_device_1 = Device { - id: NoId, - name: "network-device-1".into(), - user_id: user_1.id, // Owned by user 1 - device_type: DeviceType::Network, - description: Some("Test network device 1".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_1 = network_device_1.save(&pool).await.unwrap(); - - let network_device_2 = Device { - id: NoId, - name: "network-device-2".into(), - user_id: user_2.id, // Owned by user 2 - device_type: DeviceType::Network, - description: Some("Test network device 2".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_2 = network_device_2.save(&pool).await.unwrap(); - - let network_device_3 = Device { - id: NoId, - name: "network-device-3".into(), - user_id: user_3.id, // Owned by user 3 - device_type: DeviceType::Network, - description: Some("Test network device 3".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_3 = network_device_3.save(&pool).await.unwrap(); - - // Add network devices to location's VPN network - let network_devices = vec![ - ( - network_device_1.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), - ), - ( - network_device_2.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), - ), - ( - network_device_3.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), - ), - ]; - - for (device_id, ip) in network_devices { - let network_device = WireguardNetworkDevice { - device_id, - wireguard_network_id: location.id, - wireguard_ips: vec![ip], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - - // Create first ACL rule - Web access - let acl_rule_1 = AclRule { - id: NoId, - name: "Web Access".into(), - all_networks: false, - expires: None, - allow_all_users: false, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: vec!["fc00::0/112".parse().unwrap()], - ports: vec![ - PortRange::new(80, 80).into(), - PortRange::new(443, 443).into(), - ], - protocols: vec![Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations = vec![location.id]; - let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web - let denied_users = vec![user_3.id]; // Third user explicitly denied - let allowed_groups = vec![group_1.id]; // First group allowed - let denied_groups = Vec::new(); - let allowed_devices = vec![network_device_1.id]; - let denied_devices = vec![network_device_2.id, network_device_3.id]; - let destination_ranges = Vec::new(); - let aliases = Vec::new(); - - let _acl_rule_1 = create_acl_rule( - &pool, - acl_rule_1, - locations, - allowed_users, - denied_users, - allowed_groups, - denied_groups, - allowed_devices, - denied_devices, - destination_ranges, - aliases, - ) - .await; - - // Create second ACL rule - DNS access - let acl_rule_2 = AclRule { - id: NoId, - name: "DNS Access".into(), - all_networks: false, - expires: None, - allow_all_users: true, // Allow all users - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead - ports: vec![PortRange::new(53, 53).into()], - protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations_2 = vec![location.id]; - let allowed_users_2 = Vec::new(); - let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS - let allowed_groups_2 = Vec::new(); - let denied_groups_2 = vec![group_2.id]; - let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed - let denied_devices_2 = vec![network_device_3.id]; // Third network device denied - let destination_ranges_2 = vec![ - ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), - ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), - ]; - let aliases_2 = Vec::new(); - - let _acl_rule_2 = create_acl_rule( - &pool, - acl_rule_2, - locations_2, - allowed_users_2, - denied_users_2, - allowed_groups_2, - denied_groups_2, - allowed_devices_2, - denied_devices_2, - destination_ranges_2, - aliases_2, - ) - .await; - - let mut conn = pool.acquire().await.unwrap(); - - // try to generate firewall config with ACL disabled - location.acl_enabled = false; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap(); - assert!(generated_firewall_config.is_none()); - - // generate firewall config with default policy Allow - location.acl_enabled = true; - location.acl_default_allow = true; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap(); - assert_eq!( - generated_firewall_config.default_policy, - i32::from(FirewallPolicy::Allow) - ); - - let generated_firewall_rules = generated_firewall_config.rules; - - assert_eq!(generated_firewall_rules.len(), 4); - - // First ACL - Web Access ALLOW - let web_allow_rule = &generated_firewall_rules[0]; - assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); - assert_eq!( - web_allow_rule.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("fc00::/112".to_string())), - }] - ); - assert_eq!( - web_allow_rule.destination_ports, - [ - Port { - port: Some(PortInner::SinglePort(80)) - }, - Port { - port: Some(PortInner::SinglePort(443)) - } - ] - ); - // Source addresses should include devices of users 1,2 and network_device_1 - assert_eq!( - web_allow_rule.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::Ip("ff00::100:1".to_string())), - }, - ] - ); - - // First ACL - Web Access DENY - let web_deny_rule = &generated_firewall_rules[2]; - assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule.protocols.is_empty()); - assert!(web_deny_rule.destination_ports.is_empty()); - assert!(web_deny_rule.source_addrs.is_empty()); - assert_eq!( - web_deny_rule.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("fc00::/112".to_string())), - }] - ); - - // Second ACL - DNS Access ALLOW - let dns_allow_rule = &generated_firewall_rules[1]; - assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!( - dns_allow_rule.protocols, - [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] - ); - assert_eq!( - dns_allow_rule.destination_ports, - [Port { - port: Some(PortInner::SinglePort(53)) - }] - ); - - let expected_destination_addrs = vec![ - IpAddress { - address: Some(Address::Ip("fc00::1:13".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:54/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:58/125".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:60/123".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:80/121".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:100/120".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:200/119".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:400/118".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:800/117".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:1000/116".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:2000/115".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:4000/114".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:8000/113".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::2:0/122".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::2:40/126".to_string())), - }, - ]; - - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::100:1".to_string(), - end: "ff00::100:2".to_string(), - })), - }, - ] - ); - assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); - - // Second ACL - DNS Access DENY - let dns_deny_rule = &generated_firewall_rules[3]; - assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule.protocols.is_empty(),); - assert!(dns_deny_rule.destination_ports.is_empty(),); - assert!(dns_deny_rule.source_addrs.is_empty(),); - assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); -} - -#[sqlx::test] -async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: false, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let mut location = location.save(&pool).await.unwrap(); - - // Setup test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - let user_3: User = rng.r#gen(); - let user_3 = user_3.save(&pool).await.unwrap(); - let user_4: User = rng.r#gen(); - let user_4 = user_4.save(&pool).await.unwrap(); - let user_5: User = rng.r#gen(); - let user_5 = user_5.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // Setup test groups - let group_1 = Group { - id: NoId, - name: "group_1".into(), - ..Default::default() - }; - let group_1 = group_1.save(&pool).await.unwrap(); - let group_2 = Group { - id: NoId, - name: "group_2".into(), - ..Default::default() - }; - let group_2 = group_2.save(&pool).await.unwrap(); - - // Assign users to groups: - // Group 1: users 1,2 - // Group 2: users 3,4 - let group_assignments = vec![ - (&group_1, vec![&user_1, &user_2]), - (&group_2, vec![&user_3, &user_4]), - ]; - - for (group, users) in group_assignments { - for user in users { - query!( - "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", - user.id, - group.id - ) - .execute(&pool) - .await - .unwrap(); - } - } - - // Create some network devices - let network_device_1 = Device { - id: NoId, - name: "network-device-1".into(), - user_id: user_1.id, // Owned by user 1 - device_type: DeviceType::Network, - description: Some("Test network device 1".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_1 = network_device_1.save(&pool).await.unwrap(); - - let network_device_2 = Device { - id: NoId, - name: "network-device-2".into(), - user_id: user_2.id, // Owned by user 2 - device_type: DeviceType::Network, - description: Some("Test network device 2".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_2 = network_device_2.save(&pool).await.unwrap(); - - let network_device_3 = Device { - id: NoId, - name: "network-device-3".into(), - user_id: user_3.id, // Owned by user 3 - device_type: DeviceType::Network, - description: Some("Test network device 3".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_3 = network_device_3.save(&pool).await.unwrap(); - - // Add network devices to location's VPN network - let network_devices = vec![ - ( - network_device_1.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), - ], - ), - ( - network_device_2.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), - ], - ), - ( - network_device_3.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), - ], - ), - ]; - - for (device_id, ips) in network_devices { - let network_device = WireguardNetworkDevice { - device_id, - wireguard_network_id: location.id, - wireguard_ips: ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - - // Create first ACL rule - Web access - let acl_rule_1 = AclRule { - id: NoId, - name: "Web Access".into(), - all_networks: false, - expires: None, - allow_all_users: false, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: vec![ - "192.168.1.0/24".parse().unwrap(), - "fc00::0/112".parse().unwrap(), - ], - ports: vec![ - PortRange::new(80, 80).into(), - PortRange::new(443, 443).into(), - ], - protocols: vec![Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations = vec![location.id]; - let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web - let denied_users = vec![user_3.id]; // Third user explicitly denied - let allowed_groups = vec![group_1.id]; // First group allowed - let denied_groups = Vec::new(); - let allowed_devices = vec![network_device_1.id]; - let denied_devices = vec![network_device_2.id, network_device_3.id]; - let destination_ranges = Vec::new(); - let aliases = Vec::new(); - - let _acl_rule_1 = create_acl_rule( - &pool, - acl_rule_1, - locations, - allowed_users, - denied_users, - allowed_groups, - denied_groups, - allowed_devices, - denied_devices, - destination_ranges, - aliases, - ) - .await; - - // Create second ACL rule - DNS access - let acl_rule_2 = AclRule { - id: NoId, - name: "DNS Access".into(), - all_networks: false, - expires: None, - allow_all_users: true, // Allow all users - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead - ports: vec![PortRange::new(53, 53).into()], - protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations_2 = vec![location.id]; - let allowed_users_2 = Vec::new(); - let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS - let allowed_groups_2 = Vec::new(); - let denied_groups_2 = vec![group_2.id]; - let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed - let denied_devices_2 = vec![network_device_3.id]; // Third network device denied - let destination_ranges_2 = vec![ - ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), - ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), - ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), - ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), - ]; - let aliases_2 = Vec::new(); - - let _acl_rule_2 = create_acl_rule( - &pool, - acl_rule_2, - locations_2, - allowed_users_2, - denied_users_2, - allowed_groups_2, - denied_groups_2, - allowed_devices_2, - denied_devices_2, - destination_ranges_2, - aliases_2, - ) - .await; - - let mut conn = pool.acquire().await.unwrap(); - - // try to generate firewall config with ACL disabled - location.acl_enabled = false; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap(); - assert!(generated_firewall_config.is_none()); - - // generate firewall config with default policy Allow - location.acl_enabled = true; - location.acl_default_allow = true; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap(); - assert_eq!( - generated_firewall_config.default_policy, - i32::from(FirewallPolicy::Allow) - ); - - let generated_firewall_rules = generated_firewall_config.rules; - - assert_eq!(generated_firewall_rules.len(), 8); - - // First ACL - Web Access ALLOW - let web_allow_rule_ipv4 = &generated_firewall_rules[0]; - assert_eq!( - web_allow_rule_ipv4.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - web_allow_rule_ipv4.protocols, - vec![i32::from(Protocol::Tcp)] - ); - assert_eq!( - web_allow_rule_ipv4.destination_addrs, - vec![IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }] - ); - assert_eq!( - web_allow_rule_ipv4.destination_ports, - vec![ - Port { - port: Some(PortInner::SinglePort(80)) - }, - Port { - port: Some(PortInner::SinglePort(443)) - } - ] - ); - // Source addresses should include devices of users 1,2 and network_device_1 - assert_eq!( - web_allow_rule_ipv4.source_addrs, - vec![ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::Ip("10.0.100.1".to_string())), - }, - ] - ); - - let web_allow_rule_ipv6 = &generated_firewall_rules[1]; - assert_eq!( - web_allow_rule_ipv6.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!(web_allow_rule_ipv6.protocols, [i32::from(Protocol::Tcp)]); - assert_eq!( - web_allow_rule_ipv6.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("fc00::/112".to_string())), - }] - ); - assert_eq!( - web_allow_rule_ipv6.destination_ports, - [ - Port { - port: Some(PortInner::SinglePort(80)) - }, - Port { - port: Some(PortInner::SinglePort(443)) - } - ] - ); - // Source addresses should include devices of users 1,2 and network_device_1 - assert_eq!( - web_allow_rule_ipv6.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::Ip("ff00::100:1".to_string())), - }, - ] - ); - - // First ACL - Web Access DENY - let web_deny_rule_ipv4 = &generated_firewall_rules[4]; - assert_eq!(web_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule_ipv4.protocols.is_empty()); - assert!(web_deny_rule_ipv4.destination_ports.is_empty()); - assert!(web_deny_rule_ipv4.source_addrs.is_empty()); - assert_eq!( - web_deny_rule_ipv4.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }] - ); - - let web_deny_rule_ipv6 = &generated_firewall_rules[5]; - assert_eq!(web_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule_ipv6.protocols.is_empty()); - assert!(web_deny_rule_ipv6.destination_ports.is_empty()); - assert!(web_deny_rule_ipv6.source_addrs.is_empty()); - assert_eq!( - web_deny_rule_ipv6.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("fc00::/112".to_string())), - }] - ); - - // Second ACL - DNS Access ALLOW - let dns_allow_rule_ipv4 = &generated_firewall_rules[2]; - assert_eq!( - dns_allow_rule_ipv4.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - dns_allow_rule_ipv4.protocols, - [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] - ); - assert_eq!( - dns_allow_rule_ipv4.destination_ports, - [Port { - port: Some(PortInner::SinglePort(53)) - }] - ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule_ipv4.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.100.1".to_string(), - end: "10.0.100.2".to_string(), - })), - }, - ] - ); - - let expected_destination_addrs_v4 = vec![ - IpAddress { - address: Some(Address::Ip("10.0.1.13".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), - }, - ]; - - assert_eq!( - dns_allow_rule_ipv4.destination_addrs, - expected_destination_addrs_v4 - ); - - let dns_allow_rule_ipv6 = &generated_firewall_rules[3]; - assert_eq!( - dns_allow_rule_ipv6.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - dns_allow_rule_ipv6.protocols, - [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] - ); - assert_eq!( - dns_allow_rule_ipv6.destination_ports, - [Port { - port: Some(PortInner::SinglePort(53)) - }] - ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule_ipv6.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::100:1".to_string(), - end: "ff00::100:2".to_string(), - })), - }, - ] - ); - - let expected_destination_addrs_v6 = vec![ - IpAddress { - address: Some(Address::Ip("fc00::1:13".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:54/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:58/125".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:60/123".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:80/121".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:100/120".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:200/119".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:400/118".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:800/117".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:1000/116".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:2000/115".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:4000/114".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:8000/113".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::2:0/122".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::2:40/126".to_string())), - }, - ]; - - assert_eq!( - dns_allow_rule_ipv6.destination_addrs, - expected_destination_addrs_v6 - ); - - // Second ACL - DNS Access DENY - let dns_deny_rule_ipv4 = &generated_firewall_rules[6]; - assert_eq!(dns_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule_ipv4.protocols.is_empty(),); - assert!(dns_deny_rule_ipv4.destination_ports.is_empty(),); - assert!(dns_deny_rule_ipv4.source_addrs.is_empty(),); - assert_eq!( - dns_deny_rule_ipv4.destination_addrs, - expected_destination_addrs_v4 - ); - - let dns_deny_rule_ipv6 = &generated_firewall_rules[7]; - assert_eq!(dns_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule_ipv6.protocols.is_empty(),); - assert!(dns_deny_rule_ipv6.destination_ports.is_empty(),); - assert!(dns_deny_rule_ipv6.source_addrs.is_empty(),); - assert_eq!( - dns_deny_rule_ipv6.destination_addrs, - expected_destination_addrs_v6 - ); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); -} - -#[sqlx::test] -async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut rng = thread_rng(); - - // Create test location - let location_1 = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location_1 = location_1.save(&pool).await.unwrap(); - - // Create another test location - let location_2 = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location_2 = location_2.save(&pool).await.unwrap(); - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 10, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // create ACL rules - let acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Applied, - destination: vec!["192.168.1.0/24".parse().unwrap()], - manual_settings: true, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - state: RuleState::Applied, - manual_settings: false, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let _acl_rule_3 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - allow_all_users: true, - state: RuleState::Applied, - manual_settings: false, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to locations - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location_1.id) - .save(&pool) - .await - .unwrap(); - } - for rule in [&acl_rule_2] { - AclRuleNetwork::new(rule.id, location_2.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 4); - - let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 3); -} - -#[sqlx::test] -async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut rng = thread_rng(); - - // Create test location - let location_1 = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location_1 = location_1.save(&pool).await.unwrap(); - - // Create another test location - let location_2 = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location_2 = location_2.save(&pool).await.unwrap(); - - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 10, - 10, - user.id as u16, - device_num as u16, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // create ACL rules - let acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Applied, - destination: vec!["fc00::0/112".parse().unwrap()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let _acl_rule_3 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - allow_all_users: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to locations - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location_1.id) - .save(&pool) - .await - .unwrap(); - } - for rule in [&acl_rule_2] { - AclRuleNetwork::new(rule.id, location_2.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 4); - - let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 3); -} - -#[sqlx::test] -async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut rng = thread_rng(); - - // Create test location - let location_1 = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location_1 = location_1.save(&pool).await.unwrap(); - - // Create another test location - let location_2 = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location_2 = location_2.save(&pool).await.unwrap(); - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 10, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 10, - 10, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // create ACL rules - let acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - allow_all_users: true, - state: RuleState::Applied, - manual_settings: true, - destination: vec![ - "192.168.1.0/24".parse().unwrap(), - "fc00::0/112".parse().unwrap(), - ], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - allow_all_users: true, - state: RuleState::Applied, - manual_settings: true, - destination: vec![ - "192.168.2.0/24".parse().unwrap(), - "fb00::0/112".parse().unwrap(), - ], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let _acl_rule_3 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - allow_all_users: true, - state: RuleState::Applied, - manual_settings: true, - destination: vec![ - "192.168.3.0/24".parse().unwrap(), - "fa00::0/112".parse().unwrap(), - ], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to locations - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location_1.id) - .save(&pool) - .await - .unwrap(); - } - for rule in [&acl_rule_2] { - AclRuleNetwork::new(rule.id, location_2.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // all rules were used to this location - assert_eq!(generated_firewall_rules.len(), 12); - - let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // rule with `all_networks` enabled was also used for this location - assert_eq!(generated_firewall_rules.len(), 8); -} - -#[sqlx::test] -async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // create ACL rule - let acl_rule = AclRule { - id: NoId, - name: "test rule".to_string(), - expires: None, - enabled: true, - state: RuleState::Applied, - destination: vec!["192.168.1.0/24".parse().unwrap()], - allow_all_users: true, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // create different kinds of aliases and add them to the rule - let destination_alias = AclAlias { - id: NoId, - name: "destination alias".to_string(), - kind: AliasKind::Destination, - ports: vec![PortRange::new(100, 200).into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let component_alias = AclAlias { - id: NoId, - kind: AliasKind::Component, - destination: vec!["10.0.2.3".parse().unwrap()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - for alias in [&destination_alias, &component_alias] { - AclRuleAlias::new(acl_rule.id, alias.id) - .save(&pool) - .await - .unwrap(); - } - - // assign rule to location - AclRuleNetwork::new(acl_rule.id, location.id) - .save(&pool) - .await - .unwrap(); - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // check generated rules - assert_eq!(generated_firewall_rules.len(), 4); - let expected_source_addrs = [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - ]; - let expected_destination_addrs = [ - IpAddress { - address: Some(Address::Ip("10.0.2.3".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }, - ]; - - let allow_rule = &generated_firewall_rules[0]; - assert_eq!(allow_rule.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!(allow_rule.source_addrs, expected_source_addrs); - assert_eq!(allow_rule.destination_addrs, expected_destination_addrs); - assert!(allow_rule.destination_ports.is_empty()); - assert!(allow_rule.protocols.is_empty()); - assert_eq!( - allow_rule.comment, - Some("ACL 1 - test rule ALLOW".to_string()) - ); - - let alias_allow_rule = &generated_firewall_rules[1]; - assert_eq!(alias_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!(alias_allow_rule.source_addrs, expected_source_addrs); - assert!(alias_allow_rule.destination_addrs.is_empty()); - assert_eq!( - alias_allow_rule.destination_ports, - vec![Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 200, - })) - }] - ); - assert!(alias_allow_rule.protocols.is_empty()); - assert_eq!( - alias_allow_rule.comment, - Some("ACL 1 - test rule, ALIAS 1 - destination alias ALLOW".to_string()) - ); - - let deny_rule = &generated_firewall_rules[2]; - assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(deny_rule.source_addrs.is_empty()); - assert_eq!(deny_rule.destination_addrs, expected_destination_addrs); - assert!(deny_rule.destination_ports.is_empty()); - assert!(deny_rule.protocols.is_empty()); - assert_eq!( - deny_rule.comment, - Some("ACL 1 - test rule DENY".to_string()) - ); - - let alias_deny_rule = &generated_firewall_rules[3]; - assert_eq!(alias_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(alias_deny_rule.source_addrs.is_empty()); - assert!(alias_deny_rule.destination_addrs.is_empty()); - assert!(alias_deny_rule.destination_ports.is_empty()); - assert!(alias_deny_rule.protocols.is_empty()); - assert_eq!( - alias_deny_rule.comment, - Some("ACL 1 - test rule, ALIAS 1 - destination alias DENY".to_string()) - ); -} - -#[sqlx::test] -async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{device_num}", user.id), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // create ACL rule without manually configured destination - let acl_rule = AclRule { - id: NoId, - name: "test rule".to_string(), - expires: None, - enabled: true, - state: RuleState::Applied, - destination: Vec::new(), - allow_all_users: true, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // create different kinds of aliases and add them to the rule - let destination_alias_1 = AclAlias { - id: NoId, - name: "postgres".to_string(), - kind: AliasKind::Destination, - destination: vec!["10.0.2.3".parse().unwrap()], - ports: vec![PortRange::new(5432, 5432).into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let destination_alias_2 = AclAlias { - id: NoId, - name: "redis".to_string(), - kind: AliasKind::Destination, - destination: vec!["10.0.2.4".parse().unwrap()], - ports: vec![PortRange::new(6379, 6379).into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - for alias in [&destination_alias_1, &destination_alias_2] { - AclRuleAlias::new(acl_rule.id, alias.id) - .save(&pool) - .await - .unwrap(); - } - - // assign rule to location - AclRuleNetwork::new(acl_rule.id, location.id) - .save(&pool) - .await - .unwrap(); - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // check generated rules - assert_eq!(generated_firewall_rules.len(), 4); - let expected_source_addrs = vec![ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - ]; - - let alias_allow_rule_1 = &generated_firewall_rules[0]; - assert_eq!(alias_allow_rule_1.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!(alias_allow_rule_1.source_addrs, expected_source_addrs); - assert_eq!( - alias_allow_rule_1.destination_addrs, - vec![IpAddress { - address: Some(Address::Ip("10.0.2.3".to_string())), - },] - ); - assert_eq!( - alias_allow_rule_1.destination_ports, - vec![Port { - port: Some(PortInner::SinglePort(5432)) - }] - ); - assert!(alias_allow_rule_1.protocols.is_empty()); - assert_eq!( - alias_allow_rule_1.comment, - Some("ACL 1 - test rule, ALIAS 1 - postgres ALLOW".to_string()) - ); - - let alias_allow_rule_2 = &generated_firewall_rules[1]; - assert_eq!(alias_allow_rule_2.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!(alias_allow_rule_2.source_addrs, expected_source_addrs); - assert_eq!( - alias_allow_rule_2.destination_addrs, - vec![IpAddress { - address: Some(Address::Ip("10.0.2.4".to_string())), - },] - ); - assert_eq!( - alias_allow_rule_2.destination_ports, - vec![Port { - port: Some(PortInner::SinglePort(6379)) - }] - ); - assert!(alias_allow_rule_2.protocols.is_empty()); - assert_eq!( - alias_allow_rule_2.comment, - Some("ACL 1 - test rule, ALIAS 2 - redis ALLOW".to_string()) - ); - - let alias_deny_rule_1 = &generated_firewall_rules[2]; - assert_eq!(alias_deny_rule_1.verdict, i32::from(FirewallPolicy::Deny)); - assert!(alias_deny_rule_1.source_addrs.is_empty()); - assert_eq!( - alias_deny_rule_1.destination_addrs, - vec![IpAddress { - address: Some(Address::Ip("10.0.2.3".to_string())), - },] - ); - assert!(alias_deny_rule_1.destination_ports.is_empty()); - assert!(alias_deny_rule_1.protocols.is_empty()); - assert_eq!( - alias_deny_rule_1.comment, - Some("ACL 1 - test rule, ALIAS 1 - postgres DENY".to_string()) - ); - - let alias_deny_rule_2 = &generated_firewall_rules[3]; - assert_eq!(alias_deny_rule_2.verdict, i32::from(FirewallPolicy::Deny)); - assert!(alias_deny_rule_2.source_addrs.is_empty()); - assert_eq!( - alias_deny_rule_2.destination_addrs, - vec![IpAddress { - address: Some(Address::Ip("10.0.2.4".to_string())), - },] - ); - assert!(alias_deny_rule_2.destination_ports.is_empty()); - assert!(alias_deny_rule_2.protocols.is_empty()); - assert_eq!( - alias_deny_rule_2.comment, - Some("ACL 1 - test rule, ALIAS 2 - redis DENY".to_string()) - ); -} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/destination.rs b/crates/defguard_core/src/enterprise/firewall/tests/destination.rs new file mode 100644 index 0000000000..fd8e62bab7 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/destination.rs @@ -0,0 +1,117 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_proto::enterprise::firewall::{IpAddress, IpRange, ip_address::Address}; + +use crate::enterprise::{ + db::models::acl::AclRuleDestinationRange, firewall::process_destination_addrs, +}; + +#[test] +fn test_process_destination_addrs_v4() { + // Test data with mixed IPv4 and IPv6 networks + let destination_ips = [ + "10.0.1.0/24".parse().unwrap(), + "10.0.2.0/24".parse().unwrap(), + "2001:db8::/64".parse().unwrap(), // Should be filtered out + "192.168.1.0/24".parse().unwrap(), + ]; + + let destination_ranges = [ + AclRuleDestinationRange { + start: IpAddr::V4(Ipv4Addr::new(10, 0, 3, 255)), + end: IpAddr::V4(Ipv4Addr::new(10, 0, 4, 0)), + ..Default::default() + }, + AclRuleDestinationRange { + start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out + end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 100)), + ..Default::default() + }, + ]; + + let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + + assert_eq!( + destination_addrs.0, + [ + IpAddress { + address: Some(Address::IpSubnet("10.0.1.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.3.255".to_string(), + end: "10.0.4.0".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = process_destination_addrs(&[], &[]); + assert!(empty_addrs.0.is_empty()); + + // Test with only IPv6 addresses - should return empty result for IPv4 + let ipv6_only = process_destination_addrs(&["2001:db8::/64".parse().unwrap()], &[]); + assert!(ipv6_only.0.is_empty()); +} + +#[test] +fn test_process_destination_addrs_v6() { + // Test data with mixed IPv4 and IPv6 networks + let destination_ips = vec![ + "2001:db8:1::/64".parse().unwrap(), + "2001:db8:2::/64".parse().unwrap(), + "10.0.1.0/24".parse().unwrap(), // Should be filtered out + "2001:db8:3::/64".parse().unwrap(), + ]; + + let destination_ranges = vec![ + AclRuleDestinationRange { + start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 1)), + end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 3)), + ..Default::default() + }, + AclRuleDestinationRange { + start: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), // Should be filtered out + end: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), + ..Default::default() + }, + ]; + + let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + + assert_eq!( + destination_addrs.1, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::/64".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:2::/64".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:3::/64".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8:4::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:4::2/127".to_string())) + } + ] + ); + + // Test with empty input + let empty_addrs = process_destination_addrs(&[], &[]); + assert!(empty_addrs.1.is_empty()); + + // Test with only IPv4 addresses - should return empty result for IPv6 + let ipv4_only = process_destination_addrs(&["192.168.1.0/24".parse().unwrap()], &[]); + assert!(ipv4_only.1.is_empty()); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs new file mode 100644 index 0000000000..577fb127eb --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs @@ -0,0 +1,213 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; +use ipnetwork::IpNetwork; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use crate::enterprise::{ + db::models::acl::{AclRule, AclRuleNetwork, RuleState}, + firewall::try_get_location_firewall_config, +}; + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs index ddd681df1a..8c20773985 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs @@ -1,3 +1,15 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use chrono::{DateTime, NaiveDateTime}; +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; +use ipnetwork::IpNetwork; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use crate::enterprise::{ + db::models::acl::{AclRule, AclRuleNetwork, RuleState}, + firewall::try_get_location_firewall_config, +}; + #[sqlx::test] async fn test_expired_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; diff --git a/crates/defguard_core/src/enterprise/firewall/tests/ip_address_handling.rs b/crates/defguard_core/src/enterprise/firewall/tests/ip_address_handling.rs new file mode 100644 index 0000000000..9b4650ad1a --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/ip_address_handling.rs @@ -0,0 +1,500 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_proto::enterprise::firewall::{ + IpAddress, IpRange, Port, PortRange as PortRangeProto, ip_address::Address, + port::Port as PortInner, +}; +use ipnetwork::Ipv6Network; + +use crate::enterprise::{ + db::models::acl::PortRange, + firewall::{ + find_largest_subnet_in_range, get_last_ip_in_v6_subnet, merge_addrs, merge_port_ranges, + }, +}; + +#[test] +fn test_merge_v4_addrs() { + let addr_ranges = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 60, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 60, 25)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 22)), + IpAddr::V4(Ipv4Addr::new(10, 0, 8, 127))..=IpAddr::V4(Ipv4Addr::new(10, 0, 9, 12)), + IpAddr::V4(Ipv4Addr::new(10, 0, 9, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 12)), + IpAddr::V4(Ipv4Addr::new(10, 0, 9, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 31)), + IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20))..=IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20)), + IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::Ip("10.0.8.127".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.8.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.9.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.10.0/27".to_string())), + }, + IpAddress { + address: Some(Address::Ip("10.0.20.20".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.60.20/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.60.24/31".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.0.20".to_string())), + }, + ] + ); + + // merge single IPs into a range + let addr_ranges = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::IpSubnet("10.0.10.0/30".to_string())), + }, + IpAddress { + address: Some(Address::Ip("10.0.10.20".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_v6_addrs() { + let addr_ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x5)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x3)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x8)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x3)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::Ip("2001:db8:1::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::2/127".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::4/126".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:1::8".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:2::1".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:3::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:3::2/127".to_string())) + } + ] + ); +} + +#[test] +fn test_merge_addrs_extracts_ipv4_subnets() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.2.0/24".to_string())) + }, + ] + ); +} + +#[test] +fn test_merge_addrs_extracts_ipv6_subnets() { + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db9::ffff".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8::/32".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db9::/112".to_string())) + }, + ] + ); +} + +#[test] +fn test_merge_addrs_falls_back_to_range_when_no_subnet_fits() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 0)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::IpRange(IpRange { + start: "192.168.1.255".to_string(), + end: "192.168.2.0".to_string(), + })), + },] + ); + + let start = "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff" + .parse::() + .unwrap(); + let end = "2001:db9::".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::IpRange(IpRange { + start: "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff".to_string(), + end: "2001:db9::".to_string(), + })), + },] + ); +} + +#[test] +fn test_merge_addrs_handles_single_ip() { + // Test case: single IP should remain as IP + let ranges = + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::Ip("192.168.1.1".to_string())), + },] + ); + + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db8::".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::Ip("2001:db8::".to_string())), + },] + ); +} + +#[test] +fn test_find_largest_ipv4_subnet_perfect_match() { + // Test /24 subnet + let start = Ipv4Addr::new(192, 168, 1, 0); + let end = Ipv4Addr::new(192, 168, 1, 255); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "192.168.1.0/24"); + + // Test /28 subnet (16 addresses) + let start = Ipv4Addr::new(192, 168, 1, 0); + let end = Ipv4Addr::new(192, 168, 1, 15); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "192.168.1.0/28"); +} + +#[test] +fn test_find_largest_ipv6_subnet_perfect_match() { + // Test /112 subnet + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db8::ffff".parse::().unwrap(); + + let result = find_largest_subnet_in_range(IpAddr::V6(start), IpAddr::V6(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "2001:db8::/112"); +} + +#[test] +fn test_find_largest_subnet_mixed_ip_versions() { + // Test mixed IP versions should return None + let start = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)); + let end = IpAddr::V6("2001:db8::1".parse().unwrap()); + + let result = find_largest_subnet_in_range(start, end); + + assert!(result.is_none()); +} + +#[test] +fn test_find_largest_subnet_invalid_range() { + // Test invalid range (start > end) should return None + let start = Ipv4Addr::new(192, 168, 1, 10); + let end = Ipv4Addr::new(192, 168, 1, 5); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_none()); +} + +#[test] +fn test_merge_addrs_subnet_at_start_of_range() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 64)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/26".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.1.64".to_string())), + }, + ] + ); + + let ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x40)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8::/122".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8::40".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_addrs_subnet_at_end_of_range() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 15))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 31)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::Ip("192.168.1.15".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.1.16/28".to_string())), + }, + ] + ); + + let ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x0f)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1f)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::Ip("2001:db8::f".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::10/124".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_port_ranges() { + // single port + let input_ranges = vec![PortRange::new(100, 100)]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::SinglePort(100)) + }] + ); + + // overlapping ranges + let input_ranges = vec![ + PortRange::new(100, 200), + PortRange::new(150, 220), + PortRange::new(210, 300), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 300 + })) + }] + ); + + // duplicate ranges + let input_ranges = vec![ + PortRange::new(100, 200), + PortRange::new(100, 200), + PortRange::new(150, 220), + PortRange::new(150, 220), + PortRange::new(210, 300), + PortRange::new(210, 300), + PortRange::new(350, 400), + PortRange::new(350, 400), + PortRange::new(350, 400), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [ + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 300 + })) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 350, + end: 400 + })) + } + ] + ); + + // non-consecutive ranges + let input_ranges = vec![ + PortRange::new(501, 699), + PortRange::new(151, 220), + PortRange::new(210, 300), + PortRange::new(800, 800), + PortRange::new(200, 210), + PortRange::new(50, 50), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [ + Port { + port: Some(PortInner::SinglePort(50)) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 151, + end: 300 + })) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 501, + end: 699 + })) + }, + Port { + port: Some(PortInner::SinglePort(800)) + } + ] + ); + + // fully contained range + let input_ranges = vec![PortRange::new(100, 200), PortRange::new(120, 180)]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 200 + })) + }] + ); +} + +#[test] +fn test_last_ip_in_v6_subnet() { + let subnet: Ipv6Network = "2001:db8:85a3::8a2e:370:7334/64".parse().unwrap(); + let last_ip = get_last_ip_in_v6_subnet(&subnet); + assert_eq!( + last_ip, + IpAddr::V6(Ipv6Addr::new( + 0x2001, 0x0db8, 0x85a3, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff + )) + ); + + let subnet: Ipv6Network = "280b:47f8:c9d7:634c:cb35:11f3:14e1:5016/119" + .parse() + .unwrap(); + let last_ip = get_last_ip_in_v6_subnet(&subnet); + assert_eq!( + last_ip, + IpAddr::V6(Ipv6Addr::new( + 0x280b, 0x47f8, 0xc9d7, 0x634c, 0xcb35, 0x11f3, 0x14e1, 0x51ff + )) + ); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index 85bcaacb67..afe18fd18e 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -1,6 +1,5 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use chrono::{DateTime, NaiveDateTime}; use defguard_common::db::{ Id, NoId, models::{ @@ -10,10 +9,10 @@ use defguard_common::db::{ setup_pool, }; use defguard_proto::enterprise::firewall::{ - FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, + FirewallPolicy, IpAddress, IpRange, Port, PortRange as PortRangeProto, Protocol, ip_address::Address, port::Port as PortInner, }; -use ipnetwork::{IpNetwork, Ipv6Network}; +use ipnetwork::IpNetwork; use rand::{Rng, thread_rng}; use sqlx::{ PgPool, @@ -21,20 +20,21 @@ use sqlx::{ query, }; -use super::{ - find_largest_subnet_in_range, get_last_ip_in_v6_subnet, get_source_users, merge_addrs, - merge_port_ranges, process_destination_addrs, -}; use crate::enterprise::{ db::models::acl::{ AclAlias, AclRule, AclRuleAlias, AclRuleDestinationRange, AclRuleDevice, AclRuleGroup, AclRuleInfo, AclRuleNetwork, AclRuleUser, AliasKind, PortRange, RuleState, }, - firewall::{get_source_addrs, get_source_network_devices, try_get_location_firewall_config}, + firewall::try_get_location_firewall_config, }; mod all_locations; +mod destination; +mod disabled_rules; mod expired_rules; +mod ip_address_handling; +mod source; +mod unapplied_rules; impl Default for AclRuleDestinationRange { fn default() -> Self { @@ -60,931 +60,616 @@ fn random_network_device_with_id(rng: &mut R, id: Id) -> Device { device } -#[test] -fn test_get_relevant_users() { - let mut rng = thread_rng(); +async fn create_acl_rule( + pool: &PgPool, + rule: AclRule, + locations: Vec, + allowed_users: Vec, + denied_users: Vec, + allowed_groups: Vec, + denied_groups: Vec, + allowed_network_devices: Vec, + denied_network_devices: Vec, + destination_ranges: Vec<(IpAddr, IpAddr)>, + aliases: Vec, +) -> AclRuleInfo { + let mut conn = pool.acquire().await.unwrap(); - // prepare allowed and denied users lists with shared elements - let user_1 = random_user_with_id(&mut rng, 1); - let user_2 = random_user_with_id(&mut rng, 2); - let user_3 = random_user_with_id(&mut rng, 3); - let user_4 = random_user_with_id(&mut rng, 4); - let user_5 = random_user_with_id(&mut rng, 5); - let allowed_users = vec![user_1.clone(), user_2.clone(), user_4.clone()]; - let denied_users = vec![user_3.clone(), user_4, user_5.clone()]; - - let users = get_source_users(allowed_users, &denied_users); - assert_eq!(users, vec![user_1, user_2]); -} + // create base rule + let rule = rule.save(&mut *conn).await.unwrap(); + let rule_id = rule.id; -#[test] -fn test_get_relevant_network_devices() { - let mut rng = thread_rng(); + // create related objects + // locations + for location_id in locations { + AclRuleNetwork::new(rule_id, location_id) + .save(&mut *conn) + .await + .unwrap(); + } - // prepare allowed and denied network devices lists with shared elements - let device_1 = random_network_device_with_id(&mut rng, 1); - let device_2 = random_network_device_with_id(&mut rng, 2); - let device_3 = random_network_device_with_id(&mut rng, 3); - let device_4 = random_network_device_with_id(&mut rng, 4); - let device_5 = random_network_device_with_id(&mut rng, 5); - let allowed_devices = vec![ - device_1.clone(), - device_3.clone(), - device_4.clone(), - device_5.clone(), - ]; - let denied_devices = vec![device_2.clone(), device_4, device_5.clone()]; + // allowed users + for user_id in allowed_users { + AclRuleUser::new(rule_id, user_id, true) + .save(&mut *conn) + .await + .unwrap(); + } - let devices = get_source_network_devices(allowed_devices, &denied_devices); - assert_eq!(devices, vec![device_1, device_3]); -} + // denied users + for user_id in denied_users { + AclRuleUser::new(rule_id, user_id, false) + .save(&mut *conn) + .await + .unwrap(); + } -#[test] -fn test_process_source_addrs_v4() { - // Test data with mixed IPv4 and IPv6 addresses - let user_device_ips = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 2)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 5)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), - ]; + // allowed groups + for group_id in allowed_groups { + AclRuleGroup::new(rule_id, group_id, true) + .save(&mut *conn) + .await + .unwrap(); + } - let network_device_ips = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 3)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 4)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), // Should be filtered out - IpAddr::V4(Ipv4Addr::new(172, 16, 1, 1)), - ]; + // denied groups + for group_id in denied_groups { + AclRuleGroup::new(rule_id, group_id, false) + .save(&mut *conn) + .await + .unwrap(); + } + + // allowed devices + for device_id in allowed_network_devices { + AclRuleDevice::new(rule_id, device_id, true) + .save(&mut *conn) + .await + .unwrap(); + } - let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv4); + // denied devices + for device_id in denied_network_devices { + AclRuleDevice::new(rule_id, device_id, false) + .save(&mut *conn) + .await + .unwrap(); + } - // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges - assert_eq!( - source_addrs, - [ - IpAddress { - address: Some(Address::Ip("10.0.1.1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.2/31".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.4/31".to_string())) - }, - IpAddress { - address: Some(Address::Ip("172.16.1.1".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.1.100".to_string())), - }, - ] - ); + // destination ranges + for range in destination_ranges { + AclRuleDestinationRange { + id: NoId, + rule_id, + start: range.0, + end: range.1, + } + .save(&mut *conn) + .await + .unwrap(); + } - // Test with empty input - let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv4); - assert!(empty_addrs.is_empty()); + // aliases + for alias_id in aliases { + AclRuleAlias::new(rule_id, alias_id) + .save(&mut *conn) + .await + .unwrap(); + } - // Test with only IPv6 addresses - should return empty result for IPv4 - let ipv6_only = get_source_addrs( - vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))], - vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2))], - IpVersion::Ipv4, - ); - assert!(ipv6_only.is_empty()); + // convert to output format + rule.to_info(&mut conn).await.unwrap() } -#[test] -fn test_process_source_addrs_v6() { - // Test data with mixed IPv4 and IPv6 addresses - let user_device_ips = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 5)), - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), // Should be filtered out - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 1)), - ]; +#[sqlx::test] +async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; - let network_device_ips = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 3)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 4)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), // Should be filtered out - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 2, 0, 0, 0, 1)), - ]; + let mut rng = thread_rng(); - let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv6); + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); - // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges - assert_eq!( - source_addrs, - [ - IpAddress { - address: Some(Address::Ip("2001:db8::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::2/127".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::4/127".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:0:1::1".to_string())), - }, - IpAddress { - address: Some(Address::Ip("2001:db8:0:2::1".to_string())), - }, - ] - ); + // Setup test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.r#gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.r#gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.r#gen(); + let user_5 = user_5.save(&pool).await.unwrap(); - // Test with empty input - let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv6); - assert!(empty_addrs.is_empty()); + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); - // Test with only IPv4 addresses - should return empty result for IPv6 - let ipv4_only = get_source_addrs( - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))], - IpVersion::Ipv6, - ); - assert!(ipv4_only.is_empty()); -} + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } -#[test] -fn test_process_destination_addrs_v4() { - // Test data with mixed IPv4 and IPv6 networks - let destination_ips = [ - "10.0.1.0/24".parse().unwrap(), - "10.0.2.0/24".parse().unwrap(), - "2001:db8::/64".parse().unwrap(), // Should be filtered out - "192.168.1.0/24".parse().unwrap(), - ]; + // Setup test groups + let group_1 = Group { + id: NoId, + name: "group_1".into(), + ..Default::default() + }; + let group_1 = group_1.save(&pool).await.unwrap(); + let group_2 = Group { + id: NoId, + name: "group_2".into(), + ..Default::default() + }; + let group_2 = group_2.save(&pool).await.unwrap(); - let destination_ranges = [ - AclRuleDestinationRange { - start: IpAddr::V4(Ipv4Addr::new(10, 0, 3, 255)), - end: IpAddr::V4(Ipv4Addr::new(10, 0, 4, 0)), - ..Default::default() - }, - AclRuleDestinationRange { - start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out - end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 100)), - ..Default::default() - }, + // Assign users to groups: + // Group 1: users 1,2 + // Group 2: users 3,4 + let group_assignments = vec![ + (&group_1, vec![&user_1, &user_2]), + (&group_2, vec![&user_3, &user_4]), ]; - let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + for (group, users) in group_assignments { + for user in users { + query!( + "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", + user.id, + group.id + ) + .execute(&pool) + .await + .unwrap(); + } + } - assert_eq!( - destination_addrs.0, - [ - IpAddress { - address: Some(Address::IpSubnet("10.0.1.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.3.255".to_string(), - end: "10.0.4.0".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }, - ] - ); + // Create some network devices + let network_device_1 = Device { + id: NoId, + name: "network-device-1".into(), + user_id: user_1.id, // Owned by user 1 + device_type: DeviceType::Network, + description: Some("Test network device 1".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_1 = network_device_1.save(&pool).await.unwrap(); - // Test with empty input - let empty_addrs = process_destination_addrs(&[], &[]); - assert!(empty_addrs.0.is_empty()); + let network_device_2 = Device { + id: NoId, + name: "network-device-2".into(), + user_id: user_2.id, // Owned by user 2 + device_type: DeviceType::Network, + description: Some("Test network device 2".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_2 = network_device_2.save(&pool).await.unwrap(); - // Test with only IPv6 addresses - should return empty result for IPv4 - let ipv6_only = process_destination_addrs(&["2001:db8::/64".parse().unwrap()], &[]); - assert!(ipv6_only.0.is_empty()); -} + let network_device_3 = Device { + id: NoId, + name: "network-device-3".into(), + user_id: user_3.id, // Owned by user 3 + device_type: DeviceType::Network, + description: Some("Test network device 3".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_3 = network_device_3.save(&pool).await.unwrap(); -#[test] -fn test_process_destination_addrs_v6() { - // Test data with mixed IPv4 and IPv6 networks - let destination_ips = vec![ - "2001:db8:1::/64".parse().unwrap(), - "2001:db8:2::/64".parse().unwrap(), - "10.0.1.0/24".parse().unwrap(), // Should be filtered out - "2001:db8:3::/64".parse().unwrap(), + // Add network devices to location's VPN network + let network_devices = vec![ + ( + network_device_1.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + ), + ( + network_device_2.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + ), + ( + network_device_3.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + ), ]; - let destination_ranges = vec![ - AclRuleDestinationRange { - start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 1)), - end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 3)), - ..Default::default() - }, - AclRuleDestinationRange { - start: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), // Should be filtered out - end: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), - ..Default::default() - }, - ]; + for (device_id, ip) in network_devices { + let network_device = WireguardNetworkDevice { + device_id, + wireguard_network_id: location.id, + wireguard_ips: vec![ip], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } - let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + // Create first ACL rule - Web access + let acl_rule_1 = AclRule { + id: NoId, + name: "Web Access".into(), + all_networks: false, + expires: None, + allow_all_users: false, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec!["192.168.1.0/24".parse().unwrap()], + ports: vec![ + PortRange::new(80, 80).into(), + PortRange::new(443, 443).into(), + ], + protocols: vec![Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + }; + let locations = vec![location.id]; + let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web + let denied_users = vec![user_3.id]; // Third user explicitly denied + let allowed_groups = vec![group_1.id]; // First group allowed + let denied_groups = Vec::new(); + let allowed_devices = vec![network_device_1.id]; + let denied_devices = vec![network_device_2.id, network_device_3.id]; + let destination_ranges = Vec::new(); + let aliases = Vec::new(); - assert_eq!( - destination_addrs.1, - [ - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::/64".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:2::/64".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:3::/64".to_string())), - }, - IpAddress { - address: Some(Address::Ip("2001:db8:4::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:4::2/127".to_string())) - } - ] - ); + let _acl_rule_1 = create_acl_rule( + &pool, + acl_rule_1, + locations, + allowed_users, + denied_users, + allowed_groups, + denied_groups, + allowed_devices, + denied_devices, + destination_ranges, + aliases, + ) + .await; - // Test with empty input - let empty_addrs = process_destination_addrs(&[], &[]); - assert!(empty_addrs.1.is_empty()); + // Create second ACL rule - DNS access + let acl_rule_2 = AclRule { + id: NoId, + name: "DNS Access".into(), + all_networks: false, + expires: None, + allow_all_users: true, // Allow all users + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: Vec::new(), // Will use destination ranges instead + ports: vec![PortRange::new(53, 53).into()], + protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + }; + let locations_2 = vec![location.id]; + let allowed_users_2 = Vec::new(); + let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS + let allowed_groups_2 = Vec::new(); + let denied_groups_2 = vec![group_2.id]; + let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed + let denied_devices_2 = vec![network_device_3.id]; // Third network device denied + let destination_ranges_2 = vec![ + ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), + ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), + ]; + let aliases_2 = Vec::new(); - // Test with only IPv4 addresses - should return empty result for IPv6 - let ipv4_only = process_destination_addrs(&["192.168.1.0/24".parse().unwrap()], &[]); - assert!(ipv4_only.1.is_empty()); -} + let _acl_rule_2 = create_acl_rule( + &pool, + acl_rule_2, + locations_2, + allowed_users_2, + denied_users_2, + allowed_groups_2, + denied_groups_2, + allowed_devices_2, + denied_devices_2, + destination_ranges_2, + aliases_2, + ) + .await; -#[test] -fn test_merge_v4_addrs() { - let addr_ranges = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 60, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 60, 25)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 22)), - IpAddr::V4(Ipv4Addr::new(10, 0, 8, 127))..=IpAddr::V4(Ipv4Addr::new(10, 0, 9, 12)), - IpAddr::V4(Ipv4Addr::new(10, 0, 9, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 12)), - IpAddr::V4(Ipv4Addr::new(10, 0, 9, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 31)), - IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20))..=IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20)), - IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20)), - ]; + let mut conn = pool.acquire().await.unwrap(); - let merged_addrs = merge_addrs(addr_ranges); + // try to generate firewall config with ACL disabled + location.acl_enabled = false; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap(); + assert!(generated_firewall_config.is_none()); + // generate firewall config with default policy Allow + location.acl_enabled = true; + location.acl_default_allow = true; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); assert_eq!( - merged_addrs, - [ - IpAddress { - address: Some(Address::Ip("10.0.8.127".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.8.128/25".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.9.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.10.0/27".to_string())), - }, - IpAddress { - address: Some(Address::Ip("10.0.20.20".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.60.20/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.60.24/31".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.0.20".to_string())), - }, - ] + generated_firewall_config.default_policy, + i32::from(FirewallPolicy::Allow) ); - // merge single IPs into a range - let addr_ranges = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20)), - ]; + let generated_firewall_rules = generated_firewall_config.rules; - let merged_addrs = merge_addrs(addr_ranges); + assert_eq!(generated_firewall_rules.len(), 4); + + // First ACL - Web Access ALLOW + let web_allow_rule = &generated_firewall_rules[0]; + assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); assert_eq!( - merged_addrs, - [ - IpAddress { - address: Some(Address::IpSubnet("10.0.10.0/30".to_string())), - }, - IpAddress { - address: Some(Address::Ip("10.0.10.20".to_string())), - }, - ] + web_allow_rule.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] ); -} - -#[test] -fn test_merge_v6_addrs() { - let addr_ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x5)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x3)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x8)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x3)), - ]; - - let merged_addrs = merge_addrs(addr_ranges); assert_eq!( - merged_addrs, + web_allow_rule.destination_ports, [ - IpAddress { - address: Some(Address::Ip("2001:db8:1::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::2/127".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::4/126".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:1::8".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:2::1".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:3::1".to_string())) + Port { + port: Some(PortInner::SinglePort(80)) }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:3::2/127".to_string())) + Port { + port: Some(PortInner::SinglePort(443)) } ] ); -} - -#[test] -fn test_merge_addrs_extracts_ipv4_subnets() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)), - ]; - - let result = merge_addrs(ranges); - + // Source addresses should include devices of users 1,2 and network_device_1 assert_eq!( - result, + web_allow_rule.source_addrs, [ IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.2.0/24".to_string())) + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), }, - ] - ); -} - -#[test] -fn test_merge_addrs_extracts_ipv6_subnets() { - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db9::ffff".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ IpAddress { - address: Some(Address::IpSubnet("2001:db8::/32".to_string())) + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), }, IpAddress { - address: Some(Address::IpSubnet("2001:db9::/112".to_string())) + address: Some(Address::Ip("10.0.100.1".to_string())), }, ] ); -} - -#[test] -fn test_merge_addrs_falls_back_to_range_when_no_subnet_fits() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 0)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [IpAddress { - address: Some(Address::IpRange(IpRange { - start: "192.168.1.255".to_string(), - end: "192.168.2.0".to_string(), - })), - },] - ); - - let start = "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff" - .parse::() - .unwrap(); - let end = "2001:db9::".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); + // First ACL - Web Access DENY + let web_deny_rule = &generated_firewall_rules[2]; + assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule.protocols.is_empty()); + assert!(web_deny_rule.destination_ports.is_empty()); + assert!(web_deny_rule.source_addrs.is_empty()); assert_eq!( - result, + web_deny_rule.destination_addrs, [IpAddress { - address: Some(Address::IpRange(IpRange { - start: "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff".to_string(), - end: "2001:db9::".to_string(), - })), - },] + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] ); -} - -#[test] -fn test_merge_addrs_handles_single_ip() { - // Test case: single IP should remain as IP - let ranges = - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))]; - - let result = merge_addrs(ranges); + // Second ACL - DNS Access ALLOW + let dns_allow_rule = &generated_firewall_rules[1]; + assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); assert_eq!( - result, - [IpAddress { - address: Some(Address::Ip("192.168.1.1".to_string())), - },] + dns_allow_rule.protocols, + [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] ); - - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db8::".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); - assert_eq!( - result, - [IpAddress { - address: Some(Address::Ip("2001:db8::".to_string())), - },] + dns_allow_rule.destination_ports, + [Port { + port: Some(PortInner::SinglePort(53)) + }] ); -} - -#[test] -fn test_find_largest_ipv4_subnet_perfect_match() { - // Test /24 subnet - let start = Ipv4Addr::new(192, 168, 1, 0); - let end = Ipv4Addr::new(192, 168, 1, 255); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "192.168.1.0/24"); - - // Test /28 subnet (16 addresses) - let start = Ipv4Addr::new(192, 168, 1, 0); - let end = Ipv4Addr::new(192, 168, 1, 15); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "192.168.1.0/28"); -} - -#[test] -fn test_find_largest_ipv6_subnet_perfect_match() { - // Test /112 subnet - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db8::ffff".parse::().unwrap(); - - let result = find_largest_subnet_in_range(IpAddr::V6(start), IpAddr::V6(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "2001:db8::/112"); -} - -#[test] -fn test_find_largest_subnet_mixed_ip_versions() { - // Test mixed IP versions should return None - let start = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)); - let end = IpAddr::V6("2001:db8::1".parse().unwrap()); - - let result = find_largest_subnet_in_range(start, end); - - assert!(result.is_none()); -} - -#[test] -fn test_find_largest_subnet_invalid_range() { - // Test invalid range (start > end) should return None - let start = Ipv4Addr::new(192, 168, 1, 10); - let end = Ipv4Addr::new(192, 168, 1, 5); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_none()); -} - -#[test] -fn test_merge_addrs_subnet_at_start_of_range() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 64)), - ]; - - let result = merge_addrs(ranges); - + // Source addresses should include network_devices 1,2 assert_eq!( - result, + dns_allow_rule.source_addrs, [ IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/26".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.1.64".to_string())), + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), }, - ] - ); - - let ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x40)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ IpAddress { - address: Some(Address::IpSubnet("2001:db8::/122".to_string())), + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), }, IpAddress { - address: Some(Address::Ip("2001:db8::40".to_string())), + address: Some(Address::IpRange(IpRange { + start: "10.0.100.1".to_string(), + end: "10.0.100.2".to_string(), + })), }, ] ); -} -#[test] -fn test_merge_addrs_subnet_at_end_of_range() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 15))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 31)), + let expected_destination_addrs = vec![ + IpAddress { + address: Some(Address::Ip("10.0.1.13".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), + }, ]; - let result = merge_addrs(ranges); + assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::Ip("192.168.1.15".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.1.16/28".to_string())), - }, - ] - ); + // Second ACL - DNS Access DENY + let dns_deny_rule = &generated_firewall_rules[3]; + assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule.protocols.is_empty(),); + assert!(dns_deny_rule.destination_ports.is_empty(),); + assert!(dns_deny_rule.source_addrs.is_empty(),); + assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); +} - let ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x0f)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1f)), - ]; +#[sqlx::test] +async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); - let result = merge_addrs(ranges); + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::Ip("2001:db8::f".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::10/124".to_string())), - }, - ] - ); -} + // Setup test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.r#gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.r#gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.r#gen(); + let user_5 = user_5.save(&pool).await.unwrap(); -#[test] -fn test_merge_port_ranges() { - // single port - let input_ranges = vec![PortRange::new(100, 100)]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::SinglePort(100)) - }] - ); + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); - // overlapping ranges - let input_ranges = vec![ - PortRange::new(100, 200), - PortRange::new(150, 220), - PortRange::new(210, 300), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 300 - })) - }] - ); + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } - // duplicate ranges - let input_ranges = vec![ - PortRange::new(100, 200), - PortRange::new(100, 200), - PortRange::new(150, 220), - PortRange::new(150, 220), - PortRange::new(210, 300), - PortRange::new(210, 300), - PortRange::new(350, 400), - PortRange::new(350, 400), - PortRange::new(350, 400), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [ - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 300 - })) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 350, - end: 400 - })) - } - ] - ); + // Setup test groups + let group_1 = Group { + id: NoId, + name: "group_1".into(), + ..Default::default() + }; + let group_1 = group_1.save(&pool).await.unwrap(); + let group_2 = Group { + id: NoId, + name: "group_2".into(), + ..Default::default() + }; + let group_2 = group_2.save(&pool).await.unwrap(); - // non-consecutive ranges - let input_ranges = vec![ - PortRange::new(501, 699), - PortRange::new(151, 220), - PortRange::new(210, 300), - PortRange::new(800, 800), - PortRange::new(200, 210), - PortRange::new(50, 50), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [ - Port { - port: Some(PortInner::SinglePort(50)) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 151, - end: 300 - })) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 501, - end: 699 - })) - }, - Port { - port: Some(PortInner::SinglePort(800)) - } - ] - ); - - // fully contained range - let input_ranges = vec![PortRange::new(100, 200), PortRange::new(120, 180)]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 200 - })) - }] - ); -} - -#[test] -fn test_last_ip_in_v6_subnet() { - let subnet: Ipv6Network = "2001:db8:85a3::8a2e:370:7334/64".parse().unwrap(); - let last_ip = get_last_ip_in_v6_subnet(&subnet); - assert_eq!( - last_ip, - IpAddr::V6(Ipv6Addr::new( - 0x2001, 0x0db8, 0x85a3, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff - )) - ); - - let subnet: Ipv6Network = "280b:47f8:c9d7:634c:cb35:11f3:14e1:5016/119" - .parse() - .unwrap(); - let last_ip = get_last_ip_in_v6_subnet(&subnet); - assert_eq!( - last_ip, - IpAddr::V6(Ipv6Addr::new( - 0x280b, 0x47f8, 0xc9d7, 0x634c, 0xcb35, 0x11f3, 0x14e1, 0x51ff - )) - ); -} - -async fn create_acl_rule( - pool: &PgPool, - rule: AclRule, - locations: Vec, - allowed_users: Vec, - denied_users: Vec, - allowed_groups: Vec, - denied_groups: Vec, - allowed_network_devices: Vec, - denied_network_devices: Vec, - destination_ranges: Vec<(IpAddr, IpAddr)>, - aliases: Vec, -) -> AclRuleInfo { - let mut conn = pool.acquire().await.unwrap(); - - // create base rule - let rule = rule.save(&mut *conn).await.unwrap(); - let rule_id = rule.id; - - // create related objects - // locations - for location_id in locations { - AclRuleNetwork::new(rule_id, location_id) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed users - for user_id in allowed_users { - AclRuleUser::new(rule_id, user_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied users - for user_id in denied_users { - AclRuleUser::new(rule_id, user_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed groups - for group_id in allowed_groups { - AclRuleGroup::new(rule_id, group_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied groups - for group_id in denied_groups { - AclRuleGroup::new(rule_id, group_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed devices - for device_id in allowed_network_devices { - AclRuleDevice::new(rule_id, device_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied devices - for device_id in denied_network_devices { - AclRuleDevice::new(rule_id, device_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // destination ranges - for range in destination_ranges { - AclRuleDestinationRange { - id: NoId, - rule_id, - start: range.0, - end: range.1, - } - .save(&mut *conn) - .await - .unwrap(); - } - - // aliases - for alias_id in aliases { - AclRuleAlias::new(rule_id, alias_id) - .save(&mut *conn) - .await - .unwrap(); - } - - // convert to output format - rule.to_info(&mut conn).await.unwrap() -} - -#[sqlx::test] -async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: false, - ..Default::default() - }; - let mut location = location.save(&pool).await.unwrap(); - - // Setup test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - let user_3: User = rng.r#gen(); - let user_3 = user_3.save(&pool).await.unwrap(); - let user_4: User = rng.r#gen(); - let user_4 = user_4.save(&pool).await.unwrap(); - let user_5: User = rng.r#gen(); - let user_5 = user_5.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // Setup test groups - let group_1 = Group { - id: NoId, - name: "group_1".into(), - ..Default::default() - }; - let group_1 = group_1.save(&pool).await.unwrap(); - let group_2 = Group { - id: NoId, - name: "group_2".into(), - ..Default::default() - }; - let group_2 = group_2.save(&pool).await.unwrap(); - - // Assign users to groups: - // Group 1: users 1,2 - // Group 2: users 3,4 - let group_assignments = vec![ - (&group_1, vec![&user_1, &user_2]), - (&group_2, vec![&user_3, &user_4]), + // Assign users to groups: + // Group 1: users 1,2 + // Group 2: users 3,4 + let group_assignments = vec![ + (&group_1, vec![&user_1, &user_2]), + (&group_2, vec![&user_3, &user_4]), ]; for (group, users) in group_assignments { @@ -1041,15 +726,15 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO let network_devices = vec![ ( network_device_1.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), ), ( network_device_2.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), ), ( network_device_3.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), ), ]; @@ -1075,7 +760,7 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: vec!["192.168.1.0/24".parse().unwrap()], + destination: vec!["fc00::0/112".parse().unwrap()], ports: vec![ PortRange::new(80, 80).into(), PortRange::new(443, 443).into(), @@ -1143,8 +828,8 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed let denied_devices_2 = vec![network_device_3.id]; // Third network device denied let destination_ranges_2 = vec![ - ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), - ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), + ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), + ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), ]; let aliases_2 = Vec::new(); @@ -1195,7 +880,7 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO assert_eq!( web_allow_rule.destination_addrs, [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + address: Some(Address::IpSubnet("fc00::/112".to_string())), }] ); assert_eq!( @@ -1215,18 +900,18 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO [ IpAddress { address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), })), }, IpAddress { address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), })), }, IpAddress { - address: Some(Address::Ip("10.0.100.1".to_string())), + address: Some(Address::Ip("ff00::100:1".to_string())), }, ] ); @@ -1240,7 +925,7 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO assert_eq!( web_deny_rule.destination_addrs, [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + address: Some(Address::IpSubnet("fc00::/112".to_string())), }] ); @@ -1257,70 +942,94 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO port: Some(PortInner::SinglePort(53)) }] ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.100.1".to_string(), - end: "10.0.100.2".to_string(), - })), - }, - ] - ); let expected_destination_addrs = vec![ IpAddress { - address: Some(Address::Ip("10.0.1.13".to_string())), + address: Some(Address::Ip("fc00::1:13".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), + address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), + address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), + address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), + address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), + address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), + address: Some(Address::IpSubnet("fc00::1:54/126".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), + address: Some(Address::IpSubnet("fc00::1:58/125".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), + address: Some(Address::IpSubnet("fc00::1:60/123".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), + address: Some(Address::IpSubnet("fc00::1:80/121".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), + address: Some(Address::IpSubnet("fc00::1:100/120".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), + address: Some(Address::IpSubnet("fc00::1:200/119".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:400/118".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:800/117".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:1000/116".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:2000/115".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:4000/114".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:8000/113".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::2:0/122".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::2:40/126".to_string())), }, ]; + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::100:1".to_string(), + end: "ff00::100:2".to_string(), + })), + }, + ] + ); assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); // Second ACL - DNS Access DENY @@ -1333,15 +1042,19 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO } #[sqlx::test] -async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { +async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + let mut rng = thread_rng(); // Create test location let location = WireguardNetwork { id: NoId, acl_enabled: false, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], ..Default::default() }; let mut location = location.save(&pool).await.unwrap(); @@ -1377,16 +1090,19 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - ))], + wireguard_ips: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], preshared_key: None, is_authorized: true, authorized_at: None, @@ -1471,23 +1187,32 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let network_devices = vec![ ( network_device_1.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), + ], ), ( network_device_2.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), + ], ), ( network_device_3.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), + ], ), ]; - for (device_id, ip) in network_devices { + for (device_id, ips) in network_devices { let network_device = WireguardNetworkDevice { device_id, wireguard_network_id: location.id, - wireguard_ips: vec![ip], + wireguard_ips: ips, preshared_key: None, is_authorized: true, authorized_at: None, @@ -1505,7 +1230,10 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: vec!["fc00::0/112".parse().unwrap()], + destination: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::0/112".parse().unwrap(), + ], ports: vec![ PortRange::new(80, 80).into(), PortRange::new(443, 443).into(), @@ -1573,6 +1301,8 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed let denied_devices_2 = vec![network_device_3.id]; // Third network device denied let destination_ranges_2 = vec![ + ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), + ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), ]; @@ -1616,20 +1346,71 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let generated_firewall_rules = generated_firewall_config.rules; - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 8); // First ACL - Web Access ALLOW - let web_allow_rule = &generated_firewall_rules[0]; - assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); + let web_allow_rule_ipv4 = &generated_firewall_rules[0]; assert_eq!( - web_allow_rule.destination_addrs, + web_allow_rule_ipv4.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + web_allow_rule_ipv4.protocols, + vec![i32::from(Protocol::Tcp)] + ); + assert_eq!( + web_allow_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); + assert_eq!( + web_allow_rule_ipv4.destination_ports, + vec![ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule_ipv4.source_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::Ip("10.0.100.1".to_string())), + }, + ] + ); + + let web_allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!( + web_allow_rule_ipv6.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!(web_allow_rule_ipv6.protocols, [i32::from(Protocol::Tcp)]); + assert_eq!( + web_allow_rule_ipv6.destination_addrs, [IpAddress { address: Some(Address::IpSubnet("fc00::/112".to_string())), }] ); assert_eq!( - web_allow_rule.destination_ports, + web_allow_rule_ipv6.destination_ports, [ Port { port: Some(PortInner::SinglePort(80)) @@ -1641,7 +1422,7 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO ); // Source addresses should include devices of users 1,2 and network_device_1 assert_eq!( - web_allow_rule.source_addrs, + web_allow_rule_ipv6.source_addrs, [ IpAddress { address: Some(Address::IpRange(IpRange { @@ -1662,35 +1443,158 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO ); // First ACL - Web Access DENY - let web_deny_rule = &generated_firewall_rules[2]; - assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule.protocols.is_empty()); - assert!(web_deny_rule.destination_ports.is_empty()); - assert!(web_deny_rule.source_addrs.is_empty()); + let web_deny_rule_ipv4 = &generated_firewall_rules[4]; + assert_eq!(web_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule_ipv4.protocols.is_empty()); + assert!(web_deny_rule_ipv4.destination_ports.is_empty()); + assert!(web_deny_rule_ipv4.source_addrs.is_empty()); assert_eq!( - web_deny_rule.destination_addrs, + web_deny_rule_ipv4.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); + + let web_deny_rule_ipv6 = &generated_firewall_rules[5]; + assert_eq!(web_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule_ipv6.protocols.is_empty()); + assert!(web_deny_rule_ipv6.destination_ports.is_empty()); + assert!(web_deny_rule_ipv6.source_addrs.is_empty()); + assert_eq!( + web_deny_rule_ipv6.destination_addrs, [IpAddress { address: Some(Address::IpSubnet("fc00::/112".to_string())), }] ); // Second ACL - DNS Access ALLOW - let dns_allow_rule = &generated_firewall_rules[1]; - assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + let dns_allow_rule_ipv4 = &generated_firewall_rules[2]; assert_eq!( - dns_allow_rule.protocols, + dns_allow_rule_ipv4.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + dns_allow_rule_ipv4.protocols, [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] ); assert_eq!( - dns_allow_rule.destination_ports, + dns_allow_rule_ipv4.destination_ports, [Port { port: Some(PortInner::SinglePort(53)) }] ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule_ipv4.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.100.1".to_string(), + end: "10.0.100.2".to_string(), + })), + }, + ] + ); - let expected_destination_addrs = vec![ + let expected_destination_addrs_v4 = vec![ IpAddress { - address: Some(Address::Ip("fc00::1:13".to_string())), + address: Some(Address::Ip("10.0.1.13".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), + }, + ]; + + assert_eq!( + dns_allow_rule_ipv4.destination_addrs, + expected_destination_addrs_v4 + ); + + let dns_allow_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!( + dns_allow_rule_ipv6.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + dns_allow_rule_ipv6.protocols, + [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule_ipv6.destination_ports, + [Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule_ipv6.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::100:1".to_string(), + end: "ff00::100:2".to_string(), + })), + }, + ] + ); + + let expected_destination_addrs_v6 = vec![ + IpAddress { + address: Some(Address::Ip("fc00::1:13".to_string())), }, IpAddress { address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), @@ -1751,1289 +1655,31 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO }, ]; - // Source addresses should include network_devices 1,2 assert_eq!( - dns_allow_rule.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::100:1".to_string(), - end: "ff00::100:2".to_string(), - })), - }, - ] + dns_allow_rule_ipv6.destination_addrs, + expected_destination_addrs_v6 ); - assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); // Second ACL - DNS Access DENY - let dns_deny_rule = &generated_firewall_rules[3]; - assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule.protocols.is_empty(),); - assert!(dns_deny_rule.destination_ports.is_empty(),); - assert!(dns_deny_rule.source_addrs.is_empty(),); - assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); -} - -#[sqlx::test] -async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: false, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let mut location = location.save(&pool).await.unwrap(); - - // Setup test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - let user_3: User = rng.r#gen(); - let user_3 = user_3.save(&pool).await.unwrap(); - let user_4: User = rng.r#gen(); - let user_4 = user_4.save(&pool).await.unwrap(); - let user_5: User = rng.r#gen(); - let user_5 = user_5.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // Setup test groups - let group_1 = Group { - id: NoId, - name: "group_1".into(), - ..Default::default() - }; - let group_1 = group_1.save(&pool).await.unwrap(); - let group_2 = Group { - id: NoId, - name: "group_2".into(), - ..Default::default() - }; - let group_2 = group_2.save(&pool).await.unwrap(); - - // Assign users to groups: - // Group 1: users 1,2 - // Group 2: users 3,4 - let group_assignments = vec![ - (&group_1, vec![&user_1, &user_2]), - (&group_2, vec![&user_3, &user_4]), - ]; - - for (group, users) in group_assignments { - for user in users { - query!( - "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", - user.id, - group.id - ) - .execute(&pool) - .await - .unwrap(); - } - } - - // Create some network devices - let network_device_1 = Device { - id: NoId, - name: "network-device-1".into(), - user_id: user_1.id, // Owned by user 1 - device_type: DeviceType::Network, - description: Some("Test network device 1".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_1 = network_device_1.save(&pool).await.unwrap(); - - let network_device_2 = Device { - id: NoId, - name: "network-device-2".into(), - user_id: user_2.id, // Owned by user 2 - device_type: DeviceType::Network, - description: Some("Test network device 2".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_2 = network_device_2.save(&pool).await.unwrap(); + let dns_deny_rule_ipv4 = &generated_firewall_rules[6]; + assert_eq!(dns_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule_ipv4.protocols.is_empty(),); + assert!(dns_deny_rule_ipv4.destination_ports.is_empty(),); + assert!(dns_deny_rule_ipv4.source_addrs.is_empty(),); + assert_eq!( + dns_deny_rule_ipv4.destination_addrs, + expected_destination_addrs_v4 + ); - let network_device_3 = Device { - id: NoId, - name: "network-device-3".into(), - user_id: user_3.id, // Owned by user 3 - device_type: DeviceType::Network, - description: Some("Test network device 3".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_3 = network_device_3.save(&pool).await.unwrap(); - - // Add network devices to location's VPN network - let network_devices = vec![ - ( - network_device_1.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), - ], - ), - ( - network_device_2.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), - ], - ), - ( - network_device_3.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), - ], - ), - ]; - - for (device_id, ips) in network_devices { - let network_device = WireguardNetworkDevice { - device_id, - wireguard_network_id: location.id, - wireguard_ips: ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - - // Create first ACL rule - Web access - let acl_rule_1 = AclRule { - id: NoId, - name: "Web Access".into(), - all_networks: false, - expires: None, - allow_all_users: false, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: vec![ - "192.168.1.0/24".parse().unwrap(), - "fc00::0/112".parse().unwrap(), - ], - ports: vec![ - PortRange::new(80, 80).into(), - PortRange::new(443, 443).into(), - ], - protocols: vec![Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations = vec![location.id]; - let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web - let denied_users = vec![user_3.id]; // Third user explicitly denied - let allowed_groups = vec![group_1.id]; // First group allowed - let denied_groups = Vec::new(); - let allowed_devices = vec![network_device_1.id]; - let denied_devices = vec![network_device_2.id, network_device_3.id]; - let destination_ranges = Vec::new(); - let aliases = Vec::new(); - - let _acl_rule_1 = create_acl_rule( - &pool, - acl_rule_1, - locations, - allowed_users, - denied_users, - allowed_groups, - denied_groups, - allowed_devices, - denied_devices, - destination_ranges, - aliases, - ) - .await; - - // Create second ACL rule - DNS access - let acl_rule_2 = AclRule { - id: NoId, - name: "DNS Access".into(), - all_networks: false, - expires: None, - allow_all_users: true, // Allow all users - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead - ports: vec![PortRange::new(53, 53).into()], - protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations_2 = vec![location.id]; - let allowed_users_2 = Vec::new(); - let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS - let allowed_groups_2 = Vec::new(); - let denied_groups_2 = vec![group_2.id]; - let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed - let denied_devices_2 = vec![network_device_3.id]; // Third network device denied - let destination_ranges_2 = vec![ - ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), - ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), - ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), - ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), - ]; - let aliases_2 = Vec::new(); - - let _acl_rule_2 = create_acl_rule( - &pool, - acl_rule_2, - locations_2, - allowed_users_2, - denied_users_2, - allowed_groups_2, - denied_groups_2, - allowed_devices_2, - denied_devices_2, - destination_ranges_2, - aliases_2, - ) - .await; - - let mut conn = pool.acquire().await.unwrap(); - - // try to generate firewall config with ACL disabled - location.acl_enabled = false; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap(); - assert!(generated_firewall_config.is_none()); - - // generate firewall config with default policy Allow - location.acl_enabled = true; - location.acl_default_allow = true; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap(); - assert_eq!( - generated_firewall_config.default_policy, - i32::from(FirewallPolicy::Allow) - ); - - let generated_firewall_rules = generated_firewall_config.rules; - - assert_eq!(generated_firewall_rules.len(), 8); - - // First ACL - Web Access ALLOW - let web_allow_rule_ipv4 = &generated_firewall_rules[0]; - assert_eq!( - web_allow_rule_ipv4.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - web_allow_rule_ipv4.protocols, - vec![i32::from(Protocol::Tcp)] - ); - assert_eq!( - web_allow_rule_ipv4.destination_addrs, - vec![IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }] - ); - assert_eq!( - web_allow_rule_ipv4.destination_ports, - vec![ - Port { - port: Some(PortInner::SinglePort(80)) - }, - Port { - port: Some(PortInner::SinglePort(443)) - } - ] - ); - // Source addresses should include devices of users 1,2 and network_device_1 - assert_eq!( - web_allow_rule_ipv4.source_addrs, - vec![ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::Ip("10.0.100.1".to_string())), - }, - ] - ); - - let web_allow_rule_ipv6 = &generated_firewall_rules[1]; - assert_eq!( - web_allow_rule_ipv6.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!(web_allow_rule_ipv6.protocols, [i32::from(Protocol::Tcp)]); - assert_eq!( - web_allow_rule_ipv6.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("fc00::/112".to_string())), - }] - ); + let dns_deny_rule_ipv6 = &generated_firewall_rules[7]; + assert_eq!(dns_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule_ipv6.protocols.is_empty(),); + assert!(dns_deny_rule_ipv6.destination_ports.is_empty(),); + assert!(dns_deny_rule_ipv6.source_addrs.is_empty(),); assert_eq!( - web_allow_rule_ipv6.destination_ports, - [ - Port { - port: Some(PortInner::SinglePort(80)) - }, - Port { - port: Some(PortInner::SinglePort(443)) - } - ] + dns_deny_rule_ipv6.destination_addrs, + expected_destination_addrs_v6 ); - // Source addresses should include devices of users 1,2 and network_device_1 - assert_eq!( - web_allow_rule_ipv6.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::Ip("ff00::100:1".to_string())), - }, - ] - ); - - // First ACL - Web Access DENY - let web_deny_rule_ipv4 = &generated_firewall_rules[4]; - assert_eq!(web_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule_ipv4.protocols.is_empty()); - assert!(web_deny_rule_ipv4.destination_ports.is_empty()); - assert!(web_deny_rule_ipv4.source_addrs.is_empty()); - assert_eq!( - web_deny_rule_ipv4.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }] - ); - - let web_deny_rule_ipv6 = &generated_firewall_rules[5]; - assert_eq!(web_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule_ipv6.protocols.is_empty()); - assert!(web_deny_rule_ipv6.destination_ports.is_empty()); - assert!(web_deny_rule_ipv6.source_addrs.is_empty()); - assert_eq!( - web_deny_rule_ipv6.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("fc00::/112".to_string())), - }] - ); - - // Second ACL - DNS Access ALLOW - let dns_allow_rule_ipv4 = &generated_firewall_rules[2]; - assert_eq!( - dns_allow_rule_ipv4.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - dns_allow_rule_ipv4.protocols, - [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] - ); - assert_eq!( - dns_allow_rule_ipv4.destination_ports, - [Port { - port: Some(PortInner::SinglePort(53)) - }] - ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule_ipv4.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.100.1".to_string(), - end: "10.0.100.2".to_string(), - })), - }, - ] - ); - - let expected_destination_addrs_v4 = vec![ - IpAddress { - address: Some(Address::Ip("10.0.1.13".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), - }, - ]; - - assert_eq!( - dns_allow_rule_ipv4.destination_addrs, - expected_destination_addrs_v4 - ); - - let dns_allow_rule_ipv6 = &generated_firewall_rules[3]; - assert_eq!( - dns_allow_rule_ipv6.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - dns_allow_rule_ipv6.protocols, - [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] - ); - assert_eq!( - dns_allow_rule_ipv6.destination_ports, - [Port { - port: Some(PortInner::SinglePort(53)) - }] - ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule_ipv6.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::100:1".to_string(), - end: "ff00::100:2".to_string(), - })), - }, - ] - ); - - let expected_destination_addrs_v6 = vec![ - IpAddress { - address: Some(Address::Ip("fc00::1:13".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:54/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:58/125".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:60/123".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:80/121".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:100/120".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:200/119".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:400/118".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:800/117".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:1000/116".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:2000/115".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:4000/114".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:8000/113".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::2:0/122".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::2:40/126".to_string())), - }, - ]; - - assert_eq!( - dns_allow_rule_ipv6.destination_addrs, - expected_destination_addrs_v6 - ); - - // Second ACL - DNS Access DENY - let dns_deny_rule_ipv4 = &generated_firewall_rules[6]; - assert_eq!(dns_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule_ipv4.protocols.is_empty(),); - assert!(dns_deny_rule_ipv4.destination_ports.is_empty(),); - assert!(dns_deny_rule_ipv4.source_addrs.is_empty(),); - assert_eq!( - dns_deny_rule_ipv4.destination_addrs, - expected_destination_addrs_v4 - ); - - let dns_deny_rule_ipv6 = &generated_firewall_rules[7]; - assert_eq!(dns_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule_ipv6.protocols.is_empty(),); - assert!(dns_deny_rule_ipv6.destination_ports.is_empty(),); - assert!(dns_deny_rule_ipv6.source_addrs.is_empty(),); - assert_eq!( - dns_deny_rule_ipv6.destination_addrs, - expected_destination_addrs_v6 - ); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] diff --git a/crates/defguard_core/src/enterprise/firewall/tests/source.rs b/crates/defguard_core/src/enterprise/firewall/tests/source.rs new file mode 100644 index 0000000000..063e18b370 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/source.rs @@ -0,0 +1,158 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_proto::enterprise::firewall::{IpAddress, IpVersion, ip_address::Address}; +use rand::thread_rng; + +use crate::enterprise::firewall::{ + get_source_addrs, get_source_network_devices, get_source_users, + tests::{random_network_device_with_id, random_user_with_id}, +}; + +#[test] +fn test_get_relevant_users() { + let mut rng = thread_rng(); + + // prepare allowed and denied users lists with shared elements + let user_1 = random_user_with_id(&mut rng, 1); + let user_2 = random_user_with_id(&mut rng, 2); + let user_3 = random_user_with_id(&mut rng, 3); + let user_4 = random_user_with_id(&mut rng, 4); + let user_5 = random_user_with_id(&mut rng, 5); + let allowed_users = vec![user_1.clone(), user_2.clone(), user_4.clone()]; + let denied_users = vec![user_3.clone(), user_4, user_5.clone()]; + + let users = get_source_users(allowed_users, &denied_users); + assert_eq!(users, vec![user_1, user_2]); +} + +#[test] +fn test_get_relevant_network_devices() { + let mut rng = thread_rng(); + + // prepare allowed and denied network devices lists with shared elements + let device_1 = random_network_device_with_id(&mut rng, 1); + let device_2 = random_network_device_with_id(&mut rng, 2); + let device_3 = random_network_device_with_id(&mut rng, 3); + let device_4 = random_network_device_with_id(&mut rng, 4); + let device_5 = random_network_device_with_id(&mut rng, 5); + let allowed_devices = vec![ + device_1.clone(), + device_3.clone(), + device_4.clone(), + device_5.clone(), + ]; + let denied_devices = vec![device_2.clone(), device_4, device_5.clone()]; + + let devices = get_source_network_devices(allowed_devices, &denied_devices); + assert_eq!(devices, vec![device_1, device_3]); +} + +#[test] +fn test_process_source_addrs_v4() { + // Test data with mixed IPv4 and IPv6 addresses + let user_device_ips = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 5)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), + ]; + + let network_device_ips = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 3)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 4)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), // Should be filtered out + IpAddr::V4(Ipv4Addr::new(172, 16, 1, 1)), + ]; + + let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv4); + + // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges + assert_eq!( + source_addrs, + [ + IpAddress { + address: Some(Address::Ip("10.0.1.1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.2/31".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.4/31".to_string())) + }, + IpAddress { + address: Some(Address::Ip("172.16.1.1".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.1.100".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv4); + assert!(empty_addrs.is_empty()); + + // Test with only IPv6 addresses - should return empty result for IPv4 + let ipv6_only = get_source_addrs( + vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))], + vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2))], + IpVersion::Ipv4, + ); + assert!(ipv6_only.is_empty()); +} + +#[test] +fn test_process_source_addrs_v6() { + // Test data with mixed IPv4 and IPv6 addresses + let user_device_ips = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 5)), + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), // Should be filtered out + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 1)), + ]; + + let network_device_ips = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 3)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 4)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), // Should be filtered out + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 2, 0, 0, 0, 1)), + ]; + + let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv6); + + // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges + assert_eq!( + source_addrs, + [ + IpAddress { + address: Some(Address::Ip("2001:db8::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::2/127".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::4/127".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:0:1::1".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8:0:2::1".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv6); + assert!(empty_addrs.is_empty()); + + // Test with only IPv4 addresses - should return empty result for IPv6 + let ipv4_only = get_source_addrs( + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))], + IpVersion::Ipv6, + ); + assert!(ipv4_only.is_empty()); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs new file mode 100644 index 0000000000..4d82a72ce4 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs @@ -0,0 +1,213 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; +use ipnetwork::IpNetwork; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use crate::enterprise::{ + db::models::acl::{AclRule, AclRuleNetwork, RuleState}, + firewall::try_get_location_firewall_config, +}; + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} From 7712c708924b8e08c5bcd3c0dc64fd9fc0796108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 4 Feb 2026 14:22:43 +0100 Subject: [PATCH 06/25] add helper for user and device setup --- .../src/enterprise/db/models/acl.rs | 27 ++- .../firewall/tests/all_locations.rs | 190 +----------------- .../src/enterprise/firewall/tests/mod.rs | 67 +++++- 3 files changed, 100 insertions(+), 184 deletions(-) diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index a39c4c0982..24eb50d533 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -237,7 +237,7 @@ impl AclRuleInfo { /// Those objects have their dedicated tables and structures so we provide /// [`AclRuleInfo`] and [`ApiAclRule`] structs that implement appropriate methods /// to combine all the related objects for easier downstream processing. -#[derive(Clone, Debug, Default, Eq, FromRow, Model, PartialEq, ToSchema)] +#[derive(Clone, Debug, Eq, FromRow, Model, PartialEq, ToSchema)] pub struct AclRule { pub id: I, // if present points to the original rule before modification / deletion @@ -266,6 +266,31 @@ pub struct AclRule { pub manual_settings: bool, } +impl Default for AclRule { + fn default() -> Self { + Self { + id: NoId, + parent_id: Default::default(), + state: RuleState::New, + name: "ACL rule".to_string(), + allow_all_users: false, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + all_networks: false, + destination: Vec::new(), + ports: Vec::new(), + protocols: Vec::new(), + enabled: true, + expires: None, + any_destination: true, + any_port: true, + any_protocol: true, + manual_settings: true, + } + } +} + impl AclRule { /// Creates new [`AclRule`] with all related objects based on [`ApiAclRule`] pub(crate) async fn create_from_api( diff --git a/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs index c3ad11a368..c32c7dfa67 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs @@ -1,17 +1,13 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use defguard_common::db::{ - NoId, - models::{Device, DeviceType, User, WireguardNetwork, device::WireguardNetworkDevice}, - setup_pool, -}; +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; use ipnetwork::IpNetwork; -use rand::{Rng, thread_rng}; +use rand::thread_rng; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use crate::enterprise::{ db::models::acl::{AclRule, AclRuleNetwork, RuleState}, - firewall::try_get_location_firewall_config, + firewall::{tests::create_test_users_and_devices, try_get_location_firewall_config}, }; #[sqlx::test] @@ -34,58 +30,9 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO ..Default::default() }; let location_2 = location_2.save(&pool).await.unwrap(); + // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 10, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } + create_test_users_and_devices(&mut rng, &pool, vec![&location_1, &location_2]).await; // create ACL rules let acl_rule_1 = AclRule { @@ -186,65 +133,7 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO let location_2 = location_2.save(&pool).await.unwrap(); // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 10, - 10, - user.id as u16, - device_num as u16, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } + create_test_users_and_devices(&mut rng, &pool, vec![&location_1, &location_2]).await; // create ACL rules let acl_rule_1 = AclRule { @@ -346,72 +235,9 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P ..Default::default() }; let location_2 = location_2.save(&pool).await.unwrap(); + // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 10, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 10, - 10, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } + create_test_users_and_devices(&mut rng, &pool, vec![&location_1, &location_2]).await; // create ACL rules let acl_rule_1 = AclRule { diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index afe18fd18e..98c6e1ae06 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -13,7 +13,7 @@ use defguard_proto::enterprise::firewall::{ ip_address::Address, port::Port as PortInner, }; use ipnetwork::IpNetwork; -use rand::{Rng, thread_rng}; +use rand::{Rng, rngs::ThreadRng, thread_rng}; use sqlx::{ PgPool, postgres::{PgConnectOptions, PgPoolOptions}, @@ -60,6 +60,71 @@ fn random_network_device_with_id(rng: &mut R, id: Id) -> Device { device } +async fn create_test_users_and_devices( + rng: &mut ThreadRng, + pool: &PgPool, + test_locations: Vec<&WireguardNetwork>, +) { + // create two users + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(pool).await.unwrap(); + + // create two devices for each user and create network configurations for all test locations + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(pool).await.unwrap(); + + // Add device to locations' VPN network + for location in test_locations.iter() { + let wireguard_ips = location + .address + .iter() + .map(|subnet| match subnet { + IpNetwork::V4(ipv4_network) => { + let octets = ipv4_network.network().octets(); + IpAddr::V4(Ipv4Addr::new( + octets[0], + octets[1], + user.id as u8, + device_num, + )) + } + IpNetwork::V6(ipv6_network) => { + let mut octets = ipv6_network.network().octets(); + // Set the last two octets (bytes 14 and 15) + octets[14] = user.id as u8; + octets[15] = device_num; + IpAddr::V6(Ipv6Addr::from(octets)) + } + }) + .collect(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips, + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(pool).await.unwrap(); + } + } + } +} + async fn create_acl_rule( pool: &PgPool, rule: AclRule, From 4e33e3d65f361180027278f531f3434465d72f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 4 Feb 2026 14:39:17 +0100 Subject: [PATCH 07/25] fix all locations tests --- .../firewall/tests/all_locations.rs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs index c32c7dfa67..aa41614181 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs @@ -19,6 +19,7 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO let location_1 = WireguardNetwork { id: NoId, acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], ..Default::default() }; let location_1 = location_1.save(&pool).await.unwrap(); @@ -27,6 +28,7 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO let location_2 = WireguardNetwork { id: NoId, acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], ..Default::default() }; let location_2 = location_2.save(&pool).await.unwrap(); @@ -39,6 +41,7 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::Applied, destination: vec!["192.168.1.0/24".parse().unwrap()], manual_settings: true, @@ -53,8 +56,10 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO expires: None, enabled: true, all_networks: true, + allow_all_users: true, state: RuleState::Applied, - manual_settings: false, + destination: vec!["192.168.2.0/24".parse().unwrap()], + manual_settings: true, ..Default::default() } .save(&pool) @@ -68,7 +73,8 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO all_networks: true, allow_all_users: true, state: RuleState::Applied, - manual_settings: false, + destination: vec!["192.168.3.0/24".parse().unwrap()], + manual_settings: true, ..Default::default() } .save(&pool) @@ -96,8 +102,8 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO .unwrap() .rules; - // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 4); + // all rules were assigned to this location + assert_eq!(generated_firewall_rules.len(), 6); let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) .await @@ -106,7 +112,7 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO .rules; // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 3); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] @@ -140,7 +146,9 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::Applied, + manual_settings: true, destination: vec!["fc00::0/112".parse().unwrap()], ..Default::default() } @@ -152,8 +160,11 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO id: NoId, expires: None, enabled: true, + allow_all_users: true, all_networks: true, state: RuleState::Applied, + manual_settings: true, + destination: vec!["fb00::0/112".parse().unwrap()], ..Default::default() } .save(&pool) @@ -167,6 +178,8 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO all_networks: true, allow_all_users: true, state: RuleState::Applied, + manual_settings: true, + destination: vec!["fa00::0/112".parse().unwrap()], ..Default::default() } .save(&pool) @@ -195,7 +208,7 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO .rules; // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 6); let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) .await @@ -204,7 +217,7 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO .rules; // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 3); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] From 163af13d43c91c5d4132f053d76f7319627f3bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 07:58:53 +0100 Subject: [PATCH 08/25] update unapplied rules tests --- .../firewall/tests/unapplied_rules.rs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs index 4d82a72ce4..7f69b1dd1f 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs @@ -2,30 +2,39 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; use ipnetwork::IpNetwork; +use rand::thread_rng; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use crate::enterprise::{ db::models::acl::{AclRule, AclRuleNetwork, RuleState}, - firewall::try_get_location_firewall_config, + firewall::{tests::create_test_users_and_devices, try_get_location_firewall_config}, }; #[sqlx::test] async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + let mut rng = thread_rng(); + // Create test location let location = WireguardNetwork { id: NoId, acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], ..Default::default() }; let location = location.save(&pool).await.unwrap(); + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + // create unapplied ACL rules let mut acl_rule_1 = AclRule { id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::New, + manual_settings: true, ..Default::default() } .save(&pool) @@ -35,7 +44,9 @@ async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptio id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::Modified, + manual_settings: true, ..Default::default() } .save(&pool) @@ -72,12 +83,14 @@ async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptio .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 2); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + let mut rng = thread_rng(); + // Create test location let location = WireguardNetwork { id: NoId, @@ -87,12 +100,17 @@ async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptio }; let location = location.save(&pool).await.unwrap(); + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + // create unapplied ACL rules let mut acl_rule_1 = AclRule { id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::New, + manual_settings: true, ..Default::default() } .save(&pool) @@ -102,7 +120,9 @@ async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptio id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::Modified, + manual_settings: true, ..Default::default() } .save(&pool) @@ -139,12 +159,14 @@ async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptio .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 2); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + let mut rng = thread_rng(); + // Create test location let location = WireguardNetwork { id: NoId, @@ -157,12 +179,17 @@ async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgCon }; let location = location.save(&pool).await.unwrap(); + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + // create unapplied ACL rules let mut acl_rule_1 = AclRule { id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::New, + manual_settings: true, ..Default::default() } .save(&pool) @@ -172,7 +199,9 @@ async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgCon id: NoId, expires: None, enabled: true, + allow_all_users: true, state: RuleState::Modified, + manual_settings: true, ..Default::default() } .save(&pool) @@ -209,5 +238,5 @@ async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgCon .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 8); } From c7b747b382ebea7f64e2fca4b47035e501f30834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 09:32:38 +0100 Subject: [PATCH 09/25] add tests from main --- .../firewall/tests/disabled_rules.rs | 37 +- .../src/enterprise/firewall/tests/gh1868.rs | 194 ++++++++++ .../src/enterprise/firewall/tests/mod.rs | 347 ++++++++++++++++-- 3 files changed, 536 insertions(+), 42 deletions(-) create mode 100644 crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs diff --git a/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs index 577fb127eb..48f38636ad 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs @@ -2,30 +2,39 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; use ipnetwork::IpNetwork; +use rand::thread_rng; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use crate::enterprise::{ db::models::acl::{AclRule, AclRuleNetwork, RuleState}, - firewall::try_get_location_firewall_config, + firewall::{tests::create_test_users_and_devices, try_get_location_firewall_config}, }; #[sqlx::test] async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + let mut rng = thread_rng(); + // Create test location let location = WireguardNetwork { id: NoId, acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], ..Default::default() }; let location = location.save(&pool).await.unwrap(); + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + // create disabled ACL rules let mut acl_rule_1 = AclRule { id: NoId, expires: None, enabled: false, state: RuleState::Applied, + allow_all_users: true, + manual_settings: true, ..Default::default() } .save(&pool) @@ -36,6 +45,8 @@ async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOption expires: None, enabled: false, state: RuleState::Applied, + allow_all_users: true, + manual_settings: true, ..Default::default() } .save(&pool) @@ -72,12 +83,14 @@ async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOption .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 2); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + let mut rng = thread_rng(); + // Create test location let location = WireguardNetwork { id: NoId, @@ -87,12 +100,17 @@ async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOption }; let location = location.save(&pool).await.unwrap(); + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + // create disabled ACL rules let mut acl_rule_1 = AclRule { id: NoId, expires: None, enabled: false, state: RuleState::Applied, + allow_all_users: true, + manual_settings: true, ..Default::default() } .save(&pool) @@ -103,6 +121,8 @@ async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOption expires: None, enabled: false, state: RuleState::Applied, + allow_all_users: true, + manual_settings: true, ..Default::default() } .save(&pool) @@ -139,12 +159,14 @@ async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOption .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 2); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + let mut rng = thread_rng(); + // Create test location let location = WireguardNetwork { id: NoId, @@ -157,12 +179,17 @@ async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConn }; let location = location.save(&pool).await.unwrap(); + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + // create disabled ACL rules let mut acl_rule_1 = AclRule { id: NoId, expires: None, enabled: false, state: RuleState::Applied, + allow_all_users: true, + manual_settings: true, ..Default::default() } .save(&pool) @@ -173,6 +200,8 @@ async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConn expires: None, enabled: false, state: RuleState::Applied, + allow_all_users: true, + manual_settings: true, ..Default::default() } .save(&pool) @@ -209,5 +238,5 @@ async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConn .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 8); } diff --git a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs new file mode 100644 index 0000000000..5738d25671 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs @@ -0,0 +1,194 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_common::db::{ + Id, NoId, + models::{Device, DeviceType, User, WireguardNetwork, device::WireguardNetworkDevice}, + setup_pool, +}; +use defguard_proto::enterprise::firewall::{FirewallPolicy, IpVersion}; +use ipnetwork::IpNetwork; +use rand::{Rng, rngs::ThreadRng, thread_rng}; +use sqlx::{ + PgPool, + postgres::{PgConnectOptions, PgPoolOptions}, +}; + +use crate::enterprise::{ + db::models::acl::{AclRule, RuleState}, + firewall::try_get_location_firewall_config, +}; + +async fn setup_user_and_device( + rng: &mut ThreadRng, + pool: &PgPool, + location: &WireguardNetwork, +) { + let user: User = rng.r#gen(); + let user = user.save(pool).await.unwrap(); + + let device = Device { + id: NoId, + name: format!("device-{}", user.id), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(pool).await.unwrap(); + + let wireguard_ips = location + .address + .iter() + .map(|subnet| match subnet { + IpNetwork::V4(ipv4_network) => { + let octets = ipv4_network.network().octets(); + IpAddr::V4(Ipv4Addr::new( + octets[0], + octets[1], + user.id as u8, + device.id as u8, + )) + } + IpNetwork::V6(ipv6_network) => { + let mut octets = ipv6_network.network().octets(); + // Set the last two octets (bytes 14 and 15) + octets[14] = user.id as u8; + octets[15] = device.id as u8; + IpAddr::V6(Ipv6Addr::from(octets)) + } + }) + .collect(); + + // assign network address to device + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips, + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(pool).await.unwrap(); +} + +#[sqlx::test] +async fn test_gh1868_ipv6_rule_is_not_created_with_v4_only_destination( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location with both IPv4 and IPv6 subnet + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 80, 1)), 24).unwrap(), + IpNetwork::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 64, + ) + .unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // setup user & device + setup_user_and_device(&mut rng, &pool, &location).await; + + // create a rule with only an IPv4 destination + let acl_rule = AclRule { + all_networks: true, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec!["192.168.1.0/24".parse().unwrap()], + manual_settings: true, + enabled: true, + state: RuleState::Applied, + ..Default::default() + }; + acl_rule.save(&pool).await.unwrap(); + + // verify only IPv4 rules are created + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + let generated_firewall_rules = generated_firewall_config.rules; + assert_eq!(generated_firewall_rules.len(), 2); + + let allow_rule = &generated_firewall_rules[0]; + assert_eq!(allow_rule.verdict(), FirewallPolicy::Allow); + assert_eq!(allow_rule.ip_version(), IpVersion::Ipv4); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict(), FirewallPolicy::Deny); + assert_eq!(allow_rule.ip_version(), IpVersion::Ipv4); +} + +#[sqlx::test] +async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test location with both IPv4 and IPv6 subnet + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 80, 1)), 24).unwrap(), + IpNetwork::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 64, + ) + .unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // setup user & device + setup_user_and_device(&mut rng, &pool, &location).await; + + // create a rule with only an IPv4 destination + let acl_rule = AclRule { + all_networks: true, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec!["fc00::0/112".parse().unwrap()], + enabled: true, + state: RuleState::Applied, + ..Default::default() + }; + acl_rule.save(&pool).await.unwrap(); + + // verify only IPv4 rules are created + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + let generated_firewall_rules = generated_firewall_config.rules; + assert_eq!(generated_firewall_rules.len(), 2); + + let allow_rule = &generated_firewall_rules[0]; + assert_eq!(allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule.ip_version, i32::from(IpVersion::Ipv6)); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(allow_rule.ip_version, i32::from(IpVersion::Ipv6)); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index 98c6e1ae06..39967c60b6 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -9,7 +9,7 @@ use defguard_common::db::{ setup_pool, }; use defguard_proto::enterprise::firewall::{ - FirewallPolicy, IpAddress, IpRange, Port, PortRange as PortRangeProto, Protocol, + FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, ip_address::Address, port::Port as PortInner, }; use ipnetwork::IpNetwork; @@ -32,6 +32,7 @@ mod all_locations; mod destination; mod disabled_rules; mod expired_rules; +mod gh1868; mod ip_address_handling; mod source; mod unapplied_rules; @@ -1945,6 +1946,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt let location = WireguardNetwork { id: NoId, acl_enabled: true, + address: vec!["10.0.0.0/16".parse().unwrap()], ..Default::default() } .save(&pool) @@ -1952,43 +1954,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt .unwrap(); // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{device_num}", user.id), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; // create ACL rule without manually configured destination let acl_rule = AclRule { @@ -1999,6 +1965,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt state: RuleState::Applied, destination: Vec::new(), allow_all_users: true, + manual_settings: false, ..Default::default() } .save(&pool) @@ -2139,3 +2106,307 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt Some("ACL 1 - test rule, ALIAS 2 - redis DENY".to_string()) ); } + +#[sqlx::test] +async fn test_no_allowed_users_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + allow_all_users: true, + manual_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + allow_all_users: true, + manual_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // only deny rules are generated + assert_eq!(generated_firewall_rules.len(), 2); + for rule in generated_firewall_rules { + assert_eq!(rule.verdict(), FirewallPolicy::Deny); + } +} + +#[sqlx::test] +async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test locations with IPv4 and IPv6 addresses + let location_ipv4 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let location_ipv6 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let location_ipv4_and_ipv6 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // Setup some test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{device_num}", user.id), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to all locations' VPN networks + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_ipv4.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_ipv6.id, + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_ipv4_and_ipv6.id, + wireguard_ips: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rule without manually configured destination and no aliases + let acl_rule = AclRule { + id: NoId, + name: "test rule".to_string(), + expires: None, + enabled: true, + state: RuleState::Applied, + destination: Vec::new(), + allow_all_users: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rule to all locations + for location in [&location_ipv4, &location_ipv6, &location_ipv4_and_ipv6] { + AclRuleNetwork::new(acl_rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + + // check generated rules for IPv4 only location + let generated_firewall_rules_ipv4 = try_get_location_firewall_config(&location_ipv4, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + assert_eq!(generated_firewall_rules_ipv4.len(), 2); + let expected_source_addrs_ipv4 = vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + ]; + let allow_rule_ipv4 = &generated_firewall_rules_ipv4[0]; + assert_eq!(allow_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(allow_rule_ipv4.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv4.source_addrs, expected_source_addrs_ipv4); + assert!(allow_rule_ipv4.destination_addrs.is_empty()); + + let deny_rule_ipv4 = &generated_firewall_rules_ipv4[1]; + assert_eq!(deny_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule_ipv4.source_addrs.is_empty()); + assert!(deny_rule_ipv4.destination_addrs.is_empty()); + + // check generated rules for IPv6 only location + let generated_firewall_rules_ipv6 = try_get_location_firewall_config(&location_ipv6, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + assert_eq!(generated_firewall_rules_ipv6.len(), 2); + let expected_source_addrs_ipv6 = vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + ]; + let allow_rule_ipv6 = &generated_firewall_rules_ipv6[0]; + assert_eq!(allow_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!(allow_rule_ipv6.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv6.source_addrs, expected_source_addrs_ipv6); + assert!(allow_rule_ipv6.destination_addrs.is_empty()); + + let deny_rule_ipv6 = &generated_firewall_rules_ipv6[1]; + assert_eq!(deny_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!(deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule_ipv6.source_addrs.is_empty()); + assert!(deny_rule_ipv6.destination_addrs.is_empty()); + + // check generated rules for IPv4 and IPv6 location + let generated_firewall_rules_ipv4_and_ipv6 = + try_get_location_firewall_config(&location_ipv4_and_ipv6, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + assert_eq!(generated_firewall_rules_ipv4_and_ipv6.len(), 4); + let allow_rule_ipv4 = &generated_firewall_rules_ipv4_and_ipv6[0]; + assert_eq!(allow_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(allow_rule_ipv4.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv4.source_addrs, expected_source_addrs_ipv4); + assert!(allow_rule_ipv4.destination_addrs.is_empty()); + + let allow_rule_ipv6 = &generated_firewall_rules_ipv4_and_ipv6[1]; + assert_eq!(allow_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!(allow_rule_ipv6.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv6.source_addrs, expected_source_addrs_ipv6); + assert!(allow_rule_ipv6.destination_addrs.is_empty()); + + let deny_rule_ipv4 = &generated_firewall_rules_ipv4_and_ipv6[2]; + assert_eq!(deny_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule_ipv4.source_addrs.is_empty()); + assert!(deny_rule_ipv4.destination_addrs.is_empty()); + + let deny_rule_ipv6 = &generated_firewall_rules_ipv4_and_ipv6[3]; + assert_eq!(deny_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!(deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule_ipv6.source_addrs.is_empty()); + assert!(deny_rule_ipv6.destination_addrs.is_empty()); +} From b02b0d4785e18d03e6c12d0dfe976c40a0ce5eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 10:07:53 +0100 Subject: [PATCH 10/25] handle new toggles when generating rules for manually specified destinations --- .../src/enterprise/firewall/mod.rs | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index d2543ff26a..365022a87b 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -99,12 +99,17 @@ pub async fn generate_firewall_rules_from_acls( &mut *conn, id, &name, + has_ipv4_addresses, + has_ipv6_addresses, (&ipv4_source_addrs, &ipv6_source_addrs), aliases, destination, destination_ranges, ports, protocols, + any_destination, + any_port, + any_protocol, ) .await?; @@ -250,12 +255,17 @@ async fn get_manual_destination_rules( conn: &mut PgConnection, rule_id: Id, rule_name: &str, + location_has_ipv4_addresses: bool, + location_has_ipv6_addresses: bool, source_addrs: (&[IpAddress], &[IpAddress]), aliases: Vec>, mut destination: Vec, destination_ranges: Vec>, mut ports: Vec, mut protocols: Vec, + any_destination: bool, + any_port: bool, + any_protocol: bool, ) -> Result<(Vec, Vec), FirewallError> { debug!("Generating firewall rules for manually configured destination in ACL rule {rule_id}"); // store alias ranges separately since they use a different struct @@ -277,20 +287,33 @@ async fn get_manual_destination_rules( process_destination_addrs(&destination, &destination_ranges); // prepare destination ports - let destination_ports = merge_port_ranges(ports); + let destination_ports = if any_port { + Vec::new() + } else { + merge_port_ranges(ports) + }; // remove duplicate protocol entries - protocols.sort_unstable(); - protocols.dedup(); + let destination_protocols = if any_protocol { + Vec::new() + } else { + protocols.sort_unstable(); + protocols.dedup(); + protocols + }; let (ipv4_source_addrs, ipv6_source_addrs) = source_addrs; - let has_ipv4_addresses = !ipv4_source_addrs.is_empty(); - let has_ipv6_addresses = !ipv6_source_addrs.is_empty(); + + // only generate rules for a given IP version if there is a destination address of a given type + let has_ipv4_destination = + !dest_addrs_v4.is_empty() || (location_has_ipv4_addresses && any_destination); + let has_ipv6_destination = + !dest_addrs_v6.is_empty() || (location_has_ipv6_addresses && any_destination); let comment = format!("ACL {} - {}", rule_id, rule_name); let mut allow_rules = Vec::new(); let mut deny_rules = Vec::new(); - if has_ipv4_addresses { + if has_ipv4_destination { // create IPv4 rules let ipv4_rules = create_rules( rule_id, @@ -298,7 +321,7 @@ async fn get_manual_destination_rules( &ipv4_source_addrs, &dest_addrs_v4, &destination_ports, - &protocols, + &destination_protocols, &comment, ); if let Some(rule) = ipv4_rules.0 { @@ -307,7 +330,7 @@ async fn get_manual_destination_rules( deny_rules.push(ipv4_rules.1); } - if has_ipv6_addresses { + if has_ipv6_destination { // create IPv6 rules let ipv6_rules = create_rules( rule_id, @@ -315,7 +338,7 @@ async fn get_manual_destination_rules( &ipv6_source_addrs, &dest_addrs_v6, &destination_ports, - &protocols, + &destination_protocols, &comment, ); if let Some(rule) = ipv6_rules.0 { From be33e81cfc32afec05396ce801299ecd47d57172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 10:08:04 +0100 Subject: [PATCH 11/25] fix existing tests --- .../src/enterprise/firewall/tests/gh1868.rs | 74 ++++++++++++++++++- .../src/enterprise/firewall/tests/mod.rs | 36 ++++----- 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs index 5738d25671..a4c3c73acc 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs @@ -107,6 +107,7 @@ async fn test_gh1868_ipv6_rule_is_not_created_with_v4_only_destination( deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, + any_destination: false, destination: vec!["192.168.1.0/24".parse().unwrap()], manual_settings: true, enabled: true, @@ -122,6 +123,7 @@ async fn test_gh1868_ipv6_rule_is_not_created_with_v4_only_destination( .unwrap() .unwrap(); let generated_firewall_rules = generated_firewall_config.rules; + println!("{generated_firewall_rules:#?}"); assert_eq!(generated_firewall_rules.len(), 2); let allow_rule = &generated_firewall_rules[0]; @@ -161,13 +163,14 @@ async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination( // setup user & device setup_user_and_device(&mut rng, &pool, &location).await; - // create a rule with only an IPv4 destination + // create a rule with only an IPv6 destination let acl_rule = AclRule { all_networks: true, allow_all_users: true, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, + any_destination: false, destination: vec!["fc00::0/112".parse().unwrap()], enabled: true, state: RuleState::Applied, @@ -175,7 +178,7 @@ async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination( }; acl_rule.save(&pool).await.unwrap(); - // verify only IPv4 rules are created + // verify only IPv6 rules are created let mut conn = pool.acquire().await.unwrap(); let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) .await @@ -192,3 +195,70 @@ async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination( assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); assert_eq!(allow_rule.ip_version, i32::from(IpVersion::Ipv6)); } + +#[sqlx::test] +async fn test_gh1868_ipv4_and_ipv6_rules_are_created_with_any_destination( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test location with both IPv4 and IPv6 subnet + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 80, 1)), 24).unwrap(), + IpNetwork::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 64, + ) + .unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // setup user & device + setup_user_and_device(&mut rng, &pool, &location).await; + + // create a rule with any destination enabled + let acl_rule = AclRule { + all_networks: true, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + any_destination: true, + destination: vec!["fc00::0/112".parse().unwrap()], + enabled: true, + state: RuleState::Applied, + ..Default::default() + }; + acl_rule.save(&pool).await.unwrap(); + + // verify only IPv4 rules are created + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + let generated_firewall_rules = generated_firewall_config.rules; + assert_eq!(generated_firewall_rules.len(), 4); + + let allow_rule_ipv4 = &generated_firewall_rules[0]; + assert_eq!(allow_rule_ipv4.verdict(), FirewallPolicy::Allow); + assert_eq!(allow_rule_ipv4.ip_version(), IpVersion::Ipv4); + let allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!(allow_rule_ipv6.verdict(), FirewallPolicy::Allow); + assert_eq!(allow_rule_ipv6.ip_version(), IpVersion::Ipv6); + + let deny_rule_ipv4 = &generated_firewall_rules[2]; + assert_eq!(deny_rule_ipv4.verdict(), FirewallPolicy::Deny); + assert_eq!(allow_rule_ipv4.ip_version(), IpVersion::Ipv4); + let deny_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!(deny_rule_ipv6.verdict(), FirewallPolicy::Deny); + assert_eq!(allow_rule_ipv6.ip_version(), IpVersion::Ipv6); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index 39967c60b6..ec2f16e4cb 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -405,9 +405,9 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, + any_destination: false, + any_port: false, + any_protocol: false, manual_settings: true, }; let locations = vec![location.id]; @@ -451,9 +451,9 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, + any_destination: false, + any_port: false, + any_protocol: false, manual_settings: true, }; let locations_2 = vec![location.id]; @@ -835,9 +835,9 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, + any_destination: false, + any_port: false, + any_protocol: false, manual_settings: true, }; let locations = vec![location.id]; @@ -881,9 +881,9 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, + any_destination: false, + any_port: false, + any_protocol: false, manual_settings: true, }; let locations_2 = vec![location.id]; @@ -1308,9 +1308,9 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, + any_destination: false, + any_port: false, + any_protocol: false, manual_settings: true, }; let locations = vec![location.id]; @@ -1354,9 +1354,9 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, + any_destination: false, + any_port: false, + any_protocol: false, manual_settings: true, }; let locations_2 = vec![location.id]; From 632fe1e70e16eb4868014c6a81f56e327a3037ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 10:33:11 +0100 Subject: [PATCH 12/25] handle generating rules for destinations --- .../src/enterprise/firewall/mod.rs | 171 ++++++++++++------ .../src/enterprise/firewall/tests/mod.rs | 2 + 2 files changed, 114 insertions(+), 59 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index 365022a87b..bd0ab8fae1 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -74,7 +74,7 @@ pub async fn generate_firewall_rules_from_acls( // extract destination parameters from ACL rule let AclRuleInfo { id, - name, + name: rule_name, destination, destination_ranges, ports, @@ -98,7 +98,7 @@ pub async fn generate_firewall_rules_from_acls( get_manual_destination_rules( &mut *conn, id, - &name, + &rule_name, has_ipv4_addresses, has_ipv6_addresses, (&ipv4_source_addrs, &ipv6_source_addrs), @@ -121,66 +121,27 @@ pub async fn generate_firewall_rules_from_acls( // process destination aliases by creating a dedicated set of rules for each of them if !destinations.is_empty() { debug!( - "Generating firewall rules for {} aliases used in ACL rule {id:?}", + "Generating firewall rules for {} pre-defined destinations used in ACL rule {id:?}", destinations.len() ); } - for alias in destinations { - debug!("Processing ACL alias: {alias:?}"); - - // fetch destination ranges for a given alias - let alias_destination_ranges = alias.get_destination_ranges(&mut *conn).await?; - - // combine destination addrs - let (dest_addrs_v4, dest_addrs_v6) = - process_alias_destination_addrs(&alias.destination, &alias_destination_ranges); - - // process alias ports - let alias_ports = alias.ports.into_iter().map(Into::into).collect::>(); - let destination_ports = merge_port_ranges(alias_ports); - - // remove duplicate protocol entries - let mut protocols = alias.protocols; - protocols.sort_unstable(); - protocols.dedup(); - - let comment = format!( - "ACL {} - {}, ALIAS {} - {}", - acl.id, name, alias.id, alias.name - ); - if has_ipv4_addresses { - // create IPv4 rules - let ipv4_rules = create_rules( - alias.id, - IpVersion::Ipv4, - &ipv4_source_addrs, - &dest_addrs_v4, - &destination_ports, - &protocols, - &comment, - ); - if let Some(rule) = ipv4_rules.0 { - allow_rules.push(rule); - } - deny_rules.push(ipv4_rules.1); - } + for destination in destinations { + debug!("Processing ACL pre-defined destination: {destination:?}"); + let (destination_allow_rules, destination_deny_rules) = + get_predefined_destination_rules( + &mut *conn, + destination, + acl.id, + &rule_name, + has_ipv4_addresses, + has_ipv6_addresses, + (&ipv4_source_addrs, &ipv6_source_addrs), + ) + .await?; - if has_ipv6_addresses { - // create IPv6 rules - let ipv6_rules = create_rules( - alias.id, - IpVersion::Ipv6, - &ipv6_source_addrs, - &dest_addrs_v6, - &destination_ports, - &protocols, - &comment, - ); - if let Some(rule) = ipv6_rules.0 { - allow_rules.push(rule); - } - deny_rules.push(ipv6_rules.1); - } + // append generated rules to output + allow_rules.extend(destination_allow_rules); + deny_rules.extend(destination_deny_rules); } } @@ -305,6 +266,7 @@ async fn get_manual_destination_rules( let (ipv4_source_addrs, ipv6_source_addrs) = source_addrs; // only generate rules for a given IP version if there is a destination address of a given type + // or any destination toggle is enabled and location uses addresses of a given type let has_ipv4_destination = !dest_addrs_v4.is_empty() || (location_has_ipv4_addresses && any_destination); let has_ipv6_destination = @@ -318,7 +280,7 @@ async fn get_manual_destination_rules( let ipv4_rules = create_rules( rule_id, IpVersion::Ipv4, - &ipv4_source_addrs, + ipv4_source_addrs, &dest_addrs_v4, &destination_ports, &destination_protocols, @@ -335,6 +297,97 @@ async fn get_manual_destination_rules( let ipv6_rules = create_rules( rule_id, IpVersion::Ipv6, + ipv6_source_addrs, + &dest_addrs_v6, + &destination_ports, + &destination_protocols, + &comment, + ); + if let Some(rule) = ipv6_rules.0 { + allow_rules.push(rule); + } + deny_rules.push(ipv6_rules.1); + } + + Ok((allow_rules, deny_rules)) +} + +/// Generates firewall rules for pre-defined destination used in ACL rule. +async fn get_predefined_destination_rules( + conn: &mut PgConnection, + destination: AclAlias, + rule_id: Id, + rule_name: &str, + location_has_ipv4_addresses: bool, + location_has_ipv6_addresses: bool, + source_addrs: (&[IpAddress], &[IpAddress]), +) -> Result<(Vec, Vec), FirewallError> { + // fetch destination ranges for a given destination + let alias_destination_ranges = destination.get_destination_ranges(&mut *conn).await?; + + // combine destination addrs + let (dest_addrs_v4, dest_addrs_v6) = + process_alias_destination_addrs(&destination.destination, &alias_destination_ranges); + + // process alias ports + let destination_ports = if destination.any_port { + Vec::new() + } else { + let alias_ports = destination + .ports + .into_iter() + .map(Into::into) + .collect::>(); + merge_port_ranges(alias_ports) + }; + + // process destination protocols + let destination_protocols = if destination.any_protocol { + Vec::new() + } else { + let mut protocols = destination.protocols; + protocols.sort_unstable(); + protocols.dedup(); + protocols + }; + + let (ipv4_source_addrs, ipv6_source_addrs) = source_addrs; + + // only generate rules for a given IP version if there is a destination address of a given type + // or any destination toggle is enabled and location uses addresses of a given type + let has_ipv4_destination = + !dest_addrs_v4.is_empty() || (location_has_ipv4_addresses && destination.any_destination); + let has_ipv6_destination = + !dest_addrs_v6.is_empty() || (location_has_ipv6_addresses && destination.any_destination); + + let comment = format!( + "ACL {} - {}, ALIAS {} - {}", + rule_id, rule_name, destination.id, destination.name + ); + let mut allow_rules = Vec::new(); + let mut deny_rules = Vec::new(); + if has_ipv4_destination { + // create IPv4 rules + let ipv4_rules = create_rules( + destination.id, + IpVersion::Ipv4, + &ipv4_source_addrs, + &dest_addrs_v4, + &destination_ports, + &destination_protocols, + &comment, + ); + if let Some(rule) = ipv4_rules.0 { + allow_rules.push(rule); + } + deny_rules.push(ipv4_rules.1); + } + + if has_ipv6_destination { + // create IPv6 rules + let ipv6_rules = create_rules( + destination.id, + IpVersion::Ipv6, &ipv6_source_addrs, &dest_addrs_v6, &destination_ports, diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index ec2f16e4cb..4e82558efa 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -1824,6 +1824,8 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { name: "destination alias".to_string(), kind: AliasKind::Destination, ports: vec![PortRange::new(100, 200).into()], + any_destination: true, + any_protocol: true, ..Default::default() } .save(&pool) From 5b5a6334e2cf9ba4879a090e17db4d9ab70e5f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 10:41:33 +0100 Subject: [PATCH 13/25] update dependencies --- Cargo.lock | 16 ++++++++-------- .../defguard_core/src/enterprise/firewall/mod.rs | 4 ++-- flake.lock | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e8d990d40..38913c1660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1156,7 +1156,7 @@ dependencies = [ "sqlx", "thiserror 2.0.18", "time", - "x509-parser 0.18.0", + "x509-parser 0.18.1", ] [[package]] @@ -4328,7 +4328,7 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "x509-parser 0.18.0", + "x509-parser 0.18.1", "yasna", ] @@ -5651,9 +5651,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -5674,9 +5674,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -6945,9 +6945,9 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs 0.7.1", "data-encoding", diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index bd0ab8fae1..5a75c3ee94 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -371,7 +371,7 @@ async fn get_predefined_destination_rules( let ipv4_rules = create_rules( destination.id, IpVersion::Ipv4, - &ipv4_source_addrs, + ipv4_source_addrs, &dest_addrs_v4, &destination_ports, &destination_protocols, @@ -388,7 +388,7 @@ async fn get_predefined_destination_rules( let ipv6_rules = create_rules( destination.id, IpVersion::Ipv6, - &ipv6_source_addrs, + ipv6_source_addrs, &dest_addrs_v6, &destination_ports, &destination_protocols, diff --git a/flake.lock b/flake.lock index fd5c915947..891fecfd3c 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770115704, - "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=", + "lastModified": 1770197578, + "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08", + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1770174315, - "narHash": "sha256-GUaMxDmJB1UULsIYpHtfblskVC6zymAaQ/Zqfo+13jc=", + "lastModified": 1770260791, + "narHash": "sha256-ADTBfENFjRVDQMcCycyX/pAy6NFI/Ct6Mrar3gsmXI0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "095c394bb91342882f27f6c73f64064fb9de9f2a", + "rev": "42ec85352e419e601775c57256a52f6d48a39906", "type": "github" }, "original": { From 52f7fec03d6de360ce0bef8a544fdd7e5f0abdd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 13:53:52 +0100 Subject: [PATCH 14/25] adjust field naming to be less confusing --- .../src/enterprise/db/models/acl.rs | 139 ++++++++++-------- .../src/enterprise/db/models/acl/tests.rs | 56 +++---- .../src/enterprise/firewall/mod.rs | 27 ++-- .../firewall/tests/all_locations.rs | 48 +++--- .../firewall/tests/disabled_rules.rs | 12 +- .../src/enterprise/firewall/tests/gh1868.rs | 20 +-- .../src/enterprise/firewall/tests/mod.rs | 74 +++++----- .../firewall/tests/unapplied_rules.rs | 12 +- .../src/enterprise/handlers/acl.rs | 73 +++++---- .../enterprise/handlers/acl/destination.rs | 2 +- crates/defguard_core/src/utility_thread.rs | 6 +- .../tests/integration/api/acl.rs | 52 ++++--- ...260127094513_[2.0.0]_aclalias_any.down.sql | 12 +- ...20260127094513_[2.0.0]_aclalias_any.up.sql | 24 ++- 14 files changed, 305 insertions(+), 252 deletions(-) diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index 24eb50d533..23a393d01d 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -163,43 +163,46 @@ pub struct AclRuleInfo { pub parent_id: Option, pub state: RuleState, pub name: String, - pub all_networks: bool, - pub networks: Vec>, + pub all_locations: bool, + pub locations: Vec>, pub expires: Option, pub enabled: bool, // source pub allow_all_users: bool, pub deny_all_users: bool, + pub allow_all_groups: bool, + pub deny_all_groups: bool, pub allow_all_network_devices: bool, pub deny_all_network_devices: bool, pub allowed_users: Vec>, pub denied_users: Vec>, pub allowed_groups: Vec>, pub denied_groups: Vec>, - pub allowed_devices: Vec>, - pub denied_devices: Vec>, + pub allowed_network_devices: Vec>, + pub denied_network_devices: Vec>, // destination - pub destination: Vec, - pub destination_ranges: Vec>, - pub aliases: Vec>, + pub addresses: Vec, + pub address_ranges: Vec>, pub ports: Vec, pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, - pub manual_destination_settings: bool, + pub use_manual_destination_settings: bool, + // aliases + pub aliases: Vec>, } impl AclRuleInfo { /// Constructs a [`String`] of comma-separated addresses and address ranges. pub(crate) fn format_destination(&self) -> String { // process single addresses - let addrs = match &self.destination { + let addrs = match &self.addresses { d if d.is_empty() => String::new(), d => d.iter().map(|a| a.to_string() + ", ").collect::(), }; // process address ranges - let ranges = match &self.destination_ranges { + let ranges = match &self.address_ranges { r if r.is_empty() => String::new(), r => r.iter().fold(String::new(), |acc, r| { acc + &format!("{}-{}, ", r.start, r.end) @@ -247,12 +250,14 @@ pub struct AclRule { pub name: String, pub allow_all_users: bool, pub deny_all_users: bool, + pub allow_all_groups: bool, + pub deny_all_groups: bool, pub allow_all_network_devices: bool, pub deny_all_network_devices: bool, - pub all_networks: bool, + pub all_locations: bool, #[model(ref)] #[schema(value_type = Vec)] - pub destination: Vec, + pub addresses: Vec, #[model(ref)] #[schema(value_type = Vec)] pub ports: Vec>, @@ -260,10 +265,10 @@ pub struct AclRule { pub protocols: Vec, pub enabled: bool, pub expires: Option, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, - pub manual_settings: bool, + pub use_manual_destination_settings: bool, } impl Default for AclRule { @@ -275,18 +280,20 @@ impl Default for AclRule { name: "ACL rule".to_string(), allow_all_users: false, deny_all_users: false, + allow_all_groups: false, + deny_all_groups: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), enabled: true, expires: None, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, } } } @@ -634,7 +641,7 @@ impl AclRule { debug!("Creating related objects for ACL rule {api_rule:?}"); // save related networks debug!("Creating related networks for ACL rule {rule_id}"); - for network_id in &api_rule.networks { + for network_id in &api_rule.locations { AclRuleNetwork::new(rule_id, *network_id) .save(&mut *transaction) .await @@ -706,7 +713,7 @@ impl AclRule { // allowed devices debug!("Creating related allowed devices for ACL rule {rule_id}"); - for device_id in &api_rule.allowed_devices { + for device_id in &api_rule.allowed_network_devices { AclRuleDevice::new(rule_id, *device_id, true) .save(&mut *transaction) .await @@ -715,7 +722,7 @@ impl AclRule { // denied devices debug!("Creating related denied devices for ACL rule {rule_id}"); - for device_id in &api_rule.denied_devices { + for device_id in &api_rule.denied_network_devices { AclRuleDevice::new(rule_id, *device_id, false) .save(&mut *transaction) .await @@ -723,7 +730,7 @@ impl AclRule { } // destination - let destination = parse_destination(&api_rule.destination)?; + let destination = parse_destination(&api_rule.addresses)?; debug!("Creating related destination ranges for ACL rule {rule_id}"); for range in destination.ranges { if range.1 <= range.0 { @@ -819,7 +826,7 @@ impl TryFrom for AclRule { fn try_from(rule: EditAclRule) -> Result { Ok(Self { - destination: parse_destination(&rule.destination)?.addrs, + addresses: parse_destination(&rule.addresses)?.addrs, ports: parse_ports(&rule.ports)? .into_iter() .map(Into::into) @@ -830,16 +837,18 @@ impl TryFrom for AclRule { name: rule.name, allow_all_users: rule.allow_all_users, deny_all_users: rule.deny_all_users, + allow_all_groups: rule.allow_all_groups, + deny_all_groups: rule.deny_all_groups, allow_all_network_devices: rule.allow_all_network_devices, deny_all_network_devices: rule.deny_all_network_devices, - all_networks: rule.all_networks, + all_locations: rule.all_locations, protocols: rule.protocols, enabled: rule.enabled, expires: rule.expires, - any_destination: rule.any_destination, + any_address: rule.any_address, any_port: rule.any_port, any_protocol: rule.any_protocol, - manual_settings: true, + use_manual_destination_settings: true, }) } } @@ -911,7 +920,7 @@ impl AclRule { where E: PgExecutor<'e>, { - if self.all_networks { + if self.all_locations { WireguardNetwork::all(executor).await } else { query_as!( @@ -942,7 +951,7 @@ impl AclRule { query_as!( AclAlias, "SELECT a.id, parent_id, name, kind \"kind: AliasKind\",state \"state: AliasState\", \ - destination, ports, protocols, any_destination, any_port, any_protocol \ + addresses, ports, protocols, any_address, any_port, any_protocol \ FROM aclrulealias r \ JOIN aclalias a \ ON a.id = r.alias_id \ @@ -1100,7 +1109,7 @@ impl AclRule { } /// Returns all [`AclRuleDestinationRanges`]es the rule applies to - pub(crate) async fn get_destination_ranges<'e, E>( + pub(crate) async fn get_destination_address_ranges<'e, E>( &self, executor: E, ) -> Result>, SqlxError> @@ -1127,9 +1136,9 @@ impl AclRule { let denied_users = self.get_users(&mut *conn, false).await?; let allowed_groups = self.get_groups(&mut *conn, true).await?; let denied_groups = self.get_groups(&mut *conn, false).await?; - let allowed_devices = self.get_network_devices(&mut *conn, true).await?; - let denied_devices = self.get_network_devices(&mut *conn, false).await?; - let destination_ranges = self.get_destination_ranges(&mut *conn).await?; + let allowed_network_devices = self.get_network_devices(&mut *conn, true).await?; + let denied_network_devices = self.get_network_devices(&mut *conn, false).await?; + let address_ranges = self.get_destination_address_ranges(&mut *conn).await?; let ports = self.ports.clone().into_iter().map(Into::into).collect(); Ok(AclRuleInfo { @@ -1139,27 +1148,29 @@ impl AclRule { name: self.name.clone(), allow_all_users: self.allow_all_users, deny_all_users: self.deny_all_users, + allow_all_groups: self.allow_all_groups, + deny_all_groups: self.deny_all_groups, allow_all_network_devices: self.allow_all_network_devices, deny_all_network_devices: self.deny_all_network_devices, - all_networks: self.all_networks, - destination: self.destination.clone(), + all_locations: self.all_locations, + addresses: self.addresses.clone(), protocols: self.protocols.clone(), enabled: self.enabled, expires: self.expires, - destination_ranges, + address_ranges, ports, aliases, - networks, + locations: networks, allowed_users, denied_users, allowed_groups, denied_groups, - allowed_devices, - denied_devices, - any_destination: self.any_destination, + allowed_network_devices, + denied_network_devices, + any_address: self.any_address, any_port: self.any_port, any_protocol: self.any_protocol, - manual_destination_settings: self.manual_settings, + use_manual_destination_settings: self.use_manual_destination_settings, }) } } @@ -1294,7 +1305,7 @@ impl AclRuleInfo { .await } else { // return explicitly configured allowed devices otherwise - Ok(self.allowed_devices.clone()) + Ok(self.allowed_network_devices.clone()) } } @@ -1326,7 +1337,7 @@ impl AclRuleInfo { .await } else { // return explicitly configured denied devices otherwise - Ok(self.denied_devices.clone()) + Ok(self.denied_network_devices.clone()) } } } @@ -1341,13 +1352,13 @@ pub(crate) struct AclAliasInfo { pub kind: AliasKind, pub state: AliasState, #[schema(value_type = Vec)] - pub destination: Vec, - pub destination_ranges: Vec>, + pub addresses: Vec, + pub address_ranges: Vec>, #[schema(value_type = Vec)] pub ports: Vec, pub protocols: Vec, pub rules: Vec>, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, } @@ -1356,12 +1367,12 @@ impl AclAliasInfo { /// Constructs a [`String`] of comma-separated addresses and address ranges pub(crate) fn format_destination(&self) -> String { // process single addresses - let addrs = match &self.destination { + let addrs = match &self.addresses { d if d.is_empty() => String::new(), d => d.iter().map(|a| a.to_string() + ", ").collect::(), }; // process address ranges - let ranges = match &self.destination_ranges { + let ranges = match &self.address_ranges { r if r.is_empty() => String::new(), r => r.iter().fold(String::new(), |acc, r| { acc + &format!("{}-{}, ", r.start, r.end) @@ -1433,12 +1444,12 @@ pub struct AclAlias { #[model(enum)] pub state: AliasState, #[model(ref)] - pub destination: Vec, + pub addresses: Vec, #[model(ref)] pub ports: Vec>, #[model(ref)] pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, } @@ -1462,10 +1473,10 @@ impl AclAlias { name: name.into(), kind, state, - destination, + addresses: destination, ports, protocols, - any_destination, + any_address: any_destination, any_port, any_protocol, } @@ -1598,7 +1609,7 @@ impl TryFrom<&EditAclAlias> for AclAlias { fn try_from(alias: &EditAclAlias) -> Result { Ok(Self { - destination: parse_destination(&alias.destination)?.addrs, + addresses: parse_destination(&alias.destination)?.addrs, ports: parse_ports(&alias.ports)? .into_iter() .map(Into::into) @@ -1609,7 +1620,7 @@ impl TryFrom<&EditAclAlias> for AclAlias { kind: AliasKind::Component, state: AliasState::Applied, protocols: alias.protocols.clone(), - any_destination: true, + any_address: true, any_port: true, any_protocol: true, }) @@ -1628,7 +1639,7 @@ impl AclAlias { sqlx::query_as!( Self, "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", \ - destination, ports, protocols, any_destination, any_port, any_protocol \ + addresses, ports, protocols, any_address, any_port, any_protocol \ FROM aclalias WHERE kind = $1", kind as AliasKind ) @@ -1647,7 +1658,7 @@ impl AclAlias { sqlx::query_as!( Self, "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", \ - destination, ports, protocols, any_destination, any_port, any_protocol \ + addresses, ports, protocols, any_address, any_port, any_protocol \ FROM aclalias WHERE id = $1 AND kind = $2", id, kind as AliasKind @@ -1662,7 +1673,7 @@ impl TryFrom<&EditAclDestination> for AclAlias { fn try_from(alias: &EditAclDestination) -> Result { Ok(Self { - destination: parse_destination(&alias.destination)?.addrs, + addresses: parse_destination(&alias.destination)?.addrs, ports: parse_ports(&alias.ports)? .into_iter() .map(Into::into) @@ -1673,7 +1684,7 @@ impl TryFrom<&EditAclDestination> for AclAlias { kind: AliasKind::Destination, state: AliasState::Applied, protocols: alias.protocols.clone(), - any_destination: alias.any_destination, + any_address: alias.any_destination, any_port: alias.any_port, any_protocol: alias.any_protocol, }) @@ -1730,9 +1741,9 @@ impl AclAlias { query_as!( AclRule, "SELECT ar.id, parent_id, state AS \"state: RuleState\", name, allow_all_users, \ - deny_all_users, allow_all_network_devices, deny_all_network_devices, all_networks, \ - destination, ports, protocols, enabled, expires, any_destination, any_port, \ - any_protocol, manual_settings \ + deny_all_users, allow_all_groups, deny_all_groups, allow_all_network_devices, deny_all_network_devices, all_locations, \ + addresses, ports, protocols, enabled, expires, any_address, any_port, \ + any_protocol, use_manual_destination_settings \ FROM aclrulealias ara \ JOIN aclrule ar ON ar.id = ara.rule_id \ WHERE ara.alias_id = $1", @@ -1754,12 +1765,12 @@ impl AclAlias { name: self.name.clone(), kind: self.kind.clone(), state: self.state.clone(), - destination: self.destination.clone(), + addresses: self.addresses.clone(), ports: self.ports.clone().into_iter().map(Into::into).collect(), protocols: self.protocols.clone(), - destination_ranges, + address_ranges: destination_ranges, rules, - any_destination: self.any_destination, + any_address: self.any_address, any_port: self.any_port, any_protocol: self.any_protocol, }) diff --git a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs index 119639c5fc..8490b8a12c 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs @@ -44,7 +44,7 @@ async fn test_alias(_: PgPoolOptions, options: PgConnectOptions) { let retrieved = AclAlias::find_by_id(&pool, 1).await.unwrap().unwrap(); assert_eq!(retrieved.id, 1); - assert_eq!(retrieved.destination, destination); + assert_eq!(retrieved.addresses, destination); assert_eq!(retrieved.ports, ports); } @@ -55,23 +55,22 @@ async fn test_allow_conflicting_sources(_: PgPoolOptions, options: PgConnectOpti // create the rule let rule = AclRule { id: NoId, - parent_id: Default::default(), - state: Default::default(), name: "rule".to_string(), enabled: true, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), expires: None, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() } .save(&pool) .await @@ -129,23 +128,22 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) { // create the rule let mut rule = AclRule { id: NoId, - parent_id: Default::default(), - state: Default::default(), name: "rule".to_string(), enabled: true, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), expires: None, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() } .save(&pool) .await @@ -327,17 +325,17 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(info.denied_groups.len(), 1); assert_eq!(info.denied_groups[0], group2); - assert_eq!(info.allowed_devices.len(), 1); - assert_eq!(info.allowed_devices[0].id, device1.id); // db modifies datetime precision + assert_eq!(info.allowed_network_devices.len(), 1); + assert_eq!(info.allowed_network_devices[0].id, device1.id); // db modifies datetime precision - assert_eq!(info.denied_devices.len(), 1); - assert_eq!(info.denied_devices[0].id, device2.id); // db modifies datetime precision + assert_eq!(info.denied_network_devices.len(), 1); + assert_eq!(info.denied_network_devices[0].id, device2.id); // db modifies datetime precision - assert_eq!(info.networks.len(), 1); - assert_eq!(info.networks[0], network1); + assert_eq!(info.locations.len(), 1); + assert_eq!(info.locations[0], network1); // test all_networks flag - rule.all_networks = true; + rule.all_locations = true; rule.save(&pool).await.unwrap(); assert_eq!(rule.get_networks(&pool).await.unwrap().len(), 2); @@ -484,18 +482,19 @@ async fn test_all_allowed_users(_: PgPoolOptions, options: PgConnectOptions) { deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), expires: None, enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() } .save(&pool) .await @@ -598,18 +597,19 @@ async fn test_all_denied_users(_: PgPoolOptions, options: PgConnectOptions) { deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), expires: None, enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() } .save(&pool) .await diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index 5a75c3ee94..3ef27d74a1 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -75,15 +75,15 @@ pub async fn generate_firewall_rules_from_acls( let AclRuleInfo { id, name: rule_name, - destination, - destination_ranges, + addresses: destination, + address_ranges: destination_ranges, ports, protocols, aliases, - any_destination, + any_address: any_destination, any_port, any_protocol, - manual_destination_settings, + use_manual_destination_settings: manual_destination_settings, .. } = acl; @@ -238,7 +238,7 @@ async fn get_manual_destination_rules( alias_destination_ranges.extend(alias.get_destination_ranges(&mut *conn).await?); // extend existing parameter lists - destination.extend(alias.destination); + destination.extend(alias.addresses); ports.extend(alias.ports.into_iter().map(Into::into).collect::>()); protocols.extend(alias.protocols); } @@ -327,7 +327,7 @@ async fn get_predefined_destination_rules( // combine destination addrs let (dest_addrs_v4, dest_addrs_v6) = - process_alias_destination_addrs(&destination.destination, &alias_destination_ranges); + process_alias_destination_addrs(&destination.addresses, &alias_destination_ranges); // process alias ports let destination_ports = if destination.any_port { @@ -356,9 +356,9 @@ async fn get_predefined_destination_rules( // only generate rules for a given IP version if there is a destination address of a given type // or any destination toggle is enabled and location uses addresses of a given type let has_ipv4_destination = - !dest_addrs_v4.is_empty() || (location_has_ipv4_addresses && destination.any_destination); + !dest_addrs_v4.is_empty() || (location_has_ipv4_addresses && destination.any_address); let has_ipv6_destination = - !dest_addrs_v6.is_empty() || (location_has_ipv6_addresses && destination.any_destination); + !dest_addrs_v6.is_empty() || (location_has_ipv6_addresses && destination.any_address); let comment = format!( "ACL {} - {}, ALIAS {} - {}", @@ -1013,13 +1013,14 @@ pub(crate) async fn get_location_active_acl_rules( ) -> Result>, SqlxError> { debug!("Fetching active ACL rules for location {location}"); let rules: Vec> = query_as( - "SELECT DISTINCT ON (a.id) a.id, name, allow_all_users, deny_all_users, all_networks, \ - allow_all_network_devices, deny_all_network_devices, destination, ports, protocols, \ - expires, enabled, parent_id, state, any_destination, any_port, any_protocol, - manual_settings \ + "SELECT DISTINCT ON (a.id) a.id, name, allow_all_users, deny_all_users, all_locations, \ + allow_all_groups, deny_all_groups, \ + allow_all_network_devices, deny_all_network_devices, addresses, ports, protocols, \ + expires, enabled, parent_id, state, any_address, any_port, any_protocol, + use_manual_destination_settings \ FROM aclrule a \ LEFT JOIN aclrulenetwork an ON a.id = an.rule_id \ - WHERE (an.network_id = $1 OR a.all_networks = true) AND enabled = true \ + WHERE (an.network_id = $1 OR a.all_locations = true) AND enabled = true \ AND state = 'applied'::aclrule_state \ AND (expires IS NULL OR expires > NOW())", ) diff --git a/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs index aa41614181..37da5cc891 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs @@ -43,8 +43,8 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO enabled: true, allow_all_users: true, state: RuleState::Applied, - destination: vec!["192.168.1.0/24".parse().unwrap()], - manual_settings: true, + addresses: vec!["192.168.1.0/24".parse().unwrap()], + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -55,11 +55,11 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO id: NoId, expires: None, enabled: true, - all_networks: true, + all_locations: true, allow_all_users: true, state: RuleState::Applied, - destination: vec!["192.168.2.0/24".parse().unwrap()], - manual_settings: true, + addresses: vec!["192.168.2.0/24".parse().unwrap()], + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -70,11 +70,11 @@ async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectO id: NoId, expires: None, enabled: true, - all_networks: true, + all_locations: true, allow_all_users: true, state: RuleState::Applied, - destination: vec!["192.168.3.0/24".parse().unwrap()], - manual_settings: true, + addresses: vec!["192.168.3.0/24".parse().unwrap()], + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -148,8 +148,8 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO enabled: true, allow_all_users: true, state: RuleState::Applied, - manual_settings: true, - destination: vec!["fc00::0/112".parse().unwrap()], + use_manual_destination_settings: true, + addresses: vec!["fc00::0/112".parse().unwrap()], ..Default::default() } .save(&pool) @@ -161,10 +161,10 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO expires: None, enabled: true, allow_all_users: true, - all_networks: true, + all_locations: true, state: RuleState::Applied, - manual_settings: true, - destination: vec!["fb00::0/112".parse().unwrap()], + use_manual_destination_settings: true, + addresses: vec!["fb00::0/112".parse().unwrap()], ..Default::default() } .save(&pool) @@ -175,11 +175,11 @@ async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectO id: NoId, expires: None, enabled: true, - all_networks: true, + all_locations: true, allow_all_users: true, state: RuleState::Applied, - manual_settings: true, - destination: vec!["fa00::0/112".parse().unwrap()], + use_manual_destination_settings: true, + addresses: vec!["fa00::0/112".parse().unwrap()], ..Default::default() } .save(&pool) @@ -259,8 +259,8 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P enabled: true, allow_all_users: true, state: RuleState::Applied, - manual_settings: true, - destination: vec![ + use_manual_destination_settings: true, + addresses: vec![ "192.168.1.0/24".parse().unwrap(), "fc00::0/112".parse().unwrap(), ], @@ -274,11 +274,11 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P id: NoId, expires: None, enabled: true, - all_networks: true, + all_locations: true, allow_all_users: true, state: RuleState::Applied, - manual_settings: true, - destination: vec![ + use_manual_destination_settings: true, + addresses: vec![ "192.168.2.0/24".parse().unwrap(), "fb00::0/112".parse().unwrap(), ], @@ -292,11 +292,11 @@ async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: P id: NoId, expires: None, enabled: true, - all_networks: true, + all_locations: true, allow_all_users: true, state: RuleState::Applied, - manual_settings: true, - destination: vec![ + use_manual_destination_settings: true, + addresses: vec![ "192.168.3.0/24".parse().unwrap(), "fa00::0/112".parse().unwrap(), ], diff --git a/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs index 48f38636ad..280e283933 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs @@ -34,7 +34,7 @@ async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOption enabled: false, state: RuleState::Applied, allow_all_users: true, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -46,7 +46,7 @@ async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOption enabled: false, state: RuleState::Applied, allow_all_users: true, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -110,7 +110,7 @@ async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOption enabled: false, state: RuleState::Applied, allow_all_users: true, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -122,7 +122,7 @@ async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOption enabled: false, state: RuleState::Applied, allow_all_users: true, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -189,7 +189,7 @@ async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConn enabled: false, state: RuleState::Applied, allow_all_users: true, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -201,7 +201,7 @@ async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConn enabled: false, state: RuleState::Applied, allow_all_users: true, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) diff --git a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs index a4c3c73acc..23c2afba28 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs @@ -102,14 +102,14 @@ async fn test_gh1868_ipv6_rule_is_not_created_with_v4_only_destination( // create a rule with only an IPv4 destination let acl_rule = AclRule { - all_networks: true, + all_locations: true, allow_all_users: true, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - any_destination: false, - destination: vec!["192.168.1.0/24".parse().unwrap()], - manual_settings: true, + any_address: false, + addresses: vec!["192.168.1.0/24".parse().unwrap()], + use_manual_destination_settings: true, enabled: true, state: RuleState::Applied, ..Default::default() @@ -165,13 +165,13 @@ async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination( // create a rule with only an IPv6 destination let acl_rule = AclRule { - all_networks: true, + all_locations: true, allow_all_users: true, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - any_destination: false, - destination: vec!["fc00::0/112".parse().unwrap()], + any_address: false, + addresses: vec!["fc00::0/112".parse().unwrap()], enabled: true, state: RuleState::Applied, ..Default::default() @@ -226,13 +226,13 @@ async fn test_gh1868_ipv4_and_ipv6_rules_are_created_with_any_destination( // create a rule with any destination enabled let acl_rule = AclRule { - all_networks: true, + all_locations: true, allow_all_users: true, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - any_destination: true, - destination: vec!["fc00::0/112".parse().unwrap()], + any_address: true, + addresses: vec!["fc00::0/112".parse().unwrap()], enabled: true, state: RuleState::Applied, ..Default::default() diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index 4e82558efa..1b17e8294f 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -390,13 +390,13 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO let acl_rule_1 = AclRule { id: NoId, name: "Web Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: vec!["192.168.1.0/24".parse().unwrap()], + addresses: vec!["192.168.1.0/24".parse().unwrap()], ports: vec![ PortRange::new(80, 80).into(), PortRange::new(443, 443).into(), @@ -405,10 +405,11 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: false, + any_address: false, any_port: false, any_protocol: false, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() }; let locations = vec![location.id]; let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web @@ -439,22 +440,23 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO let acl_rule_2 = AclRule { id: NoId, name: "DNS Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: true, // Allow all users deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead + addresses: Vec::new(), // Will use destination ranges instead ports: vec![PortRange::new(53, 53).into()], protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: false, + any_address: false, any_port: false, any_protocol: false, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() }; let locations_2 = vec![location.id]; let allowed_users_2 = Vec::new(); @@ -820,13 +822,13 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let acl_rule_1 = AclRule { id: NoId, name: "Web Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: vec!["fc00::0/112".parse().unwrap()], + addresses: vec!["fc00::0/112".parse().unwrap()], ports: vec![ PortRange::new(80, 80).into(), PortRange::new(443, 443).into(), @@ -835,10 +837,11 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: false, + any_address: false, any_port: false, any_protocol: false, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() }; let locations = vec![location.id]; let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web @@ -869,22 +872,23 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let acl_rule_2 = AclRule { id: NoId, name: "DNS Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: true, // Allow all users deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead + addresses: Vec::new(), // Will use destination ranges instead ports: vec![PortRange::new(53, 53).into()], protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: false, + any_address: false, any_port: false, any_protocol: false, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() }; let locations_2 = vec![location.id]; let allowed_users_2 = Vec::new(); @@ -1290,13 +1294,13 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P let acl_rule_1 = AclRule { id: NoId, name: "Web Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: vec![ + addresses: vec![ "192.168.1.0/24".parse().unwrap(), "fc00::0/112".parse().unwrap(), ], @@ -1308,10 +1312,11 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: false, + any_address: false, any_port: false, any_protocol: false, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() }; let locations = vec![location.id]; let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web @@ -1342,22 +1347,23 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P let acl_rule_2 = AclRule { id: NoId, name: "DNS Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: true, // Allow all users deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead + addresses: Vec::new(), // Will use destination ranges instead ports: vec![PortRange::new(53, 53).into()], protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: false, + any_address: false, any_port: false, any_protocol: false, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() }; let locations_2 = vec![location.id]; let allowed_users_2 = Vec::new(); @@ -1810,7 +1816,7 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { expires: None, enabled: true, state: RuleState::Applied, - destination: vec!["192.168.1.0/24".parse().unwrap()], + addresses: vec!["192.168.1.0/24".parse().unwrap()], allow_all_users: true, ..Default::default() } @@ -1824,7 +1830,7 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { name: "destination alias".to_string(), kind: AliasKind::Destination, ports: vec![PortRange::new(100, 200).into()], - any_destination: true, + any_address: true, any_protocol: true, ..Default::default() } @@ -1834,7 +1840,7 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { let component_alias = AclAlias { id: NoId, kind: AliasKind::Component, - destination: vec!["10.0.2.3".parse().unwrap()], + addresses: vec!["10.0.2.3".parse().unwrap()], ..Default::default() } .save(&pool) @@ -1965,9 +1971,9 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt expires: None, enabled: true, state: RuleState::Applied, - destination: Vec::new(), + addresses: Vec::new(), allow_all_users: true, - manual_settings: false, + use_manual_destination_settings: false, ..Default::default() } .save(&pool) @@ -1979,7 +1985,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt id: NoId, name: "postgres".to_string(), kind: AliasKind::Destination, - destination: vec!["10.0.2.3".parse().unwrap()], + addresses: vec!["10.0.2.3".parse().unwrap()], ports: vec![PortRange::new(5432, 5432).into()], ..Default::default() } @@ -1990,7 +1996,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt id: NoId, name: "redis".to_string(), kind: AliasKind::Destination, - destination: vec!["10.0.2.4".parse().unwrap()], + addresses: vec!["10.0.2.4".parse().unwrap()], ports: vec![PortRange::new(6379, 6379).into()], ..Default::default() } @@ -2129,7 +2135,7 @@ async fn test_no_allowed_users_ipv4(_: PgPoolOptions, options: PgConnectOptions) enabled: true, state: RuleState::Applied, allow_all_users: true, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -2141,7 +2147,7 @@ async fn test_no_allowed_users_ipv4(_: PgPoolOptions, options: PgConnectOptions) enabled: true, state: RuleState::Applied, allow_all_users: true, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -2293,7 +2299,7 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon expires: None, enabled: true, state: RuleState::Applied, - destination: Vec::new(), + addresses: Vec::new(), allow_all_users: true, ..Default::default() } diff --git a/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs index 7f69b1dd1f..630e8025a6 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs @@ -34,7 +34,7 @@ async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptio enabled: true, allow_all_users: true, state: RuleState::New, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -46,7 +46,7 @@ async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptio enabled: true, allow_all_users: true, state: RuleState::Modified, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -110,7 +110,7 @@ async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptio enabled: true, allow_all_users: true, state: RuleState::New, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -122,7 +122,7 @@ async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptio enabled: true, allow_all_users: true, state: RuleState::Modified, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -189,7 +189,7 @@ async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgCon enabled: true, allow_all_users: true, state: RuleState::New, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -201,7 +201,7 @@ async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgCon enabled: true, allow_all_users: true, state: RuleState::Modified, - manual_settings: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) diff --git a/crates/defguard_core/src/enterprise/handlers/acl.rs b/crates/defguard_core/src/enterprise/handlers/acl.rs index c13d36a71f..421a7e9fc2 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl.rs @@ -28,57 +28,62 @@ pub struct ApiAclRule { pub parent_id: Option, pub state: RuleState, pub name: String, - pub all_networks: bool, - pub networks: Vec, + pub all_locations: bool, + pub locations: Vec, pub expires: Option, pub enabled: bool, // source pub allow_all_users: bool, pub deny_all_users: bool, + pub allow_all_groups: bool, + pub deny_all_groups: bool, pub allow_all_network_devices: bool, pub deny_all_network_devices: bool, pub allowed_users: Vec, pub denied_users: Vec, pub allowed_groups: Vec, pub denied_groups: Vec, - pub allowed_devices: Vec, - pub denied_devices: Vec, + pub allowed_network_devices: Vec, + pub denied_network_devices: Vec, // destination - pub destination: String, - pub aliases: Vec, + pub addresses: String, pub ports: String, pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, + // aliases + pub aliases: Vec, } impl From> for ApiAclRule { fn from(info: AclRuleInfo) -> Self { Self { - destination: info.format_destination(), + addresses: info.format_destination(), ports: info.format_ports(), id: info.id, parent_id: info.parent_id, state: info.state, name: info.name, - all_networks: info.all_networks, - networks: info.networks.iter().map(|v| v.id).collect(), + all_locations: info.all_locations, + locations: info.locations.iter().map(|v| v.id).collect(), expires: info.expires, allow_all_users: info.allow_all_users, deny_all_users: info.deny_all_users, + allow_all_groups: info.allow_all_groups, + deny_all_groups: info.deny_all_groups, allow_all_network_devices: info.allow_all_network_devices, deny_all_network_devices: info.deny_all_network_devices, allowed_users: info.allowed_users.iter().map(|v| v.id).collect(), denied_users: info.denied_users.iter().map(|v| v.id).collect(), allowed_groups: info.allowed_groups.iter().map(|v| v.id).collect(), denied_groups: info.denied_groups.iter().map(|v| v.id).collect(), - allowed_devices: info.allowed_devices.iter().map(|v| v.id).collect(), - denied_devices: info.denied_devices.iter().map(|v| v.id).collect(), + allowed_network_devices: info.allowed_network_devices.iter().map(|v| v.id).collect(), + denied_network_devices: info.denied_network_devices.iter().map(|v| v.id).collect(), aliases: info.aliases.iter().map(|v| v.id).collect(), protocols: info.protocols, enabled: info.enabled, - any_destination: info.any_destination, + any_address: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, } @@ -89,39 +94,43 @@ impl From> for ApiAclRule { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)] pub struct EditAclRule { pub name: String, - pub all_networks: bool, - pub networks: Vec, + pub all_locations: bool, + pub locations: Vec, pub expires: Option, pub enabled: bool, // source pub allow_all_users: bool, pub deny_all_users: bool, + pub allow_all_groups: bool, + pub deny_all_groups: bool, pub allow_all_network_devices: bool, pub deny_all_network_devices: bool, pub allowed_users: Vec, pub denied_users: Vec, pub allowed_groups: Vec, pub denied_groups: Vec, - pub allowed_devices: Vec, - pub denied_devices: Vec, + pub allowed_network_devices: Vec, + pub denied_network_devices: Vec, // destination - pub destination: String, - pub aliases: Vec, + pub addresses: String, pub ports: String, pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, + // aliases + pub aliases: Vec, } impl EditAclRule { pub fn validate(&self) -> Result<(), WebError> { // check if some allowed users/group/devices are configured - if !(self.allow_all_users - || self.allow_all_network_devices - || !self.allowed_users.is_empty() - || !self.allowed_groups.is_empty() - || !self.allowed_devices.is_empty()) + if !self.allow_all_users + && !self.allow_all_groups + && !self.allow_all_network_devices + && self.allowed_users.is_empty() + && self.allowed_groups.is_empty() + && self.allowed_network_devices.is_empty() { return Err(WebError::BadRequest( "Must provide some allowed users, groups or devices".to_string(), @@ -135,26 +144,28 @@ impl EditAclRule { impl From> for EditAclRule { fn from(info: AclRuleInfo) -> Self { Self { - destination: info.format_destination(), + addresses: info.format_destination(), ports: info.format_ports(), name: info.name, - all_networks: info.all_networks, - networks: info.networks.iter().map(|v| v.id).collect(), + all_locations: info.all_locations, + locations: info.locations.iter().map(|v| v.id).collect(), expires: info.expires, allow_all_users: info.allow_all_users, deny_all_users: info.deny_all_users, + allow_all_groups: info.allow_all_groups, + deny_all_groups: info.deny_all_groups, allow_all_network_devices: info.allow_all_network_devices, deny_all_network_devices: info.deny_all_network_devices, allowed_users: info.allowed_users.iter().map(|v| v.id).collect(), denied_users: info.denied_users.iter().map(|v| v.id).collect(), allowed_groups: info.allowed_groups.iter().map(|v| v.id).collect(), denied_groups: info.denied_groups.iter().map(|v| v.id).collect(), - allowed_devices: info.allowed_devices.iter().map(|v| v.id).collect(), - denied_devices: info.denied_devices.iter().map(|v| v.id).collect(), + allowed_network_devices: info.allowed_network_devices.iter().map(|v| v.id).collect(), + denied_network_devices: info.denied_network_devices.iter().map(|v| v.id).collect(), aliases: info.aliases.iter().map(|v| v.id).collect(), protocols: info.protocols, enabled: info.enabled, - any_destination: info.any_destination, + any_address: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, } diff --git a/crates/defguard_core/src/enterprise/handlers/acl/destination.rs b/crates/defguard_core/src/enterprise/handlers/acl/destination.rs index 6986346cd5..cb55a85556 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl/destination.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl/destination.rs @@ -179,7 +179,7 @@ impl From for ApiAclDestination { state: info.state, protocols: info.protocols, rules: info.rules.iter().map(|v| v.id).collect(), - any_destination: info.any_destination, + any_destination: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, } diff --git a/crates/defguard_core/src/utility_thread.rs b/crates/defguard_core/src/utility_thread.rs index 123cb619b6..c4d1f416d2 100644 --- a/crates/defguard_core/src/utility_thread.rs +++ b/crates/defguard_core/src/utility_thread.rs @@ -240,9 +240,9 @@ async fn expired_acl_rules_check( "UPDATE aclrule SET state = 'expired'::aclrule_state \ WHERE state = 'applied'::aclrule_state AND expires < NOW() \ RETURNING id, parent_id, state AS \"state: RuleState\", name, allow_all_users, \ - deny_all_users, allow_all_network_devices, deny_all_network_devices, all_networks, \ - destination, ports, protocols, enabled, expires, any_destination, any_port, \ - any_protocol, manual_settings" + deny_all_users, allow_all_groups, deny_all_groups, allow_all_network_devices, deny_all_network_devices, all_locations, \ + addresses, ports, protocols, enabled, expires, any_address, any_port, \ + any_protocol, use_manual_destination_settings" ) .fetch_all(pool) .await?; diff --git a/crates/defguard_core/tests/integration/api/acl.rs b/crates/defguard_core/tests/integration/api/acl.rs index 93740547da..ac72b7387c 100644 --- a/crates/defguard_core/tests/integration/api/acl.rs +++ b/crates/defguard_core/tests/integration/api/acl.rs @@ -50,27 +50,29 @@ async fn make_client_v2(pool: PgPool, config: DefGuardConfig) -> TestClient { fn make_rule() -> EditAclRule { EditAclRule { name: "rule".to_string(), - all_networks: false, - networks: Vec::new(), + all_locations: false, + locations: Vec::new(), expires: None, allow_all_users: false, deny_all_users: false, + allow_all_groups: false, + deny_all_groups: false, allow_all_network_devices: false, deny_all_network_devices: false, allowed_users: vec![1], denied_users: Vec::new(), allowed_groups: Vec::new(), denied_groups: Vec::new(), - allowed_devices: Vec::new(), - denied_devices: Vec::new(), - destination: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), + allowed_network_devices: Vec::new(), + denied_network_devices: Vec::new(), + addresses: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), aliases: Vec::new(), enabled: true, protocols: vec![6, 17], ports: "1, 2, 3, 10-20, 30-40".to_string(), - any_destination: true, - any_port: true, - any_protocol: true, + any_address: false, + any_port: false, + any_protocol: false, } } @@ -101,25 +103,27 @@ fn edit_rule_data_into_api_response( parent_id, state, name: data.name.clone(), - all_networks: data.all_networks, - networks: data.networks.clone(), + all_locations: data.all_locations, + locations: data.locations.clone(), expires: data.expires, enabled: data.enabled, allow_all_users: data.allow_all_users, deny_all_users: data.deny_all_users, + allow_all_groups: data.allow_all_groups, + deny_all_groups: data.deny_all_groups, allow_all_network_devices: data.allow_all_network_devices, deny_all_network_devices: data.deny_all_network_devices, allowed_users: data.allowed_users.clone(), denied_users: data.denied_users.clone(), allowed_groups: data.allowed_groups.clone(), denied_groups: data.denied_groups.clone(), - allowed_devices: data.allowed_devices.clone(), - denied_devices: data.denied_devices.clone(), - destination: data.destination.clone(), + allowed_network_devices: data.allowed_network_devices.clone(), + denied_network_devices: data.denied_network_devices.clone(), + addresses: data.addresses.clone(), aliases: data.aliases.clone(), ports: data.ports.clone(), protocols: data.protocols.clone(), - any_destination: data.any_destination, + any_address: data.any_address, any_port: data.any_port, any_protocol: data.any_protocol, } @@ -344,7 +348,7 @@ async fn test_empty_strings(_: PgPoolOptions, options: PgConnectOptions) { // rule let mut rule = make_rule(); - rule.destination = String::new(); + rule.addresses = String::new(); rule.ports = String::new(); let response = client.post("/api/v1/acl/rule").json(&rule).send().await; @@ -521,10 +525,10 @@ async fn test_related_objects(_: PgPoolOptions, options: PgConnectOptions) { // create an acl rule with related objects let mut rule = make_rule(); - rule.networks = vec![1, 2]; + rule.locations = vec![1, 2]; rule.allowed_users = vec![1, 2]; rule.allowed_groups = vec![1, 2]; - rule.allowed_devices = vec![1, 2]; + rule.allowed_network_devices = vec![1, 2]; rule.aliases = vec![1, 2]; // create @@ -574,7 +578,7 @@ async fn test_invalid_related_objects(_: PgPoolOptions, options: PgConnectOption // networks let mut rule = make_rule(); - rule.networks = vec![100]; + rule.locations = vec![100]; let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let response = client.put("/api/v1/acl/rule/1").json(&rule).send().await; @@ -622,7 +626,7 @@ async fn test_invalid_related_objects(_: PgPoolOptions, options: PgConnectOption // allowed_devices let mut rule = make_rule(); - rule.allowed_devices = vec![100]; + rule.allowed_network_devices = vec![100]; let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let response = client.put("/api/v1/acl/rule/1").json(&rule).send().await; @@ -630,7 +634,7 @@ async fn test_invalid_related_objects(_: PgPoolOptions, options: PgConnectOption // denied_devices let mut rule = make_rule(); - rule.denied_devices = vec![100]; + rule.denied_network_devices = vec![100]; let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let response = client.put("/api/v1/acl/rule/1").json(&rule).send().await; @@ -690,11 +694,11 @@ async fn test_invalid_data(_: PgPoolOptions, options: PgConnectOptions) { // invalid ip range let mut rule = make_rule(); - rule.destination = "10.10.10.20-10.10.10.10".into(); + rule.addresses = "10.10.10.20-10.10.10.10".into(); let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); - rule.destination = "10.10.10.10-10.10.10.20".into(); + rule.addresses = "10.10.10.10-10.10.10.20".into(); let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::CREATED); } @@ -804,7 +808,7 @@ async fn test_rule_delete_state_applied(_: PgPoolOptions, options: PgConnectOpti // test APPLIED rule deletion let mut rule = make_rule(); - rule.networks = vec![1]; + rule.locations = vec![1]; let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::CREATED); assert_eq!(AclRule::all(&pool).await.unwrap().len(), 1); @@ -825,7 +829,7 @@ async fn test_rule_delete_state_applied(_: PgPoolOptions, options: PgConnectOpti assert_eq!(rule_after_mods, rule_child); // related networks are returned correctly - assert_eq!(rule_child.networks, vec![1]); + assert_eq!(rule_child.locations, vec![1]); // cannot modify a DELETED rule let response = client diff --git a/migrations/20260127094513_[2.0.0]_aclalias_any.down.sql b/migrations/20260127094513_[2.0.0]_aclalias_any.down.sql index cbcfad0bf5..fd8d3a00ac 100644 --- a/migrations/20260127094513_[2.0.0]_aclalias_any.down.sql +++ b/migrations/20260127094513_[2.0.0]_aclalias_any.down.sql @@ -1,9 +1,15 @@ ALTER TABLE aclrule - DROP COLUMN any_destination, + DROP COLUMN any_address, DROP COLUMN any_port, DROP COLUMN any_protocol, - DROP COLUMN manual_settings; + DROP COLUMN use_manual_destination_settings, + 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_destination, + DROP COLUMN any_address, DROP COLUMN any_port, DROP COLUMN any_protocol; +ALTER TABLE aclalias RENAME COLUMN addresses TO destination; diff --git a/migrations/20260127094513_[2.0.0]_aclalias_any.up.sql b/migrations/20260127094513_[2.0.0]_aclalias_any.up.sql index 52083d52ef..a652c451b6 100644 --- a/migrations/20260127094513_[2.0.0]_aclalias_any.up.sql +++ b/migrations/20260127094513_[2.0.0]_aclalias_any.up.sql @@ -1,17 +1,31 @@ +-- add new toggle columns ALTER TABLE aclalias - ADD COLUMN any_destination boolean NOT NULL DEFAULT false, + ADD COLUMN any_address boolean NOT NULL DEFAULT false, ADD COLUMN any_port boolean NOT NULL DEFAULT false, ADD COLUMN any_protocol boolean NOT NULL DEFAULT false; + +-- set values for new columns based on existing data UPDATE aclalias SET - any_destination = array_length(destination, 1) IS NULL, + any_address = array_length(destination, 1) IS NULL, any_port = array_length(ports, 1) IS NULL, any_protocol = array_length(protocols, 1) IS NULL; + +-- rename destination column to avoid confusion +ALTER TABLE aclalias RENAME COLUMN destination TO addresses; + +-- do the same for the aclrule table itself ALTER TABLE aclrule - ADD COLUMN any_destination boolean NOT NULL DEFAULT false, + ADD COLUMN any_address boolean NOT NULL DEFAULT false, ADD COLUMN any_port boolean NOT NULL DEFAULT false, ADD COLUMN any_protocol boolean NOT NULL DEFAULT false, - ADD COLUMN manual_settings boolean NOT NULL DEFAULT false; + ADD COLUMN use_manual_destination_settings boolean NOT NULL DEFAULT true, + ADD COLUMN allow_all_groups boolean NOT NULL DEFAULT false, + ADD COLUMN deny_all_groups boolean NOT NULL DEFAULT false; + UPDATE aclrule SET - any_destination = array_length(destination, 1) IS NULL, + any_address = array_length(destination, 1) IS NULL, any_port = array_length(ports, 1) IS NULL, any_protocol = array_length(protocols, 1) IS NULL; + +ALTER TABLE aclrule RENAME COLUMN destination TO addresses; +ALTER TABLE aclrule RENAME COLUMN all_networks TO all_locations; From 72d3ed98173c17b0ea02e5d68c2b6a801d09f445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 13:54:18 +0100 Subject: [PATCH 15/25] update query data --- ...157859aa5901d8bc74fa23c121a2a8c13c4d.json} | 6 ++- ...5aa5f93430b81d20e6e5b84d664590ba73d6.json} | 6 ++- ...ae180d62382bec045c3ac32c76f163fe3adf.json} | 8 ++-- ...db6026387d79e1975dd7ed44cd9ff4600214.json} | 44 ++++++++++++------- ...c7741c93938e28cc40d57dc43b035852c707.json} | 44 ++++++++++++------- ...b54c25f6ce675b8d445cd4a8e60f5617ebd0.json} | 4 +- ...db4985eca201b7a01b5cec090606c355d1d0.json} | 8 ++-- ...59c79bd103c034de0ac684649ee40ebc04c7.json} | 44 ++++++++++++------- ...c22c8e9013eff9505b9d06a2e55f5ac62d8d.json} | 8 ++-- ...2bbe18c9871cf30ad82fbea85f9b459002c2.json} | 4 +- ...a079437dab3a0540cbbbceac6874505d672d.json} | 44 ++++++++++++------- ...3390820e042ecaf4e542fcdd413c9a7ae507.json} | 8 ++-- ...ed0eb93c05d42b6d864f354ffe0135d9d094.json} | 8 ++-- 13 files changed, 144 insertions(+), 92 deletions(-) rename .sqlx/{query-6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097.json => query-1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d.json} (64%) rename .sqlx/{query-f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5.json => query-2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6.json} (63%) rename .sqlx/{query-1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a.json => query-3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf.json} (84%) rename .sqlx/{query-617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32.json => query-605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214.json} (78%) rename .sqlx/{query-e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89.json => query-707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707.json} (75%) rename .sqlx/{query-4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c.json => query-9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0.json} (79%) rename .sqlx/{query-07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009.json => query-958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0.json} (84%) rename .sqlx/{query-e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25.json => query-9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7.json} (74%) rename .sqlx/{query-f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde.json => query-a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d.json} (88%) rename .sqlx/{query-2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b.json => query-bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2.json} (76%) rename .sqlx/{query-e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8.json => query-d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d.json} (74%) rename .sqlx/{query-2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264.json => query-d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507.json} (83%) rename .sqlx/{query-25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622.json => query-ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094.json} (88%) diff --git a/.sqlx/query-6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097.json b/.sqlx/query-1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d.json similarity index 64% rename from .sqlx/query-6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097.json rename to .sqlx/query-1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d.json index a4a593a9a9..e3c2d1a12e 100644 --- a/.sqlx/query-6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097.json +++ b/.sqlx/query-1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"aclrule\" SET \"parent_id\" = $2,\"state\" = $3,\"name\" = $4,\"allow_all_users\" = $5,\"deny_all_users\" = $6,\"allow_all_network_devices\" = $7,\"deny_all_network_devices\" = $8,\"all_networks\" = $9,\"destination\" = $10,\"ports\" = $11,\"protocols\" = $12,\"enabled\" = $13,\"expires\" = $14,\"any_destination\" = $15,\"any_port\" = $16,\"any_protocol\" = $17,\"manual_settings\" = $18 WHERE id = $1", + "query": "UPDATE \"aclrule\" SET \"parent_id\" = $2,\"state\" = $3,\"name\" = $4,\"allow_all_users\" = $5,\"deny_all_users\" = $6,\"allow_all_groups\" = $7,\"deny_all_groups\" = $8,\"allow_all_network_devices\" = $9,\"deny_all_network_devices\" = $10,\"all_locations\" = $11,\"addresses\" = $12,\"ports\" = $13,\"protocols\" = $14,\"enabled\" = $15,\"expires\" = $16,\"any_address\" = $17,\"any_port\" = $18,\"any_protocol\" = $19,\"use_manual_destination_settings\" = $20 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -27,6 +27,8 @@ "Bool", "Bool", "Bool", + "Bool", + "Bool", "InetArray", "Int4RangeArray", "Int4Array", @@ -40,5 +42,5 @@ }, "nullable": [] }, - "hash": "6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097" + "hash": "1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d" } diff --git a/.sqlx/query-f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5.json b/.sqlx/query-2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6.json similarity index 63% rename from .sqlx/query-f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5.json rename to .sqlx/query-2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6.json index 73de745775..a891ef095e 100644 --- a/.sqlx/query-f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5.json +++ b/.sqlx/query-2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"aclrule\" (\"parent_id\",\"state\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_networks\",\"destination\",\"ports\",\"protocols\",\"enabled\",\"expires\",\"any_destination\",\"any_port\",\"any_protocol\",\"manual_settings\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) RETURNING id", + "query": "INSERT INTO \"aclrule\" (\"parent_id\",\"state\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_groups\",\"deny_all_groups\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_locations\",\"addresses\",\"ports\",\"protocols\",\"enabled\",\"expires\",\"any_address\",\"any_port\",\"any_protocol\",\"use_manual_destination_settings\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) RETURNING id", "describe": { "columns": [ { @@ -32,6 +32,8 @@ "Bool", "Bool", "Bool", + "Bool", + "Bool", "InetArray", "Int4RangeArray", "Int4Array", @@ -47,5 +49,5 @@ false ] }, - "hash": "f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5" + "hash": "2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6" } diff --git a/.sqlx/query-1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a.json b/.sqlx/query-3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf.json similarity index 84% rename from .sqlx/query-1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a.json rename to .sqlx/query-3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf.json index 3c72f27599..99434543c2 100644 --- a/.sqlx/query-1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a.json +++ b/.sqlx/query-3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"parent_id\",\"name\",\"kind\" \"kind: _\",\"state\" \"state: _\",\"destination\" \"destination: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"any_destination\",\"any_port\",\"any_protocol\" FROM \"aclalias\"", + "query": "SELECT id, \"parent_id\",\"name\",\"kind\" \"kind: _\",\"state\" \"state: _\",\"addresses\" \"addresses: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"any_address\",\"any_port\",\"any_protocol\" FROM \"aclalias\"", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination: _", + "name": "addresses: _", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -96,5 +96,5 @@ false ] }, - "hash": "1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a" + "hash": "3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf" } diff --git a/.sqlx/query-617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32.json b/.sqlx/query-605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214.json similarity index 78% rename from .sqlx/query-617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32.json rename to .sqlx/query-605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214.json index b974c4dfe5..cdee5a491d 100644 --- a/.sqlx/query-617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32.json +++ b/.sqlx/query-605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE aclrule SET state = 'expired'::aclrule_state WHERE state = 'applied'::aclrule_state AND expires < NOW() RETURNING id, parent_id, state AS \"state: RuleState\", name, allow_all_users, deny_all_users, allow_all_network_devices, deny_all_network_devices, all_networks, destination, ports, protocols, enabled, expires, any_destination, any_port, any_protocol, manual_settings", + "query": "UPDATE aclrule SET state = 'expired'::aclrule_state WHERE state = 'applied'::aclrule_state AND expires < NOW() RETURNING id, parent_id, state AS \"state: RuleState\", name, allow_all_users, deny_all_users, allow_all_groups, deny_all_groups, allow_all_network_devices, deny_all_network_devices, all_locations, addresses, ports, protocols, enabled, expires, any_address, any_port, any_protocol, use_manual_destination_settings", "describe": { "columns": [ { @@ -48,62 +48,72 @@ }, { "ordinal": 6, - "name": "allow_all_network_devices", + "name": "allow_all_groups", "type_info": "Bool" }, { "ordinal": 7, - "name": "deny_all_network_devices", + "name": "deny_all_groups", "type_info": "Bool" }, { "ordinal": 8, - "name": "all_networks", + "name": "allow_all_network_devices", "type_info": "Bool" }, { "ordinal": 9, - "name": "destination", - "type_info": "InetArray" + "name": "deny_all_network_devices", + "type_info": "Bool" }, { "ordinal": 10, + "name": "all_locations", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "addresses", + "type_info": "InetArray" + }, + { + "ordinal": 12, "name": "ports", "type_info": "Int4RangeArray" }, { - "ordinal": 11, + "ordinal": 13, "name": "protocols", "type_info": "Int4Array" }, { - "ordinal": 12, + "ordinal": 14, "name": "enabled", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "expires", "type_info": "Timestamp" }, { - "ordinal": 14, - "name": "any_destination", + "ordinal": 16, + "name": "any_address", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 17, "name": "any_port", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 18, "name": "any_protocol", "type_info": "Bool" }, { - "ordinal": 17, - "name": "manual_settings", + "ordinal": 19, + "name": "use_manual_destination_settings", "type_info": "Bool" } ], @@ -124,6 +134,8 @@ false, false, false, + false, + false, true, false, false, @@ -131,5 +143,5 @@ false ] }, - "hash": "617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32" + "hash": "605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214" } diff --git a/.sqlx/query-e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89.json b/.sqlx/query-707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707.json similarity index 75% rename from .sqlx/query-e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89.json rename to .sqlx/query-707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707.json index 21b384d146..5ea7d38dd7 100644 --- a/.sqlx/query-e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89.json +++ b/.sqlx/query-707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT ar.id, parent_id, state AS \"state: RuleState\", name, allow_all_users, deny_all_users, allow_all_network_devices, deny_all_network_devices, all_networks, destination, ports, protocols, enabled, expires, any_destination, any_port, any_protocol, manual_settings FROM aclrulealias ara JOIN aclrule ar ON ar.id = ara.rule_id WHERE ara.alias_id = $1", + "query": "SELECT ar.id, parent_id, state AS \"state: RuleState\", name, allow_all_users, deny_all_users, allow_all_groups, deny_all_groups, allow_all_network_devices, deny_all_network_devices, all_locations, addresses, ports, protocols, enabled, expires, any_address, any_port, any_protocol, use_manual_destination_settings FROM aclrulealias ara JOIN aclrule ar ON ar.id = ara.rule_id WHERE ara.alias_id = $1", "describe": { "columns": [ { @@ -48,62 +48,72 @@ }, { "ordinal": 6, - "name": "allow_all_network_devices", + "name": "allow_all_groups", "type_info": "Bool" }, { "ordinal": 7, - "name": "deny_all_network_devices", + "name": "deny_all_groups", "type_info": "Bool" }, { "ordinal": 8, - "name": "all_networks", + "name": "allow_all_network_devices", "type_info": "Bool" }, { "ordinal": 9, - "name": "destination", - "type_info": "InetArray" + "name": "deny_all_network_devices", + "type_info": "Bool" }, { "ordinal": 10, + "name": "all_locations", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "addresses", + "type_info": "InetArray" + }, + { + "ordinal": 12, "name": "ports", "type_info": "Int4RangeArray" }, { - "ordinal": 11, + "ordinal": 13, "name": "protocols", "type_info": "Int4Array" }, { - "ordinal": 12, + "ordinal": 14, "name": "enabled", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "expires", "type_info": "Timestamp" }, { - "ordinal": 14, - "name": "any_destination", + "ordinal": 16, + "name": "any_address", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 17, "name": "any_port", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 18, "name": "any_protocol", "type_info": "Bool" }, { - "ordinal": 17, - "name": "manual_settings", + "ordinal": 19, + "name": "use_manual_destination_settings", "type_info": "Bool" } ], @@ -126,6 +136,8 @@ false, false, false, + false, + false, true, false, false, @@ -133,5 +145,5 @@ false ] }, - "hash": "e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89" + "hash": "707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707" } diff --git a/.sqlx/query-4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c.json b/.sqlx/query-9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0.json similarity index 79% rename from .sqlx/query-4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c.json rename to .sqlx/query-9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0.json index 2e8999a6a5..21b3d35624 100644 --- a/.sqlx/query-4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c.json +++ b/.sqlx/query-9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"aclalias\" (\"parent_id\",\"name\",\"kind\",\"state\",\"destination\",\"ports\",\"protocols\",\"any_destination\",\"any_port\",\"any_protocol\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING id", + "query": "INSERT INTO \"aclalias\" (\"parent_id\",\"name\",\"kind\",\"state\",\"addresses\",\"ports\",\"protocols\",\"any_address\",\"any_port\",\"any_protocol\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING id", "describe": { "columns": [ { @@ -47,5 +47,5 @@ false ] }, - "hash": "4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c" + "hash": "9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0" } diff --git a/.sqlx/query-07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009.json b/.sqlx/query-958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0.json similarity index 84% rename from .sqlx/query-07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009.json rename to .sqlx/query-958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0.json index f351aead3d..ad8e6500cd 100644 --- a/.sqlx/query-07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009.json +++ b/.sqlx/query-958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT a.id, parent_id, name, kind \"kind: AliasKind\",state \"state: AliasState\", destination, ports, protocols, any_destination, any_port, any_protocol FROM aclrulealias r JOIN aclalias a ON a.id = r.alias_id WHERE r.rule_id = $1", + "query": "SELECT a.id, parent_id, name, kind \"kind: AliasKind\",state \"state: AliasState\", addresses, ports, protocols, any_address, any_port, any_protocol FROM aclrulealias r JOIN aclalias a ON a.id = r.alias_id WHERE r.rule_id = $1", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination", + "name": "addresses", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -98,5 +98,5 @@ false ] }, - "hash": "07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009" + "hash": "958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0" } diff --git a/.sqlx/query-e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25.json b/.sqlx/query-9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7.json similarity index 74% rename from .sqlx/query-e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25.json rename to .sqlx/query-9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7.json index 133445ad9d..7bb0f97454 100644 --- a/.sqlx/query-e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25.json +++ b/.sqlx/query-9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"parent_id\",\"state\" \"state: _\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_networks\",\"destination\" \"destination: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"enabled\",\"expires\",\"any_destination\",\"any_port\",\"any_protocol\",\"manual_settings\" FROM \"aclrule\"", + "query": "SELECT id, \"parent_id\",\"state\" \"state: _\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_groups\",\"deny_all_groups\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_locations\",\"addresses\" \"addresses: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"enabled\",\"expires\",\"any_address\",\"any_port\",\"any_protocol\",\"use_manual_destination_settings\" FROM \"aclrule\"", "describe": { "columns": [ { @@ -48,62 +48,72 @@ }, { "ordinal": 6, - "name": "allow_all_network_devices", + "name": "allow_all_groups", "type_info": "Bool" }, { "ordinal": 7, - "name": "deny_all_network_devices", + "name": "deny_all_groups", "type_info": "Bool" }, { "ordinal": 8, - "name": "all_networks", + "name": "allow_all_network_devices", "type_info": "Bool" }, { "ordinal": 9, - "name": "destination: _", - "type_info": "InetArray" + "name": "deny_all_network_devices", + "type_info": "Bool" }, { "ordinal": 10, + "name": "all_locations", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "addresses: _", + "type_info": "InetArray" + }, + { + "ordinal": 12, "name": "ports: _", "type_info": "Int4RangeArray" }, { - "ordinal": 11, + "ordinal": 13, "name": "protocols: _", "type_info": "Int4Array" }, { - "ordinal": 12, + "ordinal": 14, "name": "enabled", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "expires", "type_info": "Timestamp" }, { - "ordinal": 14, - "name": "any_destination", + "ordinal": 16, + "name": "any_address", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 17, "name": "any_port", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 18, "name": "any_protocol", "type_info": "Bool" }, { - "ordinal": 17, - "name": "manual_settings", + "ordinal": 19, + "name": "use_manual_destination_settings", "type_info": "Bool" } ], @@ -124,6 +134,8 @@ false, false, false, + false, + false, true, false, false, @@ -131,5 +143,5 @@ false ] }, - "hash": "e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25" + "hash": "9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7" } diff --git a/.sqlx/query-f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde.json b/.sqlx/query-a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d.json similarity index 88% rename from .sqlx/query-f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde.json rename to .sqlx/query-a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d.json index 93792833c5..c3dc6a2353 100644 --- a/.sqlx/query-f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde.json +++ b/.sqlx/query-a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", destination, ports, protocols, any_destination, any_port, any_protocol FROM aclalias WHERE id = $1 AND kind = $2", + "query": "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", addresses, ports, protocols, any_address, any_port, any_protocol FROM aclalias WHERE id = $1 AND kind = $2", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination", + "name": "addresses", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -109,5 +109,5 @@ false ] }, - "hash": "f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde" + "hash": "a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d" } diff --git a/.sqlx/query-2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b.json b/.sqlx/query-bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2.json similarity index 76% rename from .sqlx/query-2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b.json rename to .sqlx/query-bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2.json index 49ac96f605..ddec1c4bd9 100644 --- a/.sqlx/query-2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b.json +++ b/.sqlx/query-bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"aclalias\" SET \"parent_id\" = $2,\"name\" = $3,\"kind\" = $4,\"state\" = $5,\"destination\" = $6,\"ports\" = $7,\"protocols\" = $8,\"any_destination\" = $9,\"any_port\" = $10,\"any_protocol\" = $11 WHERE id = $1", + "query": "UPDATE \"aclalias\" SET \"parent_id\" = $2,\"name\" = $3,\"kind\" = $4,\"state\" = $5,\"addresses\" = $6,\"ports\" = $7,\"protocols\" = $8,\"any_address\" = $9,\"any_port\" = $10,\"any_protocol\" = $11 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -40,5 +40,5 @@ }, "nullable": [] }, - "hash": "2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b" + "hash": "bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2" } diff --git a/.sqlx/query-e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8.json b/.sqlx/query-d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d.json similarity index 74% rename from .sqlx/query-e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8.json rename to .sqlx/query-d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d.json index 07966a453e..50f82f5c41 100644 --- a/.sqlx/query-e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8.json +++ b/.sqlx/query-d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"parent_id\",\"state\" \"state: _\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_networks\",\"destination\" \"destination: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"enabled\",\"expires\",\"any_destination\",\"any_port\",\"any_protocol\",\"manual_settings\" FROM \"aclrule\" WHERE id = $1", + "query": "SELECT id, \"parent_id\",\"state\" \"state: _\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_groups\",\"deny_all_groups\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_locations\",\"addresses\" \"addresses: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"enabled\",\"expires\",\"any_address\",\"any_port\",\"any_protocol\",\"use_manual_destination_settings\" FROM \"aclrule\" WHERE id = $1", "describe": { "columns": [ { @@ -48,62 +48,72 @@ }, { "ordinal": 6, - "name": "allow_all_network_devices", + "name": "allow_all_groups", "type_info": "Bool" }, { "ordinal": 7, - "name": "deny_all_network_devices", + "name": "deny_all_groups", "type_info": "Bool" }, { "ordinal": 8, - "name": "all_networks", + "name": "allow_all_network_devices", "type_info": "Bool" }, { "ordinal": 9, - "name": "destination: _", - "type_info": "InetArray" + "name": "deny_all_network_devices", + "type_info": "Bool" }, { "ordinal": 10, + "name": "all_locations", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "addresses: _", + "type_info": "InetArray" + }, + { + "ordinal": 12, "name": "ports: _", "type_info": "Int4RangeArray" }, { - "ordinal": 11, + "ordinal": 13, "name": "protocols: _", "type_info": "Int4Array" }, { - "ordinal": 12, + "ordinal": 14, "name": "enabled", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "expires", "type_info": "Timestamp" }, { - "ordinal": 14, - "name": "any_destination", + "ordinal": 16, + "name": "any_address", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 17, "name": "any_port", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 18, "name": "any_protocol", "type_info": "Bool" }, { - "ordinal": 17, - "name": "manual_settings", + "ordinal": 19, + "name": "use_manual_destination_settings", "type_info": "Bool" } ], @@ -126,6 +136,8 @@ false, false, false, + false, + false, true, false, false, @@ -133,5 +145,5 @@ false ] }, - "hash": "e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8" + "hash": "d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d" } diff --git a/.sqlx/query-2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264.json b/.sqlx/query-d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507.json similarity index 83% rename from .sqlx/query-2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264.json rename to .sqlx/query-d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507.json index 18448beb88..2800fd43fe 100644 --- a/.sqlx/query-2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264.json +++ b/.sqlx/query-d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"parent_id\",\"name\",\"kind\" \"kind: _\",\"state\" \"state: _\",\"destination\" \"destination: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"any_destination\",\"any_port\",\"any_protocol\" FROM \"aclalias\" WHERE id = $1", + "query": "SELECT id, \"parent_id\",\"name\",\"kind\" \"kind: _\",\"state\" \"state: _\",\"addresses\" \"addresses: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"any_address\",\"any_port\",\"any_protocol\" FROM \"aclalias\" WHERE id = $1", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination: _", + "name": "addresses: _", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -98,5 +98,5 @@ false ] }, - "hash": "2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264" + "hash": "d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507" } diff --git a/.sqlx/query-25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622.json b/.sqlx/query-ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094.json similarity index 88% rename from .sqlx/query-25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622.json rename to .sqlx/query-ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094.json index adbaa53331..831f3d4c92 100644 --- a/.sqlx/query-25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622.json +++ b/.sqlx/query-ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", destination, ports, protocols, any_destination, any_port, any_protocol FROM aclalias WHERE kind = $1", + "query": "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", addresses, ports, protocols, any_address, any_port, any_protocol FROM aclalias WHERE kind = $1", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination", + "name": "addresses", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -108,5 +108,5 @@ false ] }, - "hash": "25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622" + "hash": "ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094" } From e2c2d51758891b9ee2b78a45606be1727291bfc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 13:56:28 +0100 Subject: [PATCH 16/25] fix trivy warning --- Cargo.lock | 23 ++++++++++++----------- Cargo.toml | 2 +- flake.nix | 2 ++ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38913c1660..2269de1503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2758,16 +2758,17 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.1" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64 0.22.1", + "getrandom 0.2.17", "js-sys", "pem", - "ring", "serde", "serde_json", + "signature", "simple_asn1", ] @@ -3760,9 +3761,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -3770,9 +3771,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -3780,9 +3781,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -3793,9 +3794,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", diff --git a/Cargo.toml b/Cargo.toml index 2a92b9e3d5..61f6aa7a21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ humantime = "2.1" # match version used by sqlx ipnetwork = "0.20" jsonwebkey = { version = "0.4", features = ["pkcs-convert"] } -jsonwebtoken = "9.3" +jsonwebtoken = "10.3" ldap3 = { version = "0.12", default-features = false, features = ["tls"] } lettre = { version = "0.11", features = ["tokio1-native-tls"] } matches = "0.1" diff --git a/flake.nix b/flake.nix index abaeea210b..a364211049 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,8 @@ playwright # release assets verification cosign + # vulnerability scanner + trivy ]; # Specify the rust-src path (many editors rely on this) From dc76294e030302ee54399b118e41f2efbf333632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 14:11:56 +0100 Subject: [PATCH 17/25] add missing feature --- Cargo.lock | 7 +++++++ Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2269de1503..09ea69be17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2763,11 +2763,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64 0.22.1", + "ed25519-dalek", "getrandom 0.2.17", + "hmac", "js-sys", + "p256", + "p384", "pem", + "rand 0.8.5", + "rsa", "serde", "serde_json", + "sha2", "signature", "simple_asn1", ] diff --git a/Cargo.toml b/Cargo.toml index 61f6aa7a21..a76bcd6b28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ humantime = "2.1" # match version used by sqlx ipnetwork = "0.20" jsonwebkey = { version = "0.4", features = ["pkcs-convert"] } -jsonwebtoken = "10.3" +jsonwebtoken = { version = "10.3", features = ["rust_crypto"] } ldap3 = { version = "0.12", default-features = false, features = ["tls"] } lettre = { version = "0.11", features = ["tokio1-native-tls"] } matches = "0.1" From 4c4cd91ab63bd09ccd7567aae96f1405bbff5cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Feb 2026 18:44:34 +0100 Subject: [PATCH 18/25] update alias related API types --- web/src/shared/api/types.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 23ca107f44..aa3f297130 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -845,11 +845,11 @@ export interface AclDestination { id: number; name: string; state: AclDeploymentStateValue; - destination: string; + addresses: string; ports: string; protocols: AclProtocolValue[]; rules: number[]; - any_destination: boolean; + any_address: boolean; any_port: boolean; any_protocol: boolean; } @@ -862,7 +862,7 @@ export interface AclAlias { id: number; name: string; state: AclDeploymentStateValue; - destination: string; + addresses: string; ports: string; protocols: AclProtocolValue[]; rules: number[]; @@ -876,28 +876,30 @@ export interface AclRule { id: number; state: AclStatusValue; name: string; - all_networks: boolean; + all_locations: boolean; allow_all_users: boolean; deny_all_users: boolean; + allow_all_groups: boolean; + deny_all_groups: boolean; allow_all_network_devices: boolean; deny_all_network_devices: boolean; - networks: number[]; + locations: number[]; enabled: boolean; allowed_users: number[]; denied_users: number[]; allowed_groups: number[]; denied_groups: number[]; - allowed_devices: number[]; - denied_devices: number[]; - destination: string; - aliases: number[]; + allowed_network_devices: number[]; + denied_network_devices: number[]; + addresses: string; ports: string; protocols: number[]; expires: string | null; parent_id: number | null; - any_destination: boolean; + any_address: boolean; any_port: boolean; any_protocol: boolean; + aliases: number[]; } export type EditAclRuleRequest = Omit; From 4c30f5e83eb58734aa2f2a3951524d6e0f647195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 6 Feb 2026 09:29:48 +0100 Subject: [PATCH 19/25] update frontend naming --- web/src/pages/AliasesPage/AliasTable.tsx | 2 +- web/src/pages/CEAliasPage/CEAliasPage.tsx | 10 +- .../CEDestinationPage/CEDestinationPage.tsx | 22 ++--- web/src/pages/CERulePage/CERulePage.tsx | 92 +++++++++---------- .../components/DestinationsTable.tsx | 4 +- web/src/pages/RulesPage/RulesTable.tsx | 10 +- web/src/shared/api/types.ts | 2 +- web/src/shared/hooks/useApp.tsx | 1 - 8 files changed, 68 insertions(+), 75 deletions(-) diff --git a/web/src/pages/AliasesPage/AliasTable.tsx b/web/src/pages/AliasesPage/AliasTable.tsx index ab4b97da89..179a4228bf 100644 --- a/web/src/pages/AliasesPage/AliasTable.tsx +++ b/web/src/pages/AliasesPage/AliasTable.tsx @@ -64,7 +64,7 @@ export const AliasTable = ({ data: rowData }: Props) => { ), }), - columnHelper.accessor('destination', { + columnHelper.accessor('addresses', { header: 'IP4/6 CIDR range address', enableSorting: false, size: 430, diff --git a/web/src/pages/CEAliasPage/CEAliasPage.tsx b/web/src/pages/CEAliasPage/CEAliasPage.tsx index 9174c774aa..e131d36225 100644 --- a/web/src/pages/CEAliasPage/CEAliasPage.tsx +++ b/web/src/pages/CEAliasPage/CEAliasPage.tsx @@ -74,7 +74,7 @@ export const CEAliasPage = ({ alias }: Props) => { const formSchema = z.object({ name: z.string(m.form_error_required()).trim().min(1, m.form_error_required()), ports: aclPortsValidator, - destination: aclDestinationValidator, + addresses: aclDestinationValidator, protocols: z.set(z.enum(AclProtocol)), }); @@ -83,7 +83,7 @@ type FormFields = z.infer; const anyComponentDefined = (fields: FormFields): boolean => { return ( fields.ports.trim().length > 0 || - fields.destination.trim().length > 0 || + fields.addresses.trim().length > 0 || fields.protocols.size > 0 ); }; @@ -95,14 +95,14 @@ const FormContent = ({ alias }: { alias?: AclAlias }) => { if (isPresent(alias)) { return { name: alias.name, - destination: alias.destination, + addresses: alias.addresses, ports: alias.ports, protocols: new Set(alias.protocols), }; } return { name: '', - destination: '', + addresses: '', ports: '', protocols: new Set(), }; @@ -165,7 +165,7 @@ const FormContent = ({ alias }: { alias?: AclAlias }) => {

{`Define the IP addresses or ranges that form the destination of this ACL rule.`}

- + {(field) => ( { - if (!values.any_destination && values.destination.trim().length === 0) { + if (!values.any_address && values.addresses.trim().length === 0) { ctx.addIssue({ code: 'custom', continue: true, - path: ['destination'], + path: ['addresses'], message: m.form_error_required(), }); } @@ -108,10 +108,10 @@ export const CEDestinationPage = ({ destination }: Props) => { if (isPresent(destination)) { return { name: destination.name, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - destination: destination.destination, + addresses: destination.addresses, ports: destination.ports, protocols: new Set(destination.protocols), }; @@ -120,10 +120,10 @@ export const CEDestinationPage = ({ destination }: Props) => { return { name: '', ports: '', - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - destination: '', + addresses: '', protocols: new Set(), }; }, [destination]); @@ -191,14 +191,14 @@ export const CEDestinationPage = ({ destination }: Props) => {

{`Define the IP addresses or ranges that form the destination of this ACL rule.`}

- + {(field) => } - !s.values.any_destination}> + !s.values.any_address}> {(open) => ( - + {(field) => ( AclProtocolName[protocol]) @@ -260,32 +260,34 @@ const Content = ({ rule: initialRule }: Props) => { z .object({ name: z.string(m.form_error_required()).min(1, m.form_error_required()), - networks: z.number().array(), + locations: z.number().array(), expires: z.string().nullable(), enabled: z.boolean(), - all_networks: z.boolean(), + all_locations: z.boolean(), allow_all_users: z.boolean(), deny_all_users: z.boolean(), + allow_all_groups: z.boolean(), + deny_all_groups: z.boolean(), allow_all_network_devices: z.boolean(), deny_all_network_devices: z.boolean(), allowed_users: z.number().array(), denied_users: z.number().array(), - allow_all_groups: z.boolean(), allowed_groups: z.number().array(), denied_groups: z.number().array(), - allowed_devices: z.number().array(), - denied_devices: z.number().array(), - destinations: z.set(z.number()), - aliases: z.set(z.number()), + allowed_network_devices: z.number().array(), + denied_network_devices: z.number().array(), + addresses: aclDestinationValidator, + ports: aclPortsValidator, protocols: z.set(z.number()), - any_protocol: z.boolean(), + any_address: z.boolean(), any_port: z.boolean(), - any_destination: z.boolean(), - destination: aclDestinationValidator, - ports: aclPortsValidator, + any_protocol: z.boolean(), + destinations: z.set(z.number()), + aliases: z.set(z.number()), }) .superRefine((vals, ctx) => { // check for collisions + // FIXME: add handling for all_groups toggles const message = 'Allow Deny conflict error placeholder.'; if (!vals.allow_all_users && !vals.deny_all_users) { if (intersection(vals.allowed_users, vals.denied_users).length) { @@ -314,7 +316,10 @@ const Content = ({ rule: initialRule }: Props) => { } } if (!vals.allow_all_network_devices && !vals.deny_all_network_devices) { - if (intersection(vals.allowed_devices, vals.denied_devices).length) { + if ( + intersection(vals.allowed_network_devices, vals.denied_network_devices) + .length + ) { ctx.addIssue({ path: ['allowed_devices'], code: 'custom', @@ -331,10 +336,11 @@ const Content = ({ rule: initialRule }: Props) => { // check if one of allowed users/groups/devices fields is set const isAllowConfigured = vals.allow_all_users || + vals.allow_all_groups || vals.allow_all_network_devices || vals.allowed_users.length !== 0 || vals.allowed_groups.length !== 0 || - vals.allowed_devices.length !== 0; + vals.allowed_network_devices.length !== 0; if (!isAllowConfigured) { const message = 'Must configure some allowed users, groups or devices'; ctx.addIssue({ @@ -363,7 +369,6 @@ const Content = ({ rule: initialRule }: Props) => { if (isPresent(initialRule)) { return { ...omit(initialRule, ['id', 'state', 'expires', 'parent_id']), - allow_all_groups: true, aliases: new Set(initialRule.aliases), destinations: new Set(), protocols: new Set(initialRule.protocols), @@ -373,27 +378,28 @@ const Content = ({ rule: initialRule }: Props) => { return { name: '', - destination: '', + addresses: '', ports: '', aliases: new Set(), destinations: new Set(), - allowed_devices: [], + allowed_network_devices: [], allowed_groups: [], allowed_users: [], - denied_devices: [], + denied_network_devices: [], denied_groups: [], denied_users: [], - networks: [], + locations: [], protocols: new Set(), - all_networks: true, - allow_all_groups: true, + all_locations: true, allow_all_users: true, + allow_all_groups: true, allow_all_network_devices: true, deny_all_users: false, + deny_all_groups: false, deny_all_network_devices: false, enabled: true, expires: null, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, }; @@ -411,36 +417,24 @@ const Content = ({ rule: initialRule }: Props) => { // FIXME: When restrictions section is reworked toSend.deny_all_network_devices = false; toSend.deny_all_users = false; - toSend.denied_devices = []; + toSend.deny_all_groups = false; + toSend.denied_network_devices = []; toSend.denied_groups = []; toSend.denied_users = []; - if (toSend.any_destination) { - toSend.destination = ''; - } - if (toSend.any_port) { - toSend.ports = ''; - } - if (toSend.any_protocol) { - toSend.protocols = new Set(); - } if (isPresent(initialRule)) { await editRule({ ...toSend, - any_destination: true, - any_port: true, - any_protocol: true, protocols: Array.from(toSend.protocols), aliases: Array.from(toSend.aliases), id: initialRule.id, + use_manual_destination_settings: manualDestination, }); } else { await addRule({ ...toSend, - any_destination: true, - any_port: true, - any_protocol: true, protocols: Array.from(toSend.protocols), aliases: Array.from(toSend.aliases), + use_manual_destination_settings: manualDestination, }); } }, @@ -471,9 +465,9 @@ const Content = ({ rule: initialRule }: Props) => {

{`Specify which locations this rule applies to. You can select all available locations or choose specific ones based on your requirements.`}

- s.values.all_networks}> + s.values.all_locations}> {(allValue) => ( - + {(field) => ( { selectionCustomItemRender={renderLocationSelectionItem} toggleValue={allValue} onToggleChange={(value) => { - form.setFieldValue('all_networks', value); + form.setFieldValue('all_locations', value); }} /> )} @@ -544,7 +538,7 @@ const Content = ({ rule: initialRule }: Props) => { AclProtocolName[p]) @@ -642,21 +636,21 @@ const Content = ({ rule: initialRule }: Props) => {

- + {(field) => } - !s.values.any_destination}> + !s.values.any_address}> {(open) => ( - + {(field) => ( )} alias.destination.split(',')), + selectedAliases.map((alias) => alias.addresses.split(',')), )} /> @@ -670,7 +664,7 @@ const Content = ({ rule: initialRule }: Props) => { - {(field) => } + {(field) => } !s.values.any_port}> {(open) => ( @@ -697,7 +691,7 @@ const Content = ({ rule: initialRule }: Props) => { - {(field) => } + {(field) => } !s.values.any_protocol}> {(open) => ( @@ -779,7 +773,7 @@ const Content = ({ rule: initialRule }: Props) => { {isPresent(networkDevicesOptions) && ( s.values.allow_all_network_devices}> {(allowAllValue) => ( - + {(field) => ( { const row = info.row.original; - if (row.any_destination) { + if (row.any_address) { return ( {`Any`} ); } - return ; + return ; }, }), columnHelper.display({ diff --git a/web/src/pages/RulesPage/RulesTable.tsx b/web/src/pages/RulesPage/RulesTable.tsx index 63d7661731..b3cb2fc36c 100644 --- a/web/src/pages/RulesPage/RulesTable.tsx +++ b/web/src/pages/RulesPage/RulesTable.tsx @@ -147,7 +147,7 @@ export const RulesTable = ({ const row = info.row.original; return ( - {row.destination} + {row.addresses} {row.aliases.map((aliasId) => { const alias = aliases[aliasId]; if (!alias) return null; @@ -170,7 +170,7 @@ export const RulesTable = ({ row.allow_all_network_devices, row.allowed_users, row.allowed_groups, - row.allowed_devices, + row.allowed_network_devices, ); }, }), @@ -187,7 +187,7 @@ export const RulesTable = ({ row.deny_all_network_devices, row.denied_users, row.denied_groups, - row.denied_devices, + row.denied_network_devices, ); }, }), @@ -197,7 +197,7 @@ export const RulesTable = ({ minSize: 220, cell: (info) => { const row = info.row.original; - if (row.all_networks) { + if (row.all_locations) { return ( ); } - const locationNames = row.networks + const locationNames = row.locations .map((locationId) => locations[locationId]?.name ?? '') .filter((name) => name.length); diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 5e927d041a..1780bb120a 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -631,7 +631,6 @@ export interface SettingsEnterprise { } export interface SettingsEssentials { - instance_name: string; initial_setup_completed: boolean; } @@ -900,6 +899,7 @@ export interface AclRule { any_address: boolean; any_port: boolean; any_protocol: boolean; + use_manual_destination_settings: boolean; aliases: number[]; } diff --git a/web/src/shared/hooks/useApp.tsx b/web/src/shared/hooks/useApp.tsx index 247a6961b7..8be265f222 100644 --- a/web/src/shared/hooks/useApp.tsx +++ b/web/src/shared/hooks/useApp.tsx @@ -32,7 +32,6 @@ const defaults: StoreValues = { version: '', }, settingsEssentials: { - instance_name: '', initial_setup_completed: false, }, }; From 5d9fee65c3cd5bf46b1b08328b393c432d3cd52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 6 Feb 2026 10:33:03 +0100 Subject: [PATCH 20/25] make basic CRUD work for ACL forms --- .../src/enterprise/db/models/acl.rs | 14 +- .../src/enterprise/handlers/acl.rs | 5 + .../src/enterprise/handlers/acl/alias.rs | 10 +- .../enterprise/handlers/acl/destination.rs | 16 +- .../tests/integration/api/acl.rs | 8 +- .../CEDestinationPage/CEDestinationPage.tsx | 8 +- web/src/pages/CERulePage/CERulePage.tsx | 307 +++++++++--------- web/src/shared/api/api.ts | 2 +- 8 files changed, 188 insertions(+), 182 deletions(-) diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index 23a393d01d..c4a7c8122b 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -559,7 +559,9 @@ pub(crate) struct ParsedDestination { /// Perses a destination string into singular ip addresses or networks and address /// ranges. We should be able to parse a string like this one: /// `10.0.0.1/24, 10.1.1.10-10.1.1.20, 192.168.1.10, 10.1.1.1-10.10.1.1` -pub(crate) fn parse_destination(destination: &str) -> Result { +pub(crate) fn parse_destination_addresses( + destination: &str, +) -> Result { debug!("Parsing destination string: {destination}"); let destination: String = destination.chars().filter(|c| !c.is_whitespace()).collect(); let mut result = ParsedDestination::default(); @@ -730,7 +732,7 @@ impl AclRule { } // destination - let destination = parse_destination(&api_rule.addresses)?; + let destination = parse_destination_addresses(&api_rule.addresses)?; debug!("Creating related destination ranges for ACL rule {rule_id}"); for range in destination.ranges { if range.1 <= range.0 { @@ -826,7 +828,7 @@ impl TryFrom for AclRule { fn try_from(rule: EditAclRule) -> Result { Ok(Self { - addresses: parse_destination(&rule.addresses)?.addrs, + addresses: parse_destination_addresses(&rule.addresses)?.addrs, ports: parse_ports(&rule.ports)? .into_iter() .map(Into::into) @@ -1609,7 +1611,7 @@ impl TryFrom<&EditAclAlias> for AclAlias { fn try_from(alias: &EditAclAlias) -> Result { Ok(Self { - addresses: parse_destination(&alias.destination)?.addrs, + addresses: parse_destination_addresses(&alias.addresses)?.addrs, ports: parse_ports(&alias.ports)? .into_iter() .map(Into::into) @@ -1673,7 +1675,7 @@ impl TryFrom<&EditAclDestination> for AclAlias { fn try_from(alias: &EditAclDestination) -> Result { Ok(Self { - addresses: parse_destination(&alias.destination)?.addrs, + addresses: parse_destination_addresses(&alias.addresses)?.addrs, ports: parse_ports(&alias.ports)? .into_iter() .map(Into::into) @@ -1684,7 +1686,7 @@ impl TryFrom<&EditAclDestination> for AclAlias { kind: AliasKind::Destination, state: AliasState::Applied, protocols: alias.protocols.clone(), - any_address: alias.any_destination, + any_address: alias.any_address, any_port: alias.any_port, any_protocol: alias.any_protocol, }) diff --git a/crates/defguard_core/src/enterprise/handlers/acl.rs b/crates/defguard_core/src/enterprise/handlers/acl.rs index 421a7e9fc2..f736d41081 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl.rs @@ -46,6 +46,7 @@ pub struct ApiAclRule { pub allowed_network_devices: Vec, pub denied_network_devices: Vec, // destination + pub use_manual_destination_settings: bool, pub addresses: String, pub ports: String, pub protocols: Vec, @@ -86,6 +87,7 @@ impl From> for ApiAclRule { any_address: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, + use_manual_destination_settings: info.use_manual_destination_settings, } } } @@ -112,6 +114,7 @@ pub struct EditAclRule { pub allowed_network_devices: Vec, pub denied_network_devices: Vec, // destination + pub use_manual_destination_settings: bool, pub addresses: String, pub ports: String, pub protocols: Vec, @@ -124,6 +127,7 @@ pub struct EditAclRule { impl EditAclRule { pub fn validate(&self) -> Result<(), WebError> { + // FIXME: validate that destination is defined // check if some allowed users/group/devices are configured if !self.allow_all_users && !self.allow_all_groups @@ -168,6 +172,7 @@ impl From> for EditAclRule { any_address: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, + use_manual_destination_settings: info.use_manual_destination_settings, } } } diff --git a/crates/defguard_core/src/enterprise/handlers/acl/alias.rs b/crates/defguard_core/src/enterprise/handlers/acl/alias.rs index d0cbb64f5f..401300b1ae 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl/alias.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl/alias.rs @@ -14,7 +14,7 @@ use crate::{ auth::{AdminRole, SessionInfo}, enterprise::db::models::acl::{ AclAlias, AclAliasDestinationRange, AclAliasInfo, AclError, AliasKind, AliasState, - Protocol, acl_delete_related_objects, parse_destination, + Protocol, acl_delete_related_objects, parse_destination_addresses, }, handlers::{ApiResponse, ApiResult}, }; @@ -23,7 +23,7 @@ use crate::{ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)] pub struct EditAclAlias { pub name: String, - pub destination: String, + pub addresses: String, pub ports: String, pub protocols: Vec, } @@ -37,7 +37,7 @@ impl EditAclAlias { ) -> Result<(), AclError> { debug!("Creating related objects for ACL alias {self:?}"); // save related destination ranges - let destination = parse_destination(&self.destination)?; + let destination = parse_destination_addresses(&self.addresses)?; for range in destination.ranges { let obj = AclAliasDestinationRange { id: NoId, @@ -63,7 +63,7 @@ pub struct ApiAclAlias { pub name: String, pub kind: AliasKind, pub state: AliasState, - pub destination: String, + pub addresses: String, pub ports: String, pub protocols: Vec, pub rules: Vec, @@ -164,7 +164,7 @@ impl ApiAclAlias { impl From for ApiAclAlias { fn from(info: AclAliasInfo) -> Self { Self { - destination: info.format_destination(), + addresses: info.format_destination(), ports: info.format_ports(), id: info.id, parent_id: info.parent_id, diff --git a/crates/defguard_core/src/enterprise/handlers/acl/destination.rs b/crates/defguard_core/src/enterprise/handlers/acl/destination.rs index cb55a85556..3a770a109c 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl/destination.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl/destination.rs @@ -14,7 +14,7 @@ use crate::{ auth::{AdminRole, SessionInfo}, enterprise::db::models::acl::{ AclAlias, AclAliasDestinationRange, AclAliasInfo, AclError, AliasKind, AliasState, - Protocol, acl_delete_related_objects, parse_destination, + Protocol, acl_delete_related_objects, parse_destination_addresses, }, handlers::{ApiResponse, ApiResult}, }; @@ -23,10 +23,10 @@ use crate::{ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)] pub(crate) struct EditAclDestination { pub name: String, - pub destination: String, + pub addresses: String, pub ports: String, pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, } @@ -40,7 +40,7 @@ impl EditAclDestination { ) -> Result<(), AclError> { debug!("Creating related objects for ACL alias {self:?}"); // save related destination ranges - let destination = parse_destination(&self.destination)?; + let destination = parse_destination_addresses(&self.addresses)?; for range in destination.ranges { let obj = AclAliasDestinationRange { id: NoId, @@ -66,11 +66,11 @@ pub(crate) struct ApiAclDestination { pub name: String, pub kind: AliasKind, pub state: AliasState, - pub destination: String, + pub addresses: String, pub ports: String, pub protocols: Vec, pub rules: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, } @@ -170,7 +170,7 @@ impl ApiAclDestination { impl From for ApiAclDestination { fn from(info: AclAliasInfo) -> Self { Self { - destination: info.format_destination(), + addresses: info.format_destination(), ports: info.format_ports(), id: info.id, parent_id: info.parent_id, @@ -179,7 +179,7 @@ impl From for ApiAclDestination { state: info.state, protocols: info.protocols, rules: info.rules.iter().map(|v| v.id).collect(), - any_destination: info.any_address, + any_address: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, } diff --git a/crates/defguard_core/tests/integration/api/acl.rs b/crates/defguard_core/tests/integration/api/acl.rs index ac72b7387c..b95a174fcb 100644 --- a/crates/defguard_core/tests/integration/api/acl.rs +++ b/crates/defguard_core/tests/integration/api/acl.rs @@ -73,6 +73,7 @@ fn make_rule() -> EditAclRule { any_address: false, any_port: false, any_protocol: false, + use_manual_destination_settings: true, } } @@ -86,7 +87,7 @@ async fn set_rule_state(pool: &PgPool, id: Id, state: RuleState, parent_id: Opti fn make_alias() -> EditAclAlias { EditAclAlias { name: "alias".to_string(), - destination: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), + addresses: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), protocols: vec![6, 17], ports: "1, 2, 3, 10-20, 30-40".to_string(), } @@ -126,6 +127,7 @@ fn edit_rule_data_into_api_response( any_address: data.any_address, any_port: data.any_port, any_protocol: data.any_protocol, + use_manual_destination_settings: data.use_manual_destination_settings, } } @@ -143,7 +145,7 @@ fn edit_alias_data_into_api_response( state, name: data.name, kind, - destination: data.destination, + addresses: data.addresses, ports: data.ports, protocols: data.protocols, rules, @@ -364,7 +366,7 @@ async fn test_empty_strings(_: PgPoolOptions, options: PgConnectOptions) { // alias let mut alias = make_alias(); - alias.destination = String::new(); + alias.addresses = String::new(); alias.ports = String::new(); let response = client.post("/api/v1/acl/alias").json(&alias).send().await; assert_eq!(response.status(), StatusCode::CREATED); diff --git a/web/src/pages/CEDestinationPage/CEDestinationPage.tsx b/web/src/pages/CEDestinationPage/CEDestinationPage.tsx index 2650a62234..203141b698 100644 --- a/web/src/pages/CEDestinationPage/CEDestinationPage.tsx +++ b/web/src/pages/CEDestinationPage/CEDestinationPage.tsx @@ -26,6 +26,7 @@ import { isPresent } from '../../shared/defguard-ui/utils/isPresent'; import { useAppForm } from '../../shared/form'; import { formChangeLogic } from '../../shared/formLogic'; import { aclDestinationValidator, aclPortsValidator } from '../../shared/validators'; +import { omit } from 'radashi'; type Props = { destination?: AclDestination; @@ -107,12 +108,7 @@ export const CEDestinationPage = ({ destination }: Props) => { const defaultValues = useMemo((): FormFields => { if (isPresent(destination)) { return { - name: destination.name, - any_address: true, - any_port: true, - any_protocol: true, - addresses: destination.addresses, - ports: destination.ports, + ...omit(destination, ['id', 'state', 'rules']), protocols: new Set(destination.protocols), }; } diff --git a/web/src/pages/CERulePage/CERulePage.tsx b/web/src/pages/CERulePage/CERulePage.tsx index 909057477b..a4e84c064a 100644 --- a/web/src/pages/CERulePage/CERulePage.tsx +++ b/web/src/pages/CERulePage/CERulePage.tsx @@ -30,7 +30,6 @@ import type { import { AppText } from '../../shared/defguard-ui/components/AppText/AppText'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import { ButtonsGroup } from '../../shared/defguard-ui/components/ButtonsGroup/ButtonsGroup'; -import { Checkbox } from '../../shared/defguard-ui/components/Checkbox/Checkbox'; import { CheckboxIndicator } from '../../shared/defguard-ui/components/CheckboxIndicator/CheckboxIndicator'; import { Chip } from '../../shared/defguard-ui/components/Chip/Chip'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; @@ -253,7 +252,7 @@ const Content = ({ rule: initialRule }: Props) => { }, [networkDevices]); const [_restrictionsPresent, _setRestrictionsPresent] = useState(false); - const [manualDestination, setManualDestination] = useState(false); + // const [manualDestination, setManualDestination] = useState(false); const formSchema = useMemo( () => @@ -284,6 +283,7 @@ const Content = ({ rule: initialRule }: Props) => { any_protocol: z.boolean(), destinations: z.set(z.number()), aliases: z.set(z.number()), + use_manual_destination_settings: z.boolean(), }) .superRefine((vals, ctx) => { // check for collisions @@ -402,6 +402,7 @@ const Content = ({ rule: initialRule }: Props) => { any_address: true, any_port: true, any_protocol: true, + use_manual_destination_settings: false, }; }, [initialRule]); @@ -427,14 +428,12 @@ const Content = ({ rule: initialRule }: Props) => { protocols: Array.from(toSend.protocols), aliases: Array.from(toSend.aliases), id: initialRule.id, - use_manual_destination_settings: manualDestination, }); } else { await addRule({ ...toSend, protocols: Array.from(toSend.protocols), aliases: Array.from(toSend.aliases), - use_manual_destination_settings: manualDestination, }); } }, @@ -563,160 +562,162 @@ const Content = ({ rule: initialRule }: Props) => {

{`Manually configure destinations parameters for this rule.`}

- { - setManualDestination((s) => !s); - }} - /> - - - - {isPresent(aliasesOptions) && aliasesOptions.length === 0 && ( -
-
- -
-

{`You don't have any aliases to use yet — create them in the “Aliases” section to create reusable elements for defining destinations in multiple firewall ACL rules.`}

-
- )} - {isPresent(aliasesOptions) && aliasesOptions.length > 0 && ( - <> - -

{`Aliases can optionally define some or all of the manual destination settings. They are combined with the values you specify to form the final destination for firewall rule generation.`}

-
- - - {(field) => ( - <> - -