From 1237e53789e9fdbaf80a303822c3d457cb52334a Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Fri, 27 Feb 2026 15:28:24 +0100
Subject: [PATCH 01/18] remove block, add info to edit location page
---
crates/defguard_core/src/handlers/wireguard.rs | 11 -----------
web/messages/en/location.json | 2 +-
web/src/pages/EditLocationPage/EditLocationPage.tsx | 3 +--
3 files changed, 2 insertions(+), 14 deletions(-)
diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs
index 3a8adda260..fa6d02b9a6 100644
--- a/crates/defguard_core/src/handlers/wireguard.rs
+++ b/crates/defguard_core/src/handlers/wireguard.rs
@@ -328,17 +328,6 @@ pub(crate) async fn modify_network(
let before = network.clone();
let new_addresses = data.parse_addresses()?;
- // Block network address changes if any device is assigned to the network
- if before.address != new_addresses
- && WireguardNetworkDevice::has_devices_in_network(&appstate.pool, network_id).await?
- {
- return Err(WebError::BadRequest(
- "Cannot change network address while devices are assigned to this network. \
- Remove all devices first."
- .into(),
- ));
- }
-
network.address = new_addresses;
network.allowed_ips = data.parse_allowed_ips();
network.name = data.name;
diff --git a/web/messages/en/location.json b/web/messages/en/location.json
index 7b7bd2fd01..22331a3644 100644
--- a/web/messages/en/location.json
+++ b/web/messages/en/location.json
@@ -3,5 +3,5 @@
"location_delete_success": "Location deleted",
"location_delete_failed": "Failed to delete location",
"location_edit_failed": "Failed to update location",
- "location_edit_failed_has_devices": "Gateway VPN IP address and netmask can’t be changed while devices are active on this network. To proceed, remove all devices from the current network first."
+ "location_edit_addresses_rewrite_warning": "Changing the Gateway VPN IP address or netmask will automatically reassign any device addresses that fall outside the new network range to a randomly selected available IP address."
}
diff --git a/web/src/pages/EditLocationPage/EditLocationPage.tsx b/web/src/pages/EditLocationPage/EditLocationPage.tsx
index e1f6440da4..5640139a9b 100644
--- a/web/src/pages/EditLocationPage/EditLocationPage.tsx
+++ b/web/src/pages/EditLocationPage/EditLocationPage.tsx
@@ -214,7 +214,7 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => {
>
@@ -223,7 +223,6 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => {
{(field) => (
)}
From 26cc321c49430ec9061df3cc082b269ea34c7975 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 10:08:17 +0100
Subject: [PATCH 02/18] move sql query before invocation
---
.../defguard_common/src/db/models/device.rs | 37 ++++++++++++++-----
.../src/db/models/wireguard.rs | 4 +-
crates/defguard_core/src/lib.rs | 2 +-
.../src/location_management/mod.rs | 14 ++++++-
4 files changed, 43 insertions(+), 14 deletions(-)
diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs
index e1a1430326..acd78ee3d1 100644
--- a/crates/defguard_common/src/db/models/device.rs
+++ b/crates/defguard_common/src/db/models/device.rs
@@ -1,4 +1,4 @@
-use std::{fmt, net::IpAddr};
+use std::{collections::HashSet, fmt, net::IpAddr};
use base64::{Engine, prelude::BASE64_STANDARD};
use chrono::{NaiveDate, NaiveDateTime, Timelike, Utc};
@@ -845,6 +845,7 @@ impl Device {
network: &WireguardNetwork,
reserved_ips: Option<&[IpAddr]>,
current_ips: Option<&[IpAddr]>,
+ used_ips: Option<&HashSet>,
) -> Result {
debug!(
"Assiging IP addresses for device: {} in network {}",
@@ -853,6 +854,19 @@ impl Device {
let mut ips = Vec::new();
let reserved = reserved_ips.unwrap_or_default();
+ let fetched;
+ let used_ips: &HashSet = match used_ips {
+ Some(set) => set,
+ None => {
+ fetched = WireguardNetworkDevice::all_for_network(&mut *transaction, network.id)
+ .await?
+ .into_iter()
+ .flat_map(|device| device.wireguard_ips)
+ .collect::>();
+ &fetched
+ }
+ };
+
// Iterate over all network addresses and assign new IP for the device in each of them
for address in &network.address {
debug!(
@@ -872,15 +886,20 @@ impl Device {
}
let mut picked = None;
for ip in address {
- if network
- .can_assign_ips(transaction, &[ip], Some(self.id))
- .await
- .is_ok()
- && !reserved.contains(&ip)
- {
- picked = Some(ip);
- break;
+ if ip == address.network() || ip == address.broadcast() || ip == address.ip() {
+ continue;
+ }
+
+ if used_ips.contains(&ip) || reserved.contains(&ip) {
+ continue;
+ }
+
+ if reserved.contains(&ip) {
+ continue;
}
+
+ picked = Some(ip);
+ break;
}
// Return error if no address can be assigned
diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs
index 4ca4998c5d..9314f2892e 100644
--- a/crates/defguard_common/src/db/models/wireguard.rs
+++ b/crates/defguard_common/src/db/models/wireguard.rs
@@ -441,7 +441,7 @@ impl WireguardNetwork {
let devices = self.get_allowed_devices(&mut *transaction).await?;
for device in devices {
device
- .assign_next_network_ip(&mut *transaction, self, None, None)
+ .assign_next_network_ip(&mut *transaction, self, None, None, None)
.await?;
}
Ok(())
@@ -459,7 +459,7 @@ impl WireguardNetwork {
let allowed_device_ids: Vec = allowed_devices.iter().map(|dev| dev.id).collect();
if allowed_device_ids.contains(&device.id) {
let wireguard_network_device = device
- .assign_next_network_ip(&mut *transaction, self, reserved_ips, None)
+ .assign_next_network_ip(&mut *transaction, self, reserved_ips, None, None)
.await?;
Ok(wireguard_network_device)
} else {
diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs
index bc6ae24ca6..ab5f120132 100644
--- a/crates/defguard_core/src/lib.rs
+++ b/crates/defguard_core/src/lib.rs
@@ -762,7 +762,7 @@ pub async fn init_dev_env(config: &DefGuardConfig) {
.await
.expect("Could not save device");
device
- .assign_next_network_ip(&mut transaction, &network, None, None)
+ .assign_next_network_ip(&mut transaction, &network, None, None, None)
.await
.expect("Could not assign IP to device");
}
diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs
index 400be93f6c..df15fb52d1 100644
--- a/crates/defguard_core/src/location_management/mod.rs
+++ b/crates/defguard_core/src/location_management/mod.rs
@@ -1,4 +1,7 @@
-use std::{collections::HashMap, net::IpAddr};
+use std::{
+ collections::{HashMap, HashSet},
+ net::IpAddr,
+};
use defguard_common::{
csv::AsCsv,
@@ -165,6 +168,12 @@ pub async fn process_device_access_changes(
// Loop through current device configurations; remove no longer allowed, readdress
// when necessary; remove processed entry from all devices list initial list should
// now contain only devices to be added.
+ let all_devices =
+ WireguardNetworkDevice::all_for_network(&mut *transaction, location.id).await?;
+ let used_ips: HashSet = all_devices
+ .into_iter()
+ .flat_map(|device| device.wireguard_ips)
+ .collect();
let mut events: Vec = Vec::new();
for device_network_config in currently_configured_devices {
// Device is allowed and an IP was already assigned
@@ -179,6 +188,7 @@ pub async fn process_device_access_changes(
location,
reserved_ips,
Some(&device_network_config.wireguard_ips),
+ Some(&used_ips),
)
.await?;
events.push(GatewayEvent::DeviceModified(DeviceInfo {
@@ -221,7 +231,7 @@ pub async fn process_device_access_changes(
// Add configs for new allowed devices
for device in allowed_devices.into_values() {
let wireguard_network_device = device
- .assign_next_network_ip(&mut *transaction, location, reserved_ips, None)
+ .assign_next_network_ip(&mut *transaction, location, reserved_ips, None, None)
.await?;
events.push(GatewayEvent::DeviceCreated(DeviceInfo {
device,
From 221e5ad98c08d2b89d13b499e3cf90a7101c6a29 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 10:32:40 +0100
Subject: [PATCH 03/18] create used_ips before
---
.../defguard_common/src/db/models/device.rs | 15 +-------------
.../src/db/models/wireguard.rs | 20 ++++++++++++++++---
crates/defguard_core/src/lib.rs | 12 +++++++++--
.../src/location_management/mod.rs | 11 +++++++---
4 files changed, 36 insertions(+), 22 deletions(-)
diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs
index acd78ee3d1..d4f1362b7e 100644
--- a/crates/defguard_common/src/db/models/device.rs
+++ b/crates/defguard_common/src/db/models/device.rs
@@ -843,9 +843,9 @@ impl Device {
&self,
transaction: &mut PgConnection,
network: &WireguardNetwork,
+ used_ips: &HashSet,
reserved_ips: Option<&[IpAddr]>,
current_ips: Option<&[IpAddr]>,
- used_ips: Option<&HashSet>,
) -> Result {
debug!(
"Assiging IP addresses for device: {} in network {}",
@@ -854,19 +854,6 @@ impl Device {
let mut ips = Vec::new();
let reserved = reserved_ips.unwrap_or_default();
- let fetched;
- let used_ips: &HashSet = match used_ips {
- Some(set) => set,
- None => {
- fetched = WireguardNetworkDevice::all_for_network(&mut *transaction, network.id)
- .await?
- .into_iter()
- .flat_map(|device| device.wireguard_ips)
- .collect::>();
- &fetched
- }
- };
-
// Iterate over all network addresses and assign new IP for the device in each of them
for address in &network.address {
debug!(
diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs
index 9314f2892e..81cbd9c87a 100644
--- a/crates/defguard_common/src/db/models/wireguard.rs
+++ b/crates/defguard_common/src/db/models/wireguard.rs
@@ -1,5 +1,5 @@
use std::{
- collections::HashMap,
+ collections::{HashMap, HashSet},
fmt::{self, Display},
iter::zip,
net::{IpAddr, Ipv4Addr},
@@ -438,10 +438,16 @@ impl WireguardNetwork {
"Assigning IPs in network {} for all existing devices ",
self
);
+ let all_devices =
+ WireguardNetworkDevice::all_for_network(&mut *transaction, self.id).await?;
+ let used_ips: HashSet = all_devices
+ .into_iter()
+ .flat_map(|device| device.wireguard_ips)
+ .collect();
let devices = self.get_allowed_devices(&mut *transaction).await?;
for device in devices {
device
- .assign_next_network_ip(&mut *transaction, self, None, None, None)
+ .assign_next_network_ip(&mut *transaction, self, &used_ips, None, None)
.await?;
}
Ok(())
@@ -457,9 +463,17 @@ impl WireguardNetwork {
info!("Assigning IP in network {self} for {device}");
let allowed_devices = self.get_allowed_devices(&mut *transaction).await?;
let allowed_device_ids: Vec = allowed_devices.iter().map(|dev| dev.id).collect();
+
+ let all_devices =
+ WireguardNetworkDevice::all_for_network(&mut *transaction, self.id).await?;
+ let used_ips: HashSet = all_devices
+ .into_iter()
+ .flat_map(|device| device.wireguard_ips)
+ .collect();
+
if allowed_device_ids.contains(&device.id) {
let wireguard_network_device = device
- .assign_next_network_ip(&mut *transaction, self, reserved_ips, None, None)
+ .assign_next_network_ip(&mut *transaction, self, &used_ips, reserved_ips, None)
.await?;
Ok(wireguard_network_device)
} else {
diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs
index ab5f120132..580dfee74c 100644
--- a/crates/defguard_core/src/lib.rs
+++ b/crates/defguard_core/src/lib.rs
@@ -1,5 +1,6 @@
#![allow(clippy::too_many_arguments)]
use std::{
+ collections::HashSet,
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::{Arc, LazyLock, Mutex, RwLock},
};
@@ -20,6 +21,7 @@ use defguard_common::{
init_db,
models::{
Device, DeviceType, Settings, User, WireguardNetwork,
+ device::WireguardNetworkDevice,
oauth2client::OAuth2Client,
settings::{initialize_current_settings, update_current_settings},
wireguard::{
@@ -738,7 +740,13 @@ pub async fn init_dev_env(config: &DefGuardConfig) {
.await
.expect("Could not save network")
};
-
+ let all_devices = WireguardNetworkDevice::all_for_network(&mut *transaction, network.id)
+ .await
+ .expect("Failed to query all devices");
+ let used_ips: HashSet = all_devices
+ .into_iter()
+ .flat_map(|device| device.wireguard_ips)
+ .collect();
if Device::find_by_pubkey(
&mut *transaction,
"gQYL5eMeFDj0R+lpC7oZyIl0/sNVmQDC6ckP7husZjc=",
@@ -762,7 +770,7 @@ pub async fn init_dev_env(config: &DefGuardConfig) {
.await
.expect("Could not save device");
device
- .assign_next_network_ip(&mut transaction, &network, None, None, None)
+ .assign_next_network_ip(&mut transaction, &network, &used_ips, None, None)
.await
.expect("Could not assign IP to device");
}
diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs
index df15fb52d1..298c29773e 100644
--- a/crates/defguard_core/src/location_management/mod.rs
+++ b/crates/defguard_core/src/location_management/mod.rs
@@ -186,9 +186,9 @@ pub async fn process_device_access_changes(
.assign_next_network_ip(
&mut *transaction,
location,
+ &used_ips,
reserved_ips,
Some(&device_network_config.wireguard_ips),
- Some(&used_ips),
)
.await?;
events.push(GatewayEvent::DeviceModified(DeviceInfo {
@@ -227,11 +227,16 @@ pub async fn process_device_access_changes(
}
}
}
-
+ let all_devices =
+ WireguardNetworkDevice::all_for_network(&mut *transaction, location.id).await?;
+ let used_ips: HashSet = all_devices
+ .into_iter()
+ .flat_map(|device| device.wireguard_ips)
+ .collect();
// Add configs for new allowed devices
for device in allowed_devices.into_values() {
let wireguard_network_device = device
- .assign_next_network_ip(&mut *transaction, location, reserved_ips, None, None)
+ .assign_next_network_ip(&mut *transaction, location, &used_ips, reserved_ips, None)
.await?;
events.push(GatewayEvent::DeviceCreated(DeviceInfo {
device,
From 4c1b249a4ce6a91e8e280f39bd8497b6785125f2 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 13:40:13 +0100
Subject: [PATCH 04/18] add helper function
---
.../src/db/models/wireguard.rs | 29 +++--
crates/defguard_core/src/lib.rs | 9 +-
.../src/location_management/mod.rs | 112 ++++++++++++++++--
.../src/vpn_session_stats.rs | 3 +-
4 files changed, 121 insertions(+), 32 deletions(-)
diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs
index 81cbd9c87a..aed2bb593d 100644
--- a/crates/defguard_common/src/db/models/wireguard.rs
+++ b/crates/defguard_common/src/db/models/wireguard.rs
@@ -438,12 +438,7 @@ impl WireguardNetwork {
"Assigning IPs in network {} for all existing devices ",
self
);
- let all_devices =
- WireguardNetworkDevice::all_for_network(&mut *transaction, self.id).await?;
- let used_ips: HashSet = all_devices
- .into_iter()
- .flat_map(|device| device.wireguard_ips)
- .collect();
+ let used_ips = self.all_used_ips_for_network(&mut *transaction).await?;
let devices = self.get_allowed_devices(&mut *transaction).await?;
for device in devices {
device
@@ -463,13 +458,7 @@ impl WireguardNetwork {
info!("Assigning IP in network {self} for {device}");
let allowed_devices = self.get_allowed_devices(&mut *transaction).await?;
let allowed_device_ids: Vec = allowed_devices.iter().map(|dev| dev.id).collect();
-
- let all_devices =
- WireguardNetworkDevice::all_for_network(&mut *transaction, self.id).await?;
- let used_ips: HashSet = all_devices
- .into_iter()
- .flat_map(|device| device.wireguard_ips)
- .collect();
+ let used_ips = self.all_used_ips_for_network(&mut *transaction).await?;
if allowed_device_ids.contains(&device.id) {
let wireguard_network_device = device
@@ -1368,6 +1357,20 @@ impl WireguardNetwork {
.fetch_all(executor)
.await
}
+
+ /// Obtain all used ips for network
+ pub async fn all_used_ips_for_network(
+ &self,
+ transaction: &mut PgConnection,
+ ) -> Result, SqlxError> {
+ let all_devices =
+ WireguardNetworkDevice::all_for_network(&mut *transaction, self.id).await?;
+ let used_ips: HashSet = all_devices
+ .into_iter()
+ .flat_map(|device| device.wireguard_ips)
+ .collect();
+ Ok(used_ips)
+ }
}
// [`IpNetwork`] does not implement [`Default`]
diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs
index 580dfee74c..3ab970166b 100644
--- a/crates/defguard_core/src/lib.rs
+++ b/crates/defguard_core/src/lib.rs
@@ -740,13 +740,10 @@ pub async fn init_dev_env(config: &DefGuardConfig) {
.await
.expect("Could not save network")
};
- let all_devices = WireguardNetworkDevice::all_for_network(&mut *transaction, network.id)
+ let used_ips = network
+ .all_used_ips_for_network(&mut *transaction)
.await
- .expect("Failed to query all devices");
- let used_ips: HashSet = all_devices
- .into_iter()
- .flat_map(|device| device.wireguard_ips)
- .collect();
+ .expect("Failed to query used ip's from database");
if Device::find_by_pubkey(
&mut *transaction,
"gQYL5eMeFDj0R+lpC7oZyIl0/sNVmQDC6ckP7husZjc=",
diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs
index 298c29773e..0c64b88ace 100644
--- a/crates/defguard_core/src/location_management/mod.rs
+++ b/crates/defguard_core/src/location_management/mod.rs
@@ -168,12 +168,7 @@ pub async fn process_device_access_changes(
// Loop through current device configurations; remove no longer allowed, readdress
// when necessary; remove processed entry from all devices list initial list should
// now contain only devices to be added.
- let all_devices =
- WireguardNetworkDevice::all_for_network(&mut *transaction, location.id).await?;
- let used_ips: HashSet = all_devices
- .into_iter()
- .flat_map(|device| device.wireguard_ips)
- .collect();
+ let used_ips = location.all_used_ips_for_network(&mut *transaction).await?;
let mut events: Vec = Vec::new();
for device_network_config in currently_configured_devices {
// Device is allowed and an IP was already assigned
@@ -227,12 +222,6 @@ pub async fn process_device_access_changes(
}
}
}
- let all_devices =
- WireguardNetworkDevice::all_for_network(&mut *transaction, location.id).await?;
- let used_ips: HashSet = all_devices
- .into_iter()
- .flat_map(|device| device.wireguard_ips)
- .collect();
// Add configs for new allowed devices
for device in allowed_devices.into_values() {
let wireguard_network_device = device
@@ -671,4 +660,103 @@ mod test {
transaction.commit().await.unwrap();
}
+
+ #[sqlx::test]
+ async fn test_readdress_on_network_change(_: PgPoolOptions, options: PgConnectOptions) {
+ use std::net::IpAddr;
+
+ let pool = setup_pool(options).await;
+
+ // Sieć z trzema podsieciami
+ let mut network = WireguardNetwork::default();
+ network
+ .try_set_address("10.0.0.1/8,12.0.0.1/8,14.0.0.1/8")
+ .unwrap();
+ let network = network.save(&pool).await.unwrap();
+
+ let user = User::new(
+ "testuser",
+ Some("pass"),
+ "Test",
+ "User",
+ "test@test.com",
+ None,
+ )
+ .save(&pool)
+ .await
+ .unwrap();
+
+ let device = Device::new(
+ "device1".into(),
+ "key1".into(),
+ user.id,
+ DeviceType::User,
+ None,
+ true,
+ )
+ .save(&pool)
+ .await
+ .unwrap();
+
+ // Ręczne przypisanie konkretnych IP do urządzenia
+ let ip_10: IpAddr = "10.0.0.10".parse().unwrap();
+ let ip_12: IpAddr = "12.0.0.12".parse().unwrap();
+ let ip_14: IpAddr = "14.0.0.15".parse().unwrap();
+
+ WireguardNetworkDevice::new(network.id, device.id, vec![ip_10, ip_12, ip_14])
+ .insert(&pool)
+ .await
+ .unwrap();
+
+ // Zmiana sieci: 12.x -> 15.x, maska 14.x zmieniona z /8 na /16
+ let mut network = network;
+ network.address = "10.0.0.1/8,15.0.0.1/8,14.0.0.1/16"
+ .split(',')
+ .map(|s| s.trim().parse::().unwrap())
+ .collect();
+
+ let mut transaction = pool.begin().await.unwrap();
+
+ let events = sync_location_allowed_devices(&network, &mut transaction, None)
+ .await
+ .unwrap();
+
+ transaction.commit().await.unwrap();
+
+ // Szukamy eventu modyfikacji dla naszego urządzenia
+ let new_ips = events
+ .iter()
+ .find_map(|e| match e {
+ GatewayEvent::DeviceModified(info) if info.device.id == device.id => {
+ Some(&info.network_info[0].device_wireguard_ips)
+ }
+ _ => None,
+ })
+ .expect("Oczekiwano zdarzenia DeviceModified dla urządzenia");
+
+ // 10.0.0.10 nadal pasuje do 10.0.0.0/8 - powinien pozostać
+ assert!(
+ new_ips.contains(&ip_10),
+ "10.0.0.10 powinno pozostać: {new_ips:?}"
+ );
+
+ // 12.0.0.12 nie pasuje do żadnej nowej podsieci - powinno zostać zastąpione przez 15.x.x.x
+ assert!(
+ !new_ips.contains(&ip_12),
+ "12.0.0.12 powinno zostać przepisane: {new_ips:?}"
+ );
+ assert!(
+ new_ips.iter().any(|ip| match ip {
+ IpAddr::V4(v4) => v4.octets()[0] == 15,
+ _ => false,
+ }),
+ "Oczekiwano adresu z zakresu 15.x.x.x: {new_ips:?}"
+ );
+
+ // 14.0.0.15 nadal pasuje do 14.0.0.0/16 - powinien pozostać
+ assert!(
+ new_ips.contains(&ip_14),
+ "14.0.0.15 powinno pozostać: {new_ips:?}"
+ );
+ }
}
diff --git a/tools/defguard_generator/src/vpn_session_stats.rs b/tools/defguard_generator/src/vpn_session_stats.rs
index a134f6b040..f9ded71e0c 100644
--- a/tools/defguard_generator/src/vpn_session_stats.rs
+++ b/tools/defguard_generator/src/vpn_session_stats.rs
@@ -70,6 +70,7 @@ pub async fn generate_vpn_session_stats(
let devices =
prepare_user_devices(&pool, &mut rng, &user, config.devices_per_user as usize).await?;
+ let used_ips = location.all_used_ips_for_network(&mut *transaction).await?;
// assign devices to the network if not already assigned
for device in &devices {
if WireguardNetworkDevice::find(&mut *transaction, device.id, location.id)
@@ -81,7 +82,7 @@ pub async fn generate_vpn_session_stats(
device.name, location.name
);
device
- .assign_next_network_ip(&mut transaction, &location, None, None)
+ .assign_next_network_ip(&mut transaction, &location, &used_ips, None, None)
.await?;
} else {
info!(
From 5800ac62aae21b97b0c07aab3584f5d8e00710e2 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 13:43:08 +0100
Subject: [PATCH 05/18] remove spacing between networks
---
web/src/pages/EditLocationPage/EditLocationPage.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/src/pages/EditLocationPage/EditLocationPage.tsx b/web/src/pages/EditLocationPage/EditLocationPage.tsx
index 5bccbf85dd..b81157f34d 100644
--- a/web/src/pages/EditLocationPage/EditLocationPage.tsx
+++ b/web/src/pages/EditLocationPage/EditLocationPage.tsx
@@ -190,9 +190,9 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => {
const defaultValues = useMemo(
(): FormFields => ({
name: location.name,
- address: location.address.join(', '),
+ address: location.address.join(','),
allowed_groups: location.allowed_groups,
- allowed_ips: location.allowed_ips.join(', '),
+ allowed_ips: location.allowed_ips.join(','),
dns: location.dns,
endpoint: location.endpoint,
keepalive_interval: location.keepalive_interval,
From 3b143ebf8c0526d28500353159a0ac0522c72fe7 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 18:19:43 +0100
Subject: [PATCH 06/18] ip reassignment test
---
.../defguard_common/src/db/models/device.rs | 259 ++++++++++++++++++
1 file changed, 259 insertions(+)
diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs
index d4f1362b7e..8d6134e85b 100644
--- a/crates/defguard_common/src/db/models/device.rs
+++ b/crates/defguard_common/src/db/models/device.rs
@@ -1135,6 +1135,265 @@ mod test {
assert!(device.is_err());
}
+ /// Test that assign_next_network_ip correctly preserves or reassigns device IPs
+ /// when a network's address list changes.
+ /// Initial network: 10.0.0.0/8, 123.10.0.0/16, 123.123.123.0/24
+ /// Device IPs: 10.0.0.234, 123.10.33.44, 123.123.123.52
+ /// New network: 10.0.0.0/16, 123.12.0.0/16, 123.123.0.0/16
+ /// Expected:
+ /// - 10.0.0.234 KEPT (still within 10.0.0.0/16)
+ /// - 123.10.33.44 CHANGED (not within 123.12.0.0/16)
+ /// - 123.123.123.52 KEPT (still within 123.123.0.0/16)
+ #[sqlx::test]
+ async fn test_assign_next_network_ip_preserves_matching_subnets(
+ _: PgPoolOptions,
+ options: PgConnectOptions,
+ ) {
+ let pool = setup_pool(options).await;
+
+ let mut network = WireguardNetwork::default();
+ network
+ .try_set_address("10.0.0.1/8,123.10.0.1/16,123.123.123.1/24")
+ .unwrap();
+ let network = network.save(&pool).await.unwrap();
+
+ let user = User::new(
+ "testuser",
+ Some("password"),
+ "Tester",
+ "Test",
+ "test@test.com",
+ None,
+ )
+ .save(&pool)
+ .await
+ .unwrap();
+
+ let device = Device::new(
+ "dev1".into(),
+ "key1".into(),
+ user.id,
+ DeviceType::User,
+ None,
+ true,
+ )
+ .save(&pool)
+ .await
+ .unwrap();
+
+ let ip = IpAddr::from_str("10.0.0.234").unwrap();
+ let ip2 = IpAddr::from_str("123.10.33.44").unwrap();
+ let ip3 = IpAddr::from_str("123.123.123.52").unwrap();
+ let initial_ips = vec![ip, ip2, ip3];
+
+ let mut conn = pool.acquire().await.unwrap();
+ WireguardNetworkDevice::new(network.id, device.id, initial_ips.clone())
+ .insert(&mut *conn)
+ .await
+ .unwrap();
+
+ let mut updated_network = network.clone();
+ updated_network.address = vec![
+ "10.0.0.0/16".parse::().unwrap(),
+ "123.12.0.0/16".parse::().unwrap(),
+ "123.123.0.0/16".parse::().unwrap(),
+ ];
+ updated_network.save(&mut *conn).await.unwrap();
+
+ let used_ips = updated_network
+ .all_used_ips_for_network(&mut *conn)
+ .await
+ .unwrap();
+
+ let result = device
+ .assign_next_network_ip(
+ &mut *conn,
+ &updated_network,
+ &used_ips,
+ None,
+ Some(&initial_ips),
+ )
+ .await
+ .unwrap();
+
+ let new_ips = &result.wireguard_ips;
+ assert_eq!(new_ips.len(), 3, "should have one IP per subnet");
+
+ assert!(
+ new_ips.contains(&ip),
+ "10.0.0.234 should be kept – it is still within 10.0.0.0/16; got {new_ips:?}"
+ );
+
+ assert!(
+ !new_ips.contains(&ip2),
+ "123.10.33.44 should be reassigned – not within 123.12.0.0/16; got {new_ips:?}"
+ );
+ let network: IpNetwork = "123.12.0.0/16".parse().unwrap();
+ assert!(
+ new_ips.iter().any(|ip| network.contains(*ip)),
+ "a new IP within 123.12.0.0/16 should be assigned; got {new_ips:?}"
+ );
+
+ assert!(
+ new_ips.contains(&ip3),
+ "123.123.123.52 should be kept – it is still within 123.123.0.0/16; got {new_ips:?}"
+ );
+ }
+ /// Initial: 10.0.0.0/8 | 10.1.0.5
+ /// Modified: 10.0.0.0/16 | 10.1.0.5 should be replaced with a 10.0.x.x address
+ #[sqlx::test]
+ async fn test_assign_next_network_ip_subnet_narrowed(
+ _: PgPoolOptions,
+ options: PgConnectOptions,
+ ) {
+ let pool = setup_pool(options).await;
+
+ let mut network = WireguardNetwork::default();
+ network.try_set_address("10.0.0.1/8").unwrap();
+ let network = network.save(&pool).await.unwrap();
+
+ let user = User::new(
+ "testuser",
+ Some("password"),
+ "Tester",
+ "Test",
+ "test@test.com",
+ None,
+ )
+ .save(&pool)
+ .await
+ .unwrap();
+
+ let device = Device::new(
+ "dev1".into(),
+ "key1".into(),
+ user.id,
+ DeviceType::User,
+ None,
+ true,
+ )
+ .save(&pool)
+ .await
+ .unwrap();
+
+ let ip = IpAddr::from_str("10.1.0.5").unwrap();
+ let initial_ips = vec![ip];
+
+ let mut conn = pool.acquire().await.unwrap();
+ WireguardNetworkDevice::new(network.id, device.id, initial_ips.clone())
+ .insert(&mut *conn)
+ .await
+ .unwrap();
+
+ let mut updated_network = network.clone();
+ updated_network.address = vec!["10.0.0.0/16".parse::().unwrap()];
+ updated_network.save(&mut *conn).await.unwrap();
+
+ let used_ips = updated_network
+ .all_used_ips_for_network(&mut *conn)
+ .await
+ .unwrap();
+
+ let result = device
+ .assign_next_network_ip(
+ &mut *conn,
+ &updated_network,
+ &used_ips,
+ None,
+ Some(&initial_ips),
+ )
+ .await
+ .unwrap();
+
+ let new_ips = &result.wireguard_ips;
+ assert_eq!(new_ips.len(), 1, "should have one IP per subnet");
+
+ assert!(
+ !new_ips.contains(&ip),
+ "10.1.0.5 should be reassigned – outside narrowed 10.0.0.0/16; got {new_ips:?}"
+ );
+ let narrowed_net: IpNetwork = "10.0.0.0/16".parse().unwrap();
+ assert!(
+ new_ips.iter().all(|ip| narrowed_net.contains(*ip)),
+ "new IP must be within 10.0.0.0/16; got {new_ips:?}"
+ );
+ }
+
+ /// Initial: 123.123.123.0/24 | 123.123.123.254
+ /// Modified: 123.123.0.0/16 | 123.123.123.254 still fits
+ #[sqlx::test]
+ async fn test_assign_next_network_ip_still_valid_after_widening(
+ _: PgPoolOptions,
+ options: PgConnectOptions,
+ ) {
+ let pool = setup_pool(options).await;
+
+ let mut network = WireguardNetwork::default();
+ network.try_set_address("123.123.123.1/24").unwrap();
+ let network = network.save(&pool).await.unwrap();
+
+ let user = User::new(
+ "testuser",
+ Some("password"),
+ "Tester",
+ "Test",
+ "test@test.com",
+ None,
+ )
+ .save(&pool)
+ .await
+ .unwrap();
+
+ let device = Device::new(
+ "dev1".into(),
+ "key1".into(),
+ user.id,
+ DeviceType::User,
+ None,
+ true,
+ )
+ .save(&pool)
+ .await
+ .unwrap();
+
+ let ip = IpAddr::from_str("123.123.123.254").unwrap();
+ let initial_ips = vec![ip];
+
+ let mut conn = pool.acquire().await.unwrap();
+ WireguardNetworkDevice::new(network.id, device.id, initial_ips.clone())
+ .insert(&mut *conn)
+ .await
+ .unwrap();
+
+ let mut updated_network = network.clone();
+ updated_network.address = vec!["123.123.0.0/16".parse::().unwrap()];
+ updated_network.save(&mut *conn).await.unwrap();
+
+ let used_ips = updated_network
+ .all_used_ips_for_network(&mut *conn)
+ .await
+ .unwrap();
+
+ let result = device
+ .assign_next_network_ip(
+ &mut *conn,
+ &updated_network,
+ &used_ips,
+ None,
+ Some(&initial_ips),
+ )
+ .await
+ .unwrap();
+
+ let new_ips = &result.wireguard_ips;
+ assert_eq!(new_ips.len(), 1, "should have one IP per subnet");
+
+ assert!(
+ new_ips.contains(&ip),
+ "123.123.123.254 should be preserved – still within widened 123.123.0.0/16; got {new_ips:?}"
+ );
+ }
+
#[test]
fn test_pubkey_validation() {
let invalid_test_key = "invalid_key";
From c9e1f6e41d0efc1064a6a82a2ea35388b11c8dbd Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 18:26:03 +0100
Subject: [PATCH 07/18] make clippy happy
---
.../src/db/models/wireguard.rs | 4 +-
crates/defguard_core/src/lib.rs | 4 +-
.../src/location_management/mod.rs | 104 +-----------------
3 files changed, 3 insertions(+), 109 deletions(-)
diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs
index aed2bb593d..0f687ea7fc 100644
--- a/crates/defguard_common/src/db/models/wireguard.rs
+++ b/crates/defguard_common/src/db/models/wireguard.rs
@@ -536,9 +536,7 @@ impl WireguardNetwork {
// split into separate stats for each device
let mut device_stats: HashMap> =
stats.into_iter().fold(HashMap::new(), |mut acc, item| {
- acc.entry(item.device_id)
- .or_insert_with(Vec::new)
- .push(item);
+ acc.entry(item.device_id).or_default().push(item);
acc
});
diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs
index 3ab970166b..849e119b72 100644
--- a/crates/defguard_core/src/lib.rs
+++ b/crates/defguard_core/src/lib.rs
@@ -1,6 +1,5 @@
#![allow(clippy::too_many_arguments)]
use std::{
- collections::HashSet,
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::{Arc, LazyLock, Mutex, RwLock},
};
@@ -21,7 +20,6 @@ use defguard_common::{
init_db,
models::{
Device, DeviceType, Settings, User, WireguardNetwork,
- device::WireguardNetworkDevice,
oauth2client::OAuth2Client,
settings::{initialize_current_settings, update_current_settings},
wireguard::{
@@ -741,7 +739,7 @@ pub async fn init_dev_env(config: &DefGuardConfig) {
.expect("Could not save network")
};
let used_ips = network
- .all_used_ips_for_network(&mut *transaction)
+ .all_used_ips_for_network(&mut transaction)
.await
.expect("Failed to query used ip's from database");
if Device::find_by_pubkey(
diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs
index 0c64b88ace..51bac5237b 100644
--- a/crates/defguard_core/src/location_management/mod.rs
+++ b/crates/defguard_core/src/location_management/mod.rs
@@ -1,7 +1,4 @@
-use std::{
- collections::{HashMap, HashSet},
- net::IpAddr,
-};
+use std::{collections::HashMap, net::IpAddr};
use defguard_common::{
csv::AsCsv,
@@ -660,103 +657,4 @@ mod test {
transaction.commit().await.unwrap();
}
-
- #[sqlx::test]
- async fn test_readdress_on_network_change(_: PgPoolOptions, options: PgConnectOptions) {
- use std::net::IpAddr;
-
- let pool = setup_pool(options).await;
-
- // Sieć z trzema podsieciami
- let mut network = WireguardNetwork::default();
- network
- .try_set_address("10.0.0.1/8,12.0.0.1/8,14.0.0.1/8")
- .unwrap();
- let network = network.save(&pool).await.unwrap();
-
- let user = User::new(
- "testuser",
- Some("pass"),
- "Test",
- "User",
- "test@test.com",
- None,
- )
- .save(&pool)
- .await
- .unwrap();
-
- let device = Device::new(
- "device1".into(),
- "key1".into(),
- user.id,
- DeviceType::User,
- None,
- true,
- )
- .save(&pool)
- .await
- .unwrap();
-
- // Ręczne przypisanie konkretnych IP do urządzenia
- let ip_10: IpAddr = "10.0.0.10".parse().unwrap();
- let ip_12: IpAddr = "12.0.0.12".parse().unwrap();
- let ip_14: IpAddr = "14.0.0.15".parse().unwrap();
-
- WireguardNetworkDevice::new(network.id, device.id, vec![ip_10, ip_12, ip_14])
- .insert(&pool)
- .await
- .unwrap();
-
- // Zmiana sieci: 12.x -> 15.x, maska 14.x zmieniona z /8 na /16
- let mut network = network;
- network.address = "10.0.0.1/8,15.0.0.1/8,14.0.0.1/16"
- .split(',')
- .map(|s| s.trim().parse::().unwrap())
- .collect();
-
- let mut transaction = pool.begin().await.unwrap();
-
- let events = sync_location_allowed_devices(&network, &mut transaction, None)
- .await
- .unwrap();
-
- transaction.commit().await.unwrap();
-
- // Szukamy eventu modyfikacji dla naszego urządzenia
- let new_ips = events
- .iter()
- .find_map(|e| match e {
- GatewayEvent::DeviceModified(info) if info.device.id == device.id => {
- Some(&info.network_info[0].device_wireguard_ips)
- }
- _ => None,
- })
- .expect("Oczekiwano zdarzenia DeviceModified dla urządzenia");
-
- // 10.0.0.10 nadal pasuje do 10.0.0.0/8 - powinien pozostać
- assert!(
- new_ips.contains(&ip_10),
- "10.0.0.10 powinno pozostać: {new_ips:?}"
- );
-
- // 12.0.0.12 nie pasuje do żadnej nowej podsieci - powinno zostać zastąpione przez 15.x.x.x
- assert!(
- !new_ips.contains(&ip_12),
- "12.0.0.12 powinno zostać przepisane: {new_ips:?}"
- );
- assert!(
- new_ips.iter().any(|ip| match ip {
- IpAddr::V4(v4) => v4.octets()[0] == 15,
- _ => false,
- }),
- "Oczekiwano adresu z zakresu 15.x.x.x: {new_ips:?}"
- );
-
- // 14.0.0.15 nadal pasuje do 14.0.0.0/16 - powinien pozostać
- assert!(
- new_ips.contains(&ip_14),
- "14.0.0.15 powinno pozostać: {new_ips:?}"
- );
- }
}
From 333cef2aa45d9b9ef4c3b2c81abcc816ad58853d Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 18:39:59 +0100
Subject: [PATCH 08/18] remove duplicated code
---
crates/defguard_common/src/db/models/device.rs | 4 ----
1 file changed, 4 deletions(-)
diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs
index 8d6134e85b..52ac245ec8 100644
--- a/crates/defguard_common/src/db/models/device.rs
+++ b/crates/defguard_common/src/db/models/device.rs
@@ -881,10 +881,6 @@ impl Device {
continue;
}
- if reserved.contains(&ip) {
- continue;
- }
-
picked = Some(ip);
break;
}
From 66ff71f4e811f4b314dec723b57dd2d87e615675 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 18:53:15 +0100
Subject: [PATCH 09/18] linter
---
web/src/pages/EditLocationPage/EditLocationPage.tsx | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/web/src/pages/EditLocationPage/EditLocationPage.tsx b/web/src/pages/EditLocationPage/EditLocationPage.tsx
index b81157f34d..d32dfc6837 100644
--- a/web/src/pages/EditLocationPage/EditLocationPage.tsx
+++ b/web/src/pages/EditLocationPage/EditLocationPage.tsx
@@ -267,10 +267,7 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => {
)}
{(field) => (
-
+
)}
From ef03a97e6491b0c554706ddbf755eb63bae503cb Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 19:01:05 +0100
Subject: [PATCH 10/18] make clippy happy 2
---
crates/defguard_common/src/db/models/device.rs | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs
index 52ac245ec8..a5831c64db 100644
--- a/crates/defguard_common/src/db/models/device.rs
+++ b/crates/defguard_common/src/db/models/device.rs
@@ -1197,13 +1197,13 @@ mod test {
updated_network.save(&mut *conn).await.unwrap();
let used_ips = updated_network
- .all_used_ips_for_network(&mut *conn)
+ .all_used_ips_for_network(&mut conn)
.await
.unwrap();
let result = device
.assign_next_network_ip(
- &mut *conn,
+ &mut conn,
&updated_network,
&used_ips,
None,
@@ -1286,13 +1286,13 @@ mod test {
updated_network.save(&mut *conn).await.unwrap();
let used_ips = updated_network
- .all_used_ips_for_network(&mut *conn)
+ .all_used_ips_for_network(&mut conn)
.await
.unwrap();
let result = device
.assign_next_network_ip(
- &mut *conn,
+ &mut conn,
&updated_network,
&used_ips,
None,
@@ -1366,13 +1366,13 @@ mod test {
updated_network.save(&mut *conn).await.unwrap();
let used_ips = updated_network
- .all_used_ips_for_network(&mut *conn)
+ .all_used_ips_for_network(&mut conn)
.await
.unwrap();
let result = device
.assign_next_network_ip(
- &mut *conn,
+ &mut conn,
&updated_network,
&used_ips,
None,
From db776cde5ed961a0c4f7fb182beb2b982e157fd1 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Mon, 2 Mar 2026 19:34:04 +0100
Subject: [PATCH 11/18] remove tests + adjust existing test
---
.../tests/integration/api/wireguard.rs | 94 +------------------
1 file changed, 2 insertions(+), 92 deletions(-)
diff --git a/crates/defguard_core/tests/integration/api/wireguard.rs b/crates/defguard_core/tests/integration/api/wireguard.rs
index ec2591b820..c293530192 100644
--- a/crates/defguard_core/tests/integration/api/wireguard.rs
+++ b/crates/defguard_core/tests/integration/api/wireguard.rs
@@ -533,7 +533,7 @@ async fn test_network_address_reassignment(_: PgPoolOptions, options: PgConnectO
vec![IpAddr::V4(Ipv4Addr::new(10, 1, 1, 3))],
);
- // trying to modify network addresses while devices exist should fail
+ // trying to modify network addresses while devices exist shouldn't fail
let network = json!({
"id": network_id,
"name": "network",
@@ -557,7 +557,7 @@ async fn test_network_address_reassignment(_: PgPoolOptions, options: PgConnectO
.json(&network)
.send()
.await;
- assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+ assert_eq!(response.status(), StatusCode::OK);
// delete both devices
let response = client
@@ -571,14 +571,6 @@ async fn test_network_address_reassignment(_: PgPoolOptions, options: PgConnectO
.await;
assert_eq!(response.status(), StatusCode::OK);
- // now modify network addresses should succeed
- let response = client
- .put(format!("/api/v1/network/{network_id}"))
- .json(&network)
- .send()
- .await;
- assert_eq!(response.status(), StatusCode::OK);
-
// re-create a device and verify it gets IPs in both subnets
let device = json!({
"name": "device3",
@@ -920,85 +912,3 @@ async fn test_network_size_validation(_: PgPoolOptions, options: PgConnectOption
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
-
-/// Test that modifying a network's address is blocked when any devices are assigned.
-/// Also verifies that non-address modifications still succeed.
-#[sqlx::test]
-async fn test_modify_network_blocked_by_devices(_: PgPoolOptions, options: PgConnectOptions) {
- let pool = setup_pool(options).await;
-
- let (client, _client_state) = make_test_client(pool).await;
-
- let auth = Auth::new("admin", "pass123");
- let response = &client.post("/api/v1/auth").json(&auth).send().await;
- assert_eq!(response.status(), StatusCode::OK);
-
- // create network
- let response = make_network(&client, "network").await;
- let network: WireguardNetwork = response.json().await;
-
- // create a device for the admin user — it gets auto-assigned to the network
- let device = json!({
- "name": "device1",
- "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=",
- });
- let response = client
- .post("/api/v1/device/admin")
- .json(&device)
- .send()
- .await;
- assert_eq!(response.status(), StatusCode::CREATED);
-
- // try to modify the network address — should be rejected because a device exists
- let modified = json!({
- "name": "network",
- "address": "10.2.2.1/24",
- "port": 55555,
- "endpoint": "192.168.4.14",
- "allowed_ips": "10.2.2.0/24",
- "dns": "1.1.1.1",
- "mtu": 1420,
- "fwmark": 0,
- "allowed_groups": [],
- "keepalive_interval": 25,
- "peer_disconnect_threshold": 300,
- "acl_enabled": false,
- "acl_default_allow": false,
- "location_mfa_mode": "disabled",
- "service_location_mode": "disabled"
- });
- let response = client
- .put(format!("/api/v1/network/{}", network.id))
- .json(&modified)
- .send()
- .await;
- assert_eq!(response.status(), StatusCode::BAD_REQUEST);
-
- let body: serde_json::Value = response.json().await;
- assert!(body["msg"].as_str().is_some());
-
- // verify that modifying other fields (not address) still works
- let modified_name_only = json!({
- "name": "renamed-network",
- "address": "10.1.1.1/24",
- "port": 55555,
- "endpoint": "192.168.4.14",
- "allowed_ips": "10.1.1.0/24",
- "dns": "1.1.1.1",
- "mtu": 1420,
- "fwmark": 0,
- "allowed_groups": [],
- "keepalive_interval": 25,
- "peer_disconnect_threshold": 300,
- "acl_enabled": false,
- "acl_default_allow": false,
- "location_mfa_mode": "disabled",
- "service_location_mode": "disabled"
- });
- let response = client
- .put(format!("/api/v1/network/{}", network.id))
- .json(&modified_name_only)
- .send()
- .await;
- assert_eq!(response.status(), StatusCode::OK);
-}
From 2647b83536550001b68033ca5b15df0ce9a8e260 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Tue, 3 Mar 2026 10:34:40 +0100
Subject: [PATCH 12/18] add used ip's to set in loop
---
crates/defguard_common/src/db/models/wireguard.rs | 5 +++--
crates/defguard_core/src/location_management/mod.rs | 4 +++-
tools/defguard_generator/src/vpn_session_stats.rs | 5 +++--
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs
index 0f687ea7fc..38535bcb01 100644
--- a/crates/defguard_common/src/db/models/wireguard.rs
+++ b/crates/defguard_common/src/db/models/wireguard.rs
@@ -438,12 +438,13 @@ impl WireguardNetwork {
"Assigning IPs in network {} for all existing devices ",
self
);
- let used_ips = self.all_used_ips_for_network(&mut *transaction).await?;
+ let mut used_ips = self.all_used_ips_for_network(&mut *transaction).await?;
let devices = self.get_allowed_devices(&mut *transaction).await?;
for device in devices {
- device
+ let device = device
.assign_next_network_ip(&mut *transaction, self, &used_ips, None, None)
.await?;
+ used_ips.extend(device.wireguard_ips);
}
Ok(())
}
diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs
index 51bac5237b..f423f48795 100644
--- a/crates/defguard_core/src/location_management/mod.rs
+++ b/crates/defguard_core/src/location_management/mod.rs
@@ -165,7 +165,7 @@ pub async fn process_device_access_changes(
// Loop through current device configurations; remove no longer allowed, readdress
// when necessary; remove processed entry from all devices list initial list should
// now contain only devices to be added.
- let used_ips = location.all_used_ips_for_network(&mut *transaction).await?;
+ let mut used_ips = location.all_used_ips_for_network(&mut *transaction).await?;
let mut events: Vec = Vec::new();
for device_network_config in currently_configured_devices {
// Device is allowed and an IP was already assigned
@@ -183,6 +183,7 @@ pub async fn process_device_access_changes(
Some(&device_network_config.wireguard_ips),
)
.await?;
+ used_ips.extend(wireguard_network_device.wireguard_ips.iter().copied());
events.push(GatewayEvent::DeviceModified(DeviceInfo {
device,
network_info: vec![DeviceNetworkInfo {
@@ -224,6 +225,7 @@ pub async fn process_device_access_changes(
let wireguard_network_device = device
.assign_next_network_ip(&mut *transaction, location, &used_ips, reserved_ips, None)
.await?;
+ used_ips.extend(wireguard_network_device.wireguard_ips.iter().copied());
events.push(GatewayEvent::DeviceCreated(DeviceInfo {
device,
network_info: vec![DeviceNetworkInfo {
diff --git a/tools/defguard_generator/src/vpn_session_stats.rs b/tools/defguard_generator/src/vpn_session_stats.rs
index f9ded71e0c..a83af4e425 100644
--- a/tools/defguard_generator/src/vpn_session_stats.rs
+++ b/tools/defguard_generator/src/vpn_session_stats.rs
@@ -70,7 +70,7 @@ pub async fn generate_vpn_session_stats(
let devices =
prepare_user_devices(&pool, &mut rng, &user, config.devices_per_user as usize).await?;
- let used_ips = location.all_used_ips_for_network(&mut *transaction).await?;
+ let mut used_ips = location.all_used_ips_for_network(&mut *transaction).await?;
// assign devices to the network if not already assigned
for device in &devices {
if WireguardNetworkDevice::find(&mut *transaction, device.id, location.id)
@@ -81,9 +81,10 @@ pub async fn generate_vpn_session_stats(
"Assigning device {} to network {} with auto-generated IP",
device.name, location.name
);
- device
+ let wireguard_network_device = device
.assign_next_network_ip(&mut transaction, &location, &used_ips, None, None)
.await?;
+ used_ips.extend(wireguard_network_device.wireguard_ips);
} else {
info!(
"Device {} already assigned to network {}",
From 5ed4bd1ad13d1c22b8caeca7c19a302a3c5d141f Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Tue, 3 Mar 2026 10:35:06 +0100
Subject: [PATCH 13/18] Update
crates/defguard_common/src/db/models/wireguard.rs
Co-authored-by: Adam
---
crates/defguard_common/src/db/models/wireguard.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs
index 38535bcb01..ac65164026 100644
--- a/crates/defguard_common/src/db/models/wireguard.rs
+++ b/crates/defguard_common/src/db/models/wireguard.rs
@@ -1357,7 +1357,7 @@ impl WireguardNetwork {
.await
}
- /// Obtain all used ips for network
+ /// Obtain all used IP addresses for network.
pub async fn all_used_ips_for_network(
&self,
transaction: &mut PgConnection,
From 614d67071f0ea6dace60ecb02d70e3d9aab4fb3d Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Tue, 3 Mar 2026 10:40:32 +0100
Subject: [PATCH 14/18] change variable name
---
crates/defguard_common/src/db/models/wireguard.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs
index ac65164026..6ebaf010a5 100644
--- a/crates/defguard_common/src/db/models/wireguard.rs
+++ b/crates/defguard_common/src/db/models/wireguard.rs
@@ -441,7 +441,7 @@ impl WireguardNetwork {
let mut used_ips = self.all_used_ips_for_network(&mut *transaction).await?;
let devices = self.get_allowed_devices(&mut *transaction).await?;
for device in devices {
- let device = device
+ let wireguard_network_device = device
.assign_next_network_ip(&mut *transaction, self, &used_ips, None, None)
.await?;
used_ips.extend(device.wireguard_ips);
From 48074ec3b0b33a99b6edd102102978b7834e67cf Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Tue, 3 Mar 2026 10:43:10 +0100
Subject: [PATCH 15/18] fix typo
---
crates/defguard_common/src/db/models/wireguard.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs
index 6ebaf010a5..04ac4fc2f2 100644
--- a/crates/defguard_common/src/db/models/wireguard.rs
+++ b/crates/defguard_common/src/db/models/wireguard.rs
@@ -444,7 +444,7 @@ impl WireguardNetwork {
let wireguard_network_device = device
.assign_next_network_ip(&mut *transaction, self, &used_ips, None, None)
.await?;
- used_ips.extend(device.wireguard_ips);
+ used_ips.extend(wireguard_network_device.wireguard_ips);
}
Ok(())
}
From f391b29f7c8d3c6af6fcec0073c6c1cee8186dc3 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Tue, 3 Mar 2026 19:50:50 +0100
Subject: [PATCH 16/18] Update crates/defguard_core/src/lib.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
crates/defguard_core/src/lib.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs
index 849e119b72..33f83bd4ae 100644
--- a/crates/defguard_core/src/lib.rs
+++ b/crates/defguard_core/src/lib.rs
@@ -741,7 +741,7 @@ pub async fn init_dev_env(config: &DefGuardConfig) {
let used_ips = network
.all_used_ips_for_network(&mut transaction)
.await
- .expect("Failed to query used ip's from database");
+ .expect("Failed to query used IPs from database");
if Device::find_by_pubkey(
&mut *transaction,
"gQYL5eMeFDj0R+lpC7oZyIl0/sNVmQDC6ckP7husZjc=",
From e45c56d63546c673e80f27c1b5e8e63d161cd2ec Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Tue, 3 Mar 2026 20:59:45 +0100
Subject: [PATCH 17/18] remove freed ips during deletion
---
crates/defguard_core/src/location_management/mod.rs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs
index f423f48795..6f7297f2bc 100644
--- a/crates/defguard_core/src/location_management/mod.rs
+++ b/crates/defguard_core/src/location_management/mod.rs
@@ -201,6 +201,8 @@ pub async fn process_device_access_changes(
device_network_config.device_id
);
device_network_config.delete(&mut *transaction).await?;
+ // Remove freed IPs so they can be reused by later assignments
+ used_ips.retain(|ip| !device_network_config.wireguard_ips.contains(ip));
if let Some(device) =
Device::find_by_id(&mut *transaction, device_network_config.device_id).await?
{
From 350031028a7fbd2c44a13df000331e0a04be0c33 Mon Sep 17 00:00:00 2001
From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com>
Date: Wed, 4 Mar 2026 12:41:31 +0100
Subject: [PATCH 18/18] fix docstring
---
crates/defguard_common/src/db/models/device.rs | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs
index a5831c64db..eb71b2f8d4 100644
--- a/crates/defguard_common/src/db/models/device.rs
+++ b/crates/defguard_common/src/db/models/device.rs
@@ -820,20 +820,25 @@ impl Device {
/// Assign the next available IP address in each subnet of the network to this device.
///
/// For every CIDR block in `network.address`, this function:
- /// 1. Iterates through the block's IPs in order.
- /// 2. Skips any IP that:
- /// - Fails the `can_assign_ips` validation (out of range, reserved, or already in use by another device), or
- /// - Appears in the optional `reserved_ips`.
+ /// 1. If `current_ips` contains an IP that already falls within the subnet, reuses it
+ /// immediately without consulting `used_ips` or scanning the address space.
+ /// 2. Otherwise, iterates through the block's IPs in order and skips any IP that is:
+ /// - The network address, broadcast address, or the subnet's host IP (gateway), or
+ /// - Present in `used_ips` (already assigned to another device), or
+ /// - Present in the optional `reserved_ips`.
/// 3. Selects the first remaining IP and records it.
///
/// If any subnet has no valid, unassigned IP, the method returns `ModelError::CannotCreate`.
///
/// # Parameters
///
- /// - `transaction`: Active PostgreSQL connection to check and insert assignments.
+ /// - `transaction`: Active PostgreSQL connection used to persist the assignment.
/// - `network`: The `WireguardNetwork` whose subnets will be assigned.
+ /// - `used_ips`: Set of IPs already assigned within the network (caller-maintained snapshot).
/// - `reserved_ips`: Optional slice of IPs that must not be assigned, even if otherwise free.
- /// - `current_ips`: Optional slice of IPs already assigned to the device - won't be reassigned if they are still valid.
+ /// - `current_ips`: Optional slice of IPs already assigned to this device. An IP that still
+ /// falls within its subnet is reused as-is; only IPs that no longer fit their subnet are
+ /// replaced.
///
/// # Returns
///