From 6b28d99822ac8ac7eb5d07d834b18233b2e01051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 4 Mar 2026 13:13:19 +0100 Subject: [PATCH 1/9] update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 52b9121c40..77910f7a75 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ ladle-build result/ .aider* .env +.zellij_layout.kdl From ef0eeff5e253441c9b16d2cda2f7c54ef34e906a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Mar 2026 08:32:07 +0100 Subject: [PATCH 2/9] add tests to reproduce issue --- .../src/enterprise/firewall/tests.rs | 253 +++++++++++++++++- 1 file changed, 250 insertions(+), 3 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index ab42d2ffb5..dc8503b994 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -4005,7 +4005,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt } #[sqlx::test] -async fn test_gh1868_ipv6_rule_is_not_created_with_v4_only_destination( +async fn test_gh1868_ipv6_firewall_rule_is_not_created_for_v4_only_destination_in_rule( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -4086,14 +4086,19 @@ async fn test_gh1868_ipv6_rule_is_not_created_with_v4_only_destination( 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::Ipv4)); + assert!(!allow_rule.destination_addrs.is_empty()); + assert!(!allow_rule.source_addrs.is_empty()); let deny_rule = &generated_firewall_rules[1]; + println!("{deny_rule:#?}"); assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert_eq!(allow_rule.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(deny_rule.ip_version, i32::from(IpVersion::Ipv4)); + assert!(!deny_rule.destination_addrs.is_empty()); + assert!(deny_rule.source_addrs.is_empty()); } #[sqlx::test] -async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination( +async fn test_gh1868_ipv4_firewall_rule_is_not_created_for_v6_only_destination_in_rule( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -4174,10 +4179,252 @@ async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination( 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)); + assert!(!allow_rule.destination_addrs.is_empty()); + assert!(!allow_rule.source_addrs.is_empty()); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(deny_rule.ip_version, i32::from(IpVersion::Ipv6)); + assert!(!deny_rule.destination_addrs.is_empty()); + assert!(deny_rule.source_addrs.is_empty()); +} + +#[sqlx::test] +async fn test_gh2117_ipv6_firewall_rule_is_not_created_for_v4_only_destination_in_alias( + _: 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, + acl_default_allow: false, + 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 + 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(); + + // assign network address to device + let mut conn = pool.acquire().await.unwrap(); + location + .add_device_to_network(&mut conn, &device, None) + .await + .unwrap(); + + // create ACL rule with no manual destination and attach destination alias + let acl_rule = AclRule { + id: NoId, + name: "Alias Only".into(), + all_networks: false, + expires: None, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: Vec::new(), + ports: Vec::new(), + protocols: Vec::new(), + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let acl_rule = acl_rule.save(&pool).await.unwrap(); + + // set only IPv4 dsestination + let destination_alias = AclAlias { + id: NoId, + name: "internal ntp".to_string(), + kind: AliasKind::Destination, + destination: vec!["192.168.1.0/24".parse().unwrap()], + ports: vec![PortRange::new(123, 123).into()], + protocols: vec![Protocol::Udp.into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let obj = AclRuleAlias { + id: NoId, + rule_id: acl_rule.id, + alias_id: destination_alias.id, + }; + obj.save(&pool).await.unwrap(); + + // assign rule to location + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + + // verify only IPv4 rules are created + let generated_firewall_config = location + .try_get_firewall_config(&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::Ipv4)); + assert!(!allow_rule.destination_addrs.is_empty()); + assert!(!allow_rule.source_addrs.is_empty()); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(deny_rule.ip_version, i32::from(IpVersion::Ipv4)); + assert!(!deny_rule.destination_addrs.is_empty()); + assert!(deny_rule.source_addrs.is_empty()); +} + +#[sqlx::test] +async fn test_gh2117_ipv4_firewall_rule_is_not_created_for_v6_only_destination_in_alias( + _: 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, + acl_default_allow: false, + 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 + 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(); + + // assign network address to device + let mut conn = pool.acquire().await.unwrap(); + location + .add_device_to_network(&mut conn, &device, None) + .await + .unwrap(); + + // create ACL rule with no manual destination and attach destination alias + let acl_rule = AclRule { + id: NoId, + name: "Alias Only".into(), + all_networks: false, + expires: None, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: Vec::new(), + ports: Vec::new(), + protocols: Vec::new(), + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let acl_rule = acl_rule.save(&pool).await.unwrap(); + + // set only IPv6 destination + let destination_alias = AclAlias { + id: NoId, + name: "internal ntp".to_string(), + kind: AliasKind::Destination, + destination: vec!["fc00::0/112".parse().unwrap()], + ports: vec![PortRange::new(123, 123).into()], + protocols: vec![Protocol::Udp.into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let obj = AclRuleAlias { + id: NoId, + rule_id: acl_rule.id, + alias_id: destination_alias.id, + }; + obj.save(&pool).await.unwrap(); + + // assign rule to location + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + + // verify only IPv6 rules are created + let generated_firewall_config = location + .try_get_firewall_config(&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)); + assert!(!allow_rule.destination_addrs.is_empty()); + assert!(!allow_rule.source_addrs.is_empty()); 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)); + assert!(!deny_rule.destination_addrs.is_empty()); + assert!(deny_rule.source_addrs.is_empty()); } #[sqlx::test] From 2109600e81536e9d236fc7faccd381536bf1615a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Mar 2026 09:01:22 +0100 Subject: [PATCH 3/9] update rule generation for destination aliases to mirror manual destinations --- crates/defguard_core/src/enterprise/firewall/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index d20e88464a..c6d594e1bf 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -228,7 +228,10 @@ pub async fn generate_firewall_rules_from_acls( "ACL {} - {}, ALIAS {} - {}", acl.id, acl.name, alias.id, alias.name ); - if location_has_ipv4_addresses { + let has_v4_destination = !dest_addrs_v4.is_empty(); + let has_v6_destination = !dest_addrs_v6.is_empty(); + let has_no_destination_address = !(has_v4_destination || has_v6_destination); + if location_has_ipv4_addresses && (has_v4_destination || has_no_destination_address) { // create IPv4 rules let ipv4_rules = create_rules( alias.id, @@ -245,7 +248,7 @@ pub async fn generate_firewall_rules_from_acls( deny_rules.push(ipv4_rules.1); } - if location_has_ipv6_addresses { + if location_has_ipv6_addresses && (has_v6_destination || has_no_destination_address) { // create IPv6 rules let ipv6_rules = create_rules( alias.id, From 9d7872a21470ded70f845960e852aacb59757905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Mar 2026 10:54:32 +0100 Subject: [PATCH 4/9] add dedicated test for empty destination aliases --- .../src/enterprise/firewall/tests.rs | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index dc8503b994..2bf22bc93f 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -4004,6 +4004,274 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt ); } +#[sqlx::test] +async fn test_empty_destination_alias_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 + 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 empty destination alias and add it to the rule + let destination_alias = AclAlias { + id: NoId, + name: "empty destination alias".to_string(), + kind: AliasKind::Destination, + destination: Vec::new(), + ports: Vec::new(), + protocols: Vec::new(), + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let obj = AclRuleAlias { + id: NoId, + rule_id: acl_rule.id, + alias_id: destination_alias.id, + }; + obj.save(&pool).await.unwrap(); + + // assign rule to all locations + for location in [&location_ipv4, &location_ipv6, &location_ipv4_and_ipv6] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + + 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 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(), + })), + }, + ]; + + // check generated rules for IPv4 only location + let generated_firewall_rules_ipv4 = location_ipv4 + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + assert_eq!(generated_firewall_rules_ipv4.len(), 2); + 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 = location_ipv6 + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + assert_eq!(generated_firewall_rules_ipv6.len(), 2); + 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 = location_ipv4_and_ipv6 + .try_get_firewall_config(&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()); +} + #[sqlx::test] async fn test_gh1868_ipv6_firewall_rule_is_not_created_for_v4_only_destination_in_rule( _: PgPoolOptions, From c0b3b9d14a4d8966fd4f6f3cba4270c9a5c75bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Mar 2026 11:26:49 +0100 Subject: [PATCH 5/9] add more ACL test cases --- .../src/enterprise/firewall/tests.rs | 779 +++++++++++++++++- 1 file changed, 777 insertions(+), 2 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index 2bf22bc93f..5d14327c97 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -25,8 +25,9 @@ use crate::{ }, enterprise::{ db::models::acl::{ - AclAlias, AclRule, AclRuleAlias, AclRuleDestinationRange, AclRuleDevice, AclRuleGroup, - AclRuleInfo, AclRuleNetwork, AclRuleUser, AliasKind, PortRange, RuleState, + AclAlias, AclAliasDestinationRange, AclRule, AclRuleAlias, AclRuleDestinationRange, + AclRuleDevice, AclRuleGroup, AclRuleInfo, AclRuleNetwork, AclRuleUser, AliasKind, + PortRange, RuleState, }, firewall::{get_source_addrs, get_source_network_devices}, }, @@ -4272,6 +4273,780 @@ async fn test_empty_destination_alias_only_acl(_: PgPoolOptions, options: PgConn assert!(deny_rule_ipv6.destination_addrs.is_empty()); } +#[sqlx::test] +async fn test_destination_alias_mixed_ip_versions(_: 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, + acl_default_allow: false, + 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 + 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(); + + // assign network address to device + let mut conn = pool.acquire().await.unwrap(); + location + .add_device_to_network(&mut conn, &device, None) + .await + .unwrap(); + + // create ACL rule with no manual destination and attach destination alias + let acl_rule = AclRule { + id: NoId, + name: "Alias Mixed".into(), + all_networks: false, + expires: None, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: Vec::new(), + ports: Vec::new(), + protocols: Vec::new(), + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let acl_rule = acl_rule.save(&pool).await.unwrap(); + + let destination_alias = AclAlias { + id: NoId, + name: "dual stack alias".to_string(), + kind: AliasKind::Destination, + destination: vec!["192.168.1.0/24".parse().unwrap(), "fc00::/112".parse().unwrap()], + ports: vec![PortRange::new(123, 123).into()], + protocols: vec![Protocol::Udp.into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let obj = AclRuleAlias { + id: NoId, + rule_id: acl_rule.id, + alias_id: destination_alias.id, + }; + obj.save(&pool).await.unwrap(); + + // assign rule to location + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + + // verify IPv4 and IPv6 rules are created and destinations are family-specific + let generated_firewall_config = location + .try_get_firewall_config(&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, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!( + allow_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) + }] + ); + + let allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!(allow_rule_ipv6.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!( + allow_rule_ipv6.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("fc00::/112".to_string())) + }] + ); + + let deny_rule_ipv4 = &generated_firewall_rules[2]; + assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(deny_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!( + deny_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) + }] + ); + + let deny_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!(deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(deny_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!( + deny_rule_ipv6.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("fc00::/112".to_string())) + }] + ); +} + +#[sqlx::test] +async fn test_destination_alias_ranges_only(_: 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, + acl_default_allow: false, + 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 + 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(); + + // assign network address to device + let mut conn = pool.acquire().await.unwrap(); + location + .add_device_to_network(&mut conn, &device, None) + .await + .unwrap(); + + // create ACL rule with no manual destination and attach destination alias + let acl_rule = AclRule { + id: NoId, + name: "Alias Ranges".into(), + all_networks: false, + expires: None, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: Vec::new(), + ports: Vec::new(), + protocols: Vec::new(), + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let acl_rule = acl_rule.save(&pool).await.unwrap(); + + let destination_alias = AclAlias { + id: NoId, + name: "range alias".to_string(), + kind: AliasKind::Destination, + destination: Vec::new(), + ports: vec![PortRange::new(123, 123).into()], + protocols: vec![Protocol::Udp.into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + AclAliasDestinationRange { + id: NoId, + alias_id: destination_alias.id, + start: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), + end: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), + } + .save(&pool) + .await + .unwrap(); + AclAliasDestinationRange { + id: NoId, + alias_id: destination_alias.id, + start: IpAddr::V6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1)), + end: IpAddr::V6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 10)), + } + .save(&pool) + .await + .unwrap(); + + let obj = AclRuleAlias { + id: NoId, + rule_id: acl_rule.id, + alias_id: destination_alias.id, + }; + obj.save(&pool).await.unwrap(); + + // assign rule to location + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + + // verify IPv4 and IPv6 rules are created from ranges only + let generated_firewall_config = location + .try_get_firewall_config(&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, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert!(!allow_rule_ipv4.destination_addrs.is_empty()); + + let allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!(allow_rule_ipv6.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert!(!allow_rule_ipv6.destination_addrs.is_empty()); + + let deny_rule_ipv4 = &generated_firewall_rules[2]; + assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(deny_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert!(!deny_rule_ipv4.destination_addrs.is_empty()); + + let deny_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!(deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(deny_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert!(!deny_rule_ipv6.destination_addrs.is_empty()); +} + +#[sqlx::test] +async fn test_destination_alias_empty_destination_with_ports_protocols( + _: 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 empty destination alias with ports and protocols + let destination_alias = AclAlias { + id: NoId, + name: "empty destination with ports".to_string(), + kind: AliasKind::Destination, + destination: Vec::new(), + ports: vec![PortRange::new(53, 53).into()], + protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let obj = AclRuleAlias { + id: NoId, + rule_id: acl_rule.id, + alias_id: destination_alias.id, + }; + obj.save(&pool).await.unwrap(); + + // assign rule to location + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // should still match any destination, but include ports and protocols + assert_eq!(generated_firewall_rules.len(), 2); + let allow_rule = &generated_firewall_rules[0]; + assert_eq!(allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert!(allow_rule.destination_addrs.is_empty()); + assert_eq!( + allow_rule.destination_ports, + vec![Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + assert!(allow_rule + .protocols + .iter() + .all(|proto| [Protocol::Tcp as i32, Protocol::Udp as i32].contains(proto))); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule.destination_addrs.is_empty()); + assert!(deny_rule.destination_ports.is_empty()); + assert!(deny_rule.protocols.is_empty()); +} + +#[sqlx::test] +async fn test_component_alias_combines_with_manual_destinations( + _: 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, + acl_default_allow: false, + 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 + 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(); + + // assign network address to device + let mut conn = pool.acquire().await.unwrap(); + location + .add_device_to_network(&mut conn, &device, None) + .await + .unwrap(); + + // create ACL rule with manual destination + let acl_rule = AclRule { + id: NoId, + name: "Manual with Component".into(), + all_networks: false, + expires: None, + 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()], + ports: vec![PortRange::new(80, 80).into()], + protocols: vec![Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let acl_rule = acl_rule.save(&pool).await.unwrap(); + + let component_alias = AclAlias { + id: NoId, + name: "component alias".to_string(), + kind: AliasKind::Component, + destination: vec!["fc00::/112".parse().unwrap()], + ports: vec![PortRange::new(443, 443).into()], + protocols: vec![Protocol::Tcp.into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let obj = AclRuleAlias { + id: NoId, + rule_id: acl_rule.id, + alias_id: component_alias.id, + }; + obj.save(&pool).await.unwrap(); + + // assign rule to location + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + + // verify both IPv4 and IPv6 rules are created with merged destinations/ports + let generated_firewall_config = location + .try_get_firewall_config(&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, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!( + allow_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) + }] + ); + assert_eq!( + allow_rule_ipv4.destination_ports, + vec![ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + + let allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!(allow_rule_ipv6.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!( + allow_rule_ipv6.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("fc00::/112".to_string())) + }] + ); + assert_eq!(allow_rule_ipv6.destination_ports, allow_rule_ipv4.destination_ports); + + let deny_rule_ipv4 = &generated_firewall_rules[2]; + assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(deny_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!( + deny_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) + }] + ); + + let deny_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!(deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(deny_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!( + deny_rule_ipv6.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("fc00::/112".to_string())) + }] + ); +} + +#[sqlx::test] +async fn test_acl_with_no_allow_sources_creates_only_deny_rules( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let mut conn = pool.acquire().await.unwrap(); + + // create ACL rule that denies all users and does not allow any + let acl_rule = AclRule { + id: NoId, + name: "deny all".to_string(), + expires: None, + enabled: true, + state: RuleState::Applied, + destination: vec!["10.0.0.0/24".parse().unwrap()], + allow_all_users: false, + deny_all_users: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + + let generated_firewall_rules = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // Only deny rules should be created since there are no allowed sources + assert_eq!(generated_firewall_rules.len(), 1); + let deny_rule = &generated_firewall_rules[0]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule.source_addrs.is_empty()); + assert!(!deny_rule.destination_addrs.is_empty()); +} + +#[sqlx::test] +async fn test_manual_and_destination_aliases_create_separate_rules( + _: 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: User = rng.r#gen(); + let user = user.save(&pool).await.unwrap(); + + 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 with manual destination + let acl_rule = AclRule { + id: NoId, + name: "manual + alias".to_string(), + expires: None, + enabled: true, + state: RuleState::Applied, + destination: vec!["10.0.2.3/32".parse().unwrap()], + allow_all_users: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // create destination alias and attach to rule + let destination_alias = AclAlias { + id: NoId, + name: "alias".to_string(), + kind: AliasKind::Destination, + destination: vec!["10.0.2.4/32".parse().unwrap()], + ports: vec![PortRange::new(22, 22).into()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let obj = AclRuleAlias { + id: NoId, + rule_id: acl_rule.id, + alias_id: destination_alias.id, + }; + obj.save(&pool).await.unwrap(); + + // assign rule to location + let obj = AclRuleNetwork { + id: NoId, + rule_id: acl_rule.id, + network_id: location.id, + }; + obj.save(&pool).await.unwrap(); + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // manual destinations and alias destinations should produce separate rules + assert_eq!(generated_firewall_rules.len(), 4); + let allow_manual = &generated_firewall_rules[0]; + assert_eq!(allow_manual.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!( + allow_manual.destination_addrs, + vec![IpAddress { + address: Some(Address::Ip("10.0.2.3".to_string())) + }] + ); + + let allow_alias = &generated_firewall_rules[1]; + assert_eq!(allow_alias.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!( + allow_alias.destination_addrs, + vec![IpAddress { + address: Some(Address::Ip("10.0.2.4".to_string())) + }] + ); + + let deny_manual = &generated_firewall_rules[2]; + assert_eq!(deny_manual.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!( + deny_manual.destination_addrs, + vec![IpAddress { + address: Some(Address::Ip("10.0.2.3".to_string())) + }] + ); + + let deny_alias = &generated_firewall_rules[3]; + assert_eq!(deny_alias.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!( + deny_alias.destination_addrs, + vec![IpAddress { + address: Some(Address::Ip("10.0.2.4".to_string())) + }] + ); +} + #[sqlx::test] async fn test_gh1868_ipv6_firewall_rule_is_not_created_for_v4_only_destination_in_rule( _: PgPoolOptions, From e010fd1ad05df3b59189ca01025fe9d8f03e5189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Mar 2026 11:35:12 +0100 Subject: [PATCH 6/9] formatting --- .../src/enterprise/firewall/tests.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index 5d14327c97..754fe275b9 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -4342,7 +4342,10 @@ async fn test_destination_alias_mixed_ip_versions(_: PgPoolOptions, options: PgC id: NoId, name: "dual stack alias".to_string(), kind: AliasKind::Destination, - destination: vec!["192.168.1.0/24".parse().unwrap(), "fc00::/112".parse().unwrap()], + destination: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::/112".parse().unwrap(), + ], ports: vec![PortRange::new(123, 123).into()], protocols: vec![Protocol::Udp.into()], ..Default::default() @@ -4678,10 +4681,12 @@ async fn test_destination_alias_empty_destination_with_ports_protocols( port: Some(PortInner::SinglePort(53)) }] ); - assert!(allow_rule - .protocols - .iter() - .all(|proto| [Protocol::Tcp as i32, Protocol::Udp as i32].contains(proto))); + assert!( + allow_rule + .protocols + .iter() + .all(|proto| [Protocol::Tcp as i32, Protocol::Udp as i32].contains(proto)) + ); let deny_rule = &generated_firewall_rules[1]; assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); @@ -4825,7 +4830,10 @@ async fn test_component_alias_combines_with_manual_destinations( address: Some(Address::IpSubnet("fc00::/112".to_string())) }] ); - assert_eq!(allow_rule_ipv6.destination_ports, allow_rule_ipv4.destination_ports); + assert_eq!( + allow_rule_ipv6.destination_ports, + allow_rule_ipv4.destination_ports + ); let deny_rule_ipv4 = &generated_firewall_rules[2]; assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); From d66e01d073ae5093ae25e6b8fde6e27e8b56cf48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Mar 2026 11:38:52 +0100 Subject: [PATCH 7/9] remove debug log --- crates/defguard_core/src/enterprise/firewall/tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index 754fe275b9..d3d388d302 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -5141,7 +5141,6 @@ async fn test_gh1868_ipv6_firewall_rule_is_not_created_for_v4_only_destination_i assert!(!allow_rule.source_addrs.is_empty()); let deny_rule = &generated_firewall_rules[1]; - println!("{deny_rule:#?}"); assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); assert_eq!(deny_rule.ip_version, i32::from(IpVersion::Ipv4)); assert!(!deny_rule.destination_addrs.is_empty()); From e7e03d4828a3e9c95339fe1393de8b23eb0dded6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Mar 2026 11:40:35 +0100 Subject: [PATCH 8/9] fix typos --- crates/defguard_core/src/enterprise/firewall/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index d3d388d302..3ded359239 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -5307,7 +5307,7 @@ async fn test_gh2117_ipv6_firewall_rule_is_not_created_for_v4_only_destination_i }; let acl_rule = acl_rule.save(&pool).await.unwrap(); - // set only IPv4 dsestination + // set only IPv4 destination let destination_alias = AclAlias { id: NoId, name: "internal ntp".to_string(), @@ -5472,7 +5472,7 @@ async fn test_gh2117_ipv4_firewall_rule_is_not_created_for_v6_only_destination_i 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)); + assert_eq!(deny_rule.ip_version, i32::from(IpVersion::Ipv6)); assert!(!deny_rule.destination_addrs.is_empty()); assert!(deny_rule.source_addrs.is_empty()); } From a2ef79ad00684fe8a65b373adca04c1825395a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 5 Mar 2026 12:43:37 +0100 Subject: [PATCH 9/9] introduce some test helpers --- .../src/enterprise/firewall/tests.rs | 1716 ++++++----------- 1 file changed, 590 insertions(+), 1126 deletions(-) diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index 3ded359239..fb1722d0e1 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -57,6 +57,272 @@ fn random_network_device_with_id(rng: &mut R, id: Id) -> Device { device } +async fn create_location_with_addresses( + pool: &PgPool, + addresses: Vec, + acl_default_allow: Option, +) -> WireguardNetwork { + let mut location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: addresses, + ..Default::default() + }; + if let Some(default_allow) = acl_default_allow { + location.acl_default_allow = default_allow; + } + location.save(pool).await.unwrap() +} + +async fn create_location_ipv4( + pool: &PgPool, + acl_default_allow: Option, +) -> WireguardNetwork { + create_location_with_addresses( + pool, + vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + acl_default_allow, + ) + .await +} + +async fn create_location_ipv6( + pool: &PgPool, + acl_default_allow: Option, +) -> WireguardNetwork { + create_location_with_addresses( + pool, + vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + acl_default_allow, + ) + .await +} + +async fn create_location_dual_stack( + pool: &PgPool, + acl_default_allow: Option, +) -> WireguardNetwork { + create_location_with_addresses( + pool, + 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(), + ], + acl_default_allow, + ) + .await +} + +async fn create_user(pool: &PgPool) -> User { + let mut rng = thread_rng(); + let user: User = rng.r#gen(); + user.save(pool).await.unwrap() +} + +async fn create_user_device(pool: &PgPool, user: &User, name: String) -> Device { + let device = Device { + id: NoId, + name, + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + device.save(pool).await.unwrap() +} + +async fn create_user_device_assigned( + pool: &PgPool, + location: &WireguardNetwork, +) -> (User, Device) { + let user = create_user(pool).await; + let device = create_user_device(pool, &user, format!("device-{}", user.id)).await; + let mut conn = pool.acquire().await.unwrap(); + location + .add_device_to_network(&mut conn, &device, None) + .await + .unwrap(); + (user, device) +} + +async fn attach_rule_to_location(pool: &PgPool, rule_id: Id, location_id: Id) { + let obj = AclRuleNetwork { + id: NoId, + rule_id, + network_id: location_id, + }; + obj.save(pool).await.unwrap(); +} + +async fn attach_alias_to_rule(pool: &PgPool, rule_id: Id, alias_id: Id) { + let obj = AclRuleAlias { + id: NoId, + rule_id, + alias_id, + }; + obj.save(pool).await.unwrap(); +} + +async fn create_acl_rule_basic( + pool: &PgPool, + name: &str, + destination: Vec, + ports: Vec, + protocols: Vec, + allow_all_users: bool, + deny_all_users: bool, + all_networks: bool, +) -> AclRule { + let acl_rule = AclRule { + id: NoId, + name: name.to_string(), + all_networks, + expires: None, + allow_all_users, + deny_all_users, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination, + ports: ports.into_iter().map(Into::into).collect(), + protocols, + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + acl_rule.save(pool).await.unwrap() +} + +async fn create_destination_alias( + pool: &PgPool, + name: &str, + destination: Vec, + ports: Vec, + protocols: Vec, +) -> AclAlias { + let alias = AclAlias { + id: NoId, + name: name.to_string(), + kind: AliasKind::Destination, + destination, + ports: ports.into_iter().map(Into::into).collect(), + protocols, + ..Default::default() + }; + alias.save(pool).await.unwrap() +} + +async fn create_component_alias( + pool: &PgPool, + name: &str, + destination: Vec, + ports: Vec, + protocols: Vec, +) -> AclAlias { + let alias = AclAlias { + id: NoId, + name: name.to_string(), + kind: AliasKind::Component, + destination, + ports: ports.into_iter().map(Into::into).collect(), + protocols, + ..Default::default() + }; + alias.save(pool).await.unwrap() +} + +async fn add_alias_destination_range(pool: &PgPool, alias_id: Id, start: IpAddr, end: IpAddr) { + AclAliasDestinationRange { + id: NoId, + alias_id, + start, + end, + } + .save(pool) + .await + .unwrap(); +} + +async fn fetch_firewall_rules( + pool: &PgPool, + location: &WireguardNetwork, +) -> Vec { + let mut conn = pool.acquire().await.unwrap(); + location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules +} + +async fn seed_users_with_devices_for_locations( + pool: &PgPool, + locations: &[&WireguardNetwork], + users: usize, + devices_per_user: u8, +) -> Vec> { + let mut rng = thread_rng(); + let mut result = Vec::with_capacity(users); + for _ in 0..users { + let user: User = rng.r#gen(); + let user = user.save(pool).await.unwrap(); + + for device_num in 1..=devices_per_user { + 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(); + + for location in locations { + let mut wireguard_ips = Vec::new(); + let has_ipv4 = location.address.iter().any(IpNetwork::is_ipv4); + let has_ipv6 = location.address.iter().any(IpNetwork::is_ipv6); + if has_ipv4 { + wireguard_ips.push(IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num))); + } + if has_ipv6 { + wireguard_ips.push(IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + ))); + } + 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(); + } + } + + result.push(user); + } + + result +} + #[test] fn test_get_relevant_users() { let mut rng = thread_rng(); @@ -3799,8 +4065,6 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { 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, @@ -3812,106 +4076,45 @@ 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(); - } - } + seed_users_with_devices_for_locations(&pool, &[&location], 2, 2).await; // 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(); + let acl_rule = create_acl_rule_basic( + &pool, + "test rule", + Vec::new(), + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; // 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] { - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: alias.id, - }; - obj.save(&pool).await.unwrap(); - } + let destination_alias_1 = create_destination_alias( + &pool, + "postgres", + vec!["10.0.2.3".parse().unwrap()], + vec![PortRange::new(5432, 5432)], + Vec::new(), + ) + .await; + let destination_alias_2 = create_destination_alias( + &pool, + "redis", + vec!["10.0.2.4".parse().unwrap()], + vec![PortRange::new(6379, 6379)], + Vec::new(), + ) + .await; + attach_alias_to_rule(&pool, acl_rule.id, destination_alias_1.id).await; + attach_alias_to_rule(&pool, acl_rule.id, destination_alias_2.id).await; // assign rule to location - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; // check generated rules assert_eq!(generated_firewall_rules.len(), 4); @@ -4009,165 +4212,57 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt async fn test_empty_destination_alias_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![ + let location_ipv4 = create_location_ipv4(&pool, None).await; + let location_ipv6 = create_location_ipv6(&pool, None).await; + let location_ipv4_and_ipv6 = create_location_with_addresses( + &pool, + vec![ IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), ], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + None, + ) + .await; // 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(); - } - } + seed_users_with_devices_for_locations( + &pool, + &[&location_ipv4, &location_ipv6, &location_ipv4_and_ipv6], + 2, + 2, + ) + .await; // 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(); + let acl_rule = create_acl_rule_basic( + &pool, + "test rule", + Vec::new(), + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; // create empty destination alias and add it to the rule - let destination_alias = AclAlias { - id: NoId, - name: "empty destination alias".to_string(), - kind: AliasKind::Destination, - destination: Vec::new(), - ports: Vec::new(), - protocols: Vec::new(), - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: destination_alias.id, - }; - obj.save(&pool).await.unwrap(); + let destination_alias = create_destination_alias( + &pool, + "empty destination alias", + Vec::new(), + Vec::new(), + Vec::new(), + ) + .await; + attach_alias_to_rule(&pool, acl_rule.id, destination_alias.id).await; // assign rule to all locations for location in [&location_ipv4, &location_ipv6, &location_ipv4_and_ipv6] { - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; } - let mut conn = pool.acquire().await.unwrap(); - let expected_source_addrs_ipv4 = vec![ IpAddress { address: Some(Address::IpRange(IpRange { @@ -4198,12 +4293,7 @@ async fn test_empty_destination_alias_only_acl(_: PgPoolOptions, options: PgConn ]; // check generated rules for IPv4 only location - let generated_firewall_rules_ipv4 = location_ipv4 - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules_ipv4 = fetch_firewall_rules(&pool, &location_ipv4).await; assert_eq!(generated_firewall_rules_ipv4.len(), 2); let allow_rule_ipv4 = &generated_firewall_rules_ipv4[0]; @@ -4219,12 +4309,7 @@ async fn test_empty_destination_alias_only_acl(_: PgPoolOptions, options: PgConn assert!(deny_rule_ipv4.destination_addrs.is_empty()); // check generated rules for IPv6 only location - let generated_firewall_rules_ipv6 = location_ipv6 - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules_ipv6 = fetch_firewall_rules(&pool, &location_ipv6).await; assert_eq!(generated_firewall_rules_ipv6.len(), 2); let allow_rule_ipv6 = &generated_firewall_rules_ipv6[0]; @@ -4240,12 +4325,8 @@ async fn test_empty_destination_alias_only_acl(_: PgPoolOptions, options: PgConn assert!(deny_rule_ipv6.destination_addrs.is_empty()); // check generated rules for IPv4 and IPv6 location - let generated_firewall_rules_ipv4_and_ipv6 = location_ipv4_and_ipv6 - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules_ipv4_and_ipv6 = + fetch_firewall_rules(&pool, &location_ipv4_and_ipv6).await; assert_eq!(generated_firewall_rules_ipv4_and_ipv6.len(), 4); let allow_rule_ipv4 = &generated_firewall_rules_ipv4_and_ipv6[0]; @@ -4277,105 +4358,44 @@ async fn test_empty_destination_alias_only_acl(_: PgPoolOptions, options: PgConn async fn test_destination_alias_mixed_ip_versions(_: 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, - acl_default_allow: false, - 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(); + let location = create_location_dual_stack(&pool, Some(false)).await; // setup user & device - 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(); - - // assign network address to device - let mut conn = pool.acquire().await.unwrap(); - location - .add_device_to_network(&mut conn, &device, None) - .await - .unwrap(); + create_user_device_assigned(&pool, &location).await; // create ACL rule with no manual destination and attach destination alias - let acl_rule = AclRule { - id: NoId, - name: "Alias Mixed".into(), - all_networks: false, - expires: None, - allow_all_users: true, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), - ports: Vec::new(), - protocols: Vec::new(), - enabled: true, - parent_id: None, - state: RuleState::Applied, - }; - let acl_rule = acl_rule.save(&pool).await.unwrap(); + let acl_rule = create_acl_rule_basic( + &pool, + "Alias Mixed", + Vec::new(), + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; - let destination_alias = AclAlias { - id: NoId, - name: "dual stack alias".to_string(), - kind: AliasKind::Destination, - destination: vec![ + let destination_alias = create_destination_alias( + &pool, + "dual stack alias", + vec![ "192.168.1.0/24".parse().unwrap(), "fc00::/112".parse().unwrap(), ], - ports: vec![PortRange::new(123, 123).into()], - protocols: vec![Protocol::Udp.into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + vec![PortRange::new(123, 123)], + vec![Protocol::Udp.into()], + ) + .await; - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: destination_alias.id, - }; - obj.save(&pool).await.unwrap(); + attach_alias_to_rule(&pool, acl_rule.id, destination_alias.id).await; // assign rule to location - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; // verify IPv4 and IPv6 rules are created and destinations are family-specific - let generated_firewall_config = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap(); - let generated_firewall_rules = generated_firewall_config.rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; assert_eq!(generated_firewall_rules.len(), 4); let allow_rule_ipv4 = &generated_firewall_rules[0]; @@ -4466,78 +4486,49 @@ async fn test_destination_alias_ranges_only(_: PgPoolOptions, options: PgConnect .unwrap(); // create ACL rule with no manual destination and attach destination alias - let acl_rule = AclRule { - id: NoId, - name: "Alias Ranges".into(), - all_networks: false, - expires: None, - allow_all_users: true, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), - ports: Vec::new(), - protocols: Vec::new(), - enabled: true, - parent_id: None, - state: RuleState::Applied, - }; - let acl_rule = acl_rule.save(&pool).await.unwrap(); + let acl_rule = create_acl_rule_basic( + &pool, + "Alias Ranges", + Vec::new(), + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; - let destination_alias = AclAlias { - id: NoId, - name: "range alias".to_string(), - kind: AliasKind::Destination, - destination: Vec::new(), - ports: vec![PortRange::new(123, 123).into()], - protocols: vec![Protocol::Udp.into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + let destination_alias = create_destination_alias( + &pool, + "range alias", + Vec::new(), + vec![PortRange::new(123, 123)], + vec![Protocol::Udp.into()], + ) + .await; - AclAliasDestinationRange { - id: NoId, - alias_id: destination_alias.id, - start: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), - end: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), - } - .save(&pool) - .await - .unwrap(); - AclAliasDestinationRange { - id: NoId, - alias_id: destination_alias.id, - start: IpAddr::V6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1)), - end: IpAddr::V6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 10)), - } - .save(&pool) - .await - .unwrap(); + add_alias_destination_range( + &pool, + destination_alias.id, + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), + ) + .await; + add_alias_destination_range( + &pool, + destination_alias.id, + IpAddr::V6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1)), + IpAddr::V6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 10)), + ) + .await; - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: destination_alias.id, - }; - obj.save(&pool).await.unwrap(); + attach_alias_to_rule(&pool, acl_rule.id, destination_alias.id).await; // assign rule to location - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; // verify IPv4 and IPv6 rules are created from ranges only - let generated_firewall_config = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap(); - let generated_firewall_rules = generated_firewall_config.rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; assert_eq!(generated_firewall_rules.len(), 4); let allow_rule_ipv4 = &generated_firewall_rules[0]; @@ -4568,8 +4559,6 @@ async fn test_destination_alias_empty_destination_with_ports_protocols( ) { let pool = setup_pool(options).await; - let mut rng = thread_rng(); - // Create test location let location = WireguardNetwork { id: NoId, @@ -4581,94 +4570,36 @@ async fn test_destination_alias_empty_destination_with_ports_protocols( .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(); - } - } + seed_users_with_devices_for_locations(&pool, &[&location], 2, 2).await; // 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(); + let acl_rule = create_acl_rule_basic( + &pool, + "test rule", + Vec::new(), + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; // create empty destination alias with ports and protocols - let destination_alias = AclAlias { - id: NoId, - name: "empty destination with ports".to_string(), - kind: AliasKind::Destination, - destination: Vec::new(), - ports: vec![PortRange::new(53, 53).into()], - protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: destination_alias.id, - }; - obj.save(&pool).await.unwrap(); + let destination_alias = create_destination_alias( + &pool, + "empty destination with ports", + Vec::new(), + vec![PortRange::new(53, 53)], + vec![Protocol::Udp.into(), Protocol::Tcp.into()], + ) + .await; + attach_alias_to_rule(&pool, acl_rule.id, destination_alias.id).await; // assign rule to location - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; // should still match any destination, but include ports and protocols assert_eq!(generated_firewall_rules.len(), 2); @@ -4702,102 +4633,41 @@ async fn test_component_alias_combines_with_manual_destinations( ) { 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, - acl_default_allow: false, - 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(); + let location = create_location_dual_stack(&pool, Some(false)).await; // setup user & device - 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(); - - // assign network address to device - let mut conn = pool.acquire().await.unwrap(); - location - .add_device_to_network(&mut conn, &device, None) - .await - .unwrap(); + create_user_device_assigned(&pool, &location).await; // create ACL rule with manual destination - let acl_rule = AclRule { - id: NoId, - name: "Manual with Component".into(), - all_networks: false, - expires: None, - 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()], - ports: vec![PortRange::new(80, 80).into()], - protocols: vec![Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - }; - let acl_rule = acl_rule.save(&pool).await.unwrap(); + let acl_rule = create_acl_rule_basic( + &pool, + "Manual with Component", + vec!["192.168.1.0/24".parse().unwrap()], + vec![PortRange::new(80, 80)], + vec![Protocol::Tcp.into()], + true, + false, + false, + ) + .await; - let component_alias = AclAlias { - id: NoId, - name: "component alias".to_string(), - kind: AliasKind::Component, - destination: vec!["fc00::/112".parse().unwrap()], - ports: vec![PortRange::new(443, 443).into()], - protocols: vec![Protocol::Tcp.into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + let component_alias = create_component_alias( + &pool, + "component alias", + vec!["fc00::/112".parse().unwrap()], + vec![PortRange::new(443, 443)], + vec![Protocol::Tcp.into()], + ) + .await; - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: component_alias.id, - }; - obj.save(&pool).await.unwrap(); + attach_alias_to_rule(&pool, acl_rule.id, component_alias.id).await; // assign rule to location - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; // verify both IPv4 and IPv6 rules are created with merged destinations/ports - let generated_firewall_config = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap(); - let generated_firewall_rules = generated_firewall_config.rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; assert_eq!(generated_firewall_rules.len(), 4); let allow_rule_ipv4 = &generated_firewall_rules[0]; @@ -4873,37 +4743,22 @@ async fn test_acl_with_no_allow_sources_creates_only_deny_rules( .await .unwrap(); - let mut conn = pool.acquire().await.unwrap(); - // create ACL rule that denies all users and does not allow any - let acl_rule = AclRule { - id: NoId, - name: "deny all".to_string(), - expires: None, - enabled: true, - state: RuleState::Applied, - destination: vec!["10.0.0.0/24".parse().unwrap()], - allow_all_users: false, - deny_all_users: true, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + let acl_rule = create_acl_rule_basic( + &pool, + "deny all", + vec!["10.0.0.0/24".parse().unwrap()], + Vec::new(), + Vec::new(), + false, + true, + false, + ) + .await; - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; - let generated_firewall_rules = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; // Only deny rules should be created since there are no allowed sources assert_eq!(generated_firewall_rules.len(), 1); @@ -4920,8 +4775,6 @@ async fn test_manual_and_destination_aliases_create_separate_rules( ) { let pool = setup_pool(options).await; - let mut rng = thread_rng(); - // Create test location let location = WireguardNetwork { id: NoId, @@ -4933,88 +4786,36 @@ async fn test_manual_and_destination_aliases_create_separate_rules( .unwrap(); // Setup some test users and their devices - let user: User = rng.r#gen(); - let user = user.save(&pool).await.unwrap(); - - 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(); - } + seed_users_with_devices_for_locations(&pool, &[&location], 1, 2).await; // create ACL rule with manual destination - let acl_rule = AclRule { - id: NoId, - name: "manual + alias".to_string(), - expires: None, - enabled: true, - state: RuleState::Applied, - destination: vec!["10.0.2.3/32".parse().unwrap()], - allow_all_users: true, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + let acl_rule = create_acl_rule_basic( + &pool, + "manual + alias", + vec!["10.0.2.3/32".parse().unwrap()], + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; // create destination alias and attach to rule - let destination_alias = AclAlias { - id: NoId, - name: "alias".to_string(), - kind: AliasKind::Destination, - destination: vec!["10.0.2.4/32".parse().unwrap()], - ports: vec![PortRange::new(22, 22).into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: destination_alias.id, - }; - obj.save(&pool).await.unwrap(); + let destination_alias = create_destination_alias( + &pool, + "alias", + vec!["10.0.2.4/32".parse().unwrap()], + vec![PortRange::new(22, 22)], + Vec::new(), + ) + .await; + attach_alias_to_rule(&pool, acl_rule.id, destination_alias.id).await; // assign rule to location - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; // manual destinations and alias destinations should produce separate rules assert_eq!(generated_firewall_rules.len(), 4); @@ -5061,77 +4862,27 @@ async fn test_gh1868_ipv6_firewall_rule_is_not_created_for_v4_only_destination_i 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(); + let location = create_location_dual_stack(&pool, None).await; // setup user & device - 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(); - - // assign network address to device - let mut conn = pool.acquire().await.unwrap(); - location - .add_device_to_network(&mut conn, &device, None) - .await - .unwrap(); + create_user_device_assigned(&pool, &location).await; // create a rule with only an IPv4 destination - let acl_rule = AclRule { - id: NoId, - name: "Web Access".into(), - all_networks: true, - expires: None, - 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()], - 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, - }; - acl_rule.save(&pool).await.unwrap(); + create_acl_rule_basic( + &pool, + "Web Access", + vec!["192.168.1.0/24".parse().unwrap()], + vec![PortRange::new(80, 80), PortRange::new(443, 443)], + vec![Protocol::Tcp.into()], + true, + false, + true, + ) + .await; // verify only IPv4 rules are created - let generated_firewall_config = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap(); - let generated_firewall_rules = generated_firewall_config.rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; assert_eq!(generated_firewall_rules.len(), 2); let allow_rule = &generated_firewall_rules[0]; @@ -5153,77 +4904,27 @@ async fn test_gh1868_ipv4_firewall_rule_is_not_created_for_v6_only_destination_i 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(); + let location = create_location_dual_stack(&pool, None).await; // setup user & device - 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(); - - // assign network address to device - let mut conn = pool.acquire().await.unwrap(); - location - .add_device_to_network(&mut conn, &device, None) - .await - .unwrap(); + create_user_device_assigned(&pool, &location).await; - // create a rule with only an IPv4 destination - let acl_rule = AclRule { - id: NoId, - name: "Web Access".into(), - all_networks: true, - expires: None, - 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()], - 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, - }; - acl_rule.save(&pool).await.unwrap(); + // create a rule with only an IPv6 destination + create_acl_rule_basic( + &pool, + "Web Access", + vec!["fc00::0/112".parse().unwrap()], + vec![PortRange::new(80, 80), PortRange::new(443, 443)], + vec![Protocol::Tcp.into()], + true, + false, + true, + ) + .await; - // verify only IPv4 rules are created - let generated_firewall_config = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap(); - let generated_firewall_rules = generated_firewall_config.rules; + // verify only IPv6 rules are created + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; assert_eq!(generated_firewall_rules.len(), 2); let allow_rule = &generated_firewall_rules[0]; @@ -5245,104 +4946,42 @@ async fn test_gh2117_ipv6_firewall_rule_is_not_created_for_v4_only_destination_i 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, - acl_default_allow: false, - 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(); + let location = create_location_dual_stack(&pool, Some(false)).await; // setup user & device - 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(); - - // assign network address to device - let mut conn = pool.acquire().await.unwrap(); - location - .add_device_to_network(&mut conn, &device, None) - .await - .unwrap(); + create_user_device_assigned(&pool, &location).await; // create ACL rule with no manual destination and attach destination alias - let acl_rule = AclRule { - id: NoId, - name: "Alias Only".into(), - all_networks: false, - expires: None, - allow_all_users: true, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), - ports: Vec::new(), - protocols: Vec::new(), - enabled: true, - parent_id: None, - state: RuleState::Applied, - }; - let acl_rule = acl_rule.save(&pool).await.unwrap(); + let acl_rule = create_acl_rule_basic( + &pool, + "Alias Only", + Vec::new(), + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; // set only IPv4 destination - let destination_alias = AclAlias { - id: NoId, - name: "internal ntp".to_string(), - kind: AliasKind::Destination, - destination: vec!["192.168.1.0/24".parse().unwrap()], - ports: vec![PortRange::new(123, 123).into()], - protocols: vec![Protocol::Udp.into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + let destination_alias = create_destination_alias( + &pool, + "internal ntp", + vec!["192.168.1.0/24".parse().unwrap()], + vec![PortRange::new(123, 123)], + vec![Protocol::Udp.into()], + ) + .await; - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: destination_alias.id, - }; - obj.save(&pool).await.unwrap(); + attach_alias_to_rule(&pool, acl_rule.id, destination_alias.id).await; // assign rule to location - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; // verify only IPv4 rules are created - let generated_firewall_config = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap(); - let generated_firewall_rules = generated_firewall_config.rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; assert_eq!(generated_firewall_rules.len(), 2); let allow_rule = &generated_firewall_rules[0]; @@ -5364,104 +5003,42 @@ async fn test_gh2117_ipv4_firewall_rule_is_not_created_for_v6_only_destination_i 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, - acl_default_allow: false, - 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(); + let location = create_location_dual_stack(&pool, Some(false)).await; // setup user & device - 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(); - - // assign network address to device - let mut conn = pool.acquire().await.unwrap(); - location - .add_device_to_network(&mut conn, &device, None) - .await - .unwrap(); + create_user_device_assigned(&pool, &location).await; // create ACL rule with no manual destination and attach destination alias - let acl_rule = AclRule { - id: NoId, - name: "Alias Only".into(), - all_networks: false, - expires: None, - allow_all_users: true, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), - ports: Vec::new(), - protocols: Vec::new(), - enabled: true, - parent_id: None, - state: RuleState::Applied, - }; - let acl_rule = acl_rule.save(&pool).await.unwrap(); + let acl_rule = create_acl_rule_basic( + &pool, + "Alias Only", + Vec::new(), + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; // set only IPv6 destination - let destination_alias = AclAlias { - id: NoId, - name: "internal ntp".to_string(), - kind: AliasKind::Destination, - destination: vec!["fc00::0/112".parse().unwrap()], - ports: vec![PortRange::new(123, 123).into()], - protocols: vec![Protocol::Udp.into()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + let destination_alias = create_destination_alias( + &pool, + "internal ntp", + vec!["fc00::0/112".parse().unwrap()], + vec![PortRange::new(123, 123)], + vec![Protocol::Udp.into()], + ) + .await; - let obj = AclRuleAlias { - id: NoId, - rule_id: acl_rule.id, - alias_id: destination_alias.id, - }; - obj.save(&pool).await.unwrap(); + attach_alias_to_rule(&pool, acl_rule.id, destination_alias.id).await; // assign rule to location - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; // verify only IPv6 rules are created - let generated_firewall_config = location - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap(); - let generated_firewall_rules = generated_firewall_config.rules; + let generated_firewall_rules = fetch_firewall_rules(&pool, &location).await; assert_eq!(generated_firewall_rules.len(), 2); let allow_rule = &generated_firewall_rules[0]; @@ -5481,152 +5058,48 @@ async fn test_gh2117_ipv4_firewall_rule_is_not_created_for_v6_only_destination_i 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![ + let location_ipv4 = create_location_ipv4(&pool, None).await; + let location_ipv6 = create_location_ipv6(&pool, None).await; + let location_ipv4_and_ipv6 = create_location_with_addresses( + &pool, + vec![ IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), ], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); + None, + ) + .await; // 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(); - } - } + seed_users_with_devices_for_locations( + &pool, + &[&location_ipv4, &location_ipv6, &location_ipv4_and_ipv6], + 2, + 2, + ) + .await; // 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(); + let acl_rule = create_acl_rule_basic( + &pool, + "test rule", + Vec::new(), + Vec::new(), + Vec::new(), + true, + false, + false, + ) + .await; // assign rule to all locations for location in [&location_ipv4, &location_ipv6, &location_ipv4_and_ipv6] { - let obj = AclRuleNetwork { - id: NoId, - rule_id: acl_rule.id, - network_id: location.id, - }; - obj.save(&pool).await.unwrap(); + attach_rule_to_location(&pool, acl_rule.id, location.id).await; } - let mut conn = pool.acquire().await.unwrap(); - // check generated rules for IPv4 only location - let generated_firewall_rules_ipv4 = location_ipv4 - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules_ipv4 = fetch_firewall_rules(&pool, &location_ipv4).await; assert_eq!(generated_firewall_rules_ipv4.len(), 2); let expected_source_addrs_ipv4 = vec![ @@ -5656,12 +5129,7 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon assert!(deny_rule_ipv4.destination_addrs.is_empty()); // check generated rules for IPv6 only location - let generated_firewall_rules_ipv6 = location_ipv6 - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules_ipv6 = fetch_firewall_rules(&pool, &location_ipv6).await; assert_eq!(generated_firewall_rules_ipv6.len(), 2); let expected_source_addrs_ipv6 = vec![ @@ -5691,12 +5159,8 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon assert!(deny_rule_ipv6.destination_addrs.is_empty()); // check generated rules for IPv4 and IPv6 location - let generated_firewall_rules_ipv4_and_ipv6 = location_ipv4_and_ipv6 - .try_get_firewall_config(&mut conn) - .await - .unwrap() - .unwrap() - .rules; + let generated_firewall_rules_ipv4_and_ipv6 = + fetch_firewall_rules(&pool, &location_ipv4_and_ipv6).await; assert_eq!(generated_firewall_rules_ipv4_and_ipv6.len(), 4); let allow_rule_ipv4 = &generated_firewall_rules_ipv4_and_ipv6[0];