Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 80 additions & 81 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ resolver = "2"

[workspace.dependencies]
# internal crates
defguard_common = { path = "./crates/defguard_common", version = "1.6.2" }
defguard_common = { path = "./crates/defguard_common", version = "1.6.3" }
defguard_core = { path = "./crates/defguard_core", version = "0.0.0" }
defguard_event_logger = { path = "./crates/defguard_event_logger", version = "0.0.0" }
defguard_event_router = { path = "./crates/defguard_event_router", version = "0.0.0" }
Expand Down
2 changes: 1 addition & 1 deletion crates/defguard_common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "defguard_common"
version = "1.6.2"
version = "1.6.3"
edition.workspace = true
license-file.workspace = true
homepage.workspace = true
Expand Down
11 changes: 8 additions & 3 deletions crates/defguard_core/src/enterprise/firewall/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,18 @@ pub async fn generate_firewall_rules_from_acls(
// at this point component aliases have been added to the manual config so they don't need to be handled separately
let has_v4_destination = !dest_addrs_v4.is_empty();
let has_v6_destination = !dest_addrs_v6.is_empty();
let has_no_manual_destination = !(has_v4_destination || has_v6_destination)
let has_no_manual_destination_address = !(has_v4_destination || has_v6_destination);
let has_no_manual_destination = has_no_manual_destination_address
&& destination_ports.is_empty()
&& protocols.is_empty();
let has_destination_aliases = !destination_aliases.is_empty();
let is_destination_alias_only_rule = has_destination_aliases && has_no_manual_destination;

if !is_destination_alias_only_rule {
let comment = format!("ACL {} - {}", acl.id, acl.name);
if location_has_ipv4_addresses && has_v4_destination {
if location_has_ipv4_addresses
&& (has_v4_destination || has_no_manual_destination_address)
{
// create IPv4 rules
let ipv4_rules = create_rules(
acl.id,
Expand All @@ -175,7 +178,9 @@ pub async fn generate_firewall_rules_from_acls(
deny_rules.push(ipv4_rules.1);
}

if location_has_ipv6_addresses && has_v6_destination {
if location_has_ipv6_addresses
&& (has_v6_destination || has_no_manual_destination_address)
{
// create IPv6 rules
let ipv6_rules = create_rules(
acl.id,
Expand Down
247 changes: 247 additions & 0 deletions crates/defguard_core/src/enterprise/firewall/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4179,3 +4179,250 @@ async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination(
assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny));
assert_eq!(allow_rule.ip_version, i32::from(IpVersion::Ipv6));
}

#[sqlx::test]
async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgConnectOptions) {
let pool = setup_pool(options).await;

let mut rng = thread_rng();

// Create test locations with IPv4 and IPv6 addresses
let location_ipv4 = WireguardNetwork {
id: NoId,
acl_enabled: true,
address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()],
..Default::default()
}
.save(&pool)
.await
.unwrap();
let location_ipv6 = WireguardNetwork {
id: NoId,
acl_enabled: true,
address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()],
..Default::default()
}
.save(&pool)
.await
.unwrap();
let location_ipv4_and_ipv6 = WireguardNetwork {
id: NoId,
acl_enabled: true,
address: vec![
IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(),
IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(),
],
..Default::default()
}
.save(&pool)
.await
.unwrap();

// Setup some test users and their devices
let user_1: User<NoId> = rng.r#gen();
let user_1 = user_1.save(&pool).await.unwrap();
let user_2: User<NoId> = rng.r#gen();
let user_2 = user_2.save(&pool).await.unwrap();

for user in [&user_1, &user_2] {
// Create 2 devices per user
for device_num in 1..3 {
let device = Device {
id: NoId,
name: format!("device-{}-{device_num}", user.id),
user_id: user.id,
device_type: DeviceType::User,
description: None,
wireguard_pubkey: Default::default(),
created: Default::default(),
configured: true,
};
let device = device.save(&pool).await.unwrap();

// Add device to all locations' VPN networks
let network_device = WireguardNetworkDevice {
device_id: device.id,
wireguard_network_id: location_ipv4.id,
wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new(
10,
0,
user.id as u8,
device_num as u8,
))],
preshared_key: None,
is_authorized: true,
authorized_at: None,
};
network_device.insert(&pool).await.unwrap();
let network_device = WireguardNetworkDevice {
device_id: device.id,
wireguard_network_id: location_ipv6.id,
wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new(
0xff00,
0,
0,
0,
0,
0,
user.id as u16,
device_num as u16,
))],
preshared_key: None,
is_authorized: true,
authorized_at: None,
};
network_device.insert(&pool).await.unwrap();
let network_device = WireguardNetworkDevice {
device_id: device.id,
wireguard_network_id: location_ipv4_and_ipv6.id,
wireguard_ips: vec![
IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)),
IpAddr::V6(Ipv6Addr::new(
0xff00,
0,
0,
0,
0,
0,
user.id as u16,
device_num as u16,
)),
],
preshared_key: None,
is_authorized: true,
authorized_at: None,
};
network_device.insert(&pool).await.unwrap();
}
}

// create ACL rule without manually configured destination and no aliases
let acl_rule = AclRule {
id: NoId,
name: "test rule".to_string(),
expires: None,
enabled: true,
state: RuleState::Applied,
destination: Vec::new(),
allow_all_users: true,
..Default::default()
}
.save(&pool)
.await
.unwrap();

// assign rule to all locations
for location in [&location_ipv4, &location_ipv6, &location_ipv4_and_ipv6] {
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();

// 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 expected_source_addrs_ipv4 = vec![
IpAddress {
address: Some(Address::IpRange(IpRange {
start: "10.0.1.1".to_string(),
end: "10.0.1.2".to_string(),
})),
},
IpAddress {
address: Some(Address::IpRange(IpRange {
start: "10.0.2.1".to_string(),
end: "10.0.2.2".to_string(),
})),
},
];
let allow_rule_ipv4 = &generated_firewall_rules_ipv4[0];
assert_eq!(allow_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4));
assert_eq!(allow_rule_ipv4.verdict, i32::from(FirewallPolicy::Allow));
assert_eq!(allow_rule_ipv4.source_addrs, expected_source_addrs_ipv4);
assert!(allow_rule_ipv4.destination_addrs.is_empty());

let deny_rule_ipv4 = &generated_firewall_rules_ipv4[1];
assert_eq!(deny_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4));
assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny));
assert!(deny_rule_ipv4.source_addrs.is_empty());
assert!(deny_rule_ipv4.destination_addrs.is_empty());

// check generated rules for IPv6 only location
let generated_firewall_rules_ipv6 = location_ipv6
.try_get_firewall_config(&mut conn)
.await
.unwrap()
.unwrap()
.rules;

assert_eq!(generated_firewall_rules_ipv6.len(), 2);
let expected_source_addrs_ipv6 = vec![
IpAddress {
address: Some(Address::IpRange(IpRange {
start: "ff00::1:1".to_string(),
end: "ff00::1:2".to_string(),
})),
},
IpAddress {
address: Some(Address::IpRange(IpRange {
start: "ff00::2:1".to_string(),
end: "ff00::2:2".to_string(),
})),
},
];
let allow_rule_ipv6 = &generated_firewall_rules_ipv6[0];
assert_eq!(allow_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6));
assert_eq!(allow_rule_ipv6.verdict, i32::from(FirewallPolicy::Allow));
assert_eq!(allow_rule_ipv6.source_addrs, expected_source_addrs_ipv6);
assert!(allow_rule_ipv6.destination_addrs.is_empty());

let deny_rule_ipv6 = &generated_firewall_rules_ipv6[1];
assert_eq!(deny_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6));
assert_eq!(deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny));
assert!(deny_rule_ipv6.source_addrs.is_empty());
assert!(deny_rule_ipv6.destination_addrs.is_empty());

// check generated rules for IPv4 and IPv6 location
let generated_firewall_rules_ipv4_and_ipv6 = 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());
}
34 changes: 17 additions & 17 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,32 @@
"keywords": [],
"author": "",
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.36.0",
"@eslint/compat": "^1.4.1",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@prettier/plugin-oxc": "^0.0.4",
"@types/node": "^22.18.6",
"@types/node": "^22.19.8",
"@types/totp-generator": "^0.0.8",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
"eslint": "^9.36.0",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
"prettier": "^3.6.2"
"prettier": "^3.8.1"
},
"dependencies": {
"@faker-js/faker": "^9.9.0",
"@playwright/test": "^1.55.1",
"@playwright/test": "^1.58.1",
"@scure/base": "^1.2.6",
"@types/lodash": "^4.17.20",
"@types/pg": "^8.15.5",
"axios": "^1.12.2",
"dotenv": "^17.2.2",
"lodash": "^4.17.21",
"pg": "^8.16.3",
"playwright": "^1.55.1",
"@types/lodash": "^4.17.23",
"@types/pg": "^8.16.0",
"axios": "^1.13.4",
"dotenv": "^17.2.3",
"lodash": "^4.17.23",
"pg": "^8.18.0",
"playwright": "^1.58.1",
"totp-generator": "^1.0.0"
},
"volta": {
Expand Down
Loading