From 46e979bd4c9bd1c97b4824aafcd4dfe765366233 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 4 Apr 2025 09:29:12 +0200 Subject: [PATCH 01/71] migrations for multi-address peers --- ...250404071457_multiple_peer_addresses.down.sql | 16 ++++++++++++++++ ...20250404071457_multiple_peer_addresses.up.sql | 15 +++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 migrations/20250404071457_multiple_peer_addresses.down.sql create mode 100644 migrations/20250404071457_multiple_peer_addresses.up.sql diff --git a/migrations/20250404071457_multiple_peer_addresses.down.sql b/migrations/20250404071457_multiple_peer_addresses.down.sql new file mode 100644 index 0000000000..b4283088d2 --- /dev/null +++ b/migrations/20250404071457_multiple_peer_addresses.down.sql @@ -0,0 +1,16 @@ +-- add old-type address column +ALTER TABLE wireguard_network_device +ADD COLUMN wireguard_ip_old inet; + +-- copy the first element of new column to old column +-- all further addresses will be lost +UPDATE wireguard_network_device +SET wireguard_ip_old = wireguard_ip[1]; + +-- drop the "new" column +ALTER TABLE wireguard_network_device +DROP COLUMN wireguard_ip; + +-- rename the column +ALTER TABLE wireguard_network_device +RENAME COLUMN wireguard_ip_old TO wireguard_ip; diff --git a/migrations/20250404071457_multiple_peer_addresses.up.sql b/migrations/20250404071457_multiple_peer_addresses.up.sql new file mode 100644 index 0000000000..1478b02af2 --- /dev/null +++ b/migrations/20250404071457_multiple_peer_addresses.up.sql @@ -0,0 +1,15 @@ +-- add new address column +ALTER TABLE wireguard_network_device +ADD COLUMN wireguard_ip_new inet[] NOT NULL DEFAULT '{}'; + +-- copy and convert existing IPs into arrays +UPDATE wireguard_network_device +SET wireguard_ip_new = ARRAY[wireguard_ip]; + +-- drop the old column +ALTER TABLE wireguard_network_device +DROP COLUMN wireguard_ip; + +-- rename the new column to the original name +ALTER TABLE wireguard_network_device +RENAME COLUMN wireguard_ip_new TO wireguard_ip; From 39ebd55332b84e9f048fc68251eac0d2a64d0bfa Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 4 Apr 2025 12:05:08 +0200 Subject: [PATCH 02/71] wip deal with most of the db issues resulting from network device address type change TODO: * fix address assignment * ... --- src/db/models/device.rs | 142 +++++++++++++++++++++++++++++++--------- src/grpc/gateway.rs | 5 +- 2 files changed, 116 insertions(+), 31 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 31adff2ca1..d572db5e01 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -34,7 +34,7 @@ pub struct DeviceConfig { pub(crate) network_name: String, pub(crate) config: String, #[schema(value_type = String)] - pub(crate) address: IpAddr, + pub(crate) address: Vec, pub(crate) endpoint: String, #[schema(value_type = String)] pub(crate) allowed_ips: Vec, @@ -142,7 +142,7 @@ pub struct DeviceInfo { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct DeviceNetworkInfo { pub network_id: Id, - pub device_wireguard_ip: IpAddr, + pub device_wireguard_ip: Vec, #[serde(skip_serializing)] pub preshared_key: Option, pub is_authorized: bool, @@ -159,7 +159,11 @@ impl DeviceInfo { debug!("Generating device info for {device}"); let network_info = query_as!( DeviceNetworkInfo, - "SELECT wireguard_network_id network_id, wireguard_ip \"device_wireguard_ip: IpAddr\", \ + "SELECT wireguard_network_id network_id, \ + ARRAY( + SELECT host(ip) + FROM unnest(wireguard_ip) as ip + ) \"wireguard_ip!: Vec\", \ preshared_key, is_authorized \ FROM wireguard_network_device \ WHERE device_id = $1", @@ -256,7 +260,7 @@ impl UserDevice { #[derive(Clone, Debug, Deserialize, FromRow, Serialize)] pub struct WireguardNetworkDevice { pub wireguard_network_id: Id, - pub wireguard_ip: IpAddr, + pub wireguard_ip: Vec, pub device_id: Id, pub preshared_key: Option, pub is_authorized: bool, @@ -278,7 +282,7 @@ pub struct ModifyDevice { impl WireguardNetworkDevice { #[must_use] - pub(crate) fn new(network_id: Id, device_id: Id, wireguard_ip: IpAddr) -> Self { + pub(crate) fn new(network_id: Id, device_id: Id, wireguard_ip: &[IpAddr]) -> Self { Self { wireguard_network_id: network_id, wireguard_ip, @@ -289,6 +293,23 @@ impl WireguardNetworkDevice { } } + #[must_use] + pub(crate) fn ips_as_network(&self) -> Vec { + self.wireguard_ip + .iter() + .map(|ip| IpNetwork::from(ip.clone())) + .collect() + } + + #[must_use] + pub(crate) fn ips_to_string(&self) -> String { + self.wireguard_ip + .iter() + .map(ToString::to_string) + .collect::>() + .join(",") + } + pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), SqlxError> where E: PgExecutor<'e>, @@ -302,7 +323,7 @@ impl WireguardNetworkDevice { DO UPDATE SET wireguard_ip = $3, is_authorized = $4", self.device_id, self.wireguard_network_id, - IpNetwork::from(self.wireguard_ip.clone()), + &self.ips_as_network(), self.is_authorized, self.authorized_at, self.preshared_key @@ -323,7 +344,7 @@ impl WireguardNetworkDevice { WHERE device_id = $1 AND wireguard_network_id = $2", self.device_id, self.wireguard_network_id, - IpNetwork::from(self.wireguard_ip.clone()), + &self.ips_as_network(), self.is_authorized, self.authorized_at, self.preshared_key, @@ -360,8 +381,12 @@ impl WireguardNetworkDevice { { let res = query_as!( Self, - "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", \ - preshared_key, is_authorized, authorized_at \ + "SELECT device_id, wireguard_network_id, \ + ARRAY( + SELECT host(ip) + FROM unnest(wireguard_ip) as ip + ) \"wireguard_ip!: Vec\", \ + preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE device_id = $1 AND wireguard_network_id = $2", device_id, @@ -384,7 +409,11 @@ impl WireguardNetworkDevice { { let res = query_as!( Self, - "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", \ + "SELECT device_id, wireguard_network_id, \ + ARRAY( + SELECT host(ip) + FROM unnest(wireguard_ip) as ip + ) \"wireguard_ip!: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE device_id = $1 ORDER BY id LIMIT 1", @@ -405,7 +434,11 @@ impl WireguardNetworkDevice { { let result = query_as!( Self, - "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", \ + "SELECT device_id, wireguard_network_id, \ + ARRAY( + SELECT host(ip) + FROM unnest(wireguard_ip) as ip + ) \"wireguard_ip!: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device WHERE device_id = $1", device_id @@ -429,7 +462,11 @@ impl WireguardNetworkDevice { { let res = query_as!( Self, - "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", \ + "SELECT device_id, wireguard_network_id, \ + ARRAY( + SELECT host(ip) + FROM unnest(wireguard_ip) as ip + ) \"wireguard_ip!: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1", @@ -454,7 +491,11 @@ impl WireguardNetworkDevice { { let res = query_as!( Self, - "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", \ + "SELECT device_id, wireguard_network_id, \ + ARRAY( + SELECT host(ip) + FROM unnest(wireguard_ip) as ip + ) \"wireguard_ip!: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1 AND device_id IN \ @@ -572,7 +613,10 @@ impl Device { {allowed_ips}\ Endpoint = {}:{}\n\ PersistentKeepalive = 300", - wireguard_network_device.wireguard_ip, network.pubkey, network.endpoint, network.port, + wireguard_network_device.ips_to_string(), + network.pubkey, + network.endpoint, + network.port, ) } @@ -590,7 +634,7 @@ impl Device { d.device_type \"device_type: DeviceType\", configured \ FROM device d \ JOIN wireguard_network_device wnd ON d.id = wnd.device_id \ - WHERE wnd.wireguard_ip = $1 AND wnd.wireguard_network_id = $2", + WHERE $1 = ANY(wnd.wireguard_ip) AND wnd.wireguard_network_id = $2", IpNetwork::from(ip), network_id ) @@ -661,7 +705,7 @@ impl Device { .ok_or_else(|| DeviceError::Unexpected("Device not found in network".into()))?; let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ip: wireguard_network_device.wireguard_ip.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; @@ -694,7 +738,7 @@ impl Device { .await?; let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ip: wireguard_network_device.wireguard_ip.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; @@ -748,12 +792,14 @@ impl Device { .await { debug!( - "Assigned IP {} for device {} (user {}) in network {network}", - wireguard_network_device.wireguard_ip, self.name, self.user_id + "Assigned IPs {} for device {} (user {}) in network {network}", + wireguard_network_device.ips_to_string(), + self.name, + self.user_id ); let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ip: wireguard_network_device.wireguard_ip.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; @@ -777,14 +823,44 @@ impl Device { Ok((network_info, configs)) } - // Assign IP to the device in a given network + // Assign IPs to the device in a given network. pub(crate) async fn assign_next_network_ip( &self, transaction: &mut PgConnection, network: &WireguardNetwork, reserved_ips: Option<&[IpAddr]>, ) -> Result { - if let Some(address) = network.address.first() { + // if let Some(address) = network.address.first() { + // let net_ip = address.ip(); + // let net_network = address.network(); + // let net_broadcast = address.broadcast(); + // for ip in address { + // if ip == net_ip || ip == net_network || ip == net_broadcast { + // continue; + // } + // if let Some(reserved_ips) = reserved_ips { + // if reserved_ips.contains(&ip) { + // continue; + // } + // } + + // // Break loop if IP is unassigned and return network device + // if Self::find_by_ip(&mut *transaction, ip, network.id) + // .await? + // .is_none() + // { + // info!("Assigned IP address {ip} for device: {}", self.name); + // let wireguard_network_device = + // WireguardNetworkDevice::new(network.id, self.id, ip); + // wireguard_network_device.insert(&mut *transaction).await?; + // return Ok(wireguard_network_device); + // } + // } + // } + // Err(ModelError::CannotCreate) + let mut ips = Vec::new(); + // Iterate over all network addresses and assign new IP for the device in each of them. + for address in &network.address { let net_ip = address.ip(); let net_network = address.network(); let net_broadcast = address.broadcast(); @@ -797,21 +873,27 @@ impl Device { continue; } } - - // Break loop if IP is unassigned and return network device + // Break the loop if IP is unassigned and push the IP into result vector if Self::find_by_ip(&mut *transaction, ip, network.id) .await? .is_none() { - info!("Assigned IP address {ip} for device: {}", self.name); - let wireguard_network_device = - WireguardNetworkDevice::new(network.id, self.id, ip); - wireguard_network_device.insert(&mut *transaction).await?; - return Ok(wireguard_network_device); + ips.push(ip); } } } - Err(ModelError::CannotCreate) + if ips.len() != network.address.len() { + error!( + "Failed to assign address for device {} in one of the networks: {:?}", + self.name, network.address + ); + return Err(ModelError::CannotCreate); + } + info!("Assigned IP addresses {ips:?} for device: {}", self.name); + let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, &ips); + wireguard_network_device.insert(&mut *transaction).await?; + + Ok(wireguard_network_device) } pub(crate) async fn assign_network_ip( diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index 9619060bf7..c11b86daa8 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -68,7 +68,10 @@ impl WireguardNetwork { debug!("Fetching all peers for network {}", self.id); let rows = query!( "SELECT d.wireguard_pubkey pubkey, preshared_key, \ - array[host(wnd.wireguard_ip)] \"allowed_ips!: Vec\" \ + ARRAY( + SELECT host(ip) + FROM unnest(wnd.wireguard_ip) AS ip + ) \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ JOIN \"user\" u ON d.user_id = u.id \ From 7f2fcd9f4e47e41294f637dfc30d5170fe78243a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 4 Apr 2025 12:38:53 +0200 Subject: [PATCH 03/71] Simplify WireguardNetworkDevice::wireguard_ip sql --- src/db/models/device.rs | 40 +++++++++++----------------------------- src/grpc/gateway.rs | 1 + 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index d572db5e01..023f7411a6 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -160,11 +160,8 @@ impl DeviceInfo { let network_info = query_as!( DeviceNetworkInfo, "SELECT wireguard_network_id network_id, \ - ARRAY( - SELECT host(ip) - FROM unnest(wireguard_ip) as ip - ) \"wireguard_ip!: Vec\", \ - preshared_key, is_authorized \ + wireguard_ip \"wireguard_ip: Vec\", \ + preshared_key, is_authorized \ FROM wireguard_network_device \ WHERE device_id = $1", device.id @@ -382,10 +379,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - ARRAY( - SELECT host(ip) - FROM unnest(wireguard_ip) as ip - ) \"wireguard_ip!: Vec\", \ + wireguard_ip \"wireguard_ip: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE device_id = $1 AND wireguard_network_id = $2", @@ -410,11 +404,8 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - ARRAY( - SELECT host(ip) - FROM unnest(wireguard_ip) as ip - ) \"wireguard_ip!: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ip \"wireguard_ip: Vec\", \ + preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE device_id = $1 ORDER BY id LIMIT 1", device_id @@ -435,11 +426,8 @@ impl WireguardNetworkDevice { let result = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - ARRAY( - SELECT host(ip) - FROM unnest(wireguard_ip) as ip - ) \"wireguard_ip!: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ip \"wireguard_ip: Vec\", \ + preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device WHERE device_id = $1", device_id ) @@ -463,11 +451,8 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - ARRAY( - SELECT host(ip) - FROM unnest(wireguard_ip) as ip - ) \"wireguard_ip!: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ip \"wireguard_ip: Vec\", \ + preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1", network_id @@ -492,11 +477,8 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - ARRAY( - SELECT host(ip) - FROM unnest(wireguard_ip) as ip - ) \"wireguard_ip!: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ip \"wireguard_ip: Vec\", \ + preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1 AND device_id IN \ (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index c11b86daa8..f20e68d246 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -68,6 +68,7 @@ impl WireguardNetwork { debug!("Fetching all peers for network {}", self.id); let rows = query!( "SELECT d.wireguard_pubkey pubkey, preshared_key, \ + -- TODO possible to not use ARRAY-unnest here? ARRAY( SELECT host(ip) FROM unnest(wnd.wireguard_ip) AS ip From 88ae9e3104dde2d56a7a768f43f7b786f1728811 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 7 Apr 2025 10:50:25 +0200 Subject: [PATCH 04/71] wip deal with typing issues after multiaddress mods TODO: multiaddress on all network and device related structs --- src/db/models/device.rs | 30 +++++++++++----- src/db/models/wireguard.rs | 29 ++++++++++++---- src/grpc/enrollment.rs | 17 +++++++-- src/grpc/gateway.rs | 14 ++++++-- src/grpc/utils.rs | 18 ++++++++-- src/handlers/network_devices.rs | 42 +++++++++++++++++++---- src/handlers/wireguard.rs | 14 ++++---- tests/wireguard_network_allowed_groups.rs | 21 +++++++++--- 8 files changed, 148 insertions(+), 37 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 023f7411a6..399a5b685d 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -160,7 +160,7 @@ impl DeviceInfo { let network_info = query_as!( DeviceNetworkInfo, "SELECT wireguard_network_id network_id, \ - wireguard_ip \"wireguard_ip: Vec\", \ + wireguard_ip \"device_wireguard_ip: Vec\", \ preshared_key, is_authorized \ FROM wireguard_network_device \ WHERE device_id = $1", @@ -279,10 +279,13 @@ pub struct ModifyDevice { impl WireguardNetworkDevice { #[must_use] - pub(crate) fn new(network_id: Id, device_id: Id, wireguard_ip: &[IpAddr]) -> Self { + pub(crate) fn new(network_id: Id, device_id: Id, wireguard_ip: I) -> Self + where + I: Into>, + { Self { wireguard_network_id: network_id, - wireguard_ip, + wireguard_ip: wireguard_ip.into(), device_id, preshared_key: None, is_authorized: false, @@ -843,6 +846,7 @@ impl Device { let mut ips = Vec::new(); // Iterate over all network addresses and assign new IP for the device in each of them. for address in &network.address { + // TODO(jck) make sure all network.address addresses are from different network? let net_ip = address.ip(); let net_network = address.network(); let net_broadcast = address.broadcast(); @@ -872,16 +876,20 @@ impl Device { return Err(ModelError::CannotCreate); } info!("Assigned IP addresses {ips:?} for device: {}", self.name); - let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, &ips); + let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, ips); wireguard_network_device.insert(&mut *transaction).await?; Ok(wireguard_network_device) } + /// Assigns specific IP address to the device in specified [`WireguardNetwork`]. + /// This method is currently used only for network devices. For regular user + /// devices [`assign_next_network_ip`] method is used. pub(crate) async fn assign_network_ip( &self, transaction: &mut PgConnection, network: &WireguardNetwork, + // TODO(jck) allow assignment of multiple ips for a network device ip: IpAddr, ) -> Result { if let Some(network_address) = network.address.first() { @@ -897,7 +905,8 @@ impl Device { .is_none() { info!("Assigned IP: {ip} for device: {}", self.name); - let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, ip); + let wireguard_network_device = + WireguardNetworkDevice::new(network.id, self.id, &[ip]); wireguard_network_device.insert(&mut *transaction).await?; return Ok(wireguard_network_device); } @@ -1029,7 +1038,7 @@ mod test { info!("Created device: {}", device.name); debug!("For user: {}", device.user_id); let wireguard_network_device = - WireguardNetworkDevice::new(network.id, device.id, ip); + WireguardNetworkDevice::new(network.id, device.id, &[ip]); wireguard_network_device.insert(pool).await?; info!( "Assigned IP: {ip} for device: {name} in network: {}", @@ -1065,7 +1074,12 @@ mod test { .await .unwrap(); assert_eq!( - wireguard_network_device.wireguard_ip.to_string(), + wireguard_network_device + .wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","), "10.1.1.2" ); @@ -1187,7 +1201,7 @@ mod test { WireguardNetworkDevice::new( network.id, device4.id, - IpAddr::from_str("10.1.1.10").unwrap(), + &[IpAddr::from_str("10.1.1.10").unwrap()], ) .insert(&mut *transaction) .await diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index f97b42f3ef..74b9ee03e8 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -452,17 +452,27 @@ impl WireguardNetwork { &self, transaction: &mut PgConnection, device: &WireguardNetworkDevice, + // TODO(jck) allow assignment of multiple ips for a network device ip: IpAddr, ) -> Result { info!( "Adding network device {} with IP {ip} to network {self}", device.device_id ); - let wireguard_network_device = WireguardNetworkDevice::new(self.id, device.device_id, ip); + let wireguard_network_device = + WireguardNetworkDevice::new(self.id, device.device_id, &[ip]); wireguard_network_device.insert(&mut *transaction).await?; Ok(wireguard_network_device) } + /// Checks if all device addresses are contained in at least one of the network addresses + fn contains_all(&self, addresses: &[IpAddr]) -> bool { + !self + .address + .iter() + .any(|net| !addresses.iter().any(|addr| net.contains(*addr))) + } + /// Works out which devices need to be added, removed, or readdressed /// based on the list of currently configured devices and the list of devices which should be allowed async fn process_device_access_changes( @@ -479,7 +489,8 @@ impl WireguardNetwork { // device is allowed and an IP was already assigned if let Some(device) = allowed_devices.remove(&device_network_config.device_id) { // network address changed and IP needs to be updated - if !self.address[0].contains(device_network_config.wireguard_ip) { + if !self.contains_all(&device_network_config.wireguard_ip) { + // TODO(jck) ensure we don't leak IP addresses here let wireguard_network_device = device .assign_next_network_ip(&mut *transaction, self, reserved_ips) .await?; @@ -661,7 +672,8 @@ impl WireguardNetwork { let wireguard_network_device = WireguardNetworkDevice::new( self.id, existing_device.id, - imported_device.wireguard_ip, + // TODO(jck) allow assignment of multiple ips for a network device + &[imported_device.wireguard_ip], ); wireguard_network_device.insert(&mut *transaction).await?; // store ID of device with already generated config @@ -743,8 +755,12 @@ impl WireguardNetwork { let mut network_info = Vec::new(); match &allowed_groups { None => { - let wireguard_network_device = - WireguardNetworkDevice::new(self.id, device.id, mapped_device.wireguard_ip); + // TODO(jck) allow assignment of multiple ips for a network device + let wireguard_network_device = WireguardNetworkDevice::new( + self.id, + device.id, + &[mapped_device.wireguard_ip], + ); wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { network_id: self.id, @@ -760,7 +776,8 @@ impl WireguardNetwork { let wireguard_network_device = WireguardNetworkDevice::new( self.id, device.id, - mapped_device.wireguard_ip, + // TODO(jck) allow assignment of multiple ips for a network device + &[mapped_device.wireguard_ip], ); wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 0c2638951a..f44e8fb72e 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -1,3 +1,5 @@ +use std::net::IpAddr; + use ipnetwork::IpNetwork; use sqlx::{PgPool, Transaction}; use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; @@ -631,7 +633,12 @@ impl EnrollmentServer { .iter() .map(|c| TemplateLocation { name: c.network_name.clone(), - assigned_ip: c.address.to_string(), + assigned_ip: c + .address + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","), }) .collect(); @@ -724,12 +731,18 @@ impl From for ProtoDeviceConfig { .map(IpNetwork::to_string) .collect::>() .join(","); + let assigned_ip = config + .address + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","); Self { network_id: config.network_id, network_name: config.network_name, config: config.config, endpoint: config.endpoint, - assigned_ip: config.address.to_string(), + assigned_ip, pubkey: config.pubkey, allowed_ips, dns: config.dns, diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index f20e68d246..c2dac10a76 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -1,4 +1,5 @@ use std::{ + net::IpAddr, pin::Pin, sync::{Arc, Mutex}, task::{Context, Poll}, @@ -285,7 +286,11 @@ impl GatewayUpdatesHandler { self.send_peer_update( Peer { pubkey: device.device.wireguard_pubkey, - allowed_ips: vec![network_info.device_wireguard_ip.to_string()], + allowed_ips: network_info + .device_wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect(), preshared_key: network_info.preshared_key.clone(), keepalive_interval: Some( self.network.keepalive_interval as u32, @@ -315,7 +320,12 @@ impl GatewayUpdatesHandler { self.send_peer_update( Peer { pubkey: device.device.wireguard_pubkey, - allowed_ips: vec![network_info.device_wireguard_ip.to_string()], + allowed_ips: vec![network_info + .device_wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(",")], preshared_key: network_info.preshared_key.clone(), keepalive_interval: Some( self.network.keepalive_interval as u32, diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs index 2213a68bfe..a4b111c4d5 100644 --- a/src/grpc/utils.rs +++ b/src/grpc/utils.rs @@ -1,3 +1,5 @@ +use std::net::IpAddr; + use ipnetwork::IpNetwork; use sqlx::PgPool; use tonic::Status; @@ -115,11 +117,17 @@ pub(crate) async fn build_device_config_response( .map(IpNetwork::to_string) .collect::>() .join(","); + let assigned_ip = wireguard_network_device + .wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","); let config = ProtoDeviceConfig { config: Device::create_config(&network, &wireguard_network_device), network_id: network.id, network_name: network.name, - assigned_ip: wireguard_network_device.wireguard_ip.to_string(), + assigned_ip, endpoint: format!("{}:{}", network.endpoint, network.port), pubkey: network.pubkey, allowed_ips, @@ -148,11 +156,17 @@ pub(crate) async fn build_device_config_response( .map(IpNetwork::to_string) .collect::>() .join(","); + let assigned_ip = wireguard_network_device + .wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","); let config = ProtoDeviceConfig { config: Device::create_config(&network, &wireguard_network_device), network_id: network.id, network_name: network.name, - assigned_ip: wireguard_network_device.wireguard_ip.to_string(), + assigned_ip, endpoint: format!("{}:{}", network.endpoint, network.port), pubkey: network.pubkey, allowed_ips, diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 720a244514..a3b8801559 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -36,6 +36,7 @@ struct NetworkDeviceLocation { struct NetworkDeviceInfo { id: Id, name: String, + // TODO(jck) mulitple ips assigned_ip: IpAddr, description: Option, added_by: String, @@ -74,11 +75,22 @@ impl NetworkDeviceInfo { "Failed to find the network address for network {}", network.name )))?; - let split_ip = split_ip(&wireguard_device.wireguard_ip, net_addr); + // TODO(jck) deal with all ips + let split_ip = split_ip( + &wireguard_device + .wireguard_ip + .first() + .expect("missing NetworkDevice IP"), + net_addr, + ); Ok(NetworkDeviceInfo { id: device.id, name: device.name, - assigned_ip: wireguard_device.wireguard_ip, + // TODO(jck) multiple IPs + assigned_ip: *wireguard_device + .wireguard_ip + .first() + .expect("missing NetworkDevice IP"), description: device.description, added_by: added_by.username, added_date: device.created, @@ -630,7 +642,12 @@ pub(crate) async fn add_network_device( let template_locations = vec![TemplateLocation { name: config.network_name.clone(), - assigned_ip: config.address.to_string(), + assigned_ip: config + .address + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","), }]; send_new_device_added_email( @@ -709,9 +726,16 @@ pub async fn modify_network_device( device.save(&mut *transaction).await?; // IP address has changed, so remove device from network and add it again with new IP address. - if new_ip != wireguard_network_device.wireguard_ip { + // TODO(jck) implement for multiple addresses + if new_ip + != *wireguard_network_device + .wireguard_ip + .first() + .expect("missing NetworkDevice IP") + { check_ip(new_ip, &device_network, &mut transaction).await?; - wireguard_network_device.wireguard_ip = new_ip; + // TODO(jck) + wireguard_network_device.wireguard_ip = vec![new_ip]; wireguard_network_device.update(&mut *transaction).await?; let device_info = DeviceInfo::from_device(&mut *transaction, device.clone()).await?; appstate.send_wireguard_event(GatewayEvent::DeviceModified(device_info)); @@ -733,7 +757,13 @@ pub async fn modify_network_device( "User {} changed IP address of network device {} from {} to {new_ip} in network {}", session.user.username, device.name, - wireguard_network_device.wireguard_ip, + // TODO(jck) + wireguard_network_device + .wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","), device_network.name ); } diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 0f32171541..c8c8b8982c 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -637,7 +637,7 @@ pub(crate) async fn add_device( ))); } - // save device + // save the device let mut transaction = appstate.pool.begin().await?; let device = Device::new( add_device.name, @@ -652,11 +652,6 @@ pub(crate) async fn add_device( let (network_info, configs) = device.add_to_all_networks(&mut transaction).await?; - let mut network_ips: Vec = Vec::new(); - for network_info_item in network_info.clone() { - network_ips.push(network_info_item.device_wireguard_ip.to_string()); - } - appstate.send_wireguard_event(GatewayEvent::DeviceCreated(DeviceInfo { device: device.clone(), network_info: network_info.clone(), @@ -668,7 +663,12 @@ pub(crate) async fn add_device( .iter() .map(|c| TemplateLocation { name: c.network_name.clone(), - assigned_ip: c.address.to_string(), + assigned_ip: c + .address + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","), }) .collect(); diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index a781dfe720..f89e6a3be5 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -1,5 +1,7 @@ pub mod common; +use std::net::IpAddr; + use claims::assert_err; use defguard::{ db::{models::device::DeviceType, Device, GatewayEvent, Group, Id, User, WireguardNetwork}, @@ -392,7 +394,12 @@ async fn test_import_network_existing_devices() { assert_eq!(device_info.network_info.len(), 1); assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( - device_info.network_info[0].device_wireguard_ip.to_string(), + device_info.network_info[0] + .device_wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","), peers[1].allowed_ips[0] ); @@ -403,7 +410,12 @@ async fn test_import_network_existing_devices() { assert_eq!(device_info.network_info.len(), 1); assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( - device_info.network_info[0].device_wireguard_ip.to_string(), + device_info.network_info[0] + .device_wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect::>() + .join(","), peers[0].allowed_ips[0] ); @@ -497,7 +509,8 @@ PersistentKeepalive = 300 assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( device_info.network_info[0].device_wireguard_ip, - mapped_devices[0].wireguard_ip + // TODO(jck) + vec![mapped_devices[0].wireguard_ip], ); let GatewayEvent::DeviceCreated(device_info) = wg_rx.try_recv().unwrap() else { @@ -511,7 +524,7 @@ PersistentKeepalive = 300 assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( device_info.network_info[0].device_wireguard_ip, - mapped_devices[1].wireguard_ip + vec![mapped_devices[1].wireguard_ip], ); assert_err!(wg_rx.try_recv()); From f804e2a96570756120407496dd97e0e038494cf2 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 7 Apr 2025 12:38:11 +0200 Subject: [PATCH 05/71] CommaSeparated helper trait --- src/db/models/device.rs | 32 ++++---------------- src/grpc/enrollment.rs | 27 +++-------------- src/grpc/gateway.rs | 7 ++--- src/grpc/utils.rs | 36 ++++------------------- src/handlers/network_devices.rs | 13 ++------ src/handlers/wireguard.rs | 7 ++--- src/lib.rs | 18 ++++++++++++ tests/wireguard_network_allowed_groups.rs | 11 ++----- 8 files changed, 43 insertions(+), 108 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 399a5b685d..707a0dbc33 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -25,7 +25,7 @@ use super::{ }; use crate::{ db::{Id, NoId, User}, - KEY_LENGTH, + CommaSeparated, KEY_LENGTH, }; #[derive(Serialize, ToSchema)] @@ -301,15 +301,6 @@ impl WireguardNetworkDevice { .collect() } - #[must_use] - pub(crate) fn ips_to_string(&self) -> String { - self.wireguard_ip - .iter() - .map(ToString::to_string) - .collect::>() - .join(",") - } - pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), SqlxError> where E: PgExecutor<'e>, @@ -576,15 +567,7 @@ impl Device { let allowed_ips = if network.allowed_ips.is_empty() { String::new() } else { - format!( - "AllowedIPs = {}\n", - network - .allowed_ips - .iter() - .map(IpNetwork::to_string) - .collect::>() - .join(",") - ) + format!("AllowedIPs = {}\n", network.allowed_ips.comma_separated()) }; format!( @@ -598,7 +581,7 @@ impl Device { {allowed_ips}\ Endpoint = {}:{}\n\ PersistentKeepalive = 300", - wireguard_network_device.ips_to_string(), + wireguard_network_device.wireguard_ip.comma_separated(), network.pubkey, network.endpoint, network.port, @@ -778,7 +761,7 @@ impl Device { { debug!( "Assigned IPs {} for device {} (user {}) in network {network}", - wireguard_network_device.ips_to_string(), + wireguard_network_device.wireguard_ip.comma_separated(), self.name, self.user_id ); @@ -1074,12 +1057,7 @@ mod test { .await .unwrap(); assert_eq!( - wireguard_network_device - .wireguard_ip - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","), + wireguard_network_device.wireguard_ip.comma_separated(), "10.1.1.2" ); diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index f44e8fb72e..707a7929ce 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -1,6 +1,3 @@ -use std::net::IpAddr; - -use ipnetwork::IpNetwork; use sqlx::{PgPool, Transaction}; use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; use tonic::Status; @@ -32,6 +29,7 @@ use crate::{ mail::Mail, server_config, templates::{self, TemplateLocation}, + CommaSeparated, }; pub(super) struct EnrollmentServer { @@ -633,12 +631,7 @@ impl EnrollmentServer { .iter() .map(|c| TemplateLocation { name: c.network_name.clone(), - assigned_ip: c - .address - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","), + assigned_ip: c.address.comma_separated(), }) .collect(); @@ -725,26 +718,14 @@ impl InitialUserInfo { impl From for ProtoDeviceConfig { fn from(config: DeviceConfig) -> Self { - let allowed_ips = config - .allowed_ips - .iter() - .map(IpNetwork::to_string) - .collect::>() - .join(","); - let assigned_ip = config - .address - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","); Self { network_id: config.network_id, network_name: config.network_name, config: config.config, endpoint: config.endpoint, - assigned_ip, + assigned_ip: config.address.comma_separated(), pubkey: config.pubkey, - allowed_ips, + allowed_ips: config.allowed_ips.comma_separated(), dns: config.dns, mfa_enabled: config.mfa_enabled, keepalive_interval: config.keepalive_interval, diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index c2dac10a76..b0636ebe79 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -28,6 +28,7 @@ use crate::{ Device, GatewayEvent, Id, NoId, }, mail::Mail, + CommaSeparated, }; /// Sends given `GatewayEvent` to be handled by gateway GRPC server @@ -320,12 +321,10 @@ impl GatewayUpdatesHandler { self.send_peer_update( Peer { pubkey: device.device.wireguard_pubkey, + // TODO(jck) allowed_ips: vec![network_info .device_wireguard_ip - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(",")], + .comma_separated()], preshared_key: network_info.preshared_key.clone(), keepalive_interval: Some( self.network.keepalive_interval as u32, diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs index a4b111c4d5..a68a9876a0 100644 --- a/src/grpc/utils.rs +++ b/src/grpc/utils.rs @@ -1,6 +1,3 @@ -use std::net::IpAddr; - -use ipnetwork::IpNetwork; use sqlx::PgPool; use tonic::Status; @@ -18,6 +15,7 @@ use crate::{ Device, Id, Settings, User, }, enterprise::db::models::enterprise_settings::EnterpriseSettings, + CommaSeparated, }; // Create a new token for configuration polling. @@ -111,26 +109,14 @@ pub(crate) async fn build_device_config_response( ); Status::internal(format!("unexpected error: {err}")) })?; - let allowed_ips = network - .allowed_ips - .iter() - .map(IpNetwork::to_string) - .collect::>() - .join(","); - let assigned_ip = wireguard_network_device - .wireguard_ip - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","); let config = ProtoDeviceConfig { config: Device::create_config(&network, &wireguard_network_device), network_id: network.id, network_name: network.name, - assigned_ip, + assigned_ip: wireguard_network_device.wireguard_ip.comma_separated(), endpoint: format!("{}:{}", network.endpoint, network.port), pubkey: network.pubkey, - allowed_ips, + allowed_ips: network.allowed_ips.comma_separated(), dns: network.dns, mfa_enabled: network.mfa_enabled, keepalive_interval: network.keepalive_interval, @@ -150,26 +136,14 @@ pub(crate) async fn build_device_config_response( Status::internal(format!("unexpected error: {err}")) })?; if let Some(wireguard_network_device) = wireguard_network_device { - let allowed_ips = network - .allowed_ips - .iter() - .map(IpNetwork::to_string) - .collect::>() - .join(","); - let assigned_ip = wireguard_network_device - .wireguard_ip - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","); let config = ProtoDeviceConfig { config: Device::create_config(&network, &wireguard_network_device), network_id: network.id, network_name: network.name, - assigned_ip, + assigned_ip: wireguard_network_device.wireguard_ip.comma_separated(), endpoint: format!("{}:{}", network.endpoint, network.port), pubkey: network.pubkey, - allowed_ips, + allowed_ips: network.allowed_ips.comma_separated(), dns: network.dns, mfa_enabled: network.mfa_enabled, keepalive_interval: network.keepalive_interval, diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index a3b8801559..a09104e31e 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -24,6 +24,7 @@ use crate::{ handlers::mail::send_new_device_added_email, server_config, templates::TemplateLocation, + CommaSeparated, }; #[derive(Serialize)] @@ -642,12 +643,7 @@ pub(crate) async fn add_network_device( let template_locations = vec![TemplateLocation { name: config.network_name.clone(), - assigned_ip: config - .address - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","), + assigned_ip: config.address.comma_separated(), }]; send_new_device_added_email( @@ -760,10 +756,7 @@ pub async fn modify_network_device( // TODO(jck) wireguard_network_device .wireguard_ip - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","), + .comma_separated(), device_network.name ); } diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index c8c8b8982c..5fe38abee0 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -18,6 +18,7 @@ use uuid::Uuid; use super::{device_for_admin_or_self, user_for_admin_or_self, ApiResponse, ApiResult, WebError}; use crate::{ + CommaSeparated, appstate::AppState, auth::{AdminRole, Claims, ClaimsType, SessionInfo}, db::{ @@ -664,11 +665,7 @@ pub(crate) async fn add_device( .map(|c| TemplateLocation { name: c.network_name.clone(), assigned_ip: c - .address - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","), + .address.comma_separated() }) .collect(); diff --git a/src/lib.rs b/src/lib.rs index b2a35004bf..c841e67bcd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -773,3 +773,21 @@ pub async fn init_vpn_location( Ok(token) } + +pub trait CommaSeparated { + fn comma_separated(&self) -> String; +} + +impl CommaSeparated for I +where + I: ?Sized + std::iter::IntoIterator, + for<'a> &'a I: IntoIterator, + T: ToString, +{ + fn comma_separated(&self) -> String { + self.into_iter() + .map(ToString::to_string) + .collect::>() + .join(",") + } +} diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index f89e6a3be5..0d86d2be5e 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -6,6 +6,7 @@ use claims::assert_err; use defguard::{ db::{models::device::DeviceType, Device, GatewayEvent, Group, Id, User, WireguardNetwork}, handlers::{wireguard::ImportedNetworkData, Auth}, + CommaSeparated, }; use matches::assert_matches; use reqwest::StatusCode; @@ -396,10 +397,7 @@ async fn test_import_network_existing_devices() { assert_eq!( device_info.network_info[0] .device_wireguard_ip - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","), + .comma_separated(), peers[1].allowed_ips[0] ); @@ -412,10 +410,7 @@ async fn test_import_network_existing_devices() { assert_eq!( device_info.network_info[0] .device_wireguard_ip - .iter() - .map(IpAddr::to_string) - .collect::>() - .join(","), + .comma_separated(), peers[0].allowed_ips[0] ); From 03ec04bfeb71d4c10b3e77be2265ed7f4188f51c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 7 Apr 2025 12:41:33 +0200 Subject: [PATCH 06/71] linting --- src/db/models/device.rs | 8 ++++---- src/db/models/wireguard.rs | 8 ++++---- src/enterprise/firewall.rs | 20 +++++++++++++++----- src/handlers/network_devices.rs | 6 ++---- src/handlers/wireguard.rs | 5 ++--- tests/wireguard_network_allowed_groups.rs | 1 - web/.vite/deps/_metadata.json | 8 ++++++++ web/.vite/deps/package.json | 3 +++ 8 files changed, 38 insertions(+), 21 deletions(-) create mode 100644 web/.vite/deps/_metadata.json create mode 100644 web/.vite/deps/package.json diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 707a0dbc33..18aed635a9 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -297,7 +297,7 @@ impl WireguardNetworkDevice { pub(crate) fn ips_as_network(&self) -> Vec { self.wireguard_ip .iter() - .map(|ip| IpNetwork::from(ip.clone())) + .map(|ip| IpNetwork::from(*ip)) .collect() } @@ -889,7 +889,7 @@ impl Device { { info!("Assigned IP: {ip} for device: {}", self.name); let wireguard_network_device = - WireguardNetworkDevice::new(network.id, self.id, &[ip]); + WireguardNetworkDevice::new(network.id, self.id, [ip]); wireguard_network_device.insert(&mut *transaction).await?; return Ok(wireguard_network_device); } @@ -1021,7 +1021,7 @@ mod test { info!("Created device: {}", device.name); debug!("For user: {}", device.user_id); let wireguard_network_device = - WireguardNetworkDevice::new(network.id, device.id, &[ip]); + WireguardNetworkDevice::new(network.id, device.id, [ip]); wireguard_network_device.insert(pool).await?; info!( "Assigned IP: {ip} for device: {name} in network: {}", @@ -1179,7 +1179,7 @@ mod test { WireguardNetworkDevice::new( network.id, device4.id, - &[IpAddr::from_str("10.1.1.10").unwrap()], + [IpAddr::from_str("10.1.1.10").unwrap()], ) .insert(&mut *transaction) .await diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 74b9ee03e8..59c055b7e3 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -460,7 +460,7 @@ impl WireguardNetwork { device.device_id ); let wireguard_network_device = - WireguardNetworkDevice::new(self.id, device.device_id, &[ip]); + WireguardNetworkDevice::new(self.id, device.device_id, [ip]); wireguard_network_device.insert(&mut *transaction).await?; Ok(wireguard_network_device) } @@ -673,7 +673,7 @@ impl WireguardNetwork { self.id, existing_device.id, // TODO(jck) allow assignment of multiple ips for a network device - &[imported_device.wireguard_ip], + [imported_device.wireguard_ip], ); wireguard_network_device.insert(&mut *transaction).await?; // store ID of device with already generated config @@ -759,7 +759,7 @@ impl WireguardNetwork { let wireguard_network_device = WireguardNetworkDevice::new( self.id, device.id, - &[mapped_device.wireguard_ip], + [mapped_device.wireguard_ip], ); wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { @@ -777,7 +777,7 @@ impl WireguardNetwork { self.id, device.id, // TODO(jck) allow assignment of multiple ips for a network device - &[mapped_device.wireguard_ip], + [mapped_device.wireguard_ip], ); wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 048ec8ac24..9931b926bb 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -1507,7 +1507,12 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location.id, - wireguard_ip: IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], preshared_key: None, is_authorized: true, authorized_at: None, @@ -1608,7 +1613,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id, wireguard_network_id: location.id, - wireguard_ip: ip, + wireguard_ip: vec![ip], preshared_key: None, is_authorized: true, authorized_at: None, @@ -2127,7 +2132,12 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_1.id, - wireguard_ip: IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], preshared_key: None, is_authorized: true, authorized_at: None, @@ -2136,12 +2146,12 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_2.id, - wireguard_ip: IpAddr::V4(Ipv4Addr::new( + wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( 10, 10, user.id as u8, device_num as u8, - )), + ))], preshared_key: None, is_authorized: true, authorized_at: None, diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index a09104e31e..18705ce947 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -78,7 +78,7 @@ impl NetworkDeviceInfo { )))?; // TODO(jck) deal with all ips let split_ip = split_ip( - &wireguard_device + wireguard_device .wireguard_ip .first() .expect("missing NetworkDevice IP"), @@ -754,9 +754,7 @@ pub async fn modify_network_device( session.user.username, device.name, // TODO(jck) - wireguard_network_device - .wireguard_ip - .comma_separated(), + wireguard_network_device.wireguard_ip.comma_separated(), device_network.name ); } diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 5fe38abee0..3b9a54c9d7 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -18,7 +18,6 @@ use uuid::Uuid; use super::{device_for_admin_or_self, user_for_admin_or_self, ApiResponse, ApiResult, WebError}; use crate::{ - CommaSeparated, appstate::AppState, auth::{AdminRole, Claims, ClaimsType, SessionInfo}, db::{ @@ -40,6 +39,7 @@ use crate::{ server_config, templates::TemplateLocation, wg_config::{parse_wireguard_config, ImportedDevice}, + CommaSeparated, }; /// Parse a string with comma-separated IP addresses. @@ -664,8 +664,7 @@ pub(crate) async fn add_device( .iter() .map(|c| TemplateLocation { name: c.network_name.clone(), - assigned_ip: c - .address.comma_separated() + assigned_ip: c.address.comma_separated(), }) .collect(); diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index 0d86d2be5e..db275ef92e 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -1,6 +1,5 @@ pub mod common; -use std::net::IpAddr; use claims::assert_err; use defguard::{ diff --git a/web/.vite/deps/_metadata.json b/web/.vite/deps/_metadata.json new file mode 100644 index 0000000000..ca2bb111f1 --- /dev/null +++ b/web/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "d569db5f", + "configHash": "15c06bd9", + "lockfileHash": "e3b0c442", + "browserHash": "6de547b8", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/web/.vite/deps/package.json b/web/.vite/deps/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/web/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} From 991085ffe5c534d7a14aa2c64b71234bfc9883b3 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 8 Apr 2025 10:40:24 +0200 Subject: [PATCH 07/71] Multi-address handling when creating new network device --- src/db/models/device.rs | 96 +++++++++++--- src/db/models/wireguard.rs | 10 +- src/handlers/network_devices.rs | 218 +++++++++++++++++++++----------- 3 files changed, 230 insertions(+), 94 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 18aed635a9..166f5b797a 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,4 +1,4 @@ -use std::{fmt, net::IpAddr}; +use std::{fmt, iter::zip, net::IpAddr}; use base64::{prelude::BASE64_STANDARD, Engine}; #[cfg(test)] @@ -698,11 +698,11 @@ impl Device { pub(crate) async fn add_to_network( &self, network: &WireguardNetwork, - ip: IpAddr, + ip: &[IpAddr], transaction: &mut PgConnection, ) -> Result<(DeviceNetworkInfo, DeviceConfig), DeviceError> { let wireguard_network_device = self - .assign_network_ip(&mut *transaction, network, ip) + .assign_network_ips(&mut *transaction, network, ip) .await?; let device_network_info = DeviceNetworkInfo { network_id: network.id, @@ -867,34 +867,96 @@ impl Device { /// Assigns specific IP address to the device in specified [`WireguardNetwork`]. /// This method is currently used only for network devices. For regular user - /// devices [`assign_next_network_ip`] method is used. - pub(crate) async fn assign_network_ip( + /// devices use [`assign_next_network_ip`] method. + pub(crate) async fn assign_network_ips( &self, transaction: &mut PgConnection, network: &WireguardNetwork, - // TODO(jck) allow assignment of multiple ips for a network device - ip: IpAddr, + ips: &[IpAddr], ) -> Result { - if let Some(network_address) = network.address.first() { + let networks = ips + .iter() + .map(|ip| { + network + .get_containing_network(*ip) + .ok_or(ModelError::CannotCreate) + }) + .collect::, ModelError>>()?; + // let networks = ips + // .iter() + // .map(|ip| network.get_containing_network(*ip)) + // .collect::>>(); + // // make sure network contains all provided ips + // if networks.iter().any(|net| net.is_none()) { + // return Err(ModelError::CannotCreate); + // } + for (ip, network_address) in zip(ips, networks) { + // validate ip address let net_ip = network_address.ip(); let net_network = network_address.network(); let net_broadcast = network_address.broadcast(); - if ip == net_ip || ip == net_network || ip == net_broadcast { + if *ip == net_ip || *ip == net_network || *ip == net_broadcast { return Err(ModelError::CannotCreate); } - if Self::find_by_ip(&mut *transaction, ip, network.id) + // make sure the ip is unassigned + if Self::find_by_ip(&mut *transaction, *ip, network.id) .await? - .is_none() + .is_some() { - info!("Assigned IP: {ip} for device: {}", self.name); - let wireguard_network_device = - WireguardNetworkDevice::new(network.id, self.id, [ip]); - wireguard_network_device.insert(&mut *transaction).await?; - return Ok(wireguard_network_device); + return Err(ModelError::CannotCreate); } } - Err(ModelError::CannotCreate) + let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, ips); + wireguard_network_device.insert(&mut *transaction).await?; + info!("Assigned IPs: {ips:?} for device: {}", self.name); + return Ok(wireguard_network_device); + // make sure network contains all provided ips + // for ip in ips { + // let Some(network_address) = network.get_containing_network(ip) else { + // return Err(ModelError::CannotCreate); + // } + // let net_ip = network_address.ip(); + // let net_network = network_address.network(); + // let net_broadcast = network_address.broadcast(); + // if ips == net_ip || ips == net_network || ips == net_broadcast { + // return Err(ModelError::CannotCreate); + // } + + // if Self::find_by_ip(&mut *transaction, ip, network.id) + // .await? + // .is_none() + // { + // info!("Assigned IP: {ip} for device: {}", self.name); + // let wireguard_network_device = + // WireguardNetworkDevice::new(network.id, self.id, ips); + // wireguard_network_device.insert(&mut *transaction).await?; + // return Ok(wireguard_network_device); + // } else { + + // return Err(ModelError::CannotCreate); + // } + // } + // for network_address in network.address { + // let net_ip = network_address.ip(); + // let net_network = network_address.network(); + // let net_broadcast = network_address.broadcast(); + // if ips == net_ip || ips == net_network || ips == net_broadcast { + // return Err(ModelError::CannotCreate); + // } + + // if Self::find_by_ip(&mut *transaction, ips, network.id) + // .await? + // .is_none() + // { + // info!("Assigned IP: {ips} for device: {}", self.name); + // let wireguard_network_device = + // WireguardNetworkDevice::new(network.id, self.id, ips); + // wireguard_network_device.insert(&mut *transaction).await?; + // return Ok(wireguard_network_device); + // } + // } + // Err(ModelError::CannotCreate) } /// Gets the first network of the network device diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 59c055b7e3..ee92cc0acb 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -459,20 +459,24 @@ impl WireguardNetwork { "Adding network device {} with IP {ip} to network {self}", device.device_id ); - let wireguard_network_device = - WireguardNetworkDevice::new(self.id, device.device_id, [ip]); + let wireguard_network_device = WireguardNetworkDevice::new(self.id, device.device_id, [ip]); wireguard_network_device.insert(&mut *transaction).await?; Ok(wireguard_network_device) } /// Checks if all device addresses are contained in at least one of the network addresses - fn contains_all(&self, addresses: &[IpAddr]) -> bool { + pub fn contains_all(&self, addresses: &[IpAddr]) -> bool { !self .address .iter() .any(|net| !addresses.iter().any(|addr| net.contains(*addr))) } + /// Finds [`IpNetwork`] containing given [`IpAddr`] + pub fn get_containing_network(&self, addr: IpAddr) -> Option { + self.address.iter().find(|net| net.contains(addr)).copied() + } + /// Works out which devices need to be added, removed, or readdressed /// based on the list of currently configured devices and the list of devices which should be allowed async fn process_device_access_changes( diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 18705ce947..8e8be38b7c 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -1,5 +1,6 @@ use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr}, + iter::zip, + net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr}, str::FromStr, }; @@ -37,15 +38,14 @@ struct NetworkDeviceLocation { struct NetworkDeviceInfo { id: Id, name: String, - // TODO(jck) mulitple ips - assigned_ip: IpAddr, + assigned_ips: Vec, description: Option, added_by: String, added_date: NaiveDateTime, location: NetworkDeviceLocation, wireguard_pubkey: String, configured: bool, - split_ip: SplitIP, + split_ips: Vec, } impl NetworkDeviceInfo { @@ -77,21 +77,22 @@ impl NetworkDeviceInfo { network.name )))?; // TODO(jck) deal with all ips - let split_ip = split_ip( - wireguard_device - .wireguard_ip - .first() - .expect("missing NetworkDevice IP"), - net_addr, - ); + // let split_ip = split_ip( + // wireguard_device + // .wireguard_ip + // .first() + // .expect("missing NetworkDevice IP"), + // net_addr, + // ); + let split_ips = wireguard_device + .wireguard_ip + .iter() + .map(|ip| split_ip(ip, net_addr)) + .collect::>(); Ok(NetworkDeviceInfo { id: device.id, name: device.name, - // TODO(jck) multiple IPs - assigned_ip: *wireguard_device - .wireguard_ip - .first() - .expect("missing NetworkDevice IP"), + assigned_ips: wireguard_device.wireguard_ip, description: device.description, added_by: added_by.username, added_date: device.created, @@ -101,7 +102,7 @@ impl NetworkDeviceInfo { name: network.name, }, configured: device.configured, - split_ip, + split_ips, }) } } @@ -204,7 +205,7 @@ pub struct AddNetworkDevice { pub name: String, pub description: Option, pub location_id: i64, - pub assigned_ip: String, + pub assigned_ips: Vec, pub wireguard_pubkey: String, } @@ -214,37 +215,87 @@ pub struct AddNetworkDeviceResult { device: NetworkDeviceInfo, } -/// Checks if the IP address falls into the range of the network -/// and if it is not already assigned to another device. -async fn check_ip( - ip_addr: IpAddr, +/// Checks if the IP addresses fall into the range of the network +/// and if they are not already assigned to another device. +async fn check_ips( + ip_addrs: &[IpAddr], network: &WireguardNetwork, transaction: &mut PgConnection, ) -> Result<(), WebError> { - if let Some(network_address) = network.address.first() { - if !network_address.contains(ip_addr) { - return Err(WebError::BadRequest(format!( - "Provided IP address {ip_addr} is not in the network ({}) range {network_address}", - network.name, - ))); - } - if ip_addr == network_address.network() || ip_addr == network_address.broadcast() { + // if let Some(network_address) = network.address.first() { + // if !network_address.contains(ip_addr) { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip_addr} is not in the network ({}) range {network_address}", + // network.name, + // ))); + // } + // if ip_addr == network_address.network() || ip_addr == network_address.broadcast() { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip_addr} is network or broadcast address of network {}", + // network.name + // ))); + // } + // if ip_addr == network_address.ip() { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip_addr} may overlap with the network's gateway IP in network {}", + // network.name + // ))); + // } + + // let device = Device::find_by_ip(transaction, ip_addr, network.id).await?; + // if let Some(device) = device { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip_addr} is already assigned to device {} in network {}", + // device.name, network.name + // ))); + // } + // } + + let networks = ip_addrs + .iter() + .map(|ip| network.get_containing_network(*ip).ok_or(())) + .collect::, ()>>() + .map_err(|_| { + WebError::BadRequest(format!( + // "Provided IP address {ip_addrs} is not in the network ({}) range {network_address}", + "Provided IP addresses {ip_addrs:?} are not in the network ({}) range {:?}", + network.name, network.address, + )) + })?; + // if !network.contains_all(ip_addrs) { + // return Err(WebError::BadRequest(format!( + // // "Provided IP address {ip_addrs} is not in the network ({}) range {network_address}", + // "Provided IP addresses {ip_addrs} are not in the network ({}) range {:?}", + // network.name, network.address, + // ))); + // } + for (ip, network_address) in zip(ip_addrs, networks) { + // if !network_address.contains(ip_addrs) { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip_addrs} is not in the network ({}) range {network_address}", + // network.name, + // ))); + // } + let net_ip = network_address.ip(); + let net_network = network_address.network(); + let net_broadcast = network_address.broadcast(); + if *ip == net_network || *ip == net_broadcast { return Err(WebError::BadRequest(format!( - "Provided IP address {ip_addr} is network or broadcast address of network {}", + "Provided IP address {ip} is network or broadcast address of network {}", network.name ))); } - if ip_addr == network_address.ip() { + if *ip == net_ip { return Err(WebError::BadRequest(format!( - "Provided IP address {ip_addr} may overlap with the network's gateway IP in network {}", + "Provided IP address {ip} may overlap with the network's gateway IP {net_ip} in network {}", network.name ))); } - let device = Device::find_by_ip(transaction, ip_addr, network.id).await?; + let device = Device::find_by_ip(&mut *transaction, *ip, network.id).await?; if let Some(device) = device { return Err(WebError::BadRequest(format!( - "Provided IP address {ip_addr} is already assigned to device {} in network {}", + "Provided IP address {ip} is already assigned to device {} in network {}", device.name, network.name ))); } @@ -404,7 +455,7 @@ pub struct StartNetworkDeviceSetup { name: String, description: Option, location_id: i64, - assigned_ip: String, + assigned_ips: Vec, } // Setup a network device to be later configured by a CLI client @@ -453,18 +504,30 @@ pub(crate) async fn start_network_device_setup( device.id ); - let ip: IpAddr = setup_start.assigned_ip.parse().map_err(|e| { - error!("Failed to add network device {device_name}, invalid IP address: {e}"); - WebError::BadRequest("Invalid IP address".to_string()) - })?; - check_ip(ip, &network, &mut transaction).await?; + // let ips: IpAddr = setup_start.assigned_ips.parse().map_err(|e| { + // error!("Failed to add network device {device_name}, invalid IP address: {e}"); + // WebError::BadRequest("Invalid IP address".to_string()) + // })?; + let ips = setup_start + .assigned_ips + .iter() + .map(|ip| IpAddr::from_str(ip)) + .collect::, AddrParseError>>() + .map_err(|e| { + let msg = + format!("Failed to add network device {device_name}, invalid IP address: {e}"); + error!(msg); + WebError::BadRequest(msg) + })?; + + check_ips(&ips, &network, &mut transaction).await?; let (_, config) = device - .add_to_network(&network, ip, &mut transaction) + .add_to_network(&network, &ips, &mut transaction) .await?; info!( - "User {} added a new unconfigured network device {device_name} with IP {ip} to network {}", + "User {} added a new unconfigured network device {device_name} with IPs {ips:?} to network {}", user.username, network.name ); @@ -616,14 +679,21 @@ pub(crate) async fn add_network_device( .save(&mut *transaction) .await?; - let ip: IpAddr = add_network_device.assigned_ip.parse().map_err(|e| { - error!("Failed to add network device {device_name}, invalid IP address: {e}"); - WebError::BadRequest("Invalid IP address".to_string()) - })?; - check_ip(ip, &network, &mut transaction).await?; + let ips = add_network_device + .assigned_ips + .iter() + .map(|ip| IpAddr::from_str(ip)) + .collect::, AddrParseError>>() + .map_err(|e| { + let msg = + format!("Failed to add network device {device_name}, invalid IP address: {e}"); + error!(msg); + WebError::BadRequest(msg) + })?; + check_ips(&ips, &network, &mut transaction).await?; let (network_info, config) = device - .add_to_network(&network, ip, &mut transaction) + .add_to_network(&network, &ips, &mut transaction) .await?; appstate.send_wireguard_event(GatewayEvent::DeviceCreated(DeviceInfo { @@ -678,7 +748,7 @@ pub(crate) async fn add_network_device( pub struct ModifyNetworkDevice { name: String, description: Option, - assigned_ip: String, + assigned_ips: Vec, } pub async fn modify_network_device( @@ -711,27 +781,27 @@ pub async fn modify_network_device( error!("Failed to update device {device_id}, device not found in any network"); WebError::ObjectNotFound(format!("Device {device_id} not found in any network")) })?; - let new_ip = IpAddr::from_str(&data.assigned_ip).map_err(|e| { - WebError::BadRequest(format!( - "Failed to update device {device_id}, invalid IP address: {e}" - )) - })?; + let new_ips = data + .assigned_ips + .iter() + .map(|ip| IpAddr::from_str(ip)) + .collect::, AddrParseError>>() + .map_err(|e| { + let msg = format!("Failed to update device {device_id}, invalid IP address: {e}"); + error!(msg); + WebError::BadRequest(msg) + })?; device.name = data.name; device.description = data.description; device.save(&mut *transaction).await?; // IP address has changed, so remove device from network and add it again with new IP address. - // TODO(jck) implement for multiple addresses - if new_ip - != *wireguard_network_device - .wireguard_ip - .first() - .expect("missing NetworkDevice IP") - { - check_ip(new_ip, &device_network, &mut transaction).await?; + // TODO(jck) order-insensitive comparison + if new_ips != *wireguard_network_device.wireguard_ip { + check_ips(&new_ips, &device_network, &mut transaction).await?; // TODO(jck) - wireguard_network_device.wireguard_ip = vec![new_ip]; + wireguard_network_device.wireguard_ip = new_ips.clone(); wireguard_network_device.update(&mut *transaction).await?; let device_info = DeviceInfo::from_device(&mut *transaction, device.clone()).await?; appstate.send_wireguard_event(GatewayEvent::DeviceModified(device_info)); @@ -750,7 +820,7 @@ pub async fn modify_network_device( } info!( - "User {} changed IP address of network device {} from {} to {new_ip} in network {}", + "User {} changed IP addresses of network device {} from {} to {new_ips:?} in network {}", session.user.username, device.name, // TODO(jck) @@ -769,7 +839,7 @@ pub async fn modify_network_device( } #[derive(Debug, Serialize)] -struct SplitIP { +struct SplitIp { network_part: String, modifiable_part: String, network_prefix: String, @@ -779,13 +849,13 @@ struct SplitIP { /// Splits the IP address (IPv4 or IPv6) into three parts: network part, modifiable part and prefix /// The network part is the part that can't be changed by the user. /// This is to display an IP address in the UI like this: 192.168.(1.1)/16, where the part in the parenthesis can be changed by the user. -// The algorithm works as follows: -// 1. Get the network address, last address and IP address segments, e.g. 192.1.1.1 would be [192, 1, 1, 1] -// 2. Iterate over the segments and compare the last address and network segments, as long as the current segments are equal, append the segment to the network part. -// If they are not equal, we found the first modifiable segment (one of the segments of an address that may change between hosts in the same network), -// append the rest of the segments to the modifiable part. -// 3. Join the segments with the delimiter and return the network part, modifiable part and the network prefix -fn split_ip(ip: &IpAddr, network: &IpNetwork) -> SplitIP { +/// The algorithm works as follows: +/// 1. Get the network address, last address and IP address segments, e.g. 192.1.1.1 would be [192, 1, 1, 1] +/// 2. Iterate over the segments and compare the last address and network segments, as long as the current segments are equal, append the segment to the network part. +/// If they are not equal, we found the first modifiable segment (one of the segments of an address that may change between hosts in the same network), +/// append the rest of the segments to the modifiable part. +/// 3. Join the segments with the delimiter and return the network part, modifiable part and the network prefix +fn split_ip(ip: &IpAddr, network: &IpNetwork) -> SplitIp { let network_addr = network.network(); let network_prefix = network.prefix(); @@ -843,7 +913,7 @@ fn split_ip(ip: &IpAddr, network: &IpNetwork) -> SplitIP { network_part.push_str(&format!("{formatted}{delimiter}")); } - SplitIP { + SplitIp { ip: ip.to_string(), network_part, modifiable_part, From bd3d796ed7088bc8ea7f57366ac4ec365fb5e24e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 8 Apr 2025 11:05:28 +0200 Subject: [PATCH 08/71] Cleanup --- src/db/models/device.rs | 87 +---------------------- src/handlers/network_devices.rs | 60 ++-------------- tests/wireguard_network_allowed_groups.rs | 1 - 3 files changed, 7 insertions(+), 141 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 166f5b797a..05b2b920fa 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -798,38 +798,9 @@ impl Device { network: &WireguardNetwork, reserved_ips: Option<&[IpAddr]>, ) -> Result { - // if let Some(address) = network.address.first() { - // let net_ip = address.ip(); - // let net_network = address.network(); - // let net_broadcast = address.broadcast(); - // for ip in address { - // if ip == net_ip || ip == net_network || ip == net_broadcast { - // continue; - // } - // if let Some(reserved_ips) = reserved_ips { - // if reserved_ips.contains(&ip) { - // continue; - // } - // } - - // // Break loop if IP is unassigned and return network device - // if Self::find_by_ip(&mut *transaction, ip, network.id) - // .await? - // .is_none() - // { - // info!("Assigned IP address {ip} for device: {}", self.name); - // let wireguard_network_device = - // WireguardNetworkDevice::new(network.id, self.id, ip); - // wireguard_network_device.insert(&mut *transaction).await?; - // return Ok(wireguard_network_device); - // } - // } - // } - // Err(ModelError::CannotCreate) let mut ips = Vec::new(); // Iterate over all network addresses and assign new IP for the device in each of them. for address in &network.address { - // TODO(jck) make sure all network.address addresses are from different network? let net_ip = address.ip(); let net_network = address.network(); let net_broadcast = address.broadcast(); @@ -874,6 +845,7 @@ impl Device { network: &WireguardNetwork, ips: &[IpAddr], ) -> Result { + // make sure the network contains all provided ips let networks = ips .iter() .map(|ip| { @@ -882,14 +854,6 @@ impl Device { .ok_or(ModelError::CannotCreate) }) .collect::, ModelError>>()?; - // let networks = ips - // .iter() - // .map(|ip| network.get_containing_network(*ip)) - // .collect::>>(); - // // make sure network contains all provided ips - // if networks.iter().any(|net| net.is_none()) { - // return Err(ModelError::CannotCreate); - // } for (ip, network_address) in zip(ips, networks) { // validate ip address let net_ip = network_address.ip(); @@ -907,56 +871,11 @@ impl Device { return Err(ModelError::CannotCreate); } } + // if all checks passed, assign the ips let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, ips); wireguard_network_device.insert(&mut *transaction).await?; info!("Assigned IPs: {ips:?} for device: {}", self.name); - return Ok(wireguard_network_device); - // make sure network contains all provided ips - // for ip in ips { - // let Some(network_address) = network.get_containing_network(ip) else { - // return Err(ModelError::CannotCreate); - // } - // let net_ip = network_address.ip(); - // let net_network = network_address.network(); - // let net_broadcast = network_address.broadcast(); - // if ips == net_ip || ips == net_network || ips == net_broadcast { - // return Err(ModelError::CannotCreate); - // } - - // if Self::find_by_ip(&mut *transaction, ip, network.id) - // .await? - // .is_none() - // { - // info!("Assigned IP: {ip} for device: {}", self.name); - // let wireguard_network_device = - // WireguardNetworkDevice::new(network.id, self.id, ips); - // wireguard_network_device.insert(&mut *transaction).await?; - // return Ok(wireguard_network_device); - // } else { - - // return Err(ModelError::CannotCreate); - // } - // } - // for network_address in network.address { - // let net_ip = network_address.ip(); - // let net_network = network_address.network(); - // let net_broadcast = network_address.broadcast(); - // if ips == net_ip || ips == net_network || ips == net_broadcast { - // return Err(ModelError::CannotCreate); - // } - - // if Self::find_by_ip(&mut *transaction, ips, network.id) - // .await? - // .is_none() - // { - // info!("Assigned IP: {ips} for device: {}", self.name); - // let wireguard_network_device = - // WireguardNetworkDevice::new(network.id, self.id, ips); - // wireguard_network_device.insert(&mut *transaction).await?; - // return Ok(wireguard_network_device); - // } - // } - // Err(ModelError::CannotCreate) + Ok(wireguard_network_device) } /// Gets the first network of the network device diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 8e8be38b7c..57b2387f90 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -76,14 +76,6 @@ impl NetworkDeviceInfo { "Failed to find the network address for network {}", network.name )))?; - // TODO(jck) deal with all ips - // let split_ip = split_ip( - // wireguard_device - // .wireguard_ip - // .first() - // .expect("missing NetworkDevice IP"), - // net_addr, - // ); let split_ips = wireguard_device .wireguard_ip .iter() @@ -222,66 +214,25 @@ async fn check_ips( network: &WireguardNetwork, transaction: &mut PgConnection, ) -> Result<(), WebError> { - // if let Some(network_address) = network.address.first() { - // if !network_address.contains(ip_addr) { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip_addr} is not in the network ({}) range {network_address}", - // network.name, - // ))); - // } - // if ip_addr == network_address.network() || ip_addr == network_address.broadcast() { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip_addr} is network or broadcast address of network {}", - // network.name - // ))); - // } - // if ip_addr == network_address.ip() { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip_addr} may overlap with the network's gateway IP in network {}", - // network.name - // ))); - // } - - // let device = Device::find_by_ip(transaction, ip_addr, network.id).await?; - // if let Some(device) = device { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip_addr} is already assigned to device {} in network {}", - // device.name, network.name - // ))); - // } - // } - + // make sure the network contains all provided ips let networks = ip_addrs .iter() .map(|ip| network.get_containing_network(*ip).ok_or(())) .collect::, ()>>() .map_err(|_| { WebError::BadRequest(format!( - // "Provided IP address {ip_addrs} is not in the network ({}) range {network_address}", "Provided IP addresses {ip_addrs:?} are not in the network ({}) range {:?}", network.name, network.address, )) })?; - // if !network.contains_all(ip_addrs) { - // return Err(WebError::BadRequest(format!( - // // "Provided IP address {ip_addrs} is not in the network ({}) range {network_address}", - // "Provided IP addresses {ip_addrs} are not in the network ({}) range {:?}", - // network.name, network.address, - // ))); - // } for (ip, network_address) in zip(ip_addrs, networks) { - // if !network_address.contains(ip_addrs) { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip_addrs} is not in the network ({}) range {network_address}", - // network.name, - // ))); - // } + // validate ip address within network let net_ip = network_address.ip(); let net_network = network_address.network(); let net_broadcast = network_address.broadcast(); if *ip == net_network || *ip == net_broadcast { return Err(WebError::BadRequest(format!( - "Provided IP address {ip} is network or broadcast address of network {}", + "Provided IP address {ip} is a network or broadcast address of network {}", network.name ))); } @@ -292,6 +243,7 @@ async fn check_ips( ))); } + // make sure the ip is unassigned let device = Device::find_by_ip(&mut *transaction, *ip, network.id).await?; if let Some(device) = device { return Err(WebError::BadRequest(format!( @@ -504,10 +456,6 @@ pub(crate) async fn start_network_device_setup( device.id ); - // let ips: IpAddr = setup_start.assigned_ips.parse().map_err(|e| { - // error!("Failed to add network device {device_name}, invalid IP address: {e}"); - // WebError::BadRequest("Invalid IP address".to_string()) - // })?; let ips = setup_start .assigned_ips .iter() diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index db275ef92e..0445d6a10e 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -1,6 +1,5 @@ pub mod common; - use claims::assert_err; use defguard::{ db::{models::device::DeviceType, Device, GatewayEvent, Group, Id, User, WireguardNetwork}, From 8383a0efefa4f1dfe59b979c9dbaa30f3b505e23 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 9 Apr 2025 07:32:02 +0200 Subject: [PATCH 09/71] update sqlx fixtures --- ...2d641a1bae1f10933c46ce68f14015fbdc80.json} | 8 ++--- ...65ee71d125c20933c6659b747cf5ccdc1d9a1.json | 2 +- ...5a0225ffe0171886e9e4c7b70346f1c37d42a.json | 2 +- ...0c7e14736e5d5e0943df2e20c7ab07c95744.json} | 8 ++--- ...6448ddc45502deb3eb9885b4df8f128cc3e2.json} | 8 ++--- ...b0b3c39a6b8c56740c6a82a19b60d216e295.json} | 8 ++--- ...312fa1812c93b402f40b915e0d262f546821.json} | 4 +-- ...a974dde20c858da0eb2773e59ed79697e49df.json | 2 +- ...4c5b48514cf99e25c5b9430da06f77a736bb.json} | 8 ++--- ...4b37c022ebca568e9af80d65730f450664313.json | 2 +- ...9c5f1109fe7b4ad1d8fe16a7898f38208873.json} | 8 ++--- ...b09f39004776d14c0b37fc9d7ac636b2b58ec.json | 2 +- ...7021d72d392f54aca70eb15522882eebc7d53.json | 35 +++++++++++++++++++ ...085a8a4a9ca555dfbe3b7eef0f043dcc557b2.json | 35 ------------------- 14 files changed, 66 insertions(+), 66 deletions(-) rename .sqlx/{query-42ea85e353deb1555b4e442a2fcdf366eb24bf7b907011a01313b77bed572176.json => query-10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80.json} (68%) rename .sqlx/{query-455cdf6e1284b773de2d44332b49b31030fe878f3001447d2c245b9502dff427.json => query-4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744.json} (73%) rename .sqlx/{query-4d43391c1eda0e6e74187d3c7ade0a852264d7465295de0223e00cf1f69c98c1.json => query-4b5f341ccfe73f789369e11c77ab6448ddc45502deb3eb9885b4df8f128cc3e2.json} (68%) rename .sqlx/{query-a74e2fda629b2b2916e3f034df84470cf7fb21a2f066559fad80a3d2396f9e21.json => query-7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295.json} (73%) rename .sqlx/{query-3d3c0799696f9c213982abe7f4633908aec2ddb747ad6e77e347365b4e61a8b0.json => query-97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821.json} (89%) rename .sqlx/{query-55568f51eda479e3cdaeefd641802ccf6cdcebe76c12cde524b162552b002d89.json => query-c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb.json} (74%) rename .sqlx/{query-fad6990b8d347099568fa0e867a30923a54812064de0b9331b2c71134e6ce29e.json => query-d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873.json} (74%) create mode 100644 .sqlx/query-decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53.json delete mode 100644 .sqlx/query-f6131bed08c9fb384b0c599e081085a8a4a9ca555dfbe3b7eef0f043dcc557b2.json diff --git a/.sqlx/query-42ea85e353deb1555b4e442a2fcdf366eb24bf7b907011a01313b77bed572176.json b/.sqlx/query-10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80.json similarity index 68% rename from .sqlx/query-42ea85e353deb1555b4e442a2fcdf366eb24bf7b907011a01313b77bed572176.json rename to .sqlx/query-10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80.json index 2df04febfc..911d425ba0 100644 --- a/.sqlx/query-42ea85e353deb1555b4e442a2fcdf366eb24bf7b907011a01313b77bed572176.json +++ b/.sqlx/query-10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1 AND device_id IN (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1 AND device_id IN (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", "describe": { "columns": [ { @@ -15,8 +15,8 @@ }, { "ordinal": 2, - "name": "wireguard_ip: IpAddr", - "type_info": "Inet" + "name": "wireguard_ip: Vec", + "type_info": "InetArray" }, { "ordinal": 3, @@ -49,5 +49,5 @@ true ] }, - "hash": "42ea85e353deb1555b4e442a2fcdf366eb24bf7b907011a01313b77bed572176" + "hash": "10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80" } diff --git a/.sqlx/query-12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1.json b/.sqlx/query-12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1.json index c2f369db0b..48feaa9d52 100644 --- a/.sqlx/query-12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1.json +++ b/.sqlx/query-12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1.json @@ -7,7 +7,7 @@ "Left": [ "Int8", "Int8", - "Inet", + "InetArray", "Bool", "Timestamp", "Text" diff --git a/.sqlx/query-21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a.json b/.sqlx/query-21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a.json index 1ac09e7027..3821a093db 100644 --- a/.sqlx/query-21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a.json +++ b/.sqlx/query-21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a.json @@ -21,7 +21,7 @@ { "ordinal": 3, "name": "device_wireguard_ip: IpAddr", - "type_info": "Inet" + "type_info": "InetArray" }, { "ordinal": 4, diff --git a/.sqlx/query-455cdf6e1284b773de2d44332b49b31030fe878f3001447d2c245b9502dff427.json b/.sqlx/query-4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744.json similarity index 73% rename from .sqlx/query-455cdf6e1284b773de2d44332b49b31030fe878f3001447d2c245b9502dff427.json rename to .sqlx/query-4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744.json index 59bb538b5f..d6a83c6cb3 100644 --- a/.sqlx/query-455cdf6e1284b773de2d44332b49b31030fe878f3001447d2c245b9502dff427.json +++ b/.sqlx/query-4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1", "describe": { "columns": [ { @@ -15,8 +15,8 @@ }, { "ordinal": 2, - "name": "wireguard_ip: IpAddr", - "type_info": "Inet" + "name": "wireguard_ip: Vec", + "type_info": "InetArray" }, { "ordinal": 3, @@ -48,5 +48,5 @@ true ] }, - "hash": "455cdf6e1284b773de2d44332b49b31030fe878f3001447d2c245b9502dff427" + "hash": "4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744" } diff --git a/.sqlx/query-4d43391c1eda0e6e74187d3c7ade0a852264d7465295de0223e00cf1f69c98c1.json b/.sqlx/query-4b5f341ccfe73f789369e11c77ab6448ddc45502deb3eb9885b4df8f128cc3e2.json similarity index 68% rename from .sqlx/query-4d43391c1eda0e6e74187d3c7ade0a852264d7465295de0223e00cf1f69c98c1.json rename to .sqlx/query-4b5f341ccfe73f789369e11c77ab6448ddc45502deb3eb9885b4df8f128cc3e2.json index e63c11d965..c693e5009f 100644 --- a/.sqlx/query-4d43391c1eda0e6e74187d3c7ade0a852264d7465295de0223e00cf1f69c98c1.json +++ b/.sqlx/query-4b5f341ccfe73f789369e11c77ab6448ddc45502deb3eb9885b4df8f128cc3e2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT wireguard_network_id network_id, wireguard_ip \"device_wireguard_ip: IpAddr\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT wireguard_network_id network_id, wireguard_ip \"device_wireguard_ip: Vec\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -10,8 +10,8 @@ }, { "ordinal": 1, - "name": "device_wireguard_ip: IpAddr", - "type_info": "Inet" + "name": "device_wireguard_ip: Vec", + "type_info": "InetArray" }, { "ordinal": 2, @@ -36,5 +36,5 @@ false ] }, - "hash": "4d43391c1eda0e6e74187d3c7ade0a852264d7465295de0223e00cf1f69c98c1" + "hash": "4b5f341ccfe73f789369e11c77ab6448ddc45502deb3eb9885b4df8f128cc3e2" } diff --git a/.sqlx/query-a74e2fda629b2b2916e3f034df84470cf7fb21a2f066559fad80a3d2396f9e21.json b/.sqlx/query-7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295.json similarity index 73% rename from .sqlx/query-a74e2fda629b2b2916e3f034df84470cf7fb21a2f066559fad80a3d2396f9e21.json rename to .sqlx/query-7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295.json index 6bc42a2bc4..d75da12aa1 100644 --- a/.sqlx/query-a74e2fda629b2b2916e3f034df84470cf7fb21a2f066559fad80a3d2396f9e21.json +++ b/.sqlx/query-7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", "describe": { "columns": [ { @@ -15,8 +15,8 @@ }, { "ordinal": 2, - "name": "wireguard_ip: IpAddr", - "type_info": "Inet" + "name": "wireguard_ip: Vec", + "type_info": "InetArray" }, { "ordinal": 3, @@ -49,5 +49,5 @@ true ] }, - "hash": "a74e2fda629b2b2916e3f034df84470cf7fb21a2f066559fad80a3d2396f9e21" + "hash": "7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295" } diff --git a/.sqlx/query-3d3c0799696f9c213982abe7f4633908aec2ddb747ad6e77e347365b4e61a8b0.json b/.sqlx/query-97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821.json similarity index 89% rename from .sqlx/query-3d3c0799696f9c213982abe7f4633908aec2ddb747ad6e77e347365b4e61a8b0.json rename to .sqlx/query-97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821.json index 83a5d73a87..32b807723a 100644 --- a/.sqlx/query-3d3c0799696f9c213982abe7f4633908aec2ddb747ad6e77e347365b4e61a8b0.json +++ b/.sqlx/query-97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT d.id, d.name, d.wireguard_pubkey, d.user_id, d.created, d.description, d.device_type \"device_type: DeviceType\", configured FROM device d JOIN wireguard_network_device wnd ON d.id = wnd.device_id WHERE wnd.wireguard_ip = $1 AND wnd.wireguard_network_id = $2", + "query": "SELECT d.id, d.name, d.wireguard_pubkey, d.user_id, d.created, d.description, d.device_type \"device_type: DeviceType\", configured FROM device d JOIN wireguard_network_device wnd ON d.id = wnd.device_id WHERE $1 = ANY(wnd.wireguard_ip) AND wnd.wireguard_network_id = $2", "describe": { "columns": [ { @@ -71,5 +71,5 @@ false ] }, - "hash": "3d3c0799696f9c213982abe7f4633908aec2ddb747ad6e77e347365b4e61a8b0" + "hash": "97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821" } diff --git a/.sqlx/query-bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df.json b/.sqlx/query-bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df.json index ae31f19fa5..5f594d0347 100644 --- a/.sqlx/query-bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df.json +++ b/.sqlx/query-bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df.json @@ -6,7 +6,7 @@ { "ordinal": 0, "name": "wireguard_ip: IpAddr", - "type_info": "Inet" + "type_info": "InetArray" } ], "parameters": { diff --git a/.sqlx/query-55568f51eda479e3cdaeefd641802ccf6cdcebe76c12cde524b162552b002d89.json b/.sqlx/query-c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb.json similarity index 74% rename from .sqlx/query-55568f51eda479e3cdaeefd641802ccf6cdcebe76c12cde524b162552b002d89.json rename to .sqlx/query-c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb.json index 819a0e66e0..89440a7fae 100644 --- a/.sqlx/query-55568f51eda479e3cdaeefd641802ccf6cdcebe76c12cde524b162552b002d89.json +++ b/.sqlx/query-c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -15,8 +15,8 @@ }, { "ordinal": 2, - "name": "wireguard_ip: IpAddr", - "type_info": "Inet" + "name": "wireguard_ip: Vec", + "type_info": "InetArray" }, { "ordinal": 3, @@ -48,5 +48,5 @@ true ] }, - "hash": "55568f51eda479e3cdaeefd641802ccf6cdcebe76c12cde524b162552b002d89" + "hash": "c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb" } diff --git a/.sqlx/query-c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313.json b/.sqlx/query-c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313.json index 8a450dac11..171e2ddda1 100644 --- a/.sqlx/query-c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313.json +++ b/.sqlx/query-c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313.json @@ -6,7 +6,7 @@ { "ordinal": 0, "name": "wireguard_ip: IpAddr", - "type_info": "Inet" + "type_info": "InetArray" } ], "parameters": { diff --git a/.sqlx/query-fad6990b8d347099568fa0e867a30923a54812064de0b9331b2c71134e6ce29e.json b/.sqlx/query-d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873.json similarity index 74% rename from .sqlx/query-fad6990b8d347099568fa0e867a30923a54812064de0b9331b2c71134e6ce29e.json rename to .sqlx/query-d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873.json index 0852c267c0..9c70740907 100644 --- a/.sqlx/query-fad6990b8d347099568fa0e867a30923a54812064de0b9331b2c71134e6ce29e.json +++ b/.sqlx/query-d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: IpAddr\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1", "describe": { "columns": [ { @@ -15,8 +15,8 @@ }, { "ordinal": 2, - "name": "wireguard_ip: IpAddr", - "type_info": "Inet" + "name": "wireguard_ip: Vec", + "type_info": "InetArray" }, { "ordinal": 3, @@ -48,5 +48,5 @@ true ] }, - "hash": "fad6990b8d347099568fa0e867a30923a54812064de0b9331b2c71134e6ce29e" + "hash": "d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873" } diff --git a/.sqlx/query-da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec.json b/.sqlx/query-da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec.json index 3ef5a4b8a6..9603aafaba 100644 --- a/.sqlx/query-da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec.json +++ b/.sqlx/query-da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec.json @@ -7,7 +7,7 @@ "Left": [ "Int8", "Int8", - "Inet", + "InetArray", "Bool", "Timestamp", "Text" diff --git a/.sqlx/query-decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53.json b/.sqlx/query-decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53.json new file mode 100644 index 0000000000..f0fe29a86e --- /dev/null +++ b/.sqlx/query-decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ip) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) AND d.configured = true AND u.is_active = true ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8", + "Bool" + ] + }, + "nullable": [ + false, + true, + null + ] + }, + "hash": "decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53" +} diff --git a/.sqlx/query-f6131bed08c9fb384b0c599e081085a8a4a9ca555dfbe3b7eef0f043dcc557b2.json b/.sqlx/query-f6131bed08c9fb384b0c599e081085a8a4a9ca555dfbe3b7eef0f043dcc557b2.json deleted file mode 100644 index 8f86869ba5..0000000000 --- a/.sqlx/query-f6131bed08c9fb384b0c599e081085a8a4a9ca555dfbe3b7eef0f043dcc557b2.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, preshared_key, array[host(wnd.wireguard_ip)] \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) AND d.configured = true AND u.is_active = true ORDER BY d.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pubkey", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "allowed_ips!: Vec", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Bool" - ] - }, - "nullable": [ - false, - true, - null - ] - }, - "hash": "f6131bed08c9fb384b0c599e081085a8a4a9ca555dfbe3b7eef0f043dcc557b2" -} From ffafdf72e3c14839b2192fd84eec98a86c65235c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 9 Apr 2025 07:33:31 +0200 Subject: [PATCH 10/71] fix tests, todos --- src/db/models/device.rs | 4 ++++ src/db/models/wireguard.rs | 1 - src/handlers/network_devices.rs | 2 ++ tests/wireguard_network_devices.rs | 10 ++++++---- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 05b2b920fa..398870c102 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -819,6 +819,7 @@ impl Device { .is_none() { ips.push(ip); + break; } } } @@ -860,6 +861,7 @@ impl Device { let net_network = network_address.network(); let net_broadcast = network_address.broadcast(); if *ip == net_ip || *ip == net_network || *ip == net_broadcast { + // TODO(jck) more relevant error return Err(ModelError::CannotCreate); } @@ -868,6 +870,7 @@ impl Device { .await? .is_some() { + // TODO(jck) more relevant error return Err(ModelError::CannotCreate); } } @@ -979,6 +982,7 @@ mod test { pubkey: String, network: &WireguardNetwork, ) -> Result<(Self, WireguardNetworkDevice), ModelError> { + // TODO(jck) if let Some(address) = network.address.first() { let net_ip = address.ip(); let net_network = address.network(); diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index ee92cc0acb..8003dd173e 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -353,7 +353,6 @@ impl WireguardNetwork { .await? } }; - Ok(devices) } diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 57b2387f90..9fd790b8a4 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -291,6 +291,7 @@ pub(crate) async fn check_ip_availability( }); }; + // TODO(jck) if let Some(network_address) = network.address.first() { if !network_address.contains(ip) { warn!( @@ -372,6 +373,7 @@ pub(crate) async fn find_available_ip( })?; let mut transaction = appstate.pool.begin().await?; + // TODO(jck) if let Some(network_address) = network.address.first() { let net_ip = network_address.ip(); let net_network = network_address.network(); diff --git a/tests/wireguard_network_devices.rs b/tests/wireguard_network_devices.rs index 939d8f371e..2dcefd9f40 100644 --- a/tests/wireguard_network_devices.rs +++ b/tests/wireguard_network_devices.rs @@ -90,11 +90,13 @@ async fn test_network_devices() { let response = client.get("/api/v1/device/network/ip/1").send().await; assert_eq!(response.status(), StatusCode::OK); let res = response.json::().await; - let ip = res["ip"].as_str().unwrap(); - let ip = ip.parse::().unwrap(); + let ips = res["ip"].as_str().unwrap(); + let ips: Vec = ips.split(",").map(|ip| ip.parse().unwrap()).collect(); let net_ip = IpAddr::from_str("10.1.1.1").unwrap(); let network_range = IpNetwork::new(net_ip, 24).unwrap(); - assert!(network_range.contains(ip)); + for ip in &ips { + assert!(network_range.contains(*ip)); + } // checking whether ip is valid/available let ip_check = json!( @@ -161,7 +163,7 @@ async fn test_network_devices() { let network_device = AddNetworkDevice { name: "device-1".into(), wireguard_pubkey: "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=".into(), - assigned_ip: ip.to_string(), + assigned_ips: ips.iter().map(IpAddr::to_string).collect(), location_id: 1, description: None, }; From 5a14b06813c43cf7275ffb445945f33b2132810f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 9 Apr 2025 09:06:48 +0200 Subject: [PATCH 11/71] Update protos --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 7cc38b099b..55184dccb5 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 7cc38b099bc12e8257d61988d162097606de4c8e +Subproject commit 55184dccb57b574e4b2cb85e038936b62679286b From c947ff59802c4cdf80226060e7d4964cd874c490 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 9 Apr 2025 12:43:01 +0200 Subject: [PATCH 12/71] wip separate firewall rules for ipv4 and ipv6 --- src/enterprise/firewall.rs | 198 ++++++++++++++++++++++++++++--------- 1 file changed, 150 insertions(+), 48 deletions(-) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 9931b926bb..067a51956d 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -59,6 +59,18 @@ pub async fn generate_firewall_rules_from_acls( // get network IPs for devices belonging to those users let user_device_ips = get_user_device_ips(&users, location_id, &mut *conn).await?; + // separate IPv4 and IPv6 user-device addresses + let user_device_ips = + user_device_ips + .iter() + .flatten() + .fold((Vec::new(), Vec::new()), |mut acc, ip| { + match ip { + IpAddr::V4(_) => acc.0.push(*ip), + IpAddr::V6(_) => acc.1.push(*ip), + }; + acc + }); // fetch allowed network devices let allowed_network_devices = acl.get_all_allowed_devices(&mut *conn).await?; @@ -71,13 +83,28 @@ pub async fn generate_firewall_rules_from_acls( get_source_network_devices(allowed_network_devices, denied_network_devices); let network_device_ips = get_network_device_ips(&network_devices, location_id, &mut *conn).await?; + // separate IPv4 and IPv6 network-device addresses + let network_device_ips = + network_device_ips + .iter() + .flatten() + .fold((Vec::new(), Vec::new()), |mut acc, ip| { + match ip { + IpAddr::V4(_) => acc.0.push(*ip), + IpAddr::V6(_) => acc.1.push(*ip), + }; + acc + }); // convert device IPs into source addresses for a firewall rule - let source_addrs = get_source_addrs(user_device_ips, network_device_ips, ip_version); + let ipv4_source_addrs = + get_source_addrs(user_device_ips.0, network_device_ips.0, IpVersion::Ipv4); + let ipv6_source_addrs = + get_source_addrs(user_device_ips.1, network_device_ips.1, IpVersion::Ipv6); // extract destination parameters from ACL rule let AclRuleInfo { - mut destination, + destination, destination_ranges, mut ports, mut protocols, @@ -85,73 +112,150 @@ pub async fn generate_firewall_rules_from_acls( .. } = acl; + // separate IPv4 and IPv6 destination addresses + let destination = destination + .iter() + .fold((Vec::new(), Vec::new()), |mut acc, ip| { + match ip { + IpNetwork::V4(_) => acc.0.push(*ip), + IpNetwork::V6(_) => acc.1.push(*ip), + }; + acc + }); + let destination_ranges = + destination_ranges + .iter() + .fold((Vec::new(), Vec::new()), |mut acc, range| { + // TODO(jck) make sure ranges only accept the same IP versions + match range.start { + IpAddr::V4(_) => acc.0.push(range.clone()), + IpAddr::V6(_) => acc.1.push(range.clone()), + }; + acc + }); + + // TODO(jck): handle aliases with separate alias.destination for IPv4 and IPv6 // store alias ranges separately since they use a different struct let mut alias_destination_ranges = Vec::new(); - - // process aliases by appending destination parameters from each of them to existing lists - for alias in aliases { - // fetch destination ranges for a fiven alias - alias_destination_ranges.extend(alias.get_destination_ranges(&mut *conn).await?); - - // extend existing parameter lists - destination.extend(alias.destination); - ports.extend( - alias - .ports - .into_iter() - .map(|port_range| port_range.into()) - .collect::>(), - ); - protocols.extend(alias.protocols); - } + // // process aliases by appending destination parameters from each of them to existing lists + // for alias in aliases { + // // fetch destination ranges for a fiven alias + // alias_destination_ranges.extend(alias.get_destination_ranges(&mut *conn).await?); + + // // extend existing parameter lists + // destination.0.extend(alias.destination); + // ports.extend( + // alias + // .ports + // .into_iter() + // .map(|port_range| port_range.into()) + // .collect::>(), + // ); + // protocols.extend(alias.protocols); + // } // prepare destination addresses - let destination_addrs = process_destination_addrs( - destination, - destination_ranges, + let ipv4_destination_addrs = process_destination_addrs( + destination.0, + destination_ranges.0, + // TODO(jck) ipv4 only + alias_destination_ranges.clone(), + IpVersion::Ipv4, + ); + + let ipv6_destination_addrs = process_destination_addrs( + destination.1, + destination_ranges.1, + // TODO(jck) ipv6 only alias_destination_ranges, - ip_version, + IpVersion::Ipv6, ); // prepare destination ports + // TODO(jck) what should happen to ports when we have separate rules for ipv4 and ipv6? let destination_ports = merge_port_ranges(ports); // remove duplicates protocol entries protocols.sort(); protocols.dedup(); + // create IPv4 ALLOW rule + // check if source addrs list is empty + if ipv4_source_addrs.is_empty() { + debug!("IPv4 source address list is empty. Skipping generating the ALLOW rule for this ACL"); + } else { + // prepare ALLOW rule for this ACL + let allow_rule = FirewallRule { + id: acl.id, + source_addrs: ipv4_source_addrs, + destination_addrs: ipv4_destination_addrs.clone(), + destination_ports: destination_ports.clone(), + protocols: protocols.clone(), + verdict: i32::from(FirewallPolicy::Allow), + comment: Some(format!("ACL {} - {} ALLOW", acl.id, acl.name)), + ip_version: i32::from(IpVersion::Ipv4), + }; + debug!("ALLOW rule generated from ACL: {allow_rule:?}"); + allow_rules.push(allow_rule); + }; + + // create IPv6 ALLOW rule // check if source addrs list is empty - if source_addrs.is_empty() { - debug!("Source address list is empty. Skipping generating the ALLOW rule for this ACL"); + if ipv6_source_addrs.is_empty() { + debug!("IPv6 source address list is empty. Skipping generating the ALLOW rule for this ACL"); } else { // prepare ALLOW rule for this ACL let allow_rule = FirewallRule { id: acl.id, - source_addrs, - destination_addrs: destination_addrs.clone(), + source_addrs: ipv6_source_addrs, + destination_addrs: ipv6_destination_addrs.clone(), destination_ports, protocols, verdict: i32::from(FirewallPolicy::Allow), comment: Some(format!("ACL {} - {} ALLOW", acl.id, acl.name)), + ip_version: i32::from(IpVersion::Ipv6), }; debug!("ALLOW rule generated from ACL: {allow_rule:?}"); allow_rules.push(allow_rule); }; - // prepare DENY rule for this ACL - // + // create IPv4 DENY rule // it should specify only the destination addrs to block all remaining traffic - let deny_rule = FirewallRule { - id: acl.id, - source_addrs: Vec::new(), - destination_addrs, - destination_ports: Vec::new(), - protocols: Vec::new(), - verdict: i32::from(FirewallPolicy::Deny), - comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), - }; - debug!("DENY rule generated from ACL: {deny_rule:?}"); - deny_rules.push(deny_rule) + if ipv4_destination_addrs.is_empty() { + debug!("IPv4 destination address list is empty. Skipping generating the DENY rule for this ACL"); + } else { + let deny_rule = FirewallRule { + id: acl.id, + source_addrs: Vec::new(), + destination_addrs: ipv4_destination_addrs, + destination_ports: Vec::new(), + protocols: Vec::new(), + verdict: i32::from(FirewallPolicy::Deny), + comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), + ip_version: i32::from(IpVersion::Ipv4), + }; + debug!("DENY rule generated from ACL: {deny_rule:?}"); + deny_rules.push(deny_rule); + } + + // create IPv6 DENY rule + // it should specify only the destination addrs to block all remaining traffic + if ipv6_destination_addrs.is_empty() { + debug!("IPv6 destination address list is empty. Skipping generating the DENY rule for this ACL"); + } else { + let deny_rule = FirewallRule { + id: acl.id, + source_addrs: Vec::new(), + destination_addrs: ipv6_destination_addrs, + destination_ports: Vec::new(), + protocols: Vec::new(), + verdict: i32::from(FirewallPolicy::Deny), + comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), + ip_version: i32::from(IpVersion::Ipv6), + }; + debug!("DENY rule generated from ACL: {deny_rule:?}"); + deny_rules.push(deny_rule); + } } // combine both rule lists @@ -172,18 +276,18 @@ fn get_source_users(allowed_users: Vec>, denied_users: Vec>) - } /// Fetches all IPs of devices belonging to specified users within a given location's VPN subnet. -// We specifically only fetch user devices since network devices are handled separately. +/// We specifically only fetch user devices since network devices are handled separately. async fn get_user_device_ips<'e, E: sqlx::PgExecutor<'e>>( users: &[User], location_id: Id, executor: E, -) -> Result, SqlxError> { - // prepeare a list of user IDs +) -> Result>, SqlxError> { + // prepare a list of user IDs let user_ids: Vec = users.iter().map(|user| user.id).collect(); // fetch network IPs query_scalar!( - "SELECT wireguard_ip \"wireguard_ip: IpAddr\" \ + "SELECT wireguard_ip \"wireguard_ip: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON d.id = wnd.device_id \ WHERE wnd.wireguard_network_id = $1 AND d.device_type = 'user'::device_type AND d.user_id = ANY($2)", @@ -215,13 +319,13 @@ async fn get_network_device_ips( network_devices: &[Device], location_id: Id, conn: &mut PgConnection, -) -> Result, SqlxError> { +) -> Result>, SqlxError> { // prepare a list of IDs let network_device_ids: Vec = network_devices.iter().map(|device| device.id).collect(); // fetch network IPs query_scalar!( - "SELECT wireguard_ip \"wireguard_ip: IpAddr\" \ + "SELECT wireguard_ip \"wireguard_ip: Vec\" \ FROM wireguard_network_device wnd \ WHERE wnd.wireguard_network_id = $1 AND wnd.device_id = ANY($2)", location_id, @@ -609,9 +713,7 @@ impl WireguardNetwork { let firewall_rules = generate_firewall_rules_from_acls(self.id, ip_version, location_acls, &mut *conn) .await?; - let firewall_config = FirewallConfig { - ip_version: ip_version.into(), default_policy: default_policy.into(), rules: firewall_rules, }; From d5f76863accc8a0cf8c6b0321f0b11e4c9c5640d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 9 Apr 2025 13:29:33 +0200 Subject: [PATCH 13/71] sqlx fixtures --- ...c1288db6952db20d0f1c6f82addd0ebfb93f9.json | 23 +++++++++++++++++++ ...a974dde20c858da0eb2773e59ed79697e49df.json | 23 ------------------- ...4b37c022ebca568e9af80d65730f450664313.json | 23 ------------------- ...30a0efb8018bbdb19dbc97b42b94142749df.json} | 6 ++--- ...764ac4f8f0ec59d43608fc3c34dc2c500a657.json | 23 +++++++++++++++++++ 5 files changed, 49 insertions(+), 49 deletions(-) create mode 100644 .sqlx/query-64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9.json delete mode 100644 .sqlx/query-bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df.json delete mode 100644 .sqlx/query-c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313.json rename .sqlx/{query-21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a.json => query-e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df.json} (71%) create mode 100644 .sqlx/query-e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657.json diff --git a/.sqlx/query-64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9.json b/.sqlx/query-64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9.json new file mode 100644 index 0000000000..8b92496c34 --- /dev/null +++ b/.sqlx/query-64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wireguard_ip \"wireguard_ip: Vec\" FROM wireguard_network_device wnd JOIN device d ON d.id = wnd.device_id WHERE wnd.wireguard_network_id = $1 AND d.device_type = 'user'::device_type AND d.user_id = ANY($2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wireguard_ip: Vec", + "type_info": "InetArray" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8Array" + ] + }, + "nullable": [ + false + ] + }, + "hash": "64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9" +} diff --git a/.sqlx/query-bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df.json b/.sqlx/query-bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df.json deleted file mode 100644 index 5f594d0347..0000000000 --- a/.sqlx/query-bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT wireguard_ip \"wireguard_ip: IpAddr\" FROM wireguard_network_device wnd JOIN device d ON d.id = wnd.device_id WHERE wnd.wireguard_network_id = $1 AND d.device_type = 'user'::device_type AND d.user_id = ANY($2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "wireguard_ip: IpAddr", - "type_info": "InetArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8Array" - ] - }, - "nullable": [ - false - ] - }, - "hash": "bbc08ad580dbb33fdafa20cdefda974dde20c858da0eb2773e59ed79697e49df" -} diff --git a/.sqlx/query-c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313.json b/.sqlx/query-c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313.json deleted file mode 100644 index 171e2ddda1..0000000000 --- a/.sqlx/query-c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT wireguard_ip \"wireguard_ip: IpAddr\" FROM wireguard_network_device wnd WHERE wnd.wireguard_network_id = $1 AND wnd.device_id = ANY($2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "wireguard_ip: IpAddr", - "type_info": "InetArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8Array" - ] - }, - "nullable": [ - false - ] - }, - "hash": "c9ccdf7be24dfe8c69f0f71242e4b37c022ebca568e9af80d65730f450664313" -} diff --git a/.sqlx/query-21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a.json b/.sqlx/query-e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df.json similarity index 71% rename from .sqlx/query-21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a.json rename to .sqlx/query-e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df.json index 3821a093db..c61eff65ab 100644 --- a/.sqlx/query-21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a.json +++ b/.sqlx/query-e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH stats AS ( SELECT DISTINCT ON (network) network, endpoint, latest_handshake FROM wireguard_peer_stats WHERE device_id = $2 ORDER BY network, collected_at DESC ) SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, wnd.wireguard_ip \"device_wireguard_ip: IpAddr\", stats.endpoint device_endpoint, stats.latest_handshake \"latest_handshake?\", COALESCE((NOW() - stats.latest_handshake) < $1, FALSE) \"is_active!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN stats ON n.id = stats.network WHERE wnd.device_id = $2", + "query": "WITH stats AS ( SELECT DISTINCT ON (network) network, endpoint, latest_handshake FROM wireguard_peer_stats WHERE device_id = $2 ORDER BY network, collected_at DESC ) SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, wnd.wireguard_ip \"device_wireguard_ip: Vec\", stats.endpoint device_endpoint, stats.latest_handshake \"latest_handshake?\", COALESCE((NOW() - stats.latest_handshake) < $1, FALSE) \"is_active!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN stats ON n.id = stats.network WHERE wnd.device_id = $2", "describe": { "columns": [ { @@ -20,7 +20,7 @@ }, { "ordinal": 3, - "name": "device_wireguard_ip: IpAddr", + "name": "device_wireguard_ip: Vec", "type_info": "InetArray" }, { @@ -55,5 +55,5 @@ null ] }, - "hash": "21fff77d228826b89bd78d27a5b5a0225ffe0171886e9e4c7b70346f1c37d42a" + "hash": "e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df" } diff --git a/.sqlx/query-e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657.json b/.sqlx/query-e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657.json new file mode 100644 index 0000000000..81955df3bf --- /dev/null +++ b/.sqlx/query-e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wireguard_ip \"wireguard_ip: Vec\" FROM wireguard_network_device wnd WHERE wnd.wireguard_network_id = $1 AND wnd.device_id = ANY($2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wireguard_ip: Vec", + "type_info": "InetArray" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8Array" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657" +} From 5709436fc5390d36b2dbdfd55e1a82b23decfe10 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 10 Apr 2025 07:45:20 +0200 Subject: [PATCH 14/71] Fix UserDevice::from_device sql query, remove WireguardNetwork::get_ip_version method --- src/db/models/device.rs | 4 ++-- src/enterprise/firewall.rs | 29 +++-------------------------- src/wg_config.rs | 1 + 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 398870c102..08b0e18474 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -208,7 +208,7 @@ impl UserDevice { ORDER BY network, collected_at DESC \ ) \ SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, \ - wnd.wireguard_ip \"device_wireguard_ip: IpAddr\", stats.endpoint device_endpoint, \ + wnd.wireguard_ip \"device_wireguard_ip: Vec\", stats.endpoint device_endpoint, \ stats.latest_handshake \"latest_handshake?\", \ COALESCE((NOW() - stats.latest_handshake) < $1, FALSE) \"is_active!\" \ FROM wireguard_network_device wnd \ @@ -238,7 +238,7 @@ impl UserDevice { network_id: r.network_id, network_name: r.network_name, network_gateway_ip: r.gateway_endpoint, - device_wireguard_ip: r.device_wireguard_ip.to_string(), + device_wireguard_ip: r.device_wireguard_ip.comma_separated(), last_connected_ip: device_ip, last_connected_location: None, last_connected_at: r.latest_handshake, diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 067a51956d..900689d4ec 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -36,11 +36,10 @@ pub enum FirewallError { /// end. This way we can avoid conflicts when some ACLs are overlapping. pub async fn generate_firewall_rules_from_acls( location_id: Id, - ip_version: IpVersion, acl_rules: Vec>, conn: &mut PgConnection, ) -> Result, FirewallError> { - debug!("Generating firewall rules for location {location_id} with IP version {ip_version:?}"); + debug!("Generating firewall rules for location {location_id}"); // initialize empty rules Vec let mut allow_rules = Vec::new(); let mut deny_rules = Vec::new(); @@ -651,6 +650,7 @@ impl WireguardNetwork { pub(crate) async fn get_active_acl_rules( &self, conn: &mut PgConnection, + // TODO(jck) maybe flatten here already? ) -> Result>, SqlxError> { debug!("Fetching active ACL rules for location {self}"); let rules: Vec> = query_as( @@ -703,15 +703,12 @@ impl WireguardNetwork { // fetch all active ACLs for location let location_acls = self.get_active_acl_rules(&mut *conn).await?; - // determine IP version based on location subnet - let ip_version = self.get_ip_version(); - let default_policy = match self.acl_default_allow { true => FirewallPolicy::Allow, false => FirewallPolicy::Deny, }; let firewall_rules = - generate_firewall_rules_from_acls(self.id, ip_version, location_acls, &mut *conn) + generate_firewall_rules_from_acls(self.id, location_acls, &mut *conn) .await?; let firewall_config = FirewallConfig { default_policy: default_policy.into(), @@ -722,22 +719,6 @@ impl WireguardNetwork { Ok(Some(firewall_config)) } - fn get_ip_version(&self) -> IpVersion { - // get the subnet from which device IPs are being assigned - // by default only the first configured subnet is being used - let vpn_subnet = self - .address - .first() - .expect("WireguardNetwork must have an address"); - - let ip_version = match vpn_subnet { - IpNetwork::V4(_ipv4_network) => IpVersion::Ipv4, - IpNetwork::V6(_ipv6_network) => IpVersion::Ipv6, - }; - debug!("VPN subnet {vpn_subnet:?} for location {self} has IP version {ip_version:?}"); - - ip_version - } } #[cfg(test)] @@ -1832,10 +1813,6 @@ mod test { generated_firewall_config.default_policy, i32::from(FirewallPolicy::Allow) ); - assert_eq!( - generated_firewall_config.ip_version, - i32::from(IpVersion::Ipv4) - ); let generated_firewall_rules = generated_firewall_config.rules; diff --git a/src/wg_config.rs b/src/wg_config.rs index 911de66508..94deb6a814 100644 --- a/src/wg_config.rs +++ b/src/wg_config.rs @@ -20,6 +20,7 @@ pub struct ImportedDevice { pub user_id: Option, pub name: String, pub wireguard_pubkey: String, + // TODO(jck) pub wireguard_ip: IpAddr, } From 34ed6618352063207d281fec292174edcef8813b Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 10 Apr 2025 07:53:02 +0200 Subject: [PATCH 15/71] update protos --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 55184dccb5..f2f2e11738 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 55184dccb57b574e4b2cb85e038936b62679286b +Subproject commit f2f2e11738df9f75e43cfcb6cf5b015056255d67 From c575d988042544afa6d23e6a9659b24801ad3ad0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 10 Apr 2025 11:31:06 +0200 Subject: [PATCH 16/71] create_rules helper function --- src/enterprise/firewall.rs | 493 ++++++++++++++++++++----------------- 1 file changed, 266 insertions(+), 227 deletions(-) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 07e1928de0..ea066f4f09 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -112,59 +112,8 @@ pub async fn generate_firewall_rules_from_acls( .. } = acl; - // // separate IPv4 and IPv6 destination addresses - // let destination = destination - // .iter() - // .fold((Vec::new(), Vec::new()), |mut acc, ip| { - // match ip { - // IpNetwork::V4(_) => acc.0.push(*ip), - // IpNetwork::V6(_) => acc.1.push(*ip), - // }; - // acc - // }); - // let destination_ranges = - // destination_ranges - // .iter() - // .fold((Vec::new(), Vec::new()), |mut acc, range| { - // // TODO(jck) make sure ranges only accept the same IP versions - // match range.start { - // IpAddr::V4(_) => acc.0.push(range.clone()), - // IpAddr::V6(_) => acc.1.push(range.clone()), - // }; - // acc - // }); - - // TODO(jck): handle aliases with separate alias.destination for IPv4 and IPv6 - // store alias ranges separately since they use a different struct - // let mut alias_destination_ranges = Vec::new(); - // // process aliases by appending destination parameters from each of them to existing lists - // for alias in aliases { - // // fetch destination ranges for a fiven alias - // alias_destination_ranges.extend(alias.get_destination_ranges(&mut *conn).await?); - - // // extend existing parameter lists - // destination.0.extend(alias.destination); - // ports.extend( - // alias - // .ports - // .into_iter() - // .map(|port_range| port_range.into()) - // .collect::>(), - // ); - // protocols.extend(alias.protocols); - // } - // prepare destination addresses - let destination_addrs = process_destination_addrs( - destination, - destination_ranges, - ); - - // let ipv6_destination_addrs = process_destination_addrs( - // destination.1, - // destination_ranges.1, - // IpVersion::Ipv6, - // ); + let destination_addrs = process_destination_addrs(destination, destination_ranges); // prepare destination ports // TODO(jck) what should happen to ports when we have separate rules for ipv4 and ipv6? @@ -174,75 +123,97 @@ pub async fn generate_firewall_rules_from_acls( protocols.sort(); protocols.dedup(); - // create IPv4 ALLOW rule - // check if source addrs list is empty - if ipv4_source_addrs.is_empty() { - debug!("IPv4 source address list is empty. Skipping generating the ALLOW rule for this ACL"); - } else { - // prepare ALLOW rule for this ACL - let allow_rule = FirewallRule { - id: acl.id, - source_addrs: ipv4_source_addrs.clone(), - destination_addrs: destination_addrs.0.clone(), - destination_ports: destination_ports.clone(), - protocols: protocols.clone(), - verdict: i32::from(FirewallPolicy::Allow), - comment: Some(format!("ACL {} - {} ALLOW", acl.id, acl.name)), - ip_version: i32::from(IpVersion::Ipv4), - }; - debug!("ALLOW rule generated from ACL: {allow_rule:?}"); - allow_rules.push(allow_rule); - }; - - // create IPv4 DENY rule - // it should specify only the destination addrs to block all remaining traffic - let deny_rule = FirewallRule { - id: acl.id, - source_addrs: Vec::new(), - destination_addrs: destination_addrs.0, - destination_ports: Vec::new(), - protocols: Vec::new(), - verdict: i32::from(FirewallPolicy::Deny), - comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), - ip_version: i32::from(IpVersion::Ipv4), - }; - debug!("DENY rule generated from ACL: {deny_rule:?}"); - deny_rules.push(deny_rule); - - // create IPv6 ALLOW rule - // check if source addrs list is empty - if ipv6_source_addrs.is_empty() { - debug!("IPv6 source address list is empty. Skipping generating the ALLOW rule for this ACL"); - } else { - // prepare ALLOW rule for this ACL - let allow_rule = FirewallRule { - id: acl.id, - source_addrs: ipv6_source_addrs.clone(), - destination_addrs: destination_addrs.1.clone(), - destination_ports, - protocols, - verdict: i32::from(FirewallPolicy::Allow), - comment: Some(format!("ACL {} - {} ALLOW", acl.id, acl.name)), - ip_version: i32::from(IpVersion::Ipv6), - }; - debug!("ALLOW rule generated from ACL: {allow_rule:?}"); - allow_rules.push(allow_rule); - }; - - // create IPv6 DENY rule - // it should specify only the destination addrs to block all remaining traffic - let deny_rule = FirewallRule { - id: acl.id, - source_addrs: Vec::new(), - destination_addrs: destination_addrs.1, - destination_ports: Vec::new(), - protocols: Vec::new(), - verdict: i32::from(FirewallPolicy::Deny), - comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), - ip_version: i32::from(IpVersion::Ipv6), - }; - debug!("DENY rule generated from ACL: {deny_rule:?}"); - deny_rules.push(deny_rule); + // create IPv4 rules + let rules = create_rules( + acl.id, + &ipv4_source_addrs, + &destination_addrs.0, + &destination_ports, + &protocols, + &format!("ACL {} - {}", acl.id, acl.name), + ); + rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(rules.1); + + // // check if source addrs list is empty + // if ipv4_source_addrs.is_empty() { + // debug!("IPv4 source address list is empty. Skipping generating the ALLOW rule for this ACL"); + // } else { + // // prepare ALLOW rule for this ACL + // let allow_rule = FirewallRule { + // id: acl.id, + // source_addrs: ipv4_source_addrs.clone(), + // destination_addrs: destination_addrs.0.clone(), + // destination_ports: destination_ports.clone(), + // protocols: protocols.clone(), + // verdict: i32::from(FirewallPolicy::Allow), + // comment: Some(format!("ACL {} - {} ALLOW", acl.id, acl.name)), + // ip_version: i32::from(IpVersion::Ipv4), + // }; + // debug!("ALLOW rule generated from ACL: {allow_rule:?}"); + // allow_rules.push(allow_rule); + // }; + + // // create IPv4 DENY rule + // // it should specify only the destination addrs to block all remaining traffic + // let deny_rule = FirewallRule { + // id: acl.id, + // source_addrs: Vec::new(), + // destination_addrs: destination_addrs.0, + // destination_ports: Vec::new(), + // protocols: Vec::new(), + // verdict: i32::from(FirewallPolicy::Deny), + // comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), + // ip_version: i32::from(IpVersion::Ipv4), + // }; + // debug!("DENY rule generated from ACL: {deny_rule:?}"); + // deny_rules.push(deny_rule); + + // create IPv6 rules + let rules = create_rules( + acl.id, + &ipv6_source_addrs, + &destination_addrs.1, + &destination_ports, + &protocols, + &format!("ACL {} - {}", acl.id, acl.name), + ); + rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(rules.1); + // // create IPv6 ALLOW rule + // // check if source addrs list is empty + // if ipv6_source_addrs.is_empty() { + // debug!("IPv6 source address list is empty. Skipping generating the ALLOW rule for this ACL"); + // } else { + // // prepare ALLOW rule for this ACL + // let allow_rule = FirewallRule { + // id: acl.id, + // source_addrs: ipv6_source_addrs.clone(), + // destination_addrs: destination_addrs.1.clone(), + // destination_ports, + // protocols, + // verdict: i32::from(FirewallPolicy::Allow), + // comment: Some(format!("ACL {} - {} ALLOW", acl.id, acl.name)), + // ip_version: i32::from(IpVersion::Ipv6), + // }; + // debug!("ALLOW rule generated from ACL: {allow_rule:?}"); + // allow_rules.push(allow_rule); + // }; + + // // create IPv6 DENY rule + // // it should specify only the destination addrs to block all remaining traffic + // let deny_rule = FirewallRule { + // id: acl.id, + // source_addrs: Vec::new(), + // destination_addrs: destination_addrs.1, + // destination_ports: Vec::new(), + // protocols: Vec::new(), + // verdict: i32::from(FirewallPolicy::Deny), + // comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), + // ip_version: i32::from(IpVersion::Ipv6), + // }; + // debug!("DENY rule generated from ACL: {deny_rule:?}"); + // deny_rules.push(deny_rule); // process aliases by creating a dedicated set of rules for each alias if !aliases.is_empty() { @@ -256,23 +227,10 @@ pub async fn generate_firewall_rules_from_acls( // fetch destination ranges for a given alias let alias_destination_ranges = alias.get_destination_ranges(&mut *conn).await?; - // let alias_destination_ranges = - // alias_destination_ranges - // .iter() - // .fold((Vec::new(), Vec::new()), |mut acc, range| { - // // TODO(jck) make sure ranges only accept the same IP versions - // match range.start { - // IpAddr::V4(_) => acc.0.push(range.clone()), - // IpAddr::V6(_) => acc.1.push(range.clone()), - // }; - // acc - // }); // combine destination addrs - let destination_addrs = process_alias_destination_addrs( - alias.destination, - alias_destination_ranges, - ); + let destination_addrs = + process_alias_destination_addrs(alias.destination, alias_destination_ranges); // process alias ports let alias_ports = alias @@ -282,94 +240,120 @@ pub async fn generate_firewall_rules_from_acls( .collect::>(); let destination_ports = merge_port_ranges(alias_ports); - // remove duplicates protocol entries + // remove duplicate protocol entries let mut protocols = alias.protocols; protocols.sort(); protocols.dedup(); - // create IPv4 ALLOW rule - if ipv4_source_addrs.is_empty() { - debug!( - "Source address list is empty. Skipping generating the ALLOW rule for this alias" - ); - } else { - // prepare ALLOW rule for this alias - let alias_allow_rule = FirewallRule { - id: acl.id, - source_addrs: ipv4_source_addrs.clone(), - destination_addrs: destination_addrs.0.clone(), - destination_ports: destination_ports.clone(), - protocols: protocols.clone(), - verdict: i32::from(FirewallPolicy::Allow), - comment: Some(format!( - "ACL {} - {}, ALIAS {} - {} ALLOW", - acl.id, acl.name, alias.id, alias.name - )), - ip_version: i32::from(IpVersion::Ipv4), - }; - debug!("ALLOW rule generated from ACL: {alias_allow_rule:?}"); - allow_rules.push(alias_allow_rule); - }; - - // create IPv4 DENY rule - // it should specify only the destination addrs to block all remaining traffic - let alias_deny_rule = FirewallRule { - id: acl.id, - source_addrs: Vec::new(), - destination_addrs: destination_addrs.0, - destination_ports: Vec::new(), - protocols: Vec::new(), - verdict: i32::from(FirewallPolicy::Deny), - comment: Some(format!( - "ACL {} - {}, ALIAS {} - {} DENY", + // create IPv4 rules + let rules = create_rules( + alias.id, + &ipv4_source_addrs, + &destination_addrs.0, + &destination_ports, + &protocols, + &format!( + "ACL {} - {}, ALIAS {} - {}", acl.id, acl.name, alias.id, alias.name - )), - ip_version: i32::from(IpVersion::Ipv4), - }; - debug!("DENY rule generated from ACL: {alias_deny_rule:?}"); - deny_rules.push(alias_deny_rule); - - // create IPv6 ALLOW rule - if ipv6_source_addrs.is_empty() { - debug!( - "Source address list is empty. Skipping generating the ALLOW rule for this alias" - ); - } else { - // prepare ALLOW rule for this alias - let alias_allow_rule = FirewallRule { - id: acl.id, - source_addrs: ipv6_source_addrs.clone(), - destination_addrs: destination_addrs.1.clone(), - destination_ports, - protocols, - verdict: i32::from(FirewallPolicy::Allow), - comment: Some(format!( - "ACL {} - {}, ALIAS {} - {} ALLOW", - acl.id, acl.name, alias.id, alias.name - )), - ip_version: i32::from(IpVersion::Ipv6), - }; - debug!("ALLOW rule generated from ACL: {alias_allow_rule:?}"); - allow_rules.push(alias_allow_rule); - }; - - // create IPv6 DENY rule - // it should specify only the destination addrs to block all remaining traffic - let alias_deny_rule = FirewallRule { - id: acl.id, - source_addrs: Vec::new(), - destination_addrs: destination_addrs.1, - destination_ports: Vec::new(), - protocols: Vec::new(), - verdict: i32::from(FirewallPolicy::Deny), - comment: Some(format!( - "ACL {} - {}, ALIAS {} - {} DENY", + ), + ); + rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(rules.1); + // // create IPv4 ALLOW rule + // if ipv4_source_addrs.is_empty() { + // debug!( + // "Source address list is empty. Skipping generating the ALLOW rule for this alias" + // ); + // } else { + // let alias_allow_rule = FirewallRule { + // id: acl.id, + // source_addrs: ipv4_source_addrs.clone(), + // destination_addrs: destination_addrs.0.clone(), + // destination_ports: destination_ports.clone(), + // protocols: protocols.clone(), + // verdict: i32::from(FirewallPolicy::Allow), + // comment: Some(format!( + // "ACL {} - {}, ALIAS {} - {} ALLOW", + // acl.id, acl.name, alias.id, alias.name + // )), + // ip_version: i32::from(IpVersion::Ipv4), + // }; + // debug!("ALLOW rule generated from ACL: {alias_allow_rule:?}"); + // allow_rules.push(alias_allow_rule); + // }; + + // // create IPv4 DENY rule + // // it should specify only the destination addrs to block all remaining traffic + // let alias_deny_rule = FirewallRule { + // id: acl.id, + // source_addrs: Vec::new(), + // destination_addrs: destination_addrs.0, + // destination_ports: Vec::new(), + // protocols: Vec::new(), + // verdict: i32::from(FirewallPolicy::Deny), + // comment: Some(format!( + // "ACL {} - {}, ALIAS {} - {} DENY", + // acl.id, acl.name, alias.id, alias.name + // )), + // ip_version: i32::from(IpVersion::Ipv4), + // }; + // debug!("DENY rule generated from ACL: {alias_deny_rule:?}"); + // deny_rules.push(alias_deny_rule); + + // create IPv6 rules + let rules = create_rules( + alias.id, + &ipv6_source_addrs, + &destination_addrs.1, + &destination_ports, + &protocols, + &format!( + "ACL {} - {}, ALIAS {} - {}", acl.id, acl.name, alias.id, alias.name - )), - ip_version: i32::from(IpVersion::Ipv6), - }; - debug!("DENY rule generated from ACL: {alias_deny_rule:?}"); - deny_rules.push(alias_deny_rule); + ), + ); + rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(rules.1); + // // create IPv6 ALLOW rule + // if ipv6_source_addrs.is_empty() { + // debug!( + // "Source address list is empty. Skipping generating the ALLOW rule for this alias" + // ); + // } else { + // let alias_allow_rule = FirewallRule { + // id: acl.id, + // source_addrs: ipv6_source_addrs.clone(), + // destination_addrs: destination_addrs.1.clone(), + // destination_ports, + // protocols, + // verdict: i32::from(FirewallPolicy::Allow), + // comment: Some(format!( + // "ACL {} - {}, ALIAS {} - {} ALLOW", + // acl.id, acl.name, alias.id, alias.name + // )), + // ip_version: i32::from(IpVersion::Ipv6), + // }; + // debug!("ALLOW rule generated from ACL: {alias_allow_rule:?}"); + // allow_rules.push(alias_allow_rule); + // }; + + // // create IPv6 DENY rule + // // it should specify only the destination addrs to block all remaining traffic + // let alias_deny_rule = FirewallRule { + // id: acl.id, + // source_addrs: Vec::new(), + // destination_addrs: destination_addrs.1, + // destination_ports: Vec::new(), + // protocols: Vec::new(), + // verdict: i32::from(FirewallPolicy::Deny), + // comment: Some(format!( + // "ACL {} - {}, ALIAS {} - {} DENY", + // acl.id, acl.name, alias.id, alias.name + // )), + // ip_version: i32::from(IpVersion::Ipv6), + // }; + // debug!("DENY rule generated from ACL: {alias_deny_rule:?}"); + // deny_rules.push(alias_deny_rule); } } @@ -377,6 +361,55 @@ pub async fn generate_firewall_rules_from_acls( Ok(allow_rules.into_iter().chain(deny_rules).collect()) } +/// Creates ALLOW and DENY rules for given set of source, destination +/// addresses, ports and protocols. The DENY rule should block all +/// remaining traffic to the destination from sources other than specified. +/// +/// Returs a 2-tuple where the first field is an `Option` with the ALLOW +/// rule if it should be created and the second field is the DENY rule. +fn create_rules( + id: Id, + source_addrs: &[IpAddress], + destination_addrs: &[IpAddress], + destination_ports: &[Port], + protocols: &[i32], + comment: &str, +) -> (Option, FirewallRule) { + let allow = if source_addrs.is_empty() { + debug!("Source address list is empty. Skipping generating the ALLOW rule for this ACL"); + None + } else { + // prepare ALLOW rule + let rule = Some(FirewallRule { + id, + source_addrs: source_addrs.to_vec(), + destination_addrs: destination_addrs.to_vec(), + destination_ports: destination_ports.to_vec(), + protocols: protocols.to_vec(), + verdict: i32::from(FirewallPolicy::Allow), + comment: Some(format!("{comment} ALLOW")), + ip_version: i32::from(IpVersion::Ipv4), + }); + debug!("ALLOW rule generated from ACL: {rule:?}"); + rule + }; + // prepare DENY rule + // it should specify only the destination addrs to block all remaining traffic + let deny = FirewallRule { + id, + source_addrs: Vec::new(), + destination_addrs: destination_addrs.to_vec(), + destination_ports: Vec::new(), + protocols: Vec::new(), + verdict: i32::from(FirewallPolicy::Deny), + comment: Some(format!("{comment} DENY")), + ip_version: i32::from(IpVersion::Ipv4), + }; + debug!("DENY rule generated from ACL: {deny:?}"); + + (allow, deny) +} + /// Prepares a list of all relevant users whose device IPs we'll need to prepare /// source config for a firewall rule. /// @@ -530,14 +563,15 @@ fn process_destination_addrs( let ipv6_destination_iterator = destination_ips .iter() .filter(|dst| dst.is_ipv6()) - .filter_map(|dst| + .filter_map(|dst| { if let IpNetwork::V6(subnet) = dst { let range_start = subnet.network().into(); let range_end = get_last_ip_in_v6_subnet(subnet); Some(ip_to_range(range_start, range_end)) } else { None - }); + } + }); // filter out destination ranges with incompatible IP version and convert to intermediate // range representation for merging @@ -577,7 +611,10 @@ fn process_destination_addrs( .collect(); // merge address ranges into non-overlapping elements - (merge_addrs(ipv4_destination_addrs), merge_addrs(ipv6_destination_addrs)) + ( + merge_addrs(ipv4_destination_addrs), + merge_addrs(ipv6_destination_addrs), + ) } /// Convert destination networks and ranges configured in an ACL alias @@ -622,14 +659,15 @@ fn process_alias_destination_addrs( let ipv6_destination_iterator = alias_destination_ips .iter() .filter(|dst| dst.is_ipv6()) - .filter_map(|dst| + .filter_map(|dst| { if let IpNetwork::V6(subnet) = dst { let range_start = subnet.network().into(); let range_end = get_last_ip_in_v6_subnet(subnet); Some(ip_to_range(range_start, range_end)) } else { None - }); + } + }); // filter out destination ranges with incompatible IP version and convert to intermediate // range representation for merging @@ -670,7 +708,10 @@ fn process_alias_destination_addrs( .collect(); // merge address ranges into non-overlapping elements - (merge_addrs(ipv4_destination_addrs), merge_addrs(ipv6_destination_addrs)) + ( + merge_addrs(ipv4_destination_addrs), + merge_addrs(ipv6_destination_addrs), + ) } fn ip_to_range(first_ip: IpAddr, last_ip: IpAddr) -> Range { @@ -928,8 +969,7 @@ impl WireguardNetwork { false => FirewallPolicy::Deny, }; let firewall_rules = - generate_firewall_rules_from_acls(self.id, location_acls, &mut *conn) - .await?; + generate_firewall_rules_from_acls(self.id, location_acls, &mut *conn).await?; let firewall_config = FirewallConfig { default_policy: default_policy.into(), rules: firewall_rules, @@ -938,7 +978,6 @@ impl WireguardNetwork { debug!("Firewall config generated for location {self}: {firewall_config:?}"); Ok(Some(firewall_config)) } - } #[cfg(test)] From 938a763dd66bc9382e04cbd03c45d25d2ca7481b Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 10 Apr 2025 11:35:57 +0200 Subject: [PATCH 17/71] cleanup --- src/enterprise/firewall.rs | 191 ++++--------------------------------- 1 file changed, 21 insertions(+), 170 deletions(-) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index ea066f4f09..6c9d3d54fe 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -124,96 +124,29 @@ pub async fn generate_firewall_rules_from_acls( protocols.dedup(); // create IPv4 rules - let rules = create_rules( + let comment = format!("ACL {} - {}", acl.id, acl.name); + let ipv4_rules = create_rules( acl.id, &ipv4_source_addrs, &destination_addrs.0, &destination_ports, &protocols, - &format!("ACL {} - {}", acl.id, acl.name), + &comment, ); - rules.0.map(|rule| allow_rules.push(rule)); - deny_rules.push(rules.1); - - // // check if source addrs list is empty - // if ipv4_source_addrs.is_empty() { - // debug!("IPv4 source address list is empty. Skipping generating the ALLOW rule for this ACL"); - // } else { - // // prepare ALLOW rule for this ACL - // let allow_rule = FirewallRule { - // id: acl.id, - // source_addrs: ipv4_source_addrs.clone(), - // destination_addrs: destination_addrs.0.clone(), - // destination_ports: destination_ports.clone(), - // protocols: protocols.clone(), - // verdict: i32::from(FirewallPolicy::Allow), - // comment: Some(format!("ACL {} - {} ALLOW", acl.id, acl.name)), - // ip_version: i32::from(IpVersion::Ipv4), - // }; - // debug!("ALLOW rule generated from ACL: {allow_rule:?}"); - // allow_rules.push(allow_rule); - // }; - - // // create IPv4 DENY rule - // // it should specify only the destination addrs to block all remaining traffic - // let deny_rule = FirewallRule { - // id: acl.id, - // source_addrs: Vec::new(), - // destination_addrs: destination_addrs.0, - // destination_ports: Vec::new(), - // protocols: Vec::new(), - // verdict: i32::from(FirewallPolicy::Deny), - // comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), - // ip_version: i32::from(IpVersion::Ipv4), - // }; - // debug!("DENY rule generated from ACL: {deny_rule:?}"); - // deny_rules.push(deny_rule); + ipv4_rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(ipv4_rules.1); // create IPv6 rules - let rules = create_rules( + let ipv6_rules = create_rules( acl.id, &ipv6_source_addrs, &destination_addrs.1, &destination_ports, &protocols, - &format!("ACL {} - {}", acl.id, acl.name), + &comment, ); - rules.0.map(|rule| allow_rules.push(rule)); - deny_rules.push(rules.1); - // // create IPv6 ALLOW rule - // // check if source addrs list is empty - // if ipv6_source_addrs.is_empty() { - // debug!("IPv6 source address list is empty. Skipping generating the ALLOW rule for this ACL"); - // } else { - // // prepare ALLOW rule for this ACL - // let allow_rule = FirewallRule { - // id: acl.id, - // source_addrs: ipv6_source_addrs.clone(), - // destination_addrs: destination_addrs.1.clone(), - // destination_ports, - // protocols, - // verdict: i32::from(FirewallPolicy::Allow), - // comment: Some(format!("ACL {} - {} ALLOW", acl.id, acl.name)), - // ip_version: i32::from(IpVersion::Ipv6), - // }; - // debug!("ALLOW rule generated from ACL: {allow_rule:?}"); - // allow_rules.push(allow_rule); - // }; - - // // create IPv6 DENY rule - // // it should specify only the destination addrs to block all remaining traffic - // let deny_rule = FirewallRule { - // id: acl.id, - // source_addrs: Vec::new(), - // destination_addrs: destination_addrs.1, - // destination_ports: Vec::new(), - // protocols: Vec::new(), - // verdict: i32::from(FirewallPolicy::Deny), - // comment: Some(format!("ACL {} - {} DENY", acl.id, acl.name)), - // ip_version: i32::from(IpVersion::Ipv6), - // }; - // debug!("DENY rule generated from ACL: {deny_rule:?}"); - // deny_rules.push(deny_rule); + ipv6_rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(ipv6_rules.1); // process aliases by creating a dedicated set of rules for each alias if !aliases.is_empty() { @@ -246,114 +179,32 @@ pub async fn generate_firewall_rules_from_acls( protocols.dedup(); // create IPv4 rules - let rules = create_rules( + let comment = format!( + "ACL {} - {}, ALIAS {} - {}", + acl.id, acl.name, alias.id, alias.name + ); + let ipv4_rules = create_rules( alias.id, &ipv4_source_addrs, &destination_addrs.0, &destination_ports, &protocols, - &format!( - "ACL {} - {}, ALIAS {} - {}", - acl.id, acl.name, alias.id, alias.name - ), + &comment, ); - rules.0.map(|rule| allow_rules.push(rule)); - deny_rules.push(rules.1); - // // create IPv4 ALLOW rule - // if ipv4_source_addrs.is_empty() { - // debug!( - // "Source address list is empty. Skipping generating the ALLOW rule for this alias" - // ); - // } else { - // let alias_allow_rule = FirewallRule { - // id: acl.id, - // source_addrs: ipv4_source_addrs.clone(), - // destination_addrs: destination_addrs.0.clone(), - // destination_ports: destination_ports.clone(), - // protocols: protocols.clone(), - // verdict: i32::from(FirewallPolicy::Allow), - // comment: Some(format!( - // "ACL {} - {}, ALIAS {} - {} ALLOW", - // acl.id, acl.name, alias.id, alias.name - // )), - // ip_version: i32::from(IpVersion::Ipv4), - // }; - // debug!("ALLOW rule generated from ACL: {alias_allow_rule:?}"); - // allow_rules.push(alias_allow_rule); - // }; - - // // create IPv4 DENY rule - // // it should specify only the destination addrs to block all remaining traffic - // let alias_deny_rule = FirewallRule { - // id: acl.id, - // source_addrs: Vec::new(), - // destination_addrs: destination_addrs.0, - // destination_ports: Vec::new(), - // protocols: Vec::new(), - // verdict: i32::from(FirewallPolicy::Deny), - // comment: Some(format!( - // "ACL {} - {}, ALIAS {} - {} DENY", - // acl.id, acl.name, alias.id, alias.name - // )), - // ip_version: i32::from(IpVersion::Ipv4), - // }; - // debug!("DENY rule generated from ACL: {alias_deny_rule:?}"); - // deny_rules.push(alias_deny_rule); + ipv4_rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(ipv4_rules.1); // create IPv6 rules - let rules = create_rules( + let ipv6_rules = create_rules( alias.id, &ipv6_source_addrs, &destination_addrs.1, &destination_ports, &protocols, - &format!( - "ACL {} - {}, ALIAS {} - {}", - acl.id, acl.name, alias.id, alias.name - ), + &comment, ); - rules.0.map(|rule| allow_rules.push(rule)); - deny_rules.push(rules.1); - // // create IPv6 ALLOW rule - // if ipv6_source_addrs.is_empty() { - // debug!( - // "Source address list is empty. Skipping generating the ALLOW rule for this alias" - // ); - // } else { - // let alias_allow_rule = FirewallRule { - // id: acl.id, - // source_addrs: ipv6_source_addrs.clone(), - // destination_addrs: destination_addrs.1.clone(), - // destination_ports, - // protocols, - // verdict: i32::from(FirewallPolicy::Allow), - // comment: Some(format!( - // "ACL {} - {}, ALIAS {} - {} ALLOW", - // acl.id, acl.name, alias.id, alias.name - // )), - // ip_version: i32::from(IpVersion::Ipv6), - // }; - // debug!("ALLOW rule generated from ACL: {alias_allow_rule:?}"); - // allow_rules.push(alias_allow_rule); - // }; - - // // create IPv6 DENY rule - // // it should specify only the destination addrs to block all remaining traffic - // let alias_deny_rule = FirewallRule { - // id: acl.id, - // source_addrs: Vec::new(), - // destination_addrs: destination_addrs.1, - // destination_ports: Vec::new(), - // protocols: Vec::new(), - // verdict: i32::from(FirewallPolicy::Deny), - // comment: Some(format!( - // "ACL {} - {}, ALIAS {} - {} DENY", - // acl.id, acl.name, alias.id, alias.name - // )), - // ip_version: i32::from(IpVersion::Ipv6), - // }; - // debug!("DENY rule generated from ACL: {alias_deny_rule:?}"); - // deny_rules.push(alias_deny_rule); + ipv6_rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(ipv6_rules.1); } } From 95f6ca651d7f952fded90c1f23c01b1a7b384bf5 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 10 Apr 2025 12:09:19 +0200 Subject: [PATCH 18/71] make tests compile --- src/enterprise/firewall.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 6c9d3d54fe..9424d617d4 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -1044,10 +1044,10 @@ mod test { ]; let destination_addrs = - process_destination_addrs(destination_ips, destination_ranges, IpVersion::Ipv4); + process_destination_addrs(destination_ips, destination_ranges); assert_eq!( - destination_addrs, + destination_addrs.0, vec![ IpAddress { address: Some(Address::IpRange(IpRange { @@ -1071,16 +1071,15 @@ mod test { ); // Test with empty input - let empty_addrs = process_destination_addrs(vec![], vec![], IpVersion::Ipv4); - assert!(empty_addrs.is_empty()); + let empty_addrs = process_destination_addrs(vec![], vec![]); + assert!(empty_addrs.0.is_empty()); // Test with only IPv6 addresses - should return empty result for IPv4 let ipv6_only = process_destination_addrs( vec!["2001:db8::/64".parse().unwrap()], vec![], - IpVersion::Ipv4, ); - assert!(ipv6_only.is_empty()); + assert!(ipv6_only.0.is_empty()); } #[test] @@ -1107,10 +1106,10 @@ mod test { ]; let destination_addrs = - process_destination_addrs(destination_ips, destination_ranges, IpVersion::Ipv6); + process_destination_addrs(destination_ips, destination_ranges); assert_eq!( - destination_addrs, + destination_addrs.1, vec![ IpAddress { address: Some(Address::IpRange(IpRange { @@ -1140,16 +1139,15 @@ mod test { ); // Test with empty input - let empty_addrs = process_destination_addrs(vec![], vec![], IpVersion::Ipv6); - assert!(empty_addrs.is_empty()); + let empty_addrs = process_destination_addrs(vec![], vec![]); + assert!(empty_addrs.1.is_empty()); // Test with only IPv4 addresses - should return empty result for IPv6 let ipv4_only = process_destination_addrs( vec!["192.168.1.0/24".parse().unwrap()], vec![], - IpVersion::Ipv6, ); - assert!(ipv4_only.is_empty()); + assert!(ipv4_only.1.is_empty()); } #[test] @@ -2364,6 +2362,7 @@ mod test { .unwrap() .rules; + println!("{generated_firewall_rules:#?}"); // both rules were assigned to this location assert_eq!(generated_firewall_rules.len(), 4); From a76e8a2b759de7002fff93adf14c4b8a88377f49 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 11 Apr 2025 08:30:04 +0200 Subject: [PATCH 19/71] fix downmigration --- migrations/20250404071457_multiple_peer_addresses.down.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20250404071457_multiple_peer_addresses.down.sql b/migrations/20250404071457_multiple_peer_addresses.down.sql index b4283088d2..bbbc6f2b54 100644 --- a/migrations/20250404071457_multiple_peer_addresses.down.sql +++ b/migrations/20250404071457_multiple_peer_addresses.down.sql @@ -1,6 +1,6 @@ -- add old-type address column ALTER TABLE wireguard_network_device -ADD COLUMN wireguard_ip_old inet; +ADD COLUMN wireguard_ip_old inet NOT NULL; -- copy the first element of new column to old column -- all further addresses will be lost From 02d6d9ddd564cab3f821bd25f24651caf9b9cff1 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 14 Apr 2025 09:53:53 +0200 Subject: [PATCH 20/71] create_rules method takes ip version as arg, fix tests --- src/enterprise/firewall.rs | 80 ++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 9424d617d4..ee0e809853 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -127,6 +127,7 @@ pub async fn generate_firewall_rules_from_acls( let comment = format!("ACL {} - {}", acl.id, acl.name); let ipv4_rules = create_rules( acl.id, + IpVersion::Ipv4, &ipv4_source_addrs, &destination_addrs.0, &destination_ports, @@ -139,6 +140,7 @@ pub async fn generate_firewall_rules_from_acls( // create IPv6 rules let ipv6_rules = create_rules( acl.id, + IpVersion::Ipv6, &ipv6_source_addrs, &destination_addrs.1, &destination_ports, @@ -185,6 +187,7 @@ pub async fn generate_firewall_rules_from_acls( ); let ipv4_rules = create_rules( alias.id, + IpVersion::Ipv4, &ipv4_source_addrs, &destination_addrs.0, &destination_ports, @@ -197,6 +200,7 @@ pub async fn generate_firewall_rules_from_acls( // create IPv6 rules let ipv6_rules = create_rules( alias.id, + IpVersion::Ipv6, &ipv6_source_addrs, &destination_addrs.1, &destination_ports, @@ -220,12 +224,14 @@ pub async fn generate_firewall_rules_from_acls( /// rule if it should be created and the second field is the DENY rule. fn create_rules( id: Id, + ip_version: IpVersion, source_addrs: &[IpAddress], destination_addrs: &[IpAddress], destination_ports: &[Port], protocols: &[i32], comment: &str, ) -> (Option, FirewallRule) { + let ip_version = i32::from(ip_version); let allow = if source_addrs.is_empty() { debug!("Source address list is empty. Skipping generating the ALLOW rule for this ACL"); None @@ -239,7 +245,7 @@ fn create_rules( protocols: protocols.to_vec(), verdict: i32::from(FirewallPolicy::Allow), comment: Some(format!("{comment} ALLOW")), - ip_version: i32::from(IpVersion::Ipv4), + ip_version, }); debug!("ALLOW rule generated from ACL: {rule:?}"); rule @@ -254,7 +260,7 @@ fn create_rules( protocols: Vec::new(), verdict: i32::from(FirewallPolicy::Deny), comment: Some(format!("{comment} DENY")), - ip_version: i32::from(IpVersion::Ipv4), + ip_version, }; debug!("DENY rule generated from ACL: {deny:?}"); @@ -1043,8 +1049,7 @@ mod test { }, ]; - let destination_addrs = - process_destination_addrs(destination_ips, destination_ranges); + let destination_addrs = process_destination_addrs(destination_ips, destination_ranges); assert_eq!( destination_addrs.0, @@ -1075,10 +1080,7 @@ mod test { assert!(empty_addrs.0.is_empty()); // Test with only IPv6 addresses - should return empty result for IPv4 - let ipv6_only = process_destination_addrs( - vec!["2001:db8::/64".parse().unwrap()], - vec![], - ); + let ipv6_only = process_destination_addrs(vec!["2001:db8::/64".parse().unwrap()], vec![]); assert!(ipv6_only.0.is_empty()); } @@ -1105,8 +1107,7 @@ mod test { }, ]; - let destination_addrs = - process_destination_addrs(destination_ips, destination_ranges); + let destination_addrs = process_destination_addrs(destination_ips, destination_ranges); assert_eq!( destination_addrs.1, @@ -1143,10 +1144,7 @@ mod test { assert!(empty_addrs.1.is_empty()); // Test with only IPv4 addresses - should return empty result for IPv6 - let ipv4_only = process_destination_addrs( - vec!["192.168.1.0/24".parse().unwrap()], - vec![], - ); + let ipv4_only = process_destination_addrs(vec!["192.168.1.0/24".parse().unwrap()], vec![]); assert!(ipv4_only.1.is_empty()); } @@ -2085,7 +2083,7 @@ mod test { .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 2); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] @@ -2154,7 +2152,7 @@ mod test { .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 2); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] @@ -2223,7 +2221,7 @@ mod test { .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 2); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] @@ -2270,12 +2268,19 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_1.id, - wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], + wireguard_ip: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xf000, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], preshared_key: None, is_authorized: true, authorized_at: None, @@ -2284,12 +2289,19 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_2.id, - wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 10, - user.id as u8, - device_num as u8, - ))], + wireguard_ip: vec![ + IpAddr::V4(Ipv4Addr::new(10, 10, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xf010, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], preshared_key: None, is_authorized: true, authorized_at: None, @@ -2304,7 +2316,10 @@ mod test { expires: None, enabled: true, state: RuleState::Applied, - destination: vec!["192.168.1.0/24".parse().unwrap()], + destination: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::0/96".parse().unwrap(), + ], ..Default::default() } .save(&pool) @@ -2362,9 +2377,8 @@ mod test { .unwrap() .rules; - println!("{generated_firewall_rules:#?}"); // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 8); let generated_firewall_rules = location_2 .try_get_firewall_config(&mut conn) @@ -2374,6 +2388,6 @@ mod test { .rules; // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 3); + assert_eq!(generated_firewall_rules.len(), 6); } } From c73552c54706b0e8501c7be9185c0f305324f4b1 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 14 Apr 2025 10:26:44 +0200 Subject: [PATCH 21/71] Only add ipv4/6 rules if network has ipv4/6 addresses --- src/enterprise/firewall.rs | 163 ++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 84 deletions(-) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index ee0e809853..427faf5ba2 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -43,6 +43,11 @@ pub async fn generate_firewall_rules_from_acls( // initialize empty rules Vec let mut allow_rules = Vec::new(); let mut deny_rules = Vec::new(); + let location = WireguardNetwork::find_by_id(&mut *conn, location_id) + .await? + .ok_or(ModelError::NotFound)?; + let has_ipv4_addresses = location.address.iter().any(|ip| ip.is_ipv4()); + let has_ipv6_addresses = location.address.iter().any(|ip| ip.is_ipv6()); // convert each ACL into a corresponding `FirewallRule`s for acl in acl_rules { @@ -123,33 +128,36 @@ pub async fn generate_firewall_rules_from_acls( protocols.sort(); protocols.dedup(); - // create IPv4 rules let comment = format!("ACL {} - {}", acl.id, acl.name); - let ipv4_rules = create_rules( - acl.id, - IpVersion::Ipv4, - &ipv4_source_addrs, - &destination_addrs.0, - &destination_ports, - &protocols, - &comment, - ); - ipv4_rules.0.map(|rule| allow_rules.push(rule)); - deny_rules.push(ipv4_rules.1); - - // create IPv6 rules - let ipv6_rules = create_rules( - acl.id, - IpVersion::Ipv6, - &ipv6_source_addrs, - &destination_addrs.1, - &destination_ports, - &protocols, - &comment, - ); - ipv6_rules.0.map(|rule| allow_rules.push(rule)); - deny_rules.push(ipv6_rules.1); + if has_ipv4_addresses { + // create IPv4 rules + let ipv4_rules = create_rules( + acl.id, + IpVersion::Ipv4, + &ipv4_source_addrs, + &destination_addrs.0, + &destination_ports, + &protocols, + &comment, + ); + ipv4_rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(ipv4_rules.1); + } + if has_ipv6_addresses { + // create IPv6 rules + let ipv6_rules = create_rules( + acl.id, + IpVersion::Ipv6, + &ipv6_source_addrs, + &destination_addrs.1, + &destination_ports, + &protocols, + &comment, + ); + ipv6_rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(ipv6_rules.1); + } // process aliases by creating a dedicated set of rules for each alias if !aliases.is_empty() { debug!( @@ -180,35 +188,39 @@ pub async fn generate_firewall_rules_from_acls( protocols.sort(); protocols.dedup(); - // create IPv4 rules let comment = format!( "ACL {} - {}, ALIAS {} - {}", acl.id, acl.name, alias.id, alias.name ); - let ipv4_rules = create_rules( - alias.id, - IpVersion::Ipv4, - &ipv4_source_addrs, - &destination_addrs.0, - &destination_ports, - &protocols, - &comment, - ); - ipv4_rules.0.map(|rule| allow_rules.push(rule)); - deny_rules.push(ipv4_rules.1); + if has_ipv4_addresses { + // create IPv4 rules + let ipv4_rules = create_rules( + alias.id, + IpVersion::Ipv4, + &ipv4_source_addrs, + &destination_addrs.0, + &destination_ports, + &protocols, + &comment, + ); + ipv4_rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(ipv4_rules.1); + } - // create IPv6 rules - let ipv6_rules = create_rules( - alias.id, - IpVersion::Ipv6, - &ipv6_source_addrs, - &destination_addrs.1, - &destination_ports, - &protocols, - &comment, - ); - ipv6_rules.0.map(|rule| allow_rules.push(rule)); - deny_rules.push(ipv6_rules.1); + if has_ipv6_addresses { + // create IPv6 rules + let ipv6_rules = create_rules( + alias.id, + IpVersion::Ipv6, + &ipv6_source_addrs, + &destination_addrs.1, + &destination_ports, + &protocols, + &comment, + ); + ipv6_rules.0.map(|rule| allow_rules.push(rule)); + deny_rules.push(ipv6_rules.1); + } } } @@ -2083,7 +2095,7 @@ mod test { .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 2); } #[sqlx::test] @@ -2152,7 +2164,7 @@ mod test { .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 2); } #[sqlx::test] @@ -2221,7 +2233,7 @@ mod test { .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 2); } #[sqlx::test] @@ -2268,19 +2280,12 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_1.id, - wireguard_ip: vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xf000, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - )), - ], + wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], preshared_key: None, is_authorized: true, authorized_at: None, @@ -2289,19 +2294,12 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_2.id, - wireguard_ip: vec![ - IpAddr::V4(Ipv4Addr::new(10, 10, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xf010, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - )), - ], + wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 10, + user.id as u8, + device_num as u8, + ))], preshared_key: None, is_authorized: true, authorized_at: None, @@ -2316,10 +2314,7 @@ mod test { expires: None, enabled: true, state: RuleState::Applied, - destination: vec![ - "192.168.1.0/24".parse().unwrap(), - "fc00::0/96".parse().unwrap(), - ], + destination: vec!["192.168.1.0/24".parse().unwrap()], ..Default::default() } .save(&pool) @@ -2378,7 +2373,7 @@ mod test { .rules; // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 8); + assert_eq!(generated_firewall_rules.len(), 4); let generated_firewall_rules = location_2 .try_get_firewall_config(&mut conn) @@ -2388,6 +2383,6 @@ mod test { .rules; // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 6); + assert_eq!(generated_firewall_rules.len(), 3); } } From b35387aca92e7d03f226bd9aec9b784ce6f89785 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 14 Apr 2025 11:19:20 +0200 Subject: [PATCH 22/71] test_generate_firewall_rules_ipv6 --- src/enterprise/firewall.rs | 434 ++++++++++++++++++++++++++++++++++++- 1 file changed, 428 insertions(+), 6 deletions(-) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 427faf5ba2..86d34c5b0f 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -140,7 +140,9 @@ pub async fn generate_firewall_rules_from_acls( &protocols, &comment, ); - ipv4_rules.0.map(|rule| allow_rules.push(rule)); + if let Some(rule) = ipv4_rules.0 { + allow_rules.push(rule) + } deny_rules.push(ipv4_rules.1); } @@ -155,7 +157,9 @@ pub async fn generate_firewall_rules_from_acls( &protocols, &comment, ); - ipv6_rules.0.map(|rule| allow_rules.push(rule)); + if let Some(rule) = ipv6_rules.0 { + allow_rules.push(rule) + } deny_rules.push(ipv6_rules.1); } // process aliases by creating a dedicated set of rules for each alias @@ -203,7 +207,9 @@ pub async fn generate_firewall_rules_from_acls( &protocols, &comment, ); - ipv4_rules.0.map(|rule| allow_rules.push(rule)); + if let Some(rule) = ipv4_rules.0 { + allow_rules.push(rule) + } deny_rules.push(ipv4_rules.1); } @@ -218,7 +224,9 @@ pub async fn generate_firewall_rules_from_acls( &protocols, &comment, ); - ipv6_rules.0.map(|rule| allow_rules.push(rule)); + if let Some(rule) = ipv6_rules.0 { + allow_rules.push(rule) + } deny_rules.push(ipv6_rules.1); } } @@ -854,7 +862,7 @@ mod test { use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use chrono::NaiveDateTime; - use ipnetwork::Ipv6Network; + use ipnetwork::{IpNetwork, Ipv6Network}; use rand::{thread_rng, Rng}; use sqlx::{query, PgPool}; @@ -1617,7 +1625,7 @@ mod test { } #[sqlx::test] - async fn test_generate_firewall_rules(pool: PgPool) { + async fn test_generate_firewall_rules_ipv4(pool: PgPool) { let mut rng = thread_rng(); // Create test location @@ -2029,6 +2037,420 @@ mod test { ); } + #[sqlx::test] + async fn test_generate_firewall_rules_ipv6(pool: PgPool) { + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); + + // Setup test users and their devices + let user_1: User = rng.gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.gen(); + let user_5 = user_5.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ip: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, 0, 0, 0, 0, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // Setup test groups + let group_1 = Group { + id: NoId, + name: "group_1".into(), + ..Default::default() + }; + let group_1 = group_1.save(&pool).await.unwrap(); + let group_2 = Group { + id: NoId, + name: "group_2".into(), + ..Default::default() + }; + let group_2 = group_2.save(&pool).await.unwrap(); + + // Assign users to groups: + // Group 1: users 1,2 + // Group 2: users 3,4 + let group_assignments = vec![ + (&group_1, vec![&user_1, &user_2]), + (&group_2, vec![&user_3, &user_4]), + ]; + + for (group, users) in group_assignments { + for user in users { + query!( + "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", + user.id, + group.id + ) + .execute(&pool) + .await + .unwrap(); + } + } + + // Create some network devices + let network_device_1 = Device { + id: NoId, + name: "network-device-1".into(), + user_id: user_1.id, // Owned by user 1 + device_type: DeviceType::Network, + description: Some("Test network device 1".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_1 = network_device_1.save(&pool).await.unwrap(); + + let network_device_2 = Device { + id: NoId, + name: "network-device-2".into(), + user_id: user_2.id, // Owned by user 2 + device_type: DeviceType::Network, + description: Some("Test network device 2".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_2 = network_device_2.save(&pool).await.unwrap(); + + let network_device_3 = Device { + id: NoId, + name: "network-device-3".into(), + user_id: user_3.id, // Owned by user 3 + device_type: DeviceType::Network, + description: Some("Test network device 3".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_3 = network_device_3.save(&pool).await.unwrap(); + + // Add network devices to location's VPN network + let network_devices = vec![ + ( + network_device_1.id, + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), + ), + ( + network_device_2.id, + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), + ), + ( + network_device_3.id, + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), + ), + ]; + + for (device_id, ip) in network_devices { + let network_device = WireguardNetworkDevice { + device_id, + wireguard_network_id: location.id, + wireguard_ip: vec![ip], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + + // Create first ACL rule - Web access + let acl_rule_1 = AclRule { + id: NoId, + name: "Web Access".into(), + all_networks: false, + expires: None, + allow_all_users: false, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec!["fc00::0/112".parse().unwrap()], + ports: vec![ + PortRange::new(80, 80).into(), + PortRange::new(443, 443).into(), + ], + protocols: vec![Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let locations = vec![location.id]; + let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web + let denied_users = vec![user_3.id]; // Third user explicitly denied + let allowed_groups = vec![group_1.id]; // First group allowed + let denied_groups = vec![]; + let allowed_devices = vec![network_device_1.id]; + let denied_devices = vec![network_device_2.id, network_device_3.id]; + let destination_ranges = vec![]; + let aliases = vec![]; + + let _acl_rule_1 = create_acl_rule( + &pool, + acl_rule_1, + locations, + allowed_users, + denied_users, + allowed_groups, + denied_groups, + allowed_devices, + denied_devices, + destination_ranges, + aliases, + ) + .await; + + // Create second ACL rule - DNS access + let acl_rule_2 = AclRule { + id: NoId, + name: "DNS Access".into(), + all_networks: false, + expires: None, + allow_all_users: true, // Allow all users + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec![], // Will use destination ranges instead + ports: vec![PortRange::new(53, 53).into()], + protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let locations_2 = vec![location.id]; + let allowed_users_2 = vec![]; + let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS + let allowed_groups_2 = vec![]; + let denied_groups_2 = vec![group_2.id]; + let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed + let denied_devices_2 = vec![network_device_3.id]; // Third network device denied + let destination_ranges_2 = vec![ + ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), + ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), + ]; + let aliases_2 = vec![]; + + let _acl_rule_2 = create_acl_rule( + &pool, + acl_rule_2, + locations_2, + allowed_users_2, + denied_users_2, + allowed_groups_2, + denied_groups_2, + allowed_devices_2, + denied_devices_2, + destination_ranges_2, + aliases_2, + ) + .await; + + let mut conn = pool.acquire().await.unwrap(); + + // try to generate firewall config with ACL disabled + location.acl_enabled = false; + let generated_firewall_config = location.try_get_firewall_config(&mut conn).await.unwrap(); + assert!(generated_firewall_config.is_none()); + + // generate firewall config with default policy Allow + location.acl_enabled = true; + location.acl_default_allow = true; + let generated_firewall_config = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap(); + assert_eq!( + generated_firewall_config.default_policy, + i32::from(FirewallPolicy::Allow) + ); + + let generated_firewall_rules = generated_firewall_config.rules; + + assert_eq!(generated_firewall_rules.len(), 4); + + // First ACL - Web Access ALLOW + let web_allow_rule = &generated_firewall_rules[0]; + assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); + assert_eq!( + web_allow_rule.destination_addrs, + vec![IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::".to_string(), + end: "fc00::ffff".to_string(), + })), + }] + ); + assert_eq!( + web_allow_rule.destination_ports, + vec![ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule.source_addrs, + 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(), + })), + }, + IpAddress { + address: Some(Address::Ip("ff00::100:1".to_string())), + }, + ] + ); + + // First ACL - Web Access DENY + let web_deny_rule = &generated_firewall_rules[2]; + assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule.protocols.is_empty()); + assert!(web_deny_rule.destination_ports.is_empty()); + assert!(web_deny_rule.source_addrs.is_empty()); + assert_eq!( + web_deny_rule.destination_addrs, + vec![IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::".to_string(), + end: "fc00::ffff".to_string(), + })), + }] + ); + + // Second ACL - DNS Access ALLOW + let dns_allow_rule = &generated_firewall_rules[1]; + assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!( + dns_allow_rule.protocols, + vec![i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule.destination_ports, + vec![Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule.source_addrs, + 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(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::100:1".to_string(), + end: "ff00::100:2".to_string(), + })), + }, + ] + ); + assert_eq!( + dns_allow_rule.destination_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::1:13".to_string(), + end: "fc00::1:43".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::1:52".to_string(), + end: "fc00::2:43".to_string(), + })), + } + ] + ); + + // Second ACL - DNS Access DENY + let dns_deny_rule = &generated_firewall_rules[3]; + assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule.protocols.is_empty(),); + assert!(dns_deny_rule.destination_ports.is_empty(),); + assert!(dns_deny_rule.source_addrs.is_empty(),); + assert_eq!( + dns_deny_rule.destination_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::1:13".to_string(), + end: "fc00::1:43".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::1:52".to_string(), + end: "fc00::2:43".to_string(), + })), + } + ] + ); + } + #[sqlx::test] async fn test_expired_acl_rules(pool: PgPool) { // Create test location From 53e74f0cee14c7c28fcf49038c57cfbe161e1529 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 14 Apr 2025 11:44:46 +0200 Subject: [PATCH 23/71] Firewall ipv6 and ipv4+ipv6 tests --- src/enterprise/firewall.rs | 994 ++++++++++++++++++++++++++++++++----- 1 file changed, 881 insertions(+), 113 deletions(-) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 86d34c5b0f..1dcebdefd1 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -2083,7 +2083,11 @@ mod test { wireguard_network_id: location.id, wireguard_ip: vec![IpAddr::V6(Ipv6Addr::new( 0xff00, - 0, 0, 0, 0, 0, + 0, + 0, + 0, + 0, + 0, user.id as u16, device_num as u16, ))], @@ -2452,7 +2456,7 @@ mod test { } #[sqlx::test] - async fn test_expired_acl_rules(pool: PgPool) { + async fn test_expired_acl_rules_ipv4(pool: PgPool) { // Create test location let location = WireguardNetwork { id: NoId, @@ -2521,20 +2525,21 @@ mod test { } #[sqlx::test] - async fn test_disabled_acl_rules(pool: PgPool) { + async fn test_expired_acl_rules_ipv6(pool: PgPool) { // Create test location let location = WireguardNetwork { id: NoId, acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], ..Default::default() }; let location = location.save(&pool).await.unwrap(); - // create disabled ACL rules + // create expired ACL rules let mut acl_rule_1 = AclRule { id: NoId, - expires: None, - enabled: false, + expires: Some(NaiveDateTime::UNIX_EPOCH), + enabled: true, state: RuleState::Applied, ..Default::default() } @@ -2543,8 +2548,8 @@ mod test { .unwrap(); let mut acl_rule_2 = AclRule { id: NoId, - expires: None, - enabled: false, + expires: Some(NaiveDateTime::UNIX_EPOCH), + enabled: true, state: RuleState::Applied, ..Default::default() } @@ -2570,14 +2575,14 @@ mod test { .unwrap() .rules; - // both rules were disabled + // both rules were expired assert_eq!(generated_firewall_rules.len(), 0); - // make both rules enabled - acl_rule_1.enabled = true; + // make both rules not expired + acl_rule_1.expires = None; acl_rule_1.save(&pool).await.unwrap(); - acl_rule_2.enabled = true; + acl_rule_2.expires = Some(NaiveDateTime::MAX); acl_rule_2.save(&pool).await.unwrap(); let generated_firewall_rules = location @@ -2590,21 +2595,25 @@ mod test { } #[sqlx::test] - async fn test_unapplied_acl_rules(pool: PgPool) { + async fn test_expired_acl_rules_ipv4_and_ipv6(pool: PgPool) { // Create test location let location = WireguardNetwork { id: NoId, acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], ..Default::default() }; let location = location.save(&pool).await.unwrap(); - // create unapplied ACL rules + // create expired ACL rules let mut acl_rule_1 = AclRule { id: NoId, - expires: None, + expires: Some(NaiveDateTime::UNIX_EPOCH), enabled: true, - state: RuleState::New, + state: RuleState::Applied, ..Default::default() } .save(&pool) @@ -2612,9 +2621,9 @@ mod test { .unwrap(); let mut acl_rule_2 = AclRule { id: NoId, - expires: None, + expires: Some(NaiveDateTime::UNIX_EPOCH), enabled: true, - state: RuleState::Modified, + state: RuleState::Applied, ..Default::default() } .save(&pool) @@ -2639,14 +2648,14 @@ mod test { .unwrap() .rules; - // both rules were not applied + // both rules were expired assert_eq!(generated_firewall_rules.len(), 0); - // make both rules applied - acl_rule_1.state = RuleState::Applied; + // make both rules not expired + acl_rule_1.expires = None; acl_rule_1.save(&pool).await.unwrap(); - acl_rule_2.state = RuleState::Applied; + acl_rule_2.expires = Some(NaiveDateTime::MAX); acl_rule_2.save(&pool).await.unwrap(); let generated_firewall_rules = location @@ -2655,112 +2664,104 @@ mod test { .unwrap() .unwrap() .rules; - assert_eq!(generated_firewall_rules.len(), 2); + assert_eq!(generated_firewall_rules.len(), 4); } #[sqlx::test] - async fn test_acl_rules_all_locations(pool: PgPool) { - let mut rng = thread_rng(); - + async fn test_disabled_acl_rules_ipv4(pool: PgPool) { // Create test location - let location_1 = WireguardNetwork { + let location = WireguardNetwork { id: NoId, acl_enabled: true, ..Default::default() }; - let location_1 = location_1.save(&pool).await.unwrap(); + let location = location.save(&pool).await.unwrap(); - // Create another test location - let location_2 = WireguardNetwork { + // create disabled ACL rules + let mut acl_rule_1 = AclRule { id: NoId, - acl_enabled: true, + expires: None, + enabled: false, + state: RuleState::Applied, ..Default::default() - }; - let location_2 = location_2.save(&pool).await.unwrap(); - // Setup some test users and their devices - let user_1: User = rng.gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 10, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } } - - // create ACL rules - let acl_rule_1 = AclRule { + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { id: NoId, expires: None, - enabled: true, + enabled: false, state: RuleState::Applied, - destination: vec!["192.168.1.0/24".parse().unwrap()], ..Default::default() } .save(&pool) .await .unwrap(); - let acl_rule_2 = AclRule { + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: 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; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); + } + + #[sqlx::test] + async fn test_disabled_acl_rules_ipv6(pool: PgPool) { + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { id: NoId, expires: None, - enabled: true, - all_networks: true, + enabled: false, state: RuleState::Applied, ..Default::default() } .save(&pool) .await .unwrap(); - - let _acl_rule_3 = AclRule { + let mut acl_rule_2 = AclRule { id: NoId, expires: None, - enabled: true, - all_networks: true, - allow_all_users: true, + enabled: false, state: RuleState::Applied, ..Default::default() } @@ -2768,43 +2769,810 @@ mod test { .await .unwrap(); - // assign rules to locations + // assign rules to location for rule in [&acl_rule_1, &acl_rule_2] { let obj = AclRuleNetwork { id: NoId, rule_id: rule.id, - network_id: location_1.id, - }; - obj.save(&pool).await.unwrap(); - } - for rule in [&acl_rule_2] { - let obj = AclRuleNetwork { - id: NoId, - rule_id: rule.id, - network_id: location_2.id, + network_id: location.id, }; obj.save(&pool).await.unwrap(); } let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = location_1 + let generated_firewall_rules = location .try_get_firewall_config(&mut conn) .await .unwrap() .unwrap() .rules; - // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 4); + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); - let generated_firewall_rules = location_2 + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = location .try_get_firewall_config(&mut conn) .await .unwrap() .unwrap() .rules; + assert_eq!(generated_firewall_rules.len(), 2); + } - // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 3); + #[sqlx::test] + async fn test_disabled_acl_rules_ipv4_and_ipv6(pool: PgPool) { + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: 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; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); + } + + #[sqlx::test] + async fn test_unapplied_acl_rules_ipv4(pool: PgPool) { + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: 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; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); + } + + #[sqlx::test] + async fn test_unapplied_acl_rules_ipv6(pool: PgPool) { + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: 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; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); + } + + #[sqlx::test] + async fn test_unapplied_acl_rules_ipv4_and_ipv6(pool: PgPool) { + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::New, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Modified, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: 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; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); + } + + #[sqlx::test] + async fn test_acl_rules_all_locations_ipv4(pool: PgPool) { + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + // Setup some test users and their devices + let user_1: User = rng.gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_1.id, + wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_2.id, + wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 10, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + destination: vec!["192.168.1.0/24".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + allow_all_users: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: rule.id, + network_id: location_1.id, + }; + obj.save(&pool).await.unwrap(); + } + for rule in [&acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: rule.id, + network_id: location_2.id, + }; + obj.save(&pool).await.unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = location_1 + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were assigned to this location + assert_eq!(generated_firewall_rules.len(), 4); + + let generated_firewall_rules = location_2 + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was used for this location + assert_eq!(generated_firewall_rules.len(), 3); + } + + #[sqlx::test] + async fn test_acl_rules_all_locations_ipv6(pool: PgPool) { + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + // Setup some test users and their devices + let user_1: User = rng.gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_1.id, + wireguard_ip: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_2.id, + wireguard_ip: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 10, + 10, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + destination: vec!["fc00::0/112".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + allow_all_users: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: rule.id, + network_id: location_1.id, + }; + obj.save(&pool).await.unwrap(); + } + for rule in [&acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: rule.id, + network_id: location_2.id, + }; + obj.save(&pool).await.unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = location_1 + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were assigned to this location + assert_eq!(generated_firewall_rules.len(), 4); + + let generated_firewall_rules = location_2 + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was used for this location + assert_eq!(generated_firewall_rules.len(), 3); + } + + #[sqlx::test] + async fn test_acl_rules_all_locations_ipv4_and_ipv6(pool: PgPool) { + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + // Setup some test users and their devices + let user_1: User = rng.gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_1.id, + wireguard_ip: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_2.id, + wireguard_ip: vec![ + IpAddr::V4(Ipv4Addr::new(10, 10, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 10, + 10, + user.id as u16, + device_num as u16, + )), + ], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + destination: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::0/112".parse().unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_networks: true, + allow_all_users: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: rule.id, + network_id: location_1.id, + }; + obj.save(&pool).await.unwrap(); + } + for rule in [&acl_rule_2] { + let obj = AclRuleNetwork { + id: NoId, + rule_id: rule.id, + network_id: location_2.id, + }; + obj.save(&pool).await.unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = location_1 + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were assigned to this location + assert_eq!(generated_firewall_rules.len(), 8); + + let generated_firewall_rules = location_2 + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was used for this location + assert_eq!(generated_firewall_rules.len(), 6); } } From 9263caff27deef0c2f15eb92bc664eee63bafcc9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 14 Apr 2025 12:25:37 +0200 Subject: [PATCH 24/71] test_generate_firewall_rules_ipv4_and_ipv6 --- src/enterprise/firewall.rs | 593 +++++++++++++++++++++++++++++++++++++ 1 file changed, 593 insertions(+) diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 1dcebdefd1..cf8fa68295 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -2455,6 +2455,599 @@ mod test { ); } + #[sqlx::test] + async fn test_generate_firewall_rules_ipv4_and_ipv6(pool: PgPool) { + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); + + // Setup test users and their devices + let user_1: User = rng.gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.gen(); + let user_5 = user_5.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ip: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // Setup test groups + let group_1 = Group { + id: NoId, + name: "group_1".into(), + ..Default::default() + }; + let group_1 = group_1.save(&pool).await.unwrap(); + let group_2 = Group { + id: NoId, + name: "group_2".into(), + ..Default::default() + }; + let group_2 = group_2.save(&pool).await.unwrap(); + + // Assign users to groups: + // Group 1: users 1,2 + // Group 2: users 3,4 + let group_assignments = vec![ + (&group_1, vec![&user_1, &user_2]), + (&group_2, vec![&user_3, &user_4]), + ]; + + for (group, users) in group_assignments { + for user in users { + query!( + "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", + user.id, + group.id + ) + .execute(&pool) + .await + .unwrap(); + } + } + + // Create some network devices + let network_device_1 = Device { + id: NoId, + name: "network-device-1".into(), + user_id: user_1.id, // Owned by user 1 + device_type: DeviceType::Network, + description: Some("Test network device 1".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_1 = network_device_1.save(&pool).await.unwrap(); + + let network_device_2 = Device { + id: NoId, + name: "network-device-2".into(), + user_id: user_2.id, // Owned by user 2 + device_type: DeviceType::Network, + description: Some("Test network device 2".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_2 = network_device_2.save(&pool).await.unwrap(); + + let network_device_3 = Device { + id: NoId, + name: "network-device-3".into(), + user_id: user_3.id, // Owned by user 3 + device_type: DeviceType::Network, + description: Some("Test network device 3".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_3 = network_device_3.save(&pool).await.unwrap(); + + // Add network devices to location's VPN network + let network_devices = vec![ + ( + network_device_1.id, + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), + ], + ), + ( + network_device_2.id, + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), + ], + ), + ( + network_device_3.id, + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), + ], + ), + ]; + + for (device_id, ips) in network_devices { + let network_device = WireguardNetworkDevice { + device_id, + wireguard_network_id: location.id, + wireguard_ip: ips, + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + + // Create first ACL rule - Web access + let acl_rule_1 = AclRule { + id: NoId, + name: "Web Access".into(), + all_networks: false, + expires: None, + allow_all_users: false, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::0/112".parse().unwrap(), + ], + ports: vec![ + PortRange::new(80, 80).into(), + PortRange::new(443, 443).into(), + ], + protocols: vec![Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let locations = vec![location.id]; + let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web + let denied_users = vec![user_3.id]; // Third user explicitly denied + let allowed_groups = vec![group_1.id]; // First group allowed + let denied_groups = vec![]; + let allowed_devices = vec![network_device_1.id]; + let denied_devices = vec![network_device_2.id, network_device_3.id]; + let destination_ranges = vec![]; + let aliases = vec![]; + + let _acl_rule_1 = create_acl_rule( + &pool, + acl_rule_1, + locations, + allowed_users, + denied_users, + allowed_groups, + denied_groups, + allowed_devices, + denied_devices, + destination_ranges, + aliases, + ) + .await; + + // Create second ACL rule - DNS access + let acl_rule_2 = AclRule { + id: NoId, + name: "DNS Access".into(), + all_networks: false, + expires: None, + allow_all_users: true, // Allow all users + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + destination: vec![], // Will use destination ranges instead + ports: vec![PortRange::new(53, 53).into()], + protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + }; + let locations_2 = vec![location.id]; + let allowed_users_2 = vec![]; + let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS + let allowed_groups_2 = vec![]; + let denied_groups_2 = vec![group_2.id]; + let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed + let denied_devices_2 = vec![network_device_3.id]; // Third network device denied + let destination_ranges_2 = vec![ + ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), + ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), + ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), + ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), + ]; + let aliases_2 = vec![]; + + let _acl_rule_2 = create_acl_rule( + &pool, + acl_rule_2, + locations_2, + allowed_users_2, + denied_users_2, + allowed_groups_2, + denied_groups_2, + allowed_devices_2, + denied_devices_2, + destination_ranges_2, + aliases_2, + ) + .await; + + let mut conn = pool.acquire().await.unwrap(); + + // try to generate firewall config with ACL disabled + location.acl_enabled = false; + let generated_firewall_config = location.try_get_firewall_config(&mut conn).await.unwrap(); + assert!(generated_firewall_config.is_none()); + + // generate firewall config with default policy Allow + location.acl_enabled = true; + location.acl_default_allow = true; + let generated_firewall_config = location + .try_get_firewall_config(&mut conn) + .await + .unwrap() + .unwrap(); + assert_eq!( + generated_firewall_config.default_policy, + i32::from(FirewallPolicy::Allow) + ); + + let generated_firewall_rules = generated_firewall_config.rules; + + assert_eq!(generated_firewall_rules.len(), 8); + + // First ACL - Web Access ALLOW + let web_allow_rule_ipv4 = &generated_firewall_rules[0]; + assert_eq!( + web_allow_rule_ipv4.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + web_allow_rule_ipv4.protocols, + vec![i32::from(Protocol::Tcp)] + ); + assert_eq!( + web_allow_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpRange(IpRange { + start: "192.168.1.0".to_string(), + end: "192.168.1.255".to_string(), + })), + }] + ); + assert_eq!( + web_allow_rule_ipv4.destination_ports, + vec![ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule_ipv4.source_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::Ip("10.0.100.1".to_string())), + }, + ] + ); + + let web_allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!( + web_allow_rule_ipv6.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + web_allow_rule_ipv6.protocols, + vec![i32::from(Protocol::Tcp)] + ); + assert_eq!( + web_allow_rule_ipv6.destination_addrs, + vec![IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::".to_string(), + end: "fc00::ffff".to_string(), + })), + }] + ); + assert_eq!( + web_allow_rule_ipv6.destination_ports, + vec![ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule_ipv6.source_addrs, + 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(), + })), + }, + IpAddress { + address: Some(Address::Ip("ff00::100:1".to_string())), + }, + ] + ); + + // First ACL - Web Access DENY + let web_deny_rule_ipv4 = &generated_firewall_rules[4]; + assert_eq!(web_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule_ipv4.protocols.is_empty()); + assert!(web_deny_rule_ipv4.destination_ports.is_empty()); + assert!(web_deny_rule_ipv4.source_addrs.is_empty()); + assert_eq!( + web_deny_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpRange(IpRange { + start: "192.168.1.0".to_string(), + end: "192.168.1.255".to_string(), + })), + }] + ); + + let web_deny_rule_ipv6 = &generated_firewall_rules[5]; + assert_eq!(web_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule_ipv6.protocols.is_empty()); + assert!(web_deny_rule_ipv6.destination_ports.is_empty()); + assert!(web_deny_rule_ipv6.source_addrs.is_empty()); + assert_eq!( + web_deny_rule_ipv6.destination_addrs, + vec![IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::".to_string(), + end: "fc00::ffff".to_string(), + })), + }] + ); + + // Second ACL - DNS Access ALLOW + let dns_allow_rule_ipv4 = &generated_firewall_rules[2]; + assert_eq!( + dns_allow_rule_ipv4.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + dns_allow_rule_ipv4.protocols, + vec![i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule_ipv4.destination_ports, + vec![Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule_ipv4.source_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.100.1".to_string(), + end: "10.0.100.2".to_string(), + })), + }, + ] + ); + assert_eq!( + dns_allow_rule_ipv4.destination_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.13".to_string(), + end: "10.0.1.43".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.52".to_string(), + end: "10.0.2.43".to_string(), + })), + }, + ] + ); + + let dns_allow_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!( + dns_allow_rule_ipv6.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + dns_allow_rule_ipv6.protocols, + vec![i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule_ipv6.destination_ports, + vec![Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule_ipv6.source_addrs, + 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(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::100:1".to_string(), + end: "ff00::100:2".to_string(), + })), + }, + ] + ); + assert_eq!( + dns_allow_rule_ipv6.destination_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::1:13".to_string(), + end: "fc00::1:43".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::1:52".to_string(), + end: "fc00::2:43".to_string(), + })), + } + ] + ); + + // Second ACL - DNS Access DENY + let dns_deny_rule_ipv4 = &generated_firewall_rules[6]; + assert_eq!(dns_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule_ipv4.protocols.is_empty(),); + assert!(dns_deny_rule_ipv4.destination_ports.is_empty(),); + assert!(dns_deny_rule_ipv4.source_addrs.is_empty(),); + assert_eq!( + dns_deny_rule_ipv4.destination_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.13".to_string(), + end: "10.0.1.43".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.52".to_string(), + end: "10.0.2.43".to_string(), + })), + }, + ] + ); + + let dns_deny_rule_ipv6 = &generated_firewall_rules[7]; + assert_eq!(dns_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule_ipv6.protocols.is_empty(),); + assert!(dns_deny_rule_ipv6.destination_ports.is_empty(),); + assert!(dns_deny_rule_ipv6.source_addrs.is_empty(),); + assert_eq!( + dns_deny_rule_ipv6.destination_addrs, + vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::1:13".to_string(), + end: "fc00::1:43".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "fc00::1:52".to_string(), + end: "fc00::2:43".to_string(), + })), + } + ] + ); + } + #[sqlx::test] async fn test_expired_acl_rules_ipv4(pool: PgPool) { // Create test location From 7af0ca8e17033f29f4189787b26eab86383bbaea Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 14 Apr 2025 13:10:44 +0200 Subject: [PATCH 25/71] fix test_network_devices test --- tests/wireguard_network_devices.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/wireguard_network_devices.rs b/tests/wireguard_network_devices.rs index 2dcefd9f40..14325761b6 100644 --- a/tests/wireguard_network_devices.rs +++ b/tests/wireguard_network_devices.rs @@ -191,7 +191,7 @@ async fn test_network_devices() { let modify_device = json!({ "name": "device-1", "description": "new description", - "assigned_ip": "10.1.1.3" + "assigned_ips": ["10.1.1.3"] }); let response = client .put(format!("/api/v1/device/network/{device_id}")) @@ -201,11 +201,10 @@ async fn test_network_devices() { assert_eq!(response.status(), StatusCode::OK); let json = response.json::().await; let description = json["description"].as_str().unwrap(); - let assigned_ip = json["assigned_ip"].as_str().unwrap(); assert_eq!(description, "new description"); assert_eq!( - assigned_ip, - IpAddr::from_str("10.1.1.3").unwrap().to_string() + json["assigned_ips"], + serde_json::from_str::("[\"10.1.1.3\"]").unwrap() ); let device = Device::find_by_id(&client_state.pool, device_id) .await @@ -241,7 +240,7 @@ async fn test_network_devices() { { "name": "device-2", "description": "new description", - "assigned_ip": "10.1.1.10", + "assigned_ips": ["10.1.1.10"], "location_id": 1, } ); From fd6f4cbad57cacbe04aa287c09597f191e0731af Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 16 Apr 2025 08:23:55 +0200 Subject: [PATCH 26/71] Imports for wg configs with multiple addresses --- src/db/models/wireguard.rs | 8 +- src/handlers/wireguard.rs | 2 +- src/wg_config.rs | 148 ++++++++++++++++++---- tests/wireguard_network_allowed_groups.rs | 12 +- tests/wireguard_network_import.rs | 12 +- 5 files changed, 146 insertions(+), 36 deletions(-) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 8003dd173e..c363ea5ba5 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -36,6 +36,7 @@ use crate::{ GatewayState, }, wg_config::ImportedDevice, + CommaSeparated, }; pub const DEFAULT_KEEPALIVE_INTERVAL: i32 = 25; @@ -669,14 +670,13 @@ impl WireguardNetwork { match allowed_devices.get(&existing_device.id) { Some(_) => { info!( - "Device with pubkey {} exists already, assigning IP {} for new network: {self}", - existing_device.wireguard_pubkey, imported_device.wireguard_ip + "Device with pubkey {} exists already, assigning IPs {} for new network: {self}", + existing_device.wireguard_pubkey, imported_device.wireguard_ips.comma_separated() ); let wireguard_network_device = WireguardNetworkDevice::new( self.id, existing_device.id, - // TODO(jck) allow assignment of multiple ips for a network device - [imported_device.wireguard_ip], + imported_device.wireguard_ips, ); wireguard_network_device.insert(&mut *transaction).await?; // store ID of device with already generated config diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 3b9a54c9d7..f9068b6b45 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -448,7 +448,7 @@ pub(crate) async fn import_network( let reserved_ips: Vec = imported_devices .iter() - .map(|dev| dev.wireguard_ip) + .flat_map(|dev| dev.wireguard_ips.clone()) .collect(); let (devices, gateway_events) = network .handle_imported_devices(&mut transaction, imported_devices) diff --git a/src/wg_config.rs b/src/wg_config.rs index 94deb6a814..a17d314704 100644 --- a/src/wg_config.rs +++ b/src/wg_config.rs @@ -1,4 +1,7 @@ -use std::{array::TryFromSliceError, net::IpAddr}; +use std::{ + array::TryFromSliceError, + net::{AddrParseError, IpAddr}, +}; use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; use ipnetwork::{IpNetwork, IpNetworkError}; @@ -20,8 +23,7 @@ pub struct ImportedDevice { pub user_id: Option, pub name: String, pub wireguard_pubkey: String, - // TODO(jck) - pub wireguard_ip: IpAddr, + pub wireguard_ips: Vec, } #[derive(Debug, Error)] @@ -36,6 +38,8 @@ pub(crate) enum WireguardConfigParseError { InvalidIp(#[from] IpNetworkError), #[error("Invalid peer IP: {0}")] InvalidPeerIp(IpAddr), + #[error("Failed to parse IP: {0}")] + AddrParseError(#[from] AddrParseError), #[error("Invalid key: {0}")] InvalidKey(String), #[error("Invalid port: {0}")] @@ -93,18 +97,20 @@ pub(crate) fn parse_wireguard_config( } } // Require at least one IP address. - let Some(network_address) = addresses.first() else { + if addresses.is_empty() { return Err(WireguardConfigParseError::MissingAddress); - }; - let allowed_ips = IpNetwork::new(network_address.network(), network_address.prefix())?; - let network_address = *network_address; + } + let allowed_ips = addresses + .iter() + .map(|addr| IpNetwork::new(addr.network(), addr.prefix())) + .collect::, _>>()?; let mut network = WireguardNetwork::new( pubkey.clone(), - addresses, + addresses.clone(), port, String::new(), dns, - vec![allowed_ips], + allowed_ips, false, DEFAULT_KEEPALIVE_INTERVAL, DEFAULT_DISCONNECT_THRESHOLD, @@ -119,18 +125,28 @@ pub(crate) fn parse_wireguard_config( let mut devices = Vec::new(); for peer in peer_sections { - let ip = peer + let allowed_ips = peer .get("AllowedIPs") .ok_or_else(|| WireguardConfigParseError::KeyNotFound("AllowedIPs"))?; - let ip_network: IpNetwork = ip.parse()?; - let ip = ip_network.ip(); - - // check if assigned IP collides with gateway IP - let net_ip = network_address.ip(); - let net_network = network_address.network(); - let net_broadcast = network_address.broadcast(); - if ip == net_ip || ip == net_network || ip == net_broadcast { - return Err(WireguardConfigParseError::InvalidPeerIp(ip)); + + let mut peer_addresses: Vec = Vec::new(); + for allowed_ip in allowed_ips.split(',') { + match allowed_ip.trim().parse::() { + Ok(ip) => { + // check if assigned IP collides with any of gateway IPs + for network_address in &addresses { + let net_ip = network_address.ip(); + let net_network = network_address.network(); + let net_broadcast = network_address.broadcast(); + if ip == net_ip || ip == net_network || ip == net_broadcast { + return Err(WireguardConfigParseError::InvalidPeerIp(ip)); + } + } + // TODO(jck) ensure at least one of the networks contains the allowed_ip + peer_addresses.push(ip); + } + Err(err) => return Err(WireguardConfigParseError::AddrParseError(err)), + } } let pubkey = peer @@ -149,7 +165,7 @@ pub(crate) fn parse_wireguard_config( user_id: None, name: pubkey.to_string(), wireguard_pubkey: pubkey.to_string(), - wireguard_ip: ip, + wireguard_ips: peer_addresses, }); } @@ -166,7 +182,7 @@ mod test { let config = " [Interface] PrivateKey = GAA2X3DW0WakGVx+DsGjhDpTgg50s1MlmrLf24Psrlg= - Address = 10.0.0.1/24, fc00::defc/64 + Address = 10.0.0.1/24 ListenPort = 55055 DNS = 10.0.0.2 @@ -187,11 +203,75 @@ mod test { ); assert_eq!(network.id, NoId); assert_eq!(network.name, "Y5ewP5RXstQd71gkmS/M0xL8wi0yVbbVY/ocLM4cQ1Y="); + assert_eq!(network.address, vec!["10.0.0.1/24".parse().unwrap()]); + assert_eq!(network.port, 55055); + assert_eq!( + network.pubkey, + "Y5ewP5RXstQd71gkmS/M0xL8wi0yVbbVY/ocLM4cQ1Y=" + ); + assert_eq!( + network.prvkey, + "GAA2X3DW0WakGVx+DsGjhDpTgg50s1MlmrLf24Psrlg=" + ); + assert_eq!(network.endpoint, ""); + assert_eq!(network.dns, Some("10.0.0.2".to_string())); + assert_eq!(network.allowed_ips, vec!["10.0.0.0/24".parse().unwrap()]); + assert_eq!(network.connected_at, None); + + assert_eq!(devices.len(), 2); + + let device1 = &devices[0]; + assert_eq!( + device1.wireguard_pubkey, + "2LYRr2HgSSpGCdXKDDAlcFe0Uuc6RR8TFgSquNc9VAE=" + ); + assert_eq!( + device1.wireguard_ips, + vec!["10.0.0.10".parse::().unwrap()] + ); + + let device2 = &devices[1]; + assert_eq!( + device2.wireguard_pubkey, + "OLQNaEH3FxW0hiodaChEHoETzd+7UzcqIbsLs+X8rD0=" + ); + assert_eq!( + device2.wireguard_ips, + vec!["10.0.0.11".parse::().unwrap()] + ); + } + + #[test] + fn test_parse_config_dualstack() { + let config = " + [Interface] + PrivateKey = GAA2X3DW0WakGVx+DsGjhDpTgg50s1MlmrLf24Psrlg= + Address = 10.0.0.1/24,fc00::/112 + ListenPort = 55055 + DNS = 10.0.0.2 + + [Peer] + PublicKey = 2LYRr2HgSSpGCdXKDDAlcFe0Uuc6RR8TFgSquNc9VAE= + AllowedIPs = 10.0.0.10/24,fc00::10 + PersistentKeepalive = 300 + + [Peer] + PublicKey = OLQNaEH3FxW0hiodaChEHoETzd+7UzcqIbsLs+X8rD0= + AllowedIPs = 10.0.0.11/24,fc00::11 + PersistentKeepalive = 300 + "; + let (network, devices) = parse_wireguard_config(config).unwrap(); + assert_eq!( + network.prvkey, + "GAA2X3DW0WakGVx+DsGjhDpTgg50s1MlmrLf24Psrlg=" + ); + assert_eq!(network.id, NoId); + assert_eq!(network.name, "Y5ewP5RXstQd71gkmS/M0xL8wi0yVbbVY/ocLM4cQ1Y="); assert_eq!( network.address, vec![ "10.0.0.1/24".parse().unwrap(), - "fc00::defc/64".parse().unwrap() + "fc00::/112".parse().unwrap() ] ); assert_eq!(network.port, 55055); @@ -205,7 +285,13 @@ mod test { ); assert_eq!(network.endpoint, ""); assert_eq!(network.dns, Some("10.0.0.2".to_string())); - assert_eq!(network.allowed_ips, vec!["10.0.0.0/24".parse().unwrap()]); + assert_eq!( + network.allowed_ips, + vec![ + "10.0.0.0/24".parse().unwrap(), + "fc00::/112".parse().unwrap(), + ] + ); assert_eq!(network.connected_at, None); assert_eq!(devices.len(), 2); @@ -215,13 +301,25 @@ mod test { device1.wireguard_pubkey, "2LYRr2HgSSpGCdXKDDAlcFe0Uuc6RR8TFgSquNc9VAE=" ); - assert_eq!(device1.wireguard_ip.to_string(), "10.0.0.10"); + assert_eq!( + device1.wireguard_ips, + vec![ + "10.0.0.10".parse::().unwrap(), + "fc00::10".parse::().unwrap() + ] + ); let device2 = &devices[1]; assert_eq!( device2.wireguard_pubkey, "OLQNaEH3FxW0hiodaChEHoETzd+7UzcqIbsLs+X8rD0=" ); - assert_eq!(device2.wireguard_ip.to_string(), "10.0.0.11"); + assert_eq!( + device2.wireguard_ips, + vec![ + "10.0.0.11".parse::().unwrap(), + "fc00::11".parse::().unwrap(), + ] + ); } } diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index 0445d6a10e..3d17f7483b 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -1,5 +1,7 @@ pub mod common; +use std::net::IpAddr; + use claims::assert_err; use defguard::{ db::{models::device::DeviceType, Device, GatewayEvent, Group, Id, User, WireguardNetwork}, @@ -374,7 +376,10 @@ async fn test_import_network_existing_devices() { response.devices[0].wireguard_pubkey, "l07+qPWs4jzW3Gp1DKbHgBMRRm4Jg3q2BJxw0ZYl6c4=" ); - assert_eq!(response.devices[0].wireguard_ip.to_string(), "10.0.0.12"); + assert_eq!( + response.devices[0].wireguard_ips, + ["10.0.0.12".parse::().unwrap()] + ); let network = response.network; let peers = network.get_peers(&client_state.pool).await.unwrap(); @@ -502,8 +507,7 @@ PersistentKeepalive = 300 assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( device_info.network_info[0].device_wireguard_ip, - // TODO(jck) - vec![mapped_devices[0].wireguard_ip], + mapped_devices[0].wireguard_ips, ); let GatewayEvent::DeviceCreated(device_info) = wg_rx.try_recv().unwrap() else { @@ -517,7 +521,7 @@ PersistentKeepalive = 300 assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( device_info.network_info[0].device_wireguard_ip, - vec![mapped_devices[1].wireguard_ip], + mapped_devices[1].wireguard_ips, ); assert_err!(wg_rx.try_recv()); diff --git a/tests/wireguard_network_import.rs b/tests/wireguard_network_import.rs index 7a77d196a2..12c5f4da60 100644 --- a/tests/wireguard_network_import.rs +++ b/tests/wireguard_network_import.rs @@ -1,5 +1,7 @@ pub mod common; +use std::net::IpAddr; + use defguard::{ db::{ models::{ @@ -153,7 +155,10 @@ async fn test_config_import() { assert_eq!(devices.len(), 2); let mut device1 = devices[0].clone(); - assert_eq!(device1.wireguard_ip.to_string(), "10.0.0.10"); + assert_eq!( + device1.wireguard_ips, + ["10.0.0.10".parse::().unwrap()] + ); assert_eq!( device1.wireguard_pubkey, "2LYRr2HgSSpGCdXKDDAlcFe0Uuc6RR8TFgSquNc9VAE=" @@ -162,7 +167,10 @@ async fn test_config_import() { assert_eq!(device1.user_id, None); let mut device2 = devices[1].clone(); - assert_eq!(device2.wireguard_ip.to_string(), "10.0.0.11"); + assert_eq!( + device2.wireguard_ips, + ["10.0.0.11".parse::().unwrap()] + ); assert_eq!( device2.wireguard_pubkey, "OLQNaEH3FxW0hiodaChEHoETzd+7UzcqIbsLs+X8rD0=" From 2aa98d1384de17336ff099e134be2920f5470c49 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 16 Apr 2025 08:44:58 +0200 Subject: [PATCH 27/71] fix mapped devices --- src/db/models/wireguard.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index c363ea5ba5..0243b87bee 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -48,7 +48,7 @@ pub struct MappedDevice { pub user_id: Id, pub name: String, pub wireguard_pubkey: String, - pub wireguard_ip: IpAddr, + pub wireguard_ip: Vec, } pub const WIREGUARD_MAX_HANDSHAKE: TimeDelta = TimeDelta::minutes(8); @@ -758,11 +758,10 @@ impl WireguardNetwork { let mut network_info = Vec::new(); match &allowed_groups { None => { - // TODO(jck) allow assignment of multiple ips for a network device let wireguard_network_device = WireguardNetworkDevice::new( self.id, device.id, - [mapped_device.wireguard_ip], + mapped_device.wireguard_ip.clone(), ); wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { @@ -779,8 +778,7 @@ impl WireguardNetwork { let wireguard_network_device = WireguardNetworkDevice::new( self.id, device.id, - // TODO(jck) allow assignment of multiple ips for a network device - [mapped_device.wireguard_ip], + mapped_device.wireguard_ip.clone(), ); wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { From 00f65faa9fa5681585a30ae463a0793f3ef14b09 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 16 Apr 2025 09:09:27 +0200 Subject: [PATCH 28/71] Update validator for "map imported device" form --- src/wg_config.rs | 14 +++++--------- .../WizardMapDevices/WizardMapDevices.tsx | 6 ++---- web/src/shared/types.ts | 2 +- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/wg_config.rs b/src/wg_config.rs index a17d314704..76e0cac1a4 100644 --- a/src/wg_config.rs +++ b/src/wg_config.rs @@ -1,7 +1,4 @@ -use std::{ - array::TryFromSliceError, - net::{AddrParseError, IpAddr}, -}; +use std::{array::TryFromSliceError, net::IpAddr}; use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; use ipnetwork::{IpNetwork, IpNetworkError}; @@ -38,8 +35,6 @@ pub(crate) enum WireguardConfigParseError { InvalidIp(#[from] IpNetworkError), #[error("Invalid peer IP: {0}")] InvalidPeerIp(IpAddr), - #[error("Failed to parse IP: {0}")] - AddrParseError(#[from] AddrParseError), #[error("Invalid key: {0}")] InvalidKey(String), #[error("Invalid port: {0}")] @@ -131,8 +126,9 @@ pub(crate) fn parse_wireguard_config( let mut peer_addresses: Vec = Vec::new(); for allowed_ip in allowed_ips.split(',') { - match allowed_ip.trim().parse::() { - Ok(ip) => { + match allowed_ip.trim().parse::() { + Ok(network) => { + let ip = network.ip(); // check if assigned IP collides with any of gateway IPs for network_address in &addresses { let net_ip = network_address.ip(); @@ -145,7 +141,7 @@ pub(crate) fn parse_wireguard_config( // TODO(jck) ensure at least one of the networks contains the allowed_ip peer_addresses.push(ip); } - Err(err) => return Err(WireguardConfigParseError::AddrParseError(err)), + Err(err) => return Err(WireguardConfigParseError::InvalidIp(err)), } } diff --git a/web/src/pages/wizard/components/WizardMapDevices/WizardMapDevices.tsx b/web/src/pages/wizard/components/WizardMapDevices/WizardMapDevices.tsx index a34f94187f..f94cf2856e 100644 --- a/web/src/pages/wizard/components/WizardMapDevices/WizardMapDevices.tsx +++ b/web/src/pages/wizard/components/WizardMapDevices/WizardMapDevices.tsx @@ -49,7 +49,7 @@ export const WizardMapDevices = () => { z.object({ devices: z.array( z.object({ - wireguard_ip: z.string().min(1, LL.form.error.required()), + wireguard_ip: z.array(z.string().min(1, LL.form.error.required())), user_id: z .number({ invalid_type_error: LL.form.error.required(), @@ -103,7 +103,7 @@ export const WizardMapDevices = () => { const getHeaders = useMemo( (): ListHeader[] => [ { text: 'Device Name', key: 0, sortable: false }, - { text: 'IP', key: 1, sortable: false }, + { text: 'IPs', key: 1, sortable: false }, { text: 'User', key: 2, sortable: false }, ], [], @@ -129,7 +129,6 @@ export const WizardMapDevices = () => { const handleInvalidSubmit: SubmitErrorHandler = () => { toaster.error(LL.wizard.deviceMap.messages.errorsInForm()); }; - const devicesList = useMemo((): DeviceRowData[] => { if (importedDevices) { return importedDevices.map((_, index) => ({ @@ -171,7 +170,6 @@ export const WizardMapDevices = () => { }, [getValues, setImportedDevices]); if (isLoading || !importedDevices || createLoading) return ; - return (
diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 0298ab7360..f69ea26cd1 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -329,7 +329,7 @@ export interface ImportNetworkResponse { export interface ImportedDevice { name: string; - wireguard_ip: string; + wireguard_ip: string[]; wireguard_pubkey: string; user_id?: number; } From ddc8cd898e3c849003cc630f492b5b360ca8c56a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 16 Apr 2025 10:12:11 +0200 Subject: [PATCH 29/71] Find IPs for a new network device in all networks --- src/db/models/wireguard.rs | 5 ++--- src/handlers/network_devices.rs | 40 +++++++++++++++++++++------------ src/lib.rs | 4 ++-- web/src/shared/types.ts | 6 ++--- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 0243b87bee..4bcb8e194b 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -466,10 +466,9 @@ impl WireguardNetwork { /// Checks if all device addresses are contained in at least one of the network addresses pub fn contains_all(&self, addresses: &[IpAddr]) -> bool { - !self - .address + addresses .iter() - .any(|net| !addresses.iter().any(|addr| net.contains(*addr))) + .all(|addr| self.address.iter().any(|net| net.contains(*addr))) } /// Finds [`IpNetwork`] containing given [`IpAddr`] diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 9fd790b8a4..8fe8936ff7 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -357,7 +357,7 @@ pub(crate) async fn check_ip_availability( } } -pub(crate) async fn find_available_ip( +pub(crate) async fn find_available_ips( _admin_role: AdminRole, Path(network_id): Path, State(appstate): State, @@ -373,8 +373,8 @@ pub(crate) async fn find_available_ip( })?; let mut transaction = appstate.pool.begin().await?; - // TODO(jck) - if let Some(network_address) = network.address.first() { + let mut split_ips = Vec::new(); + for network_address in &network.address { let net_ip = network_address.ip(); let net_network = network_address.network(); let net_broadcast = network_address.broadcast(); @@ -383,25 +383,37 @@ pub(crate) async fn find_available_ip( continue; } - // Break loop if IP is unassigned and return network device + // Break the loop if IP is unassigned and return network device if Device::find_by_ip(&mut *transaction, ip, network.id) .await? .is_none() { - let split_ip = split_ip(&ip, network_address); - transaction.commit().await?; - return Ok(ApiResponse { - json: json!(split_ip), - status: StatusCode::OK, - }); + split_ips.push(split_ip(&ip, network_address)); + break; } } } - Ok(ApiResponse { - json: json!({}), - status: StatusCode::NOT_FOUND, - }) + transaction.commit().await?; + if split_ips.len() != network.address.len() { + warn!( + "Failed to find available IPs for new device in network {} ({:?})", + network.name, network.address + ); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::NOT_FOUND, + }) + } else { + debug!( + "Found addresses {:?} for new device i network {} ({:?})", + split_ips, network.name, network.address + ); + return Ok(ApiResponse { + json: json!(split_ips), + status: StatusCode::OK, + }); + } } #[derive(Serialize, Deserialize, Debug)] diff --git a/src/lib.rs b/src/lib.rs index f011260458..71d8c9d9ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ use handlers::{ group::{bulk_assign_to_groups, list_groups_info}, network_devices::{ add_network_device, check_ip_availability, download_network_device_config, - find_available_ip, get_network_device, list_network_devices, modify_network_device, + find_available_ips, get_network_device, list_network_devices, modify_network_device, start_network_device_setup, start_network_device_setup_for_device, }, ssh_authorized_keys::{ @@ -513,7 +513,7 @@ pub fn build_webapp( // Network devices, as opposed to user devices .route("/device/network", post(add_network_device)) .route("/device/network", get(list_network_devices)) - .route("/device/network/ip/{network_id}", get(find_available_ip)) + .route("/device/network/ip/{network_id}", get(find_available_ips)) .route( "/device/network/ip/{network_id}", post(check_ip_availability), diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index f69ea26cd1..1bd283e7d5 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -1181,7 +1181,7 @@ export type DirsyncTestResponse = { export type CreateStandaloneDeviceRequest = { name: string; location_id: number; - assigned_ip: string; + assigned_ips: string; wireguard_pubkey?: string; description?: string; }; @@ -1210,7 +1210,7 @@ export type GetAvailableLocationIpResponse = { export type StandaloneDevice = { id: number; name: string; - assigned_ip: string; + assigned_ips: string; description?: string; added_by: string; added_date: string; @@ -1247,7 +1247,7 @@ export type CreateStandaloneDeviceResponse = { export type StandaloneDeviceEditRequest = { id: number; - assigned_ip: string; + assigned_ips: string; description?: string; name: string; }; From 06f8e72f49488b9fb6bc28601df064a2d29eea2b Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 16 Apr 2025 13:01:27 +0200 Subject: [PATCH 30/71] wip --- .../steps/SetupCliStep/SetupCliStep.tsx | 2 +- .../steps/SetupManualStep/SetupManualStep.tsx | 4 +- .../modals/AddStandaloneDeviceModal/types.ts | 2 +- .../EditStandaloneModal.tsx | 6 +- .../StandaloneDeviceModalForm.tsx | 105 ++++++++++++------ web/src/shared/types.ts | 16 +-- 6 files changed, 89 insertions(+), 46 deletions(-) diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupCliStep/SetupCliStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupCliStep/SetupCliStep.tsx index 791f77a622..0810114947 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupCliStep/SetupCliStep.tsx +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupCliStep/SetupCliStep.tsx @@ -95,7 +95,7 @@ export const SetupCliStep = () => { const handleSubmit = useCallback( async (values: AddStandaloneDeviceFormFields) => { const response = await mutateAsync({ - assigned_ip: values.modifiableIpPart, + assigned_ips: values.modifiableIpPart, location_id: values.location_id, name: values.name, description: values.description, diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupManualStep/SetupManualStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupManualStep/SetupManualStep.tsx index 1674a19461..d732966625 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupManualStep/SetupManualStep.tsx +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupManualStep/SetupManualStep.tsx @@ -82,7 +82,7 @@ export const SetupManualStep = () => { }); } const response = await mutateAsync({ - assigned_ip: values.modifiableIpPart, + assigned_ips: values.modifiableIpParts, location_id: values.location_id, name: values.name, description: values.description, @@ -100,7 +100,7 @@ export const SetupManualStep = () => { const defaultFormValues = useMemo(() => { if (locationOptions && initialIpResponse) { const res: AddStandaloneDeviceFormFields = { - modifiableIpPart: initialIpResponse.modifiable_part, + modifiableIpParts: initialIpResponse.map((ip) => ip.modifiable_part), generationChoice: WGConfigGenChoice.AUTO, location_id: locationOptions[0].value, name: '', diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts b/web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts index d971f27bc0..5f409a46eb 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts @@ -19,7 +19,7 @@ export enum WGConfigGenChoice { export type AddStandaloneDeviceFormFields = { name: string; location_id: number; - modifiableIpPart: string; + modifiableIpParts: string[]; wireguard_pubkey: string; generationChoice: WGConfigGenChoice; description?: string; diff --git a/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx b/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx index 76ffc5062a..9763656ab6 100644 --- a/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx +++ b/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx @@ -103,7 +103,7 @@ const ModalContent = () => { const defaultValues = useMemo(() => { if (locationOptions && device) { - let modifiablePart = device.assigned_ip.split(device.split_ip.network_part)[1]; + let modifiablePart = device.assigned_ips.split(device.split_ip.network_part)[1]; if (modifiablePart === undefined) { modifiablePart = device.split_ip.modifiable_part; @@ -126,7 +126,7 @@ const ModalContent = () => { async (values: AddStandaloneDeviceFormFields) => { if (device) { await mutateAsync({ - assigned_ip: values.modifiableIpPart, + assigned_ips: values.modifiableIpPart, id: device.id, name: values.name, description: values.description, @@ -152,7 +152,7 @@ const ModalContent = () => { submitSubject={submitSubject} reservedNames={reservedDeviceNames} initialIpRecommendation={{ - ip: device.assigned_ip, + ip: device.assigned_ips, ...device.split_ip, }} /> diff --git a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx index 03c1882f3e..80f292e106 100644 --- a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx +++ b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx @@ -47,7 +47,7 @@ export const StandaloneDeviceModalForm = ({ reservedNames, initialIpRecommendation, }: Props) => { - const [internalRecommendedIp, setInternalRecommendedIp] = useState< + const [internalRecommendedIps, setInternalRecommendedIps] = useState< GetAvailableLocationIpResponse | undefined >(); const { LL } = useI18nContext(); @@ -160,22 +160,25 @@ export const StandaloneDeviceModalForm = ({ formValues, ) => { const values = formValues; - const { modifiableIpPart } = values; + const { modifiableIpParts: modifiableIpPart } = values; values.description = values.description?.trim(); values.name = values.name.trim(); - const currentIpResp = internalRecommendedIp ?? initialIpRecommendation; - values.modifiableIpPart = - currentIpResp.network_part + formValues.modifiableIpPart.trim(); + const currentIpResp = internalRecommendedIps ?? initialIpRecommendation; + {/* values.modifiableIpParts = */} + {/* currentIpResp.network_part + formValues.modifiableIpParts.trim(); */} + values.modifiableIpParts = + currentIpResp.map((resp, i) => resp.network_part + formValues.modifiableIpParts[i].trim()); + console.log("modifiableIpParts: ", values.modifiableIpParts); if ( mode === StandaloneDeviceModalFormMode.EDIT && - modifiableIpPart === defaults.modifiableIpPart + modifiableIpPart === defaults.modifiableIpParts ) { await onSubmit(values); return; } try { const response = await validateLocationIp({ - ip: values.modifiableIpPart, + ips: values.modifiableIpParts, location: values.location_id, }); const { available, valid } = response; @@ -183,12 +186,12 @@ export const StandaloneDeviceModalForm = ({ await onSubmit(values); } else { if (!available) { - setError('modifiableIpPart', { + setError('modifiableIpParts', { message: LL.form.error.reservedIp(), }); } if (!valid) { - setError('modifiableIpPart', { + setError('modifiableIpParts', { message: LL.form.error.invalidIp(), }); } @@ -207,9 +210,9 @@ export const StandaloneDeviceModalForm = ({ locationId, }) .then((resp) => { - setInternalRecommendedIp(resp); - resetField('modifiableIpPart', { - defaultValue: resp.modifiable_part, + setInternalRecommendedIps(resp); + resetField('modifiableIpParts', { + defaultValue: resp.map((r) => r.modifiable_part), }); }) .finally(() => { @@ -236,36 +239,76 @@ export const StandaloneDeviceModalForm = ({ return () => sub.unsubscribe(); }, [submitSubject]); + let ips = [ + { + "ip": "10.1.1.2", + "modifiable_part": "2", + "network_part": "10.1.1.", + "network_prefix": "24" + }, + { + "ip": "fc00::2", + "modifiable_part": "0002", + "network_part": "fc00:0000:0000:0000:0000:0000:0000:", + "network_prefix": "112" + } + ] return ( -
- + + + {ips.map((ip, i) => { -
- + })} + {/* {internalRecommendedIps?.map((ip, i) => { */} + {/* */} + {/* })} */} + {/* */} {mode === StandaloneDeviceModalFormMode.CREATE_MANUAL && ( <> Promise; validateLocationIp: ( - data: ValidateLocationIpRequest, - ) => Promise; + data: ValidateLocationIpsRequest, + ) => Promise; getDevicesList: () => Promise; getDeviceConfig: (deviceId: number | string) => Promise; generateAuthToken: (deviceId: number | string) => Promise; @@ -1181,17 +1181,17 @@ export type DirsyncTestResponse = { export type CreateStandaloneDeviceRequest = { name: string; location_id: number; - assigned_ips: string; + assigned_ips: string[]; wireguard_pubkey?: string; description?: string; }; -export type ValidateLocationIpRequest = { - ip: string; +export type ValidateLocationIpsRequest = { + ips: string[]; location: number | string; }; -export type ValidateLocationIpResponse = { +export type ValidateLocationIpsResponse = { available: boolean; valid: boolean; }; @@ -1200,12 +1200,12 @@ export type GetAvailableLocationIpRequest = { locationId: number | string; }; -export type GetAvailableLocationIpResponse = { +export type GetAvailableLocationIpResponse = [{ ip: string; network_part: string; modifiable_part: string; network_prefix: string; -}; +}]; export type StandaloneDevice = { id: number; From f4c9e51cc617e3b49192b5cec1394f9b44070020 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 17 Apr 2025 09:57:30 +0200 Subject: [PATCH 31/71] frontend displayes multiple ip recommendations --- .../steps/SetupCliStep/SetupCliStep.tsx | 4 +- .../EditStandaloneModal.tsx | 4 +- .../StandaloneDeviceModalForm.tsx | 52 ++----------------- 3 files changed, 9 insertions(+), 51 deletions(-) diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupCliStep/SetupCliStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupCliStep/SetupCliStep.tsx index 0810114947..49e7fcb1a7 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupCliStep/SetupCliStep.tsx +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/SetupCliStep/SetupCliStep.tsx @@ -80,7 +80,7 @@ export const SetupCliStep = () => { const defaultValues = useMemo(() => { if (initIpResponse && locationOptions) { const res: AddStandaloneDeviceFormFields = { - modifiableIpPart: initIpResponse.modifiable_part, + modifiableIpParts: initIpResponse.map((ip) => ip.modifiable_part), generationChoice: WGConfigGenChoice.AUTO, location_id: locationOptions[0].value, name: '', @@ -95,7 +95,7 @@ export const SetupCliStep = () => { const handleSubmit = useCallback( async (values: AddStandaloneDeviceFormFields) => { const response = await mutateAsync({ - assigned_ips: values.modifiableIpPart, + assigned_ips: values.modifiableIpParts, location_id: values.location_id, name: values.name, description: values.description, diff --git a/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx b/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx index 9763656ab6..8f6b96a6e4 100644 --- a/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx +++ b/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx @@ -111,7 +111,7 @@ const ModalContent = () => { const res: AddStandaloneDeviceFormFields = { name: device?.name, - modifiableIpPart: modifiablePart, + modifiableIpParts: modifiablePart, location_id: device.location.id, description: device.description, generationChoice: WGConfigGenChoice.AUTO, @@ -126,7 +126,7 @@ const ModalContent = () => { async (values: AddStandaloneDeviceFormFields) => { if (device) { await mutateAsync({ - assigned_ips: values.modifiableIpPart, + assigned_ips: values.modifiableIpParts, id: device.id, name: values.name, description: values.description, diff --git a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx index 80f292e106..de4749c23b 100644 --- a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx +++ b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx @@ -164,11 +164,8 @@ export const StandaloneDeviceModalForm = ({ values.description = values.description?.trim(); values.name = values.name.trim(); const currentIpResp = internalRecommendedIps ?? initialIpRecommendation; - {/* values.modifiableIpParts = */} - {/* currentIpResp.network_part + formValues.modifiableIpParts.trim(); */} values.modifiableIpParts = currentIpResp.map((resp, i) => resp.network_part + formValues.modifiableIpParts[i].trim()); - console.log("modifiableIpParts: ", values.modifiableIpParts); if ( mode === StandaloneDeviceModalFormMode.EDIT && modifiableIpPart === defaults.modifiableIpParts @@ -239,20 +236,7 @@ export const StandaloneDeviceModalForm = ({ return () => sub.unsubscribe(); }, [submitSubject]); - let ips = [ - { - "ip": "10.1.1.2", - "modifiable_part": "2", - "network_part": "10.1.1.", - "network_prefix": "24" - }, - { - "ip": "fc00::2", - "modifiable_part": "0002", - "network_part": "fc00:0000:0000:0000:0000:0000:0000:", - "network_prefix": "112" - } - ] + const recommendedIps = internalRecommendedIps || initialIpRecommendation; return ( @@ -269,46 +253,20 @@ export const StandaloneDeviceModalForm = ({ controller={{ control, name: 'description' }} label={labels.description()} /> - {ips.map((ip, i) => { + {recommendedIps.map((ip, i) => ( - })} - {/* {internalRecommendedIps?.map((ip, i) => { */} - {/* */} - {/* })} */} - {/* */} + ))} {mode === StandaloneDeviceModalFormMode.CREATE_MANUAL && ( <> Date: Thu, 17 Apr 2025 11:15:55 +0200 Subject: [PATCH 32/71] wip further frontend mods related to multi-address functionality --- .../components/DevicesList/DevicesList.tsx | 6 +++--- .../EditStandaloneModal.tsx | 20 +++++++++---------- .../StandaloneDeviceModalForm.tsx | 13 ++++++------ web/src/shared/types.ts | 20 ++++++++++--------- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/web/src/pages/devices/components/DevicesList/DevicesList.tsx b/web/src/pages/devices/components/DevicesList/DevicesList.tsx index 4780c25869..fe5ec1aeba 100644 --- a/web/src/pages/devices/components/DevicesList/DevicesList.tsx +++ b/web/src/pages/devices/components/DevicesList/DevicesList.tsx @@ -83,7 +83,7 @@ export const DevicesList = () => { }; const DeviceRow = (props: StandaloneDevice) => { - const { description, id, location, name, added_by, added_date, assigned_ip } = props; + const { description, id, location, name, added_by, added_date, assigned_ips } = props; const formatDate = useMemo(() => { const day = dayjs(added_date); return day.format('DD.MM.YYYY | HH:mm'); @@ -114,12 +114,12 @@ const DeviceRow = (props: StandaloneDevice) => {
{ - void writeToClipboard(assigned_ip); + void writeToClipboard(assigned_ips); }} > diff --git a/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx b/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx index 8f6b96a6e4..362be4b095 100644 --- a/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx +++ b/web/src/pages/devices/modals/EditStandaloneDeviceModal/EditStandaloneModal.tsx @@ -103,15 +103,15 @@ const ModalContent = () => { const defaultValues = useMemo(() => { if (locationOptions && device) { - let modifiablePart = device.assigned_ips.split(device.split_ip.network_part)[1]; - - if (modifiablePart === undefined) { - modifiablePart = device.split_ip.modifiable_part; - } + const modifiableParts = device.assigned_ips.map( + (ip, i) => + ip.split(device.split_ips[i].network_part)[1] || + device.split_ips[i].modifiable_part, + ); const res: AddStandaloneDeviceFormFields = { name: device?.name, - modifiableIpParts: modifiablePart, + modifiableIpParts: modifiableParts, location_id: device.location.id, description: device.description, generationChoice: WGConfigGenChoice.AUTO, @@ -151,10 +151,10 @@ const ModalContent = () => { onSubmit={handleSubmit} submitSubject={submitSubject} reservedNames={reservedDeviceNames} - initialIpRecommendation={{ - ip: device.assigned_ips, - ...device.split_ip, - }} + initialIpRecommendation={device.assigned_ips.map((_, i) => ({ + ip: device.assigned_ips[i], + ...device.split_ips[i], + }))} /> )} {!defaultValues && ( diff --git a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx index de4749c23b..89dfe0b364 100644 --- a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx +++ b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx @@ -113,7 +113,7 @@ export const StandaloneDeviceModalForm = ({ }, LL.form.error.reservedName()), location_id: z.number(), description: z.string(), - modifiableIpPart: z.string().min(1, LL.form.error.required()), + modifiableIpParts: z.array(z.string().min(1, LL.form.error.required())), generationChoice: z.nativeEnum(WGConfigGenChoice), wireguard_pubkey: z.string().optional(), }) @@ -164,8 +164,9 @@ export const StandaloneDeviceModalForm = ({ values.description = values.description?.trim(); values.name = values.name.trim(); const currentIpResp = internalRecommendedIps ?? initialIpRecommendation; - values.modifiableIpParts = - currentIpResp.map((resp, i) => resp.network_part + formValues.modifiableIpParts[i].trim()); + values.modifiableIpParts = currentIpResp.map( + (resp, i) => resp.network_part + formValues.modifiableIpParts[i].trim(), + ); if ( mode === StandaloneDeviceModalFormMode.EDIT && modifiableIpPart === defaults.modifiableIpParts @@ -258,10 +259,8 @@ export const StandaloneDeviceModalForm = ({ key={i} controller={{ control, name: `modifiableIpParts.${i}` }} data={{ - networkPart: - ip?.network_part, - networkPrefix: - ip?.network_prefix, + networkPart: ip?.network_part, + networkPrefix: ip?.network_prefix, }} label={labels.assignedAddress()} disabled={ipIsLoading} diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 3e988e7b26..4075b17da9 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -1200,17 +1200,17 @@ export type GetAvailableLocationIpRequest = { locationId: number | string; }; -export type GetAvailableLocationIpResponse = [{ +export type GetAvailableLocationIpResponse = { ip: string; network_part: string; modifiable_part: string; network_prefix: string; -}]; +}[]; export type StandaloneDevice = { id: number; name: string; - assigned_ips: string; + assigned_ips: string[]; description?: string; added_by: string; added_date: string; @@ -1221,11 +1221,13 @@ export type StandaloneDevice = { id: number; name: string; }; - split_ip: { - network_part: string; - modifiable_part: string; - network_prefix: string; - }; + split_ips: [ + { + network_part: string; + modifiable_part: string; + network_prefix: string; + }, + ]; }; export type DeviceConfigurationResponse = { @@ -1247,7 +1249,7 @@ export type CreateStandaloneDeviceResponse = { export type StandaloneDeviceEditRequest = { id: number; - assigned_ips: string; + assigned_ips: string[]; description?: string; name: string; }; From 8ae756f68942c350a609960b8e9e4b2abe08d402 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 17 Apr 2025 13:07:40 +0200 Subject: [PATCH 33/71] fix form validation --- web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts | 2 +- .../StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts b/web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts index 5f409a46eb..0c43cda035 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/types.ts @@ -20,7 +20,7 @@ export type AddStandaloneDeviceFormFields = { name: string; location_id: number; modifiableIpParts: string[]; - wireguard_pubkey: string; + wireguard_pubkey?: string; generationChoice: WGConfigGenChoice; description?: string; }; diff --git a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx index 89dfe0b364..846258992d 100644 --- a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx +++ b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx @@ -112,7 +112,7 @@ export const StandaloneDeviceModalForm = ({ return !reservedNames.includes(value.trim()); }, LL.form.error.reservedName()), location_id: z.number(), - description: z.string(), + description: z.string().optional(), modifiableIpParts: z.array(z.string().min(1, LL.form.error.required())), generationChoice: z.nativeEnum(WGConfigGenChoice), wireguard_pubkey: z.string().optional(), From b26330ea3e493869c07569b583bdac9752beb307 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 17 Apr 2025 13:37:43 +0200 Subject: [PATCH 34/71] Vec of IPs in UserDeviceNetworkInfo, rename wireguard_ip -> wireguard_ips --- src/db/models/device.rs | 18 +++++++++++------- src/db/models/wireguard.rs | 12 ++++++------ src/grpc/desktop_client_mfa.rs | 2 +- src/grpc/gateway.rs | 4 ++-- src/handlers/wireguard.rs | 2 +- src/wireguard_peer_disconnect.rs | 2 +- tests/wireguard_network_allowed_groups.rs | 8 ++++---- tests/wireguard_network_import.rs | 18 +++++++++--------- 8 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 08b0e18474..be5fb8ae6a 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -142,7 +142,7 @@ pub struct DeviceInfo { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct DeviceNetworkInfo { pub network_id: Id, - pub device_wireguard_ip: Vec, + pub device_wireguard_ips: Vec, #[serde(skip_serializing)] pub preshared_key: Option, pub is_authorized: bool, @@ -160,7 +160,7 @@ impl DeviceInfo { let network_info = query_as!( DeviceNetworkInfo, "SELECT wireguard_network_id network_id, \ - wireguard_ip \"device_wireguard_ip: Vec\", \ + wireguard_ip \"device_wireguard_ips: Vec\", \ preshared_key, is_authorized \ FROM wireguard_network_device \ WHERE device_id = $1", @@ -190,7 +190,7 @@ pub struct UserDeviceNetworkInfo { pub network_id: Id, pub network_name: String, pub network_gateway_ip: String, - pub device_wireguard_ip: String, + pub device_wireguard_ips: Vec, pub last_connected_ip: Option, pub last_connected_location: Option, pub last_connected_at: Option, @@ -238,7 +238,11 @@ impl UserDevice { network_id: r.network_id, network_name: r.network_name, network_gateway_ip: r.gateway_endpoint, - device_wireguard_ip: r.device_wireguard_ip.comma_separated(), + device_wireguard_ips: r + .device_wireguard_ip + .iter() + .map(IpAddr::to_string) + .collect(), last_connected_ip: device_ip, last_connected_location: None, last_connected_at: r.latest_handshake, @@ -673,7 +677,7 @@ impl Device { .ok_or_else(|| DeviceError::Unexpected("Device not found in network".into()))?; let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip.clone(), + device_wireguard_ips: wireguard_network_device.wireguard_ip.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; @@ -706,7 +710,7 @@ impl Device { .await?; let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip.clone(), + device_wireguard_ips: wireguard_network_device.wireguard_ip.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; @@ -767,7 +771,7 @@ impl Device { ); let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip.clone(), + device_wireguard_ips: wireguard_network_device.wireguard_ip.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 4bcb8e194b..83b82e83c6 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -501,7 +501,7 @@ impl WireguardNetwork { device, network_info: vec![DeviceNetworkInfo { network_id: self.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }], @@ -521,7 +521,7 @@ impl WireguardNetwork { device, network_info: vec![DeviceNetworkInfo { network_id: self.id, - device_wireguard_ip: device_network_config.wireguard_ip, + device_wireguard_ips: device_network_config.wireguard_ip, preshared_key: device_network_config.preshared_key, is_authorized: device_network_config.is_authorized, }], @@ -543,7 +543,7 @@ impl WireguardNetwork { device, network_info: vec![DeviceNetworkInfo { network_id: self.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }], @@ -685,7 +685,7 @@ impl WireguardNetwork { device: existing_device, network_info: vec![DeviceNetworkInfo { network_id: self.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }], @@ -765,7 +765,7 @@ impl WireguardNetwork { wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { network_id: self.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }); @@ -782,7 +782,7 @@ impl WireguardNetwork { wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { network_id: self.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }); diff --git a/src/grpc/desktop_client_mfa.rs b/src/grpc/desktop_client_mfa.rs index 15d21d5548..4418e92949 100644 --- a/src/grpc/desktop_client_mfa.rs +++ b/src/grpc/desktop_client_mfa.rs @@ -264,7 +264,7 @@ impl ClientMfaServer { device: device.clone(), network_info: vec![DeviceNetworkInfo { network_id: location.id, - device_wireguard_ip: network_device.wireguard_ip, + device_wireguard_ips: network_device.wireguard_ip, preshared_key: network_device.preshared_key, is_authorized: network_device.is_authorized, }], diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index b0636ebe79..331a206740 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -288,7 +288,7 @@ impl GatewayUpdatesHandler { Peer { pubkey: device.device.wireguard_pubkey, allowed_ips: network_info - .device_wireguard_ip + .device_wireguard_ips .iter() .map(IpAddr::to_string) .collect(), @@ -323,7 +323,7 @@ impl GatewayUpdatesHandler { pubkey: device.device.wireguard_pubkey, // TODO(jck) allowed_ips: vec![network_info - .device_wireguard_ip + .device_wireguard_ips .comma_separated()], preshared_key: network_info.preshared_key.clone(), keepalive_interval: Some( diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index f9068b6b45..b9a45d4c0e 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -780,7 +780,7 @@ pub(crate) async fn modify_device( if let Some(wireguard_network_device) = wireguard_network_device { let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ip: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }; diff --git a/src/wireguard_peer_disconnect.rs b/src/wireguard_peer_disconnect.rs index 16f67a87a8..32d3740847 100644 --- a/src/wireguard_peer_disconnect.rs +++ b/src/wireguard_peer_disconnect.rs @@ -105,7 +105,7 @@ pub async fn run_periodic_peer_disconnect( device, network_info: vec![DeviceNetworkInfo { network_id: location.id, - device_wireguard_ip: device_network_config.wireguard_ip, + device_wireguard_ips: device_network_config.wireguard_ip, preshared_key: device_network_config.preshared_key, is_authorized: device_network_config.is_authorized, }], diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index 3d17f7483b..78c5777d79 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -399,7 +399,7 @@ async fn test_import_network_existing_devices() { assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( device_info.network_info[0] - .device_wireguard_ip + .device_wireguard_ips .comma_separated(), peers[1].allowed_ips[0] ); @@ -412,7 +412,7 @@ async fn test_import_network_existing_devices() { assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( device_info.network_info[0] - .device_wireguard_ip + .device_wireguard_ips .comma_separated(), peers[0].allowed_ips[0] ); @@ -506,7 +506,7 @@ PersistentKeepalive = 300 assert_eq!(device_info.network_info.len(), 1); assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( - device_info.network_info[0].device_wireguard_ip, + device_info.network_info[0].device_wireguard_ips, mapped_devices[0].wireguard_ips, ); @@ -520,7 +520,7 @@ PersistentKeepalive = 300 assert_eq!(device_info.network_info.len(), 1); assert_eq!(device_info.network_info[0].network_id, 1); assert_eq!( - device_info.network_info[0].device_wireguard_ip, + device_info.network_info[0].device_wireguard_ips, mapped_devices[1].wireguard_ips, ); diff --git a/tests/wireguard_network_import.rs b/tests/wireguard_network_import.rs index 12c5f4da60..07fb7b4aa4 100644 --- a/tests/wireguard_network_import.rs +++ b/tests/wireguard_network_import.rs @@ -141,7 +141,7 @@ async fn test_config_import() { .unwrap() .unwrap(); assert_eq!(user_device_1.networks.len(), 2); - assert_eq!(user_device_1.networks[1].device_wireguard_ip, "10.0.0.12"); + assert_eq!(user_device_1.networks[1].device_wireguard_ips, vec!["10.0.0.12"]); // generated IP for other existing device assert_matches!(wg_rx.try_recv().unwrap(), GatewayEvent::DeviceCreated(..)); let user_device_2 = UserDevice::from_device(&pool, device_2) @@ -217,23 +217,23 @@ async fn test_config_import() { assert_eq!(user_info.devices.len(), 4); assert_eq!(user_info.devices[0].device.name, "test device"); assert_eq!( - user_info.devices[0].networks[1].device_wireguard_ip, - "10.0.0.12" + user_info.devices[0].networks[1].device_wireguard_ips, + vec!["10.0.0.12"] ); assert_eq!(user_info.devices[1].device.name, "another test device"); assert_eq!( - user_info.devices[1].networks[1].device_wireguard_ip, - "10.0.0.2" + user_info.devices[1].networks[1].device_wireguard_ips, + vec!["10.0.0.2"] ); assert_eq!(user_info.devices[2].device.name, "device_1"); assert_eq!( - user_info.devices[2].networks[1].device_wireguard_ip, - "10.0.0.10" + user_info.devices[2].networks[1].device_wireguard_ips, + vec!["10.0.0.10"] ); assert_eq!(user_info.devices[3].device.name, "device_2"); assert_eq!( - user_info.devices[3].networks[1].device_wireguard_ip, - "10.0.0.11" + user_info.devices[3].networks[1].device_wireguard_ips, + vec!["10.0.0.11"] ); } From 0750516827c9ac2800844075d2d54d3423501dd7 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 17 Apr 2025 13:51:06 +0200 Subject: [PATCH 35/71] More renames and ip arrays --- src/grpc/enrollment.rs | 2 +- src/handlers/network_devices.rs | 6 +++--- src/handlers/wireguard.rs | 2 +- src/templates.rs | 6 +++--- tests/wireguard_network_import.rs | 5 ++++- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 707a7929ce..51c09b66e6 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -631,7 +631,7 @@ impl EnrollmentServer { .iter() .map(|c| TemplateLocation { name: c.network_name.clone(), - assigned_ip: c.address.comma_separated(), + assigned_ips: c.address.comma_separated(), }) .collect(); diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 8fe8936ff7..bdc44c0403 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -409,10 +409,10 @@ pub(crate) async fn find_available_ips( "Found addresses {:?} for new device i network {} ({:?})", split_ips, network.name, network.address ); - return Ok(ApiResponse { + Ok(ApiResponse { json: json!(split_ips), status: StatusCode::OK, - }); + }) } } @@ -675,7 +675,7 @@ pub(crate) async fn add_network_device( let template_locations = vec![TemplateLocation { name: config.network_name.clone(), - assigned_ip: config.address.comma_separated(), + assigned_ips: config.address.comma_separated(), }]; send_new_device_added_email( diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index b9a45d4c0e..4e3063d26d 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -664,7 +664,7 @@ pub(crate) async fn add_device( .iter() .map(|c| TemplateLocation { name: c.network_name.clone(), - assigned_ip: c.address.comma_separated(), + assigned_ips: c.address.comma_separated(), }) .collect(); diff --git a/src/templates.rs b/src/templates.rs index 84bd498868..926789861e 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -177,7 +177,7 @@ pub fn support_data_mail() -> Result { #[derive(Serialize, Debug, Clone)] pub struct TemplateLocation { pub name: String, - pub assigned_ip: String, + pub assigned_ips: String, } pub fn new_device_added_mail( @@ -408,11 +408,11 @@ mod test { let template_locations: Vec = vec![ TemplateLocation { name: "Test 01".into(), - assigned_ip: "10.0.0.10".into(), + assigned_ips: "10.0.0.10".into(), }, TemplateLocation { name: "Test 02".into(), - assigned_ip: "10.0.0.10".into(), + assigned_ips: "10.0.0.10".into(), }, ]; assert_ok!(new_device_added_mail( diff --git a/tests/wireguard_network_import.rs b/tests/wireguard_network_import.rs index 07fb7b4aa4..5713072ece 100644 --- a/tests/wireguard_network_import.rs +++ b/tests/wireguard_network_import.rs @@ -141,7 +141,10 @@ async fn test_config_import() { .unwrap() .unwrap(); assert_eq!(user_device_1.networks.len(), 2); - assert_eq!(user_device_1.networks[1].device_wireguard_ips, vec!["10.0.0.12"]); + assert_eq!( + user_device_1.networks[1].device_wireguard_ips, + vec!["10.0.0.12"] + ); // generated IP for other existing device assert_matches!(wg_rx.try_recv().unwrap(), GatewayEvent::DeviceCreated(..)); let user_device_2 = UserDevice::from_device(&pool, device_2) From 6eeac01683db5b911117bf00dec4873301e6d20a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 17 Apr 2025 16:34:33 +0200 Subject: [PATCH 36/71] wireguard_ips array in stats types --- web/src/shared/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 4075b17da9..b4078685ef 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -1109,7 +1109,7 @@ export interface NetworkDeviceStats { id: number; name: string; public_ip: string; - wireguard_ip: string; + wireguard_ips: string[]; stats: NetworkSpeedStats[]; } @@ -1123,7 +1123,7 @@ export type StandaloneDeviceStats = { stats: NetworkSpeedStats[]; user_id: number; name: string; - wireguard_ip?: string; + wireguard_ips: string[]; public_ip?: string; connected_at?: string; }; From 423729d43ab87507379de12801113c62aaaf2fa9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 18 Apr 2025 10:10:59 +0200 Subject: [PATCH 37/71] WireguardDeviceStatsRow as Vec --- src/db/models/wireguard.rs | 11 +++-- src/db/models/wireguard_peer_stats.rs | 70 +++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 83b82e83c6..488f069076 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -943,13 +943,16 @@ impl WireguardNetwork { let mut result = Vec::new(); for device in devices { let latest_stats = WireguardPeerStats::fetch_latest(conn, device.id, self.id).await?; + let wireguard_ips = if let Some(stats) = &latest_stats { + stats.trim_allowed_ips() + } else { + Vec::new() + }; result.push(WireguardDeviceStatsRow { id: device.id, user_id: device.user_id, name: device.name.clone(), - wireguard_ip: latest_stats - .as_ref() - .and_then(WireguardPeerStats::trim_allowed_ips), + wireguard_ips, public_ip: latest_stats .as_ref() .and_then(WireguardPeerStats::endpoint_without_port), @@ -1195,7 +1198,7 @@ pub struct WireguardDeviceStatsRow { pub stats: Vec, pub user_id: Id, pub name: String, - pub wireguard_ip: Option, + pub wireguard_ips: Vec, pub public_ip: Option, pub connected_at: Option, } diff --git a/src/db/models/wireguard_peer_stats.rs b/src/db/models/wireguard_peer_stats.rs index 8c3878a663..b3b6fed199 100644 --- a/src/db/models/wireguard_peer_stats.rs +++ b/src/db/models/wireguard_peer_stats.rs @@ -2,6 +2,7 @@ use std::time::Duration; use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc}; use humantime::format_duration; +use ipnetwork::IpNetwork; use model_derive::Model; use sqlx::{query, query_as, query_scalar, PgExecutor, PgPool}; @@ -144,10 +145,69 @@ impl WireguardPeerStats { }) } - /// Trim `allowed_ips` returning the first one without CIDR. - pub(crate) fn trim_allowed_ips(&self) -> Option { - self.allowed_ips - .as_ref() - .and_then(|ips| Some(ips.split_once('/')?.0.to_owned())) + /// Returns a `Vec` of `allowed_ips` without a CIDR mask. + /// Non-parsable addresses are omitted. + pub(crate) fn trim_allowed_ips(&self) -> Vec { + let Some(allowed_ips) = &self.allowed_ips else { + return Vec::new(); + }; + allowed_ips + .split(',') + .filter_map(|addr| Some(addr.trim().parse::().ok()?.ip().to_string())) + .collect() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_trim_allowed_ips() { + let mut stats = WireguardPeerStats { + id: 1, + device_id: 1, + collected_at: Utc::now().naive_utc(), + network: 1, + endpoint: None, + upload: 100, + download: 100, + latest_handshake: Utc::now().naive_utc(), + allowed_ips: None, + }; + assert!(stats.trim_allowed_ips().is_empty()); + + stats.allowed_ips = Some("10.1.1.1".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["10.1.1.1"]); + + stats.allowed_ips = Some("10.1.1.1/24".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["10.1.1.1"]); + + stats.allowed_ips = Some("10.1.1.1/24, 10.1.1.2".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["10.1.1.1", "10.1.1.2"]); + + stats.allowed_ips = Some("10.1.1.1/24, 10.1.1.2/24".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["10.1.1.1", "10.1.1.2"]); + + stats.allowed_ips = Some("fc00::1".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["fc00::1"]); + + stats.allowed_ips = Some("fc00::1/112".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["fc00::1"]); + + stats.allowed_ips = Some("fc00::1/112,fc00::2".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["fc00::1", "fc00::2"]); + + stats.allowed_ips = Some("fc00::1/112,fc00::2/112".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["fc00::1", "fc00::2"]); + + stats.allowed_ips = Some("10.1.1.1, fc00::1".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["10.1.1.1", "fc00::1"]); + + stats.allowed_ips = Some("10.1.1.1/24, fc00::1/112".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["10.1.1.1", "fc00::1"]); + + stats.allowed_ips = Some("nonparsable, fc00::1/112".to_string()); + assert_eq!(stats.trim_allowed_ips(), vec!["fc00::1"]); } } From a6c642fc7738aa1751f3cec05962daeee65bf207 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 18 Apr 2025 12:28:23 +0200 Subject: [PATCH 38/71] update defguard-ui --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index b12e3f35cd..d80e5794e5 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit b12e3f35cd03140db654ed6a39c0efc75854ad7d +Subproject commit d80e5794e5b618cd2acb8aaf4daf121777fdc266 From f6e6ff87e3819e5ac8c9a9d660149f4e85abd0bf Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 18 Apr 2025 12:44:50 +0200 Subject: [PATCH 39/71] fix new device email template --- templates/mail_new_device_added.tera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/mail_new_device_added.tera b/templates/mail_new_device_added.tera index 3b24e2972d..4309486c69 100644 --- a/templates/mail_new_device_added.tera +++ b/templates/mail_new_device_added.tera @@ -11,7 +11,7 @@ assigned_ip -> ip of device in location {# Generate locations list#} {% macro device_locations(locations) %} {% for location in locations %} -{{ macros::paragraph_with_title(title=location.name ~ ":", content=location.assigned_ip)}} +{{ macros::paragraph_with_title(title=location.name ~ ":", content=location.assigned_ips)}} {% endfor %} {% endmacro device_locations %} {# mail content #} From 3868faad0b2a02e0000134c2b7b5a5d8a44d7b3e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 17 Apr 2025 13:08:22 +0200 Subject: [PATCH 40/71] wip deduplicate ip check utility functions --- src/db/models/group.rs | 2 + src/db/models/wireguard.rs | 100 +++++++++++++++++++ src/handlers/network_devices.rs | 167 +++++++++++--------------------- 3 files changed, 160 insertions(+), 109 deletions(-) diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 4e03336c2e..136a967de4 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -293,6 +293,8 @@ impl WireguardNetwork { ); Ok(()) } + + } #[cfg(test)] diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 1362c6e866..2c740385c5 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, fmt, + iter::zip, net::{IpAddr, Ipv4Addr}, }; @@ -170,6 +171,24 @@ pub enum WireguardNetworkError { FirewallError(#[from] FirewallError), } +#[derive(Debug, Error)] +pub(crate) enum NetworkIpAssignmentError { + #[error( + "Location {0} has no network that could contain IP address {2}, available networks: {1:?}" + )] + NoContainingNetwork(String, IpAddr, Vec), + #[error("IP address {1} is reserved for gateway in location {0}")] + ReservedForGateway(String, IpAddr), + #[error("IP address {1} is network broadcast address in location {0}")] + IsBroadcastAddress(String, IpAddr), + #[error("IP address {1} is network address in location {0}")] + IsNetworkAddress(String, IpAddr), + #[error("IP address {1} is already assigned in location {0}")] + AddressAlreadyAssigned(String, IpAddr), + #[error(transparent)] + DbError(#[from] sqlx::Error), +} + impl WireguardNetwork { pub fn new( name: String, @@ -1144,6 +1163,87 @@ impl WireguardNetwork { .fetch_all(executor) .await } + + /// Checks if the IP addresses fall into the range of the network + /// and if they are not already assigned to another device. + pub async fn can_assign_ips( + &self, + ip_addrs: &[IpAddr], + transaction: &mut PgConnection, + ) -> Result { + // make sure the network contains all provided ips + let networks = ip_addrs + .iter() + .map(|ip| self.get_containing_network(*ip).ok_or(*ip)) + .collect::, IpAddr>>() + .map_err(|ip| { + // WebError::BadRequest(format!( + // "Provided IP addresses {ip_addrs:?} are not in the network ({}) range {:?}", + // network.name, network.address, + // )) + NetworkIpAssignmentError::NoContainingNetwork( + self.name.clone(), + ip, + self.address.clone(), + ) + })?; + for (ip, network_address) in zip(ip_addrs, networks) { + // validate ip address within network + let net_ip = network_address.ip(); + let net_network = network_address.network(); + let net_broadcast = network_address.broadcast(); + // if *ip == net_network || *ip == net_broadcast { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip} is a network or broadcast address of network {}", + // network.name + // ))); + // } + if *ip == net_network { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip} is a network address of network {}", + // network.name + // ))); + return Err(NetworkIpAssignmentError::IsNetworkAddress( + self.name.clone(), + *ip, + )); + } else if *ip == net_broadcast { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip} is a network address of network {}", + // "Provided IP address {ip} is a network broadcast address of network {}", + // network.name + // ))); + return Err(NetworkIpAssignmentError::IsBroadcastAddress( + self.name.clone(), + *ip, + )); + } else if *ip == net_ip { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip} may overlap with the network's gateway IP {net_ip} in network {}", + // network.name + // ))); + return Err(NetworkIpAssignmentError::ReservedForGateway( + self.name.clone(), + *ip, + )); + } + + // make sure the ip is unassigned + let device = Device::find_by_ip(&mut *transaction, *ip, self.id).await?; + if let Some(device) = device { + // return Err(WebError::BadRequest(format!( + // "Provided IP address {ip} is already assigned to device {} in network {}", + // device.name, network.name + // ))); + return Err(NetworkIpAssignmentError::AddressAlreadyAssigned( + self.name.clone(), + *ip, + )); + } + } + + Ok(true) + } } // [`IpNetwork`] does not implement [`Default`] diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index bdc44c0403..9fe207a7e7 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -1,5 +1,4 @@ use std::{ - iter::zip, net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr}, str::FromStr, }; @@ -18,7 +17,10 @@ use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, db::{ - models::device::{DeviceConfig, DeviceInfo, DeviceType, WireguardNetworkDevice}, + models::{ + device::{DeviceConfig, DeviceInfo, DeviceType, WireguardNetworkDevice}, + wireguard::NetworkIpAssignmentError, + }, Device, GatewayEvent, Id, User, WireguardNetwork, }, enterprise::limits::update_counts, @@ -207,65 +209,16 @@ pub struct AddNetworkDeviceResult { device: NetworkDeviceInfo, } -/// Checks if the IP addresses fall into the range of the network -/// and if they are not already assigned to another device. -async fn check_ips( - ip_addrs: &[IpAddr], - network: &WireguardNetwork, - transaction: &mut PgConnection, -) -> Result<(), WebError> { - // make sure the network contains all provided ips - let networks = ip_addrs - .iter() - .map(|ip| network.get_containing_network(*ip).ok_or(())) - .collect::, ()>>() - .map_err(|_| { - WebError::BadRequest(format!( - "Provided IP addresses {ip_addrs:?} are not in the network ({}) range {:?}", - network.name, network.address, - )) - })?; - for (ip, network_address) in zip(ip_addrs, networks) { - // validate ip address within network - let net_ip = network_address.ip(); - let net_network = network_address.network(); - let net_broadcast = network_address.broadcast(); - if *ip == net_network || *ip == net_broadcast { - return Err(WebError::BadRequest(format!( - "Provided IP address {ip} is a network or broadcast address of network {}", - network.name - ))); - } - if *ip == net_ip { - return Err(WebError::BadRequest(format!( - "Provided IP address {ip} may overlap with the network's gateway IP {net_ip} in network {}", - network.name - ))); - } - - // make sure the ip is unassigned - let device = Device::find_by_ip(&mut *transaction, *ip, network.id).await?; - if let Some(device) = device { - return Err(WebError::BadRequest(format!( - "Provided IP address {ip} is already assigned to device {} in network {}", - device.name, network.name - ))); - } - } - - Ok(()) -} - #[derive(Deserialize)] pub struct IpAvailabilityCheck { - ip: String, + ips: Vec, } pub(crate) async fn check_ip_availability( _admin_role: AdminRole, Path(network_id): Path, State(appstate): State, - Json(ip): Json, + Json(check): Json, ) -> ApiResult { let mut transaction = appstate.pool.begin().await?; let network = WireguardNetwork::find_by_id(&appstate.pool, network_id) @@ -277,10 +230,15 @@ pub(crate) async fn check_ip_availability( ); WebError::BadRequest("Failed to check IP availability, network not found".into()) })?; - - let Ok(ip) = IpAddr::from_str(&ip.ip) else { + let ips = check + .ips + .iter() + .map(|ip| IpAddr::from_str(ip)) + .collect::, AddrParseError>>(); + let Ok(ips) = ips else { warn!( - "Failed to check IP availability for network with ID {network_id}, invalid IP address", + "Failed to check IP availability for network {}, invalid IP address", + network.name ); return Ok(ApiResponse { json: json!({ @@ -291,70 +249,54 @@ pub(crate) async fn check_ip_availability( }); }; - // TODO(jck) - if let Some(network_address) = network.address.first() { - if !network_address.contains(ip) { + let mkresponse = |available: bool, valid: bool| { + Ok(ApiResponse { + json: json!({ + "available": available, + "valid": valid, + }), + status: StatusCode::OK, + }) + }; + return match network.can_assign_ips(&ips, &mut *transaction).await { + Ok(_) => mkresponse(true, true), + Err(NetworkIpAssignmentError::NoContainingNetwork(ip)) => { warn!( - "Provided device IP address is not in the network ({}) range {network_address}", + "Provided device IP address {ip} is not in the network {} range: {:?}", + network.name, network.address + ); + mkresponse(false, false) + } + Err(NetworkIpAssignmentError::ReservedForGateway(ip)) => { + warn!( + "Provided device IP {ip} address may overlap with the gateway's IP address on network {}", network.name ); - return Ok(ApiResponse { - json: json!({ - "available": false, - "valid": false, - }), - status: StatusCode::OK, - }); + mkresponse(false, true) } - if ip == network_address.network() || ip == network_address.broadcast() { + Err(NetworkIpAssignmentError::IsBroadcastAddress(ip)) => { warn!( - "Provided device IP address is network or broadcast address of network {}", + "Provided device IP address {ip} is broadcast address of network {}", network.name ); - return Ok(ApiResponse { - json: json!({ - "available": false, - "valid": true, - }), - status: StatusCode::OK, - }); + mkresponse(false, true) } - if ip == network_address.ip() { + Err(NetworkIpAssignmentError::IsNetworkAddress(ip)) => { warn!( - "Provided device IP address may overlap with the gateway's IP address on network {}", + "Provided device IP address {ip} is network address of network {}", network.name ); - return Ok(ApiResponse { - json: json!({ - "available": false, - "valid": true, - }), - status: StatusCode::OK, - }); + mkresponse(false, true) } - } - - if let Some(device) = Device::find_by_ip(&mut *transaction, ip, network.id).await? { - warn!( - "Provided device IP is already assigned to device {} in network {}", - device.name, network.name - ); - Ok(ApiResponse { - json: json!({ - "available": false, - "valid": true, - }), - status: StatusCode::OK, - }) - } else { - Ok(ApiResponse { - json: json!({ - "available": true, - "valid": true, - }), - status: StatusCode::OK, - }) - } + Err(NetworkIpAssignmentError::AddressAlreadyAssigned(ip)) => { + warn!( + "Provided device IP {ip} is already assigned in network {}", + network.name + ); + mkresponse(false, true) + } + Err(NetworkIpAssignmentError::DbError(err)) => Err(err)?, + }; } pub(crate) async fn find_available_ips( @@ -761,7 +703,14 @@ pub async fn modify_network_device( // IP address has changed, so remove device from network and add it again with new IP address. // TODO(jck) order-insensitive comparison if new_ips != *wireguard_network_device.wireguard_ip { - check_ips(&new_ips, &device_network, &mut transaction).await?; + // check_ips(&new_ips, &device_network, &mut transaction).await?; + match device_network + .can_assign_ips(&new_ips, &mut *transaction) + .await + { + Ok(_) => (), + Err(_) => todo!(), + }; // TODO(jck) wireguard_network_device.wireguard_ip = new_ips.clone(); wireguard_network_device.update(&mut *transaction).await?; From 51b04a705001805c595205deb005fc75ef4c1c6b Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 18 Apr 2025 14:46:27 +0200 Subject: [PATCH 41/71] IP assignment validation --- src/db/models/device.rs | 51 +++++++++++-------------------- src/db/models/group.rs | 2 -- src/db/models/wireguard.rs | 42 +++---------------------- src/error.rs | 2 +- src/handlers/network_devices.rs | 54 ++++++++++++++------------------- 5 files changed, 44 insertions(+), 107 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 1eb8e63c51..e2b1dfb5b8 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,4 +1,4 @@ -use std::{fmt, iter::zip, net::IpAddr}; +use std::{fmt, net::IpAddr}; use base64::{prelude::BASE64_STANDARD, Engine}; #[cfg(test)] @@ -21,7 +21,7 @@ use utoipa::ToSchema; use super::{ error::ModelError, - wireguard::{WireguardNetwork, WIREGUARD_MAX_HANDSHAKE}, + wireguard::{NetworkIpAssignmentError, WireguardNetwork, WIREGUARD_MAX_HANDSHAKE}, }; use crate::{ db::{Id, NoId, User}, @@ -515,8 +515,8 @@ pub enum DeviceError { PubkeyConflict(Device, String), #[error("Database error")] DatabaseError(#[from] sqlx::Error), - #[error("Model error")] - ModelError(#[from] ModelError), + #[error(transparent)] + NetworkIpAssignmentError(#[from] NetworkIpAssignmentError), #[error("Unexpected error: {0}")] Unexpected(String), } @@ -849,39 +849,22 @@ impl Device { transaction: &mut PgConnection, network: &WireguardNetwork, ips: &[IpAddr], - ) -> Result { - // make sure the network contains all provided ips - let networks = ips - .iter() - .map(|ip| { - network - .get_containing_network(*ip) - .ok_or(ModelError::CannotCreate) - }) - .collect::, ModelError>>()?; - for (ip, network_address) in zip(ips, networks) { - // validate ip address - let net_ip = network_address.ip(); - let net_network = network_address.network(); - let net_broadcast = network_address.broadcast(); - if *ip == net_ip || *ip == net_network || *ip == net_broadcast { - // TODO(jck) more relevant error - return Err(ModelError::CannotCreate); - } - - // make sure the ip is unassigned - if Self::find_by_ip(&mut *transaction, *ip, network.id) - .await? - .is_some() - { - // TODO(jck) more relevant error - return Err(ModelError::CannotCreate); - } + ) -> Result { + debug!( + "Assigning IPs: {ips:?} for device: {} in network {}", + self.name, network.name + ); + // ensure assignment is valid + if let Err(err) = network.can_assign_ips(ips, &mut *transaction).await { + error!("Invalid network IP assignment: {err}"); + return Err(err); } - // if all checks passed, assign the ips let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, ips); wireguard_network_device.insert(&mut *transaction).await?; - info!("Assigned IPs: {ips:?} for device: {}", self.name); + info!( + "Assigned IPs: {ips:?} for device: {} in network {}", + self.name, network.name + ); Ok(wireguard_network_device) } diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 136a967de4..4e03336c2e 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -293,8 +293,6 @@ impl WireguardNetwork { ); Ok(()) } - - } #[cfg(test)] diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 2c740385c5..0af2b44fe5 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -172,9 +172,9 @@ pub enum WireguardNetworkError { } #[derive(Debug, Error)] -pub(crate) enum NetworkIpAssignmentError { +pub enum NetworkIpAssignmentError { #[error( - "Location {0} has no network that could contain IP address {2}, available networks: {1:?}" + "Location {0} has no network that could contain IP address {1}, available networks: {2:?}" )] NoContainingNetwork(String, IpAddr, Vec), #[error("IP address {1} is reserved for gateway in location {0}")] @@ -1166,7 +1166,7 @@ impl WireguardNetwork { /// Checks if the IP addresses fall into the range of the network /// and if they are not already assigned to another device. - pub async fn can_assign_ips( + pub(crate) async fn can_assign_ips( &self, ip_addrs: &[IpAddr], transaction: &mut PgConnection, @@ -1177,10 +1177,6 @@ impl WireguardNetwork { .map(|ip| self.get_containing_network(*ip).ok_or(*ip)) .collect::, IpAddr>>() .map_err(|ip| { - // WebError::BadRequest(format!( - // "Provided IP addresses {ip_addrs:?} are not in the network ({}) range {:?}", - // network.name, network.address, - // )) NetworkIpAssignmentError::NoContainingNetwork( self.name.clone(), ip, @@ -1188,40 +1184,20 @@ impl WireguardNetwork { ) })?; for (ip, network_address) in zip(ip_addrs, networks) { - // validate ip address within network let net_ip = network_address.ip(); let net_network = network_address.network(); let net_broadcast = network_address.broadcast(); - // if *ip == net_network || *ip == net_broadcast { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip} is a network or broadcast address of network {}", - // network.name - // ))); - // } if *ip == net_network { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip} is a network address of network {}", - // network.name - // ))); return Err(NetworkIpAssignmentError::IsNetworkAddress( self.name.clone(), *ip, )); } else if *ip == net_broadcast { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip} is a network address of network {}", - // "Provided IP address {ip} is a network broadcast address of network {}", - // network.name - // ))); return Err(NetworkIpAssignmentError::IsBroadcastAddress( self.name.clone(), *ip, )); } else if *ip == net_ip { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip} may overlap with the network's gateway IP {net_ip} in network {}", - // network.name - // ))); return Err(NetworkIpAssignmentError::ReservedForGateway( self.name.clone(), *ip, @@ -1230,11 +1206,7 @@ impl WireguardNetwork { // make sure the ip is unassigned let device = Device::find_by_ip(&mut *transaction, *ip, self.id).await?; - if let Some(device) = device { - // return Err(WebError::BadRequest(format!( - // "Provided IP address {ip} is already assigned to device {} in network {}", - // device.name, network.name - // ))); + if device.is_some() { return Err(NetworkIpAssignmentError::AddressAlreadyAssigned( self.name.clone(), *ip, @@ -1342,7 +1314,6 @@ mod test { #[sqlx::test] async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let mut network = WireguardNetwork::default(); network.try_set_address("10.1.1.1/29").unwrap(); let network = network.save(&pool).await.unwrap(); @@ -1407,7 +1378,6 @@ mod test { #[sqlx::test] async fn test_connected_at_always_connected(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let mut network = WireguardNetwork::default(); network.try_set_address("10.1.1.1/29").unwrap(); let network = network.save(&pool).await.unwrap(); @@ -1470,7 +1440,6 @@ mod test { #[sqlx::test] async fn test_get_allowed_devices_for_user(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let mut network = WireguardNetwork::default(); network.try_set_address("10.1.1.1/29").unwrap(); let network = network.save(&pool).await.unwrap(); @@ -1563,7 +1532,6 @@ mod test { options: PgConnectOptions, ) { let pool = setup_pool(options).await; - let mut network = WireguardNetwork::default(); network.try_set_address("10.1.1.1/29").unwrap(); let network = network.save(&pool).await.unwrap(); @@ -1646,7 +1614,6 @@ mod test { #[sqlx::test] async fn test_sync_allowed_devices_for_user(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let mut network = WireguardNetwork::default(); network.try_set_address("10.1.1.1/29").unwrap(); let network = network.save(&pool).await.unwrap(); @@ -1759,7 +1726,6 @@ mod test { options: PgConnectOptions, ) { let pool = setup_pool(options).await; - let mut network = WireguardNetwork::default(); network.try_set_address("10.1.1.1/29").unwrap(); let network = network.save(&pool).await.unwrap(); diff --git a/src/error.rs b/src/error.rs index a0518f2330..5908ba8233 100644 --- a/src/error.rs +++ b/src/error.rs @@ -100,7 +100,7 @@ impl From for WebError { match error { DeviceError::PubkeyConflict(..) => Self::PubkeyValidation(error.to_string()), DeviceError::DatabaseError(_) => Self::DbError(error.to_string()), - DeviceError::ModelError(_) => Self::ModelError(error.to_string()), + DeviceError::NetworkIpAssignmentError(_) => Self::ModelError(error.to_string()), DeviceError::Unexpected(_) => Self::Http(StatusCode::INTERNAL_SERVER_ERROR), } } diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 9fe207a7e7..0cb599e0ea 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -258,41 +258,30 @@ pub(crate) async fn check_ip_availability( status: StatusCode::OK, }) }; - return match network.can_assign_ips(&ips, &mut *transaction).await { + return match network.can_assign_ips(&ips, &mut transaction).await { Ok(_) => mkresponse(true, true), - Err(NetworkIpAssignmentError::NoContainingNetwork(ip)) => { + Err(NetworkIpAssignmentError::NoContainingNetwork(name, ip, networks)) => { warn!( - "Provided device IP address {ip} is not in the network {} range: {:?}", - network.name, network.address + "Provided device IP address {ip} is not in the network {name} range: {networks:?}" ); mkresponse(false, false) } - Err(NetworkIpAssignmentError::ReservedForGateway(ip)) => { + Err(NetworkIpAssignmentError::ReservedForGateway(name, ip)) => { warn!( - "Provided device IP {ip} address may overlap with the gateway's IP address on network {}", - network.name + "Provided device IP address {ip} may overlap with the gateway's IP address on network {name}", ); mkresponse(false, true) } - Err(NetworkIpAssignmentError::IsBroadcastAddress(ip)) => { - warn!( - "Provided device IP address {ip} is broadcast address of network {}", - network.name - ); + Err(NetworkIpAssignmentError::IsBroadcastAddress(name, ip)) => { + warn!("Provided device IP address {ip} is broadcast address of network {name}"); mkresponse(false, true) } - Err(NetworkIpAssignmentError::IsNetworkAddress(ip)) => { - warn!( - "Provided device IP address {ip} is network address of network {}", - network.name - ); + Err(NetworkIpAssignmentError::IsNetworkAddress(name, ip)) => { + warn!("Provided device IP address {ip} is network address of network {name}"); mkresponse(false, true) } - Err(NetworkIpAssignmentError::AddressAlreadyAssigned(ip)) => { - warn!( - "Provided device IP {ip} is already assigned in network {}", - network.name - ); + Err(NetworkIpAssignmentError::AddressAlreadyAssigned(name, ip)) => { + warn!("Provided device IP {ip} is already assigned in network {name}"); mkresponse(false, true) } Err(NetworkIpAssignmentError::DbError(err)) => Err(err)?, @@ -366,6 +355,12 @@ pub struct StartNetworkDeviceSetup { assigned_ips: Vec, } +impl From for WebError { + fn from(error: NetworkIpAssignmentError) -> Self { + WebError::BadRequest(error.to_string()) + } +} + // Setup a network device to be later configured by a CLI client pub(crate) async fn start_network_device_setup( _admin_role: AdminRole, @@ -424,7 +419,7 @@ pub(crate) async fn start_network_device_setup( WebError::BadRequest(msg) })?; - check_ips(&ips, &network, &mut transaction).await?; + network.can_assign_ips(&ips, &mut transaction).await?; let (_, config) = device .add_to_network(&network, &ips, &mut transaction) @@ -594,7 +589,7 @@ pub(crate) async fn add_network_device( error!(msg); WebError::BadRequest(msg) })?; - check_ips(&ips, &network, &mut transaction).await?; + network.can_assign_ips(&ips, &mut transaction).await?; let (network_info, config) = device .add_to_network(&network, &ips, &mut transaction) @@ -703,14 +698,9 @@ pub async fn modify_network_device( // IP address has changed, so remove device from network and add it again with new IP address. // TODO(jck) order-insensitive comparison if new_ips != *wireguard_network_device.wireguard_ip { - // check_ips(&new_ips, &device_network, &mut transaction).await?; - match device_network - .can_assign_ips(&new_ips, &mut *transaction) - .await - { - Ok(_) => (), - Err(_) => todo!(), - }; + device_network + .can_assign_ips(&new_ips, &mut transaction) + .await?; // TODO(jck) wireguard_network_device.wireguard_ip = new_ips.clone(); wireguard_network_device.update(&mut *transaction).await?; From 7d4a186a9bee1ad08d314aa58251c9d36b86e11c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Fri, 18 Apr 2025 11:59:50 +0200 Subject: [PATCH 42/71] fix overview types --- web/src/i18n/en/index.ts | 2 +- web/src/i18n/i18n-types.ts | 8 ++-- .../components/DevicesList/DevicesList.tsx | 2 +- .../UserConnectionCard/UserConnectionCard.tsx | 21 +++++---- .../UserConnectionCard/style.scss | 46 +++++++++---------- 5 files changed, 41 insertions(+), 38 deletions(-) diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index edd264778d..1b5c404f1c 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -2194,7 +2194,7 @@ Any other requests you can reach us at: support@defguard.net labels: { name: 'Device Name', location: 'Location', - assignedIp: 'IP Address', + assignedIps: 'IP Addresses', description: 'Description', addedBy: 'Added By', addedAt: 'Add Date', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 92102b7828..c10a1271f0 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -5168,9 +5168,9 @@ type RootTranslation = { */ location: string /** - * I​P​ ​A​d​d​r​e​s​s + * I​P​ ​A​d​d​r​e​s​s​e​s */ - assignedIp: string + assignedIps: string /** * D​e​s​c​r​i​p​t​i​o​n */ @@ -10921,9 +10921,9 @@ export type TranslationFunctions = { */ location: () => LocalizedString /** - * IP Address + * IP Addresses */ - assignedIp: () => LocalizedString + assignedIps: () => LocalizedString /** * Description */ diff --git a/web/src/pages/devices/components/DevicesList/DevicesList.tsx b/web/src/pages/devices/components/DevicesList/DevicesList.tsx index fe5ec1aeba..5022df94a3 100644 --- a/web/src/pages/devices/components/DevicesList/DevicesList.tsx +++ b/web/src/pages/devices/components/DevicesList/DevicesList.tsx @@ -48,7 +48,7 @@ export const DevicesList = () => { sortDirection: ListSortDirection.DESC, }, { key: 1, text: labels.location() }, - { key: 2, text: labels.assignedIp() }, + { key: 2, text: labels.assignedIps() }, { key: 3, text: labels.description() }, { key: 4, text: labels.addedBy() }, { key: 5, text: labels.addedAt() }, diff --git a/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/UserConnectionCard.tsx b/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/UserConnectionCard.tsx index 967d2873cf..a4cd7eda54 100644 --- a/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/UserConnectionCard.tsx +++ b/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/UserConnectionCard.tsx @@ -1,3 +1,4 @@ +// eslint-disable-next-line simple-import-sort/imports import './style.scss'; import classNames from 'classnames'; @@ -33,6 +34,7 @@ import { } from '../../helpers/stats'; import { NetworkUsageChart } from '../shared/components/NetworkUsageChart/NetworkUsageChart'; import { formatConnectionTime } from './formatConnectionTime'; +import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent'; type DeviceConnectionCardProps = { data: StandaloneDeviceStats; @@ -98,7 +100,7 @@ const DeviceCardContent = (props: { data: StandaloneDeviceStats }) => {
@@ -163,7 +165,7 @@ const MainCardContent = ({ data }: MainCardContentProps) => {
@@ -202,21 +204,22 @@ const MainCardContent = ({ data }: MainCardContentProps) => { interface NameBoxProps { name: string; publicIp?: string; - wireguardIp?: string; + wireguardIps?: string[]; } -const NameBox = ({ name, publicIp, wireguardIp }: NameBoxProps) => { +const NameBox = ({ name, publicIp, wireguardIps }: NameBoxProps) => { return (
{name} - {(publicIp || wireguardIp) && ( + {(isPresent(publicIp) || isPresent(wireguardIps)) && (
{publicIp !== undefined && publicIp.length > 0 && ( )} - {wireguardIp !== undefined && wireguardIp.length > 0 && ( - - )} + {isPresent(wireguardIps) && + wireguardIps.map((ip) => ( + + ))}
)}
@@ -332,7 +335,7 @@ const ExpandedDeviceCard = ({ data }: ExpandedDeviceCardProps) => {
diff --git a/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/style.scss b/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/style.scss index a65daf9b9b..f60f3fd607 100644 --- a/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/style.scss +++ b/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/style.scss @@ -23,7 +23,7 @@ border-color: var(--gray-border); } - & > .card-expand { + &>.card-expand { width: 40px; height: 40px; border: 0 solid transparent; @@ -34,16 +34,16 @@ cursor: pointer; } - & > .devices { + &>.devices { grid-row: 2; grid-column: 1 / 2; display: grid; grid-template-columns: 1fr; grid-template-rows: auto; - & > .device { + &>.device { display: grid; - grid-template-rows: 40px 50px; + grid-template-rows: auto 50px; grid-template-columns: 1fr; width: 100%; row-gap: 20px; @@ -52,7 +52,7 @@ overflow: hidden; border-top: 1px solid var(--gray-border); - & > .upper { + &>.upper { grid-row: 1; grid-column: 1 / 2; display: grid; @@ -65,7 +65,7 @@ height: 100%; } - & > .lower { + &>.lower { grid-row: 2; grid-column: 1 / 2; display: grid; @@ -80,7 +80,7 @@ } } - & > .user-info { + &>.user-info { grid-row: 1; grid-column: 1 / 2; display: grid; @@ -96,7 +96,7 @@ grid-template-rows: 40px 50px; } - & > .upper { + &>.upper { width: 100%; max-width: 100%; overflow: hidden; @@ -107,12 +107,12 @@ box-sizing: border-box; padding-right: 25px; - & > .user-initials-box { + &>.user-initials-box { grid-row: 1; grid-column: 1 / 2; } - & > .name-box { + &>.name-box { grid-row: 1; grid-column: 2 / 3; } @@ -128,7 +128,7 @@ } } - & > .lower { + &>.lower { width: 100%; display: grid; @@ -190,7 +190,7 @@ align-content: flex-start; justify-content: flex-start; - & > .content-wrapper { + &>.content-wrapper { display: flex; flex-direction: row; align-items: center; @@ -223,7 +223,7 @@ box-sizing: border-box; background-color: var(--bg-light); - & > span { + &>span { @include typography-legacy(12px, 14px, medium, var(--gray-light)); } } @@ -235,12 +235,12 @@ max-width: 100%; overflow: hidden; display: grid; - grid-template-rows: 1fr 15px; + grid-template-rows: auto 1fr; grid-template-columns: 1fr; row-gap: 8px; overflow: hidden; - & > .name { + &>.name { grid-row: 1; grid-column: 1 / 2; @include typography-legacy(15px, 21px, medium, $font-family: 'Poppins'); @@ -248,16 +248,16 @@ user-select: none; } - & > .lower { + &>.lower { grid-row: 2; grid-column: 1 / 2; display: flex; - flex-flow: row nowrap; + flex-flow: row wrap; overflow: hidden; align-items: center; align-content: center; justify-content: flex-start; - column-gap: 5px; + gap: 5px; } } @@ -266,7 +266,7 @@ grid-template-rows: auto 30px; grid-template-columns: auto; - & > .time { + &>.time { display: flex; flex-flow: row nowrap; column-gap: 5px; @@ -274,7 +274,7 @@ justify-content: center; height: 100%; - & > span { + &>span { @include typography-legacy(15px, 18px, medium); @include text-overflow-dots; @@ -295,7 +295,7 @@ width: 100%; height: 100%; - & > .network-usage-stats { + &>.network-usage-stats { grid-row: 1; grid-column: 1 / 2; display: flex; @@ -309,7 +309,7 @@ } } - & > .chart { + &>.chart { grid-row: 2; grid-column: 1 / 2; } @@ -321,4 +321,4 @@ @include typography-legacy(12px, 14px, medium, var(--gray-light)); } } -} +} \ No newline at end of file From c23015e9d19fb09b66ea5e126c7a8073afacac3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Fri, 18 Apr 2025 13:18:51 +0200 Subject: [PATCH 43/71] multiple addresses fixes --- .../components/DevicesList/DevicesList.tsx | 26 ++++++------- .../devices/components/DevicesList/style.scss | 2 +- .../UserConnectionCard/style.scss | 38 +++++++++---------- .../UserConnectionListItem.tsx | 12 +++--- .../UserDevices/DeviceCard/DeviceCard.tsx | 28 +++++++------- web/src/shared/types.ts | 2 +- 6 files changed, 53 insertions(+), 55 deletions(-) diff --git a/web/src/pages/devices/components/DevicesList/DevicesList.tsx b/web/src/pages/devices/components/DevicesList/DevicesList.tsx index 5022df94a3..dc71bf6bff 100644 --- a/web/src/pages/devices/components/DevicesList/DevicesList.tsx +++ b/web/src/pages/devices/components/DevicesList/DevicesList.tsx @@ -6,6 +6,7 @@ import { useCallback, useMemo } from 'react'; import { shallow } from 'zustand/shallow'; import { useI18nContext } from '../../../../i18n/i18n-react'; +import { ListCellTags } from '../../../../shared/components/Layout/ListCellTags/ListCellTags'; import SvgIconCopy from '../../../../shared/components/svg/IconCopy'; import { DeviceAvatar } from '../../../../shared/defguard-ui/components/Layout/DeviceAvatar/DeviceAvatar'; import { EditButton } from '../../../../shared/defguard-ui/components/Layout/EditButton/EditButton'; @@ -21,6 +22,7 @@ import useApi from '../../../../shared/hooks/useApi'; import { useClipboard } from '../../../../shared/hooks/useClipboard'; import { useToaster } from '../../../../shared/hooks/useToaster'; import { StandaloneDevice } from '../../../../shared/types'; +import { ListCellTag } from '../../../acl/AclIndexPage/components/shared/types'; import { useDeleteStandaloneDeviceModal } from '../../hooks/useDeleteStandaloneDeviceModal'; import { useDevicesPage } from '../../hooks/useDevicesPage'; import { useEditStandaloneDeviceModal } from '../../hooks/useEditStandaloneDeviceModal'; @@ -84,6 +86,15 @@ export const DevicesList = () => { const DeviceRow = (props: StandaloneDevice) => { const { description, id, location, name, added_by, added_date, assigned_ips } = props; + const ipsTags = useMemo( + (): ListCellTag[] => + assigned_ips.map((ip) => ({ + key: ip, + label: ip, + displayAsTag: false, + })), + [assigned_ips], + ); const formatDate = useMemo(() => { const day = dayjs(added_date); return day.format('DD.MM.YYYY | HH:mm'); @@ -112,20 +123,7 @@ const DeviceRow = (props: StandaloneDevice) => { />
- { - void writeToClipboard(assigned_ips); - }} - > - - - } - /> +
diff --git a/web/src/pages/devices/components/DevicesList/style.scss b/web/src/pages/devices/components/DevicesList/style.scss index de0166f604..a8c0a2131c 100644 --- a/web/src/pages/devices/components/DevicesList/style.scss +++ b/web/src/pages/devices/components/DevicesList/style.scss @@ -1,7 +1,7 @@ @mixin spacing { display: inline-grid; grid-template-rows: 1fr; - grid-template-columns: 250px repeat(2, 150px) 1fr 180px 200px 50px; + grid-template-columns: 250px 150px 350px 1fr 180px 200px 50px; column-gap: 15px; } diff --git a/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/style.scss b/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/style.scss index f60f3fd607..9e010fa1b6 100644 --- a/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/style.scss +++ b/web/src/pages/overview/OverviewConnectedUsers/UserConnectionCard/style.scss @@ -23,7 +23,7 @@ border-color: var(--gray-border); } - &>.card-expand { + & > .card-expand { width: 40px; height: 40px; border: 0 solid transparent; @@ -34,14 +34,14 @@ cursor: pointer; } - &>.devices { + & > .devices { grid-row: 2; grid-column: 1 / 2; display: grid; grid-template-columns: 1fr; grid-template-rows: auto; - &>.device { + & > .device { display: grid; grid-template-rows: auto 50px; grid-template-columns: 1fr; @@ -52,7 +52,7 @@ overflow: hidden; border-top: 1px solid var(--gray-border); - &>.upper { + & > .upper { grid-row: 1; grid-column: 1 / 2; display: grid; @@ -65,7 +65,7 @@ height: 100%; } - &>.lower { + & > .lower { grid-row: 2; grid-column: 1 / 2; display: grid; @@ -80,7 +80,7 @@ } } - &>.user-info { + & > .user-info { grid-row: 1; grid-column: 1 / 2; display: grid; @@ -96,7 +96,7 @@ grid-template-rows: 40px 50px; } - &>.upper { + & > .upper { width: 100%; max-width: 100%; overflow: hidden; @@ -107,12 +107,12 @@ box-sizing: border-box; padding-right: 25px; - &>.user-initials-box { + & > .user-initials-box { grid-row: 1; grid-column: 1 / 2; } - &>.name-box { + & > .name-box { grid-row: 1; grid-column: 2 / 3; } @@ -128,7 +128,7 @@ } } - &>.lower { + & > .lower { width: 100%; display: grid; @@ -190,7 +190,7 @@ align-content: flex-start; justify-content: flex-start; - &>.content-wrapper { + & > .content-wrapper { display: flex; flex-direction: row; align-items: center; @@ -223,7 +223,7 @@ box-sizing: border-box; background-color: var(--bg-light); - &>span { + & > span { @include typography-legacy(12px, 14px, medium, var(--gray-light)); } } @@ -240,7 +240,7 @@ row-gap: 8px; overflow: hidden; - &>.name { + & > .name { grid-row: 1; grid-column: 1 / 2; @include typography-legacy(15px, 21px, medium, $font-family: 'Poppins'); @@ -248,7 +248,7 @@ user-select: none; } - &>.lower { + & > .lower { grid-row: 2; grid-column: 1 / 2; display: flex; @@ -266,7 +266,7 @@ grid-template-rows: auto 30px; grid-template-columns: auto; - &>.time { + & > .time { display: flex; flex-flow: row nowrap; column-gap: 5px; @@ -274,7 +274,7 @@ justify-content: center; height: 100%; - &>span { + & > span { @include typography-legacy(15px, 18px, medium); @include text-overflow-dots; @@ -295,7 +295,7 @@ width: 100%; height: 100%; - &>.network-usage-stats { + & > .network-usage-stats { grid-row: 1; grid-column: 1 / 2; display: flex; @@ -309,7 +309,7 @@ } } - &>.chart { + & > .chart { grid-row: 2; grid-column: 1 / 2; } @@ -321,4 +321,4 @@ @include typography-legacy(12px, 14px, medium, var(--gray-light)); } } -} \ No newline at end of file +} diff --git a/web/src/pages/overview/OverviewConnectedUsers/UserConnectionListItem/UserConnectionListItem.tsx b/web/src/pages/overview/OverviewConnectedUsers/UserConnectionListItem/UserConnectionListItem.tsx index e26904329e..d7b026f3fc 100644 --- a/web/src/pages/overview/OverviewConnectedUsers/UserConnectionListItem/UserConnectionListItem.tsx +++ b/web/src/pages/overview/OverviewConnectedUsers/UserConnectionListItem/UserConnectionListItem.tsx @@ -83,7 +83,7 @@ const UserRow = ({ data }: UserRowProps) => {
@@ -138,7 +138,7 @@ const DeviceRow = ({ data }: DeviceRowProps) => {
- +
@@ -194,14 +194,16 @@ const ActiveDevices = ({ data }: ActiveDevicesProps) => { interface DeviceIpsProps { publicIp: string; - wireguardIp: string; + wireguardIps: string[]; } -const DeviceIps = ({ publicIp, wireguardIp }: DeviceIpsProps) => { +const DeviceIps = ({ publicIp, wireguardIps }: DeviceIpsProps) => { return (
- + {wireguardIps.map((ip) => ( + + ))}
); }; diff --git a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx index 58de136eaa..b2b9ad6419 100644 --- a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx +++ b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx @@ -7,6 +7,7 @@ import { isUndefined, orderBy } from 'lodash-es'; import { useMemo, useState } from 'react'; import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { ListCellTags } from '../../../../../shared/components/Layout/ListCellTags/ListCellTags'; import IconClip from '../../../../../shared/components/svg/IconClip'; import SvgIconCollapse from '../../../../../shared/components/svg/IconCollapse'; import SvgIconCopy from '../../../../../shared/components/svg/IconCopy'; @@ -26,6 +27,7 @@ import { useUserProfileStore } from '../../../../../shared/hooks/store/useUserPr import { useClipboard } from '../../../../../shared/hooks/useClipboard'; import { Device, DeviceNetworkInfo } from '../../../../../shared/types'; import { sortByDate } from '../../../../../shared/utils/sortByDate'; +import { ListCellTag } from '../../../../acl/AclIndexPage/components/shared/types'; import { useDeleteDeviceModal } from '../hooks/useDeleteDeviceModal'; import { useDeviceConfigModal } from '../hooks/useDeviceConfigModal'; import { useEditDeviceModal } from '../hooks/useEditDeviceModal'; @@ -234,11 +236,20 @@ const DeviceLocation = ({ network_gateway_ip, last_connected_ip, last_connected_at, - device_wireguard_ip, + device_wireguard_ips, }, }: DeviceLocationProps) => { const { LL } = useI18nContext(); const { writeToClipboard } = useClipboard(); + const ipsTags = useMemo( + (): ListCellTag[] => + device_wireguard_ips.map((ip) => ({ + key: ip, + label: ip, + displayAsTag: false, + })), + [device_wireguard_ips], + ); return (
@@ -282,20 +293,7 @@ const DeviceLocation = ({
- { - void writeToClipboard(device_wireguard_ip); - }} - > - - - } - /> +
diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 44a9b1b99f..7ff8fae004 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -90,7 +90,7 @@ export interface Device { } export type DeviceNetworkInfo = { - device_wireguard_ip: string; + device_wireguard_ips: string[]; is_active: boolean; network_gateway_ip: string; network_id: number; From 699706942b8c955c85b777df9d17aa7c59de728f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 22 Apr 2025 10:23:19 +0200 Subject: [PATCH 44/71] Fix NetworkDeviceInfo::from_device function for multiple addresses. --- src/handlers/network_devices.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 0cb599e0ea..e70638d8b6 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -71,18 +71,22 @@ impl NetworkDeviceInfo { device.name, network.name )))?; let added_by = device.get_owner(&mut *transaction).await?; - let net_addr = network - .address - .first() - .ok_or(WebError::ObjectNotFound(format!( - "Failed to find the network address for network {}", - network.name - )))?; - let split_ips = wireguard_device + let split_ips: Vec = wireguard_device .wireguard_ip .iter() - .map(|ip| split_ip(ip, net_addr)) - .collect::>(); + .copied() + .map(|ip| { + network + .get_containing_network(ip) + .map(|net_addr| split_ip(&ip, &net_addr)) + .ok_or_else(|| { + WebError::ObjectNotFound(format!( + "Failed to find the network address for network {}", + network.name + )) + }) + }) + .collect::>()?; Ok(NetworkDeviceInfo { id: device.id, name: device.name, From 216528495a02e674ca8fba825ffd4d95860050c0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 22 Apr 2025 13:02:13 +0200 Subject: [PATCH 45/71] Fix network device address reassignment --- src/db/models/device.rs | 5 ++- src/db/models/wireguard.rs | 39 ++++++++++++++++--- src/handlers/network_devices.rs | 8 ++-- .../StandaloneDeviceModalForm.tsx | 8 +++- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index e2b1dfb5b8..da637edb53 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -855,7 +855,10 @@ impl Device { self.name, network.name ); // ensure assignment is valid - if let Err(err) = network.can_assign_ips(ips, &mut *transaction).await { + if let Err(err) = network + .can_assign_ips(&mut *transaction, ips, Some(self.id)) + .await + { error!("Invalid network IP assignment: {err}"); return Err(err); } diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 0af2b44fe5..ffc15eb100 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -1164,13 +1164,40 @@ impl WireguardNetwork { .await } - /// Checks if the IP addresses fall into the range of the network - /// and if they are not already assigned to another device. + /// Determine if a set of IP addresses can be safely assigned on this network. + /// + /// This method runs three categories of checks in sequence: + /// 1. **Range validation** + /// Every address in `ip_addrs` must lie within one of the network's CIDR. + /// Fails with `NoContainingNetwork` if any IP falls outside. + /// + /// 2. **Reserved‐address checks** + /// - Rejects the network address itself (`IsNetworkAddress`). + /// - Rejects the broadcast address (`IsBroadcastAddress`). + /// - Rejects the gateway/reserved host address (`ReservedForGateway`). + /// + /// 3. **Conflict detection** + /// Queries the database to see if an IP is already claimed. + /// - If `device_id` is `Some(id)`, any IP already bound to that same device is exempt. + /// - Otherwise, or if bound to a different device, fails with `AddressAlreadyAssigned`. + /// + /// # Parameters + /// + /// - `transaction`: Open PostgreSQL transaction to check existing assignments. + /// - `ip_addrs`: Candidate `IpAddr`s to validate. + /// - `device_id`: If `Some(id)`, IPs already assigned to this device are treated as free; + /// if `None`, all existing assignments count as conflicts. + /// + /// # Returns + /// + /// - `Ok(())`: All addresses passed every check. + /// - `Err(NetworkIpAssignmentError)`: The first failing check pub(crate) async fn can_assign_ips( &self, - ip_addrs: &[IpAddr], transaction: &mut PgConnection, - ) -> Result { + ip_addrs: &[IpAddr], + device_id: Option, + ) -> Result<(), NetworkIpAssignmentError> { // make sure the network contains all provided ips let networks = ip_addrs .iter() @@ -1206,7 +1233,7 @@ impl WireguardNetwork { // make sure the ip is unassigned let device = Device::find_by_ip(&mut *transaction, *ip, self.id).await?; - if device.is_some() { + if device.is_some_and(|device| device_id != Some(device.id)) { return Err(NetworkIpAssignmentError::AddressAlreadyAssigned( self.name.clone(), *ip, @@ -1214,7 +1241,7 @@ impl WireguardNetwork { } } - Ok(true) + Ok(()) } } diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index e70638d8b6..bc5a1ada4a 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -262,7 +262,7 @@ pub(crate) async fn check_ip_availability( status: StatusCode::OK, }) }; - return match network.can_assign_ips(&ips, &mut transaction).await { + return match network.can_assign_ips(&mut transaction, &ips, None).await { Ok(_) => mkresponse(true, true), Err(NetworkIpAssignmentError::NoContainingNetwork(name, ip, networks)) => { warn!( @@ -423,7 +423,7 @@ pub(crate) async fn start_network_device_setup( WebError::BadRequest(msg) })?; - network.can_assign_ips(&ips, &mut transaction).await?; + network.can_assign_ips(&mut transaction, &ips, None).await?; let (_, config) = device .add_to_network(&network, &ips, &mut transaction) @@ -593,7 +593,7 @@ pub(crate) async fn add_network_device( error!(msg); WebError::BadRequest(msg) })?; - network.can_assign_ips(&ips, &mut transaction).await?; + network.can_assign_ips(&mut transaction, &ips, None).await?; let (network_info, config) = device .add_to_network(&network, &ips, &mut transaction) @@ -703,7 +703,7 @@ pub async fn modify_network_device( // TODO(jck) order-insensitive comparison if new_ips != *wireguard_network_device.wireguard_ip { device_network - .can_assign_ips(&new_ips, &mut transaction) + .can_assign_ips(&mut transaction, &new_ips, Some(device.id)) .await?; // TODO(jck) wireguard_network_device.wireguard_ip = new_ips.clone(); diff --git a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx index 846258992d..f51ef136e4 100644 --- a/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx +++ b/web/src/pages/devices/modals/components/StandaloneDeviceModalForm/StandaloneDeviceModalForm.tsx @@ -156,6 +156,12 @@ export const StandaloneDeviceModalForm = ({ const generationChoiceValue = watch('generationChoice'); + function newIps(formIps: string[]): string[] { + const initialIpsSet = + new Set(initialIpRecommendation.map((ip) => ip.network_part + ip.modifiable_part)); + const formIpsSet = new Set(formIps); + return Array.from(formIpsSet.difference(initialIpsSet)) + } const submitHandler: SubmitHandler = async ( formValues, ) => { @@ -176,7 +182,7 @@ export const StandaloneDeviceModalForm = ({ } try { const response = await validateLocationIp({ - ips: values.modifiableIpParts, + ips: newIps(values.modifiableIpParts), location: values.location_id, }); const { available, valid } = response; From f14d1cc1e12daf4f39b3ce08e155e69950be7564 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 22 Apr 2025 13:12:26 +0200 Subject: [PATCH 46/71] Fix down migration --- migrations/20250404071457_multiple_peer_addresses.down.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/migrations/20250404071457_multiple_peer_addresses.down.sql b/migrations/20250404071457_multiple_peer_addresses.down.sql index bbbc6f2b54..ec51cbe6c9 100644 --- a/migrations/20250404071457_multiple_peer_addresses.down.sql +++ b/migrations/20250404071457_multiple_peer_addresses.down.sql @@ -1,12 +1,16 @@ -- add old-type address column ALTER TABLE wireguard_network_device -ADD COLUMN wireguard_ip_old inet NOT NULL; +ADD COLUMN wireguard_ip_old inet; -- copy the first element of new column to old column -- all further addresses will be lost UPDATE wireguard_network_device SET wireguard_ip_old = wireguard_ip[1]; +-- add not-null modifier to old-type address column +ALTER TABLE wireguard_network_device +ALTER COLUMN wireguard_ip_old SET NOT NULL; + -- drop the "new" column ALTER TABLE wireguard_network_device DROP COLUMN wireguard_ip; From 4866151f50054a66b34da67554da1dbddd0f650e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 22 Apr 2025 13:51:48 +0200 Subject: [PATCH 47/71] Rename wireguard_ip -> wireguard_ips --- e2e/types.ts | 2 +- ...404071457_multiple_peer_addresses.down.sql | 12 ++--- ...50404071457_multiple_peer_addresses.up.sql | 8 +-- src/db/models/device.rs | 50 +++++++++---------- src/db/models/wireguard.rs | 24 ++++----- src/enterprise/firewall.rs | 28 +++++------ src/grpc/desktop_client_mfa.rs | 2 +- src/grpc/gateway.rs | 2 +- src/grpc/utils.rs | 4 +- src/handlers/network_devices.rs | 10 ++-- src/handlers/wireguard.rs | 2 +- src/wireguard_peer_disconnect.rs | 2 +- .../WizardMapDevices/WizardMapDevices.tsx | 2 +- web/src/shared/types.ts | 2 +- 14 files changed, 71 insertions(+), 79 deletions(-) diff --git a/e2e/types.ts b/e2e/types.ts index 369edb6301..00a10bf9d1 100644 --- a/e2e/types.ts +++ b/e2e/types.ts @@ -1,5 +1,5 @@ export type DeviceNetworkInfo = { - device_wireguard_ip: string; + device_wireguard_ips: string[]; is_active: boolean; network_gateway_ip: string; network_id: number; diff --git a/migrations/20250404071457_multiple_peer_addresses.down.sql b/migrations/20250404071457_multiple_peer_addresses.down.sql index ec51cbe6c9..0304d19e18 100644 --- a/migrations/20250404071457_multiple_peer_addresses.down.sql +++ b/migrations/20250404071457_multiple_peer_addresses.down.sql @@ -1,20 +1,16 @@ -- add old-type address column ALTER TABLE wireguard_network_device -ADD COLUMN wireguard_ip_old inet; +ADD COLUMN wireguard_ip inet; -- copy the first element of new column to old column -- all further addresses will be lost UPDATE wireguard_network_device -SET wireguard_ip_old = wireguard_ip[1]; +SET wireguard_ip = wireguard_ips[1]; -- add not-null modifier to old-type address column ALTER TABLE wireguard_network_device -ALTER COLUMN wireguard_ip_old SET NOT NULL; +ALTER COLUMN wireguard_ip SET NOT NULL; -- drop the "new" column ALTER TABLE wireguard_network_device -DROP COLUMN wireguard_ip; - --- rename the column -ALTER TABLE wireguard_network_device -RENAME COLUMN wireguard_ip_old TO wireguard_ip; +DROP COLUMN wireguard_ips; diff --git a/migrations/20250404071457_multiple_peer_addresses.up.sql b/migrations/20250404071457_multiple_peer_addresses.up.sql index 1478b02af2..1872dfd1fc 100644 --- a/migrations/20250404071457_multiple_peer_addresses.up.sql +++ b/migrations/20250404071457_multiple_peer_addresses.up.sql @@ -1,15 +1,11 @@ -- add new address column ALTER TABLE wireguard_network_device -ADD COLUMN wireguard_ip_new inet[] NOT NULL DEFAULT '{}'; +ADD COLUMN wireguard_ips inet[] NOT NULL DEFAULT '{}'; -- copy and convert existing IPs into arrays UPDATE wireguard_network_device -SET wireguard_ip_new = ARRAY[wireguard_ip]; +SET wireguard_ips = ARRAY[wireguard_ip]; -- drop the old column ALTER TABLE wireguard_network_device DROP COLUMN wireguard_ip; - --- rename the new column to the original name -ALTER TABLE wireguard_network_device -RENAME COLUMN wireguard_ip_new TO wireguard_ip; diff --git a/src/db/models/device.rs b/src/db/models/device.rs index da637edb53..efa456a732 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -160,7 +160,7 @@ impl DeviceInfo { let network_info = query_as!( DeviceNetworkInfo, "SELECT wireguard_network_id network_id, \ - wireguard_ip \"device_wireguard_ips: Vec\", \ + wireguard_ips \"device_wireguard_ips: Vec\", \ preshared_key, is_authorized \ FROM wireguard_network_device \ WHERE device_id = $1", @@ -208,7 +208,7 @@ impl UserDevice { ORDER BY network, collected_at DESC \ ) \ SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, \ - wnd.wireguard_ip \"device_wireguard_ip: Vec\", stats.endpoint device_endpoint, \ + wnd.wireguard_ips \"device_wireguard_ips: Vec\", stats.endpoint device_endpoint, \ stats.latest_handshake \"latest_handshake?\", \ COALESCE((NOW() - stats.latest_handshake) < $1, FALSE) \"is_active!\" \ FROM wireguard_network_device wnd \ @@ -239,7 +239,7 @@ impl UserDevice { network_name: r.network_name, network_gateway_ip: r.gateway_endpoint, device_wireguard_ips: r - .device_wireguard_ip + .device_wireguard_ips .iter() .map(IpAddr::to_string) .collect(), @@ -261,7 +261,7 @@ impl UserDevice { #[derive(Clone, Debug, Deserialize, FromRow, Serialize)] pub struct WireguardNetworkDevice { pub wireguard_network_id: Id, - pub wireguard_ip: Vec, + pub wireguard_ips: Vec, pub device_id: Id, pub preshared_key: Option, pub is_authorized: bool, @@ -283,13 +283,13 @@ pub struct ModifyDevice { impl WireguardNetworkDevice { #[must_use] - pub(crate) fn new(network_id: Id, device_id: Id, wireguard_ip: I) -> Self + pub(crate) fn new(network_id: Id, device_id: Id, wireguard_ips: I) -> Self where I: Into>, { Self { wireguard_network_id: network_id, - wireguard_ip: wireguard_ip.into(), + wireguard_ips: wireguard_ips.into(), device_id, preshared_key: None, is_authorized: false, @@ -299,7 +299,7 @@ impl WireguardNetworkDevice { #[must_use] pub(crate) fn ips_as_network(&self) -> Vec { - self.wireguard_ip + self.wireguard_ips .iter() .map(|ip| IpNetwork::from(*ip)) .collect() @@ -311,11 +311,11 @@ impl WireguardNetworkDevice { { query!( "INSERT INTO wireguard_network_device \ - (device_id, wireguard_network_id, wireguard_ip, is_authorized, authorized_at, \ + (device_id, wireguard_network_id, wireguard_ips, is_authorized, authorized_at, \ preshared_key) \ VALUES ($1, $2, $3, $4, $5, $6) \ ON CONFLICT ON CONSTRAINT device_network \ - DO UPDATE SET wireguard_ip = $3, is_authorized = $4", + DO UPDATE SET wireguard_ips = $3, is_authorized = $4", self.device_id, self.wireguard_network_id, &self.ips_as_network(), @@ -335,7 +335,7 @@ impl WireguardNetworkDevice { { query!( "UPDATE wireguard_network_device \ - SET wireguard_ip = $3, is_authorized = $4, authorized_at = $5, preshared_key = $6 \ + SET wireguard_ips = $3, is_authorized = $4, authorized_at = $5, preshared_key = $6 \ WHERE device_id = $1 AND wireguard_network_id = $2", self.device_id, self.wireguard_network_id, @@ -377,7 +377,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ip \"wireguard_ip: Vec\", \ + wireguard_ips \"wireguard_ips: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE device_id = $1 AND wireguard_network_id = $2", @@ -402,7 +402,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ip \"wireguard_ip: Vec\", \ + wireguard_ips \"wireguard_ips: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE device_id = $1 ORDER BY id LIMIT 1", @@ -424,7 +424,7 @@ impl WireguardNetworkDevice { let result = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ip \"wireguard_ip: Vec\", \ + wireguard_ips \"wireguard_ips: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device WHERE device_id = $1", device_id @@ -449,7 +449,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ip \"wireguard_ip: Vec\", \ + wireguard_ips \"wireguard_ips: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1", @@ -475,7 +475,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ip \"wireguard_ip: Vec\", \ + wireguard_ips \"wireguard_ips: Vec\", \ preshared_key, is_authorized, authorized_at \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1 AND device_id IN \ @@ -585,7 +585,7 @@ impl Device { {allowed_ips}\ Endpoint = {}:{}\n\ PersistentKeepalive = 300", - wireguard_network_device.wireguard_ip.comma_separated(), + wireguard_network_device.wireguard_ips.comma_separated(), network.pubkey, network.endpoint, network.port, @@ -606,7 +606,7 @@ impl Device { d.device_type \"device_type: DeviceType\", configured \ FROM device d \ JOIN wireguard_network_device wnd ON d.id = wnd.device_id \ - WHERE $1 = ANY(wnd.wireguard_ip) AND wnd.wireguard_network_id = $2", + WHERE $1 = ANY(wnd.wireguard_ips) AND wnd.wireguard_network_id = $2", IpNetwork::from(ip), network_id ) @@ -677,7 +677,7 @@ impl Device { .ok_or_else(|| DeviceError::Unexpected("Device not found in network".into()))?; let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip.clone(), + device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; @@ -688,7 +688,7 @@ impl Device { network_name: network.name.clone(), config, endpoint: format!("{}:{}", network.endpoint, network.port), - address: wireguard_network_device.wireguard_ip, + address: wireguard_network_device.wireguard_ips, allowed_ips: network.allowed_ips.clone(), pubkey: network.pubkey.clone(), dns: network.dns.clone(), @@ -710,7 +710,7 @@ impl Device { .await?; let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip.clone(), + device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; @@ -721,7 +721,7 @@ impl Device { network_name: network.name.clone(), config, endpoint: format!("{}:{}", network.endpoint, network.port), - address: wireguard_network_device.wireguard_ip, + address: wireguard_network_device.wireguard_ips, allowed_ips: network.allowed_ips.clone(), pubkey: network.pubkey.clone(), dns: network.dns.clone(), @@ -765,13 +765,13 @@ impl Device { { debug!( "Assigned IPs {} for device {} (user {}) in network {network}", - wireguard_network_device.wireguard_ip.comma_separated(), + wireguard_network_device.wireguard_ips.comma_separated(), self.name, self.user_id ); let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip.clone(), + device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), preshared_key: wireguard_network_device.preshared_key.clone(), is_authorized: wireguard_network_device.is_authorized, }; @@ -783,7 +783,7 @@ impl Device { network_name: network.name, config, endpoint: format!("{}:{}", network.endpoint, network.port), - address: wireguard_network_device.wireguard_ip, + address: wireguard_network_device.wireguard_ips, allowed_ips: network.allowed_ips, pubkey: network.pubkey, dns: network.dns, @@ -1035,7 +1035,7 @@ mod test { .await .unwrap(); assert_eq!( - wireguard_network_device.wireguard_ip.comma_separated(), + wireguard_network_device.wireguard_ips.comma_separated(), "10.1.1.2" ); diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index ffc15eb100..886dde57d4 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -49,7 +49,7 @@ pub struct MappedDevice { pub user_id: Id, pub name: String, pub wireguard_pubkey: String, - pub wireguard_ip: Vec, + pub wireguard_ips: Vec, } pub const WIREGUARD_MAX_HANDSHAKE: TimeDelta = TimeDelta::minutes(8); @@ -512,7 +512,7 @@ impl WireguardNetwork { // device is allowed and an IP was already assigned if let Some(device) = allowed_devices.remove(&device_network_config.device_id) { // network address changed and IP needs to be updated - if !self.contains_all(&device_network_config.wireguard_ip) { + if !self.contains_all(&device_network_config.wireguard_ips) { // TODO(jck) ensure we don't leak IP addresses here let wireguard_network_device = device .assign_next_network_ip(&mut *transaction, self, reserved_ips) @@ -521,7 +521,7 @@ impl WireguardNetwork { device, network_info: vec![DeviceNetworkInfo { network_id: self.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ips, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }], @@ -541,7 +541,7 @@ impl WireguardNetwork { device, network_info: vec![DeviceNetworkInfo { network_id: self.id, - device_wireguard_ips: device_network_config.wireguard_ip, + device_wireguard_ips: device_network_config.wireguard_ips, preshared_key: device_network_config.preshared_key, is_authorized: device_network_config.is_authorized, }], @@ -563,7 +563,7 @@ impl WireguardNetwork { device, network_info: vec![DeviceNetworkInfo { network_id: self.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ips, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }], @@ -705,7 +705,7 @@ impl WireguardNetwork { device: existing_device, network_info: vec![DeviceNetworkInfo { network_id: self.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ips, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }], @@ -780,12 +780,12 @@ impl WireguardNetwork { let wireguard_network_device = WireguardNetworkDevice::new( self.id, device.id, - mapped_device.wireguard_ip.clone(), + mapped_device.wireguard_ips.clone(), ); wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { network_id: self.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ips, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }); @@ -797,12 +797,12 @@ impl WireguardNetwork { let wireguard_network_device = WireguardNetworkDevice::new( self.id, device.id, - mapped_device.wireguard_ip.clone(), + mapped_device.wireguard_ips.clone(), ); wireguard_network_device.insert(&mut *transaction).await?; network_info.push(DeviceNetworkInfo { network_id: self.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ips, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }); @@ -1186,12 +1186,12 @@ impl WireguardNetwork { /// - `transaction`: Open PostgreSQL transaction to check existing assignments. /// - `ip_addrs`: Candidate `IpAddr`s to validate. /// - `device_id`: If `Some(id)`, IPs already assigned to this device are treated as free; - /// if `None`, all existing assignments count as conflicts. + /// if `None`, all existing assignments count as conflicts. /// /// # Returns /// /// - `Ok(())`: All addresses passed every check. - /// - `Err(NetworkIpAssignmentError)`: The first failing check + /// - `Err(NetworkIpAssignmentError)`: The first failing check. pub(crate) async fn can_assign_ips( &self, transaction: &mut PgConnection, diff --git a/src/enterprise/firewall.rs b/src/enterprise/firewall.rs index 0808fcc779..c6d37fedf2 100644 --- a/src/enterprise/firewall.rs +++ b/src/enterprise/firewall.rs @@ -313,7 +313,7 @@ async fn get_user_device_ips<'e, E: sqlx::PgExecutor<'e>>( // fetch network IPs query_scalar!( - "SELECT wireguard_ip \"wireguard_ip: Vec\" \ + "SELECT wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON d.id = wnd.device_id \ WHERE wnd.wireguard_network_id = $1 AND d.device_type = 'user'::device_type AND d.user_id = ANY($2)", @@ -352,7 +352,7 @@ async fn get_network_device_ips( // fetch network IPs query_scalar!( - "SELECT wireguard_ip \"wireguard_ip: Vec\" \ + "SELECT wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device wnd \ WHERE wnd.wireguard_network_id = $1 AND wnd.device_id = ANY($2)", location_id, @@ -1672,7 +1672,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location.id, - wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( 10, 0, user.id as u8, @@ -1778,7 +1778,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id, wireguard_network_id: location.id, - wireguard_ip: vec![ip], + wireguard_ips: vec![ip], preshared_key: None, is_authorized: true, authorized_at: None, @@ -2087,7 +2087,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location.id, - wireguard_ip: vec![IpAddr::V6(Ipv6Addr::new( + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( 0xff00, 0, 0, @@ -2197,7 +2197,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id, wireguard_network_id: location.id, - wireguard_ip: vec![ip], + wireguard_ips: vec![ip], preshared_key: None, is_authorized: true, authorized_at: None, @@ -2508,7 +2508,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location.id, - wireguard_ip: vec![ + wireguard_ips: vec![ IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), IpAddr::V6(Ipv6Addr::new( 0xff00, @@ -2630,7 +2630,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id, wireguard_network_id: location.id, - wireguard_ip: ips, + wireguard_ips: ips, preshared_key: None, is_authorized: true, authorized_at: None, @@ -3741,7 +3741,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_1.id, - wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( 10, 0, user.id as u8, @@ -3755,7 +3755,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_2.id, - wireguard_ip: vec![IpAddr::V4(Ipv4Addr::new( + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( 10, 10, user.id as u8, @@ -3893,7 +3893,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_1.id, - wireguard_ip: vec![IpAddr::V6(Ipv6Addr::new( + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( 0xff00, 0, 0, @@ -3911,7 +3911,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_2.id, - wireguard_ip: vec![IpAddr::V6(Ipv6Addr::new( + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( 0xff00, 0, 0, @@ -4059,7 +4059,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_1.id, - wireguard_ip: vec![ + wireguard_ips: vec![ IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), IpAddr::V6(Ipv6Addr::new( 0xff00, @@ -4080,7 +4080,7 @@ mod test { let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location_2.id, - wireguard_ip: vec![ + wireguard_ips: vec![ IpAddr::V4(Ipv4Addr::new(10, 10, user.id as u8, device_num as u8)), IpAddr::V6(Ipv6Addr::new( 0xff00, diff --git a/src/grpc/desktop_client_mfa.rs b/src/grpc/desktop_client_mfa.rs index 4418e92949..422b172c1b 100644 --- a/src/grpc/desktop_client_mfa.rs +++ b/src/grpc/desktop_client_mfa.rs @@ -264,7 +264,7 @@ impl ClientMfaServer { device: device.clone(), network_info: vec![DeviceNetworkInfo { network_id: location.id, - device_wireguard_ips: network_device.wireguard_ip, + device_wireguard_ips: network_device.wireguard_ips, preshared_key: network_device.preshared_key, is_authorized: network_device.is_authorized, }], diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index 9f8c8da286..062d7dc8b5 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -73,7 +73,7 @@ impl WireguardNetwork { -- TODO possible to not use ARRAY-unnest here? ARRAY( SELECT host(ip) - FROM unnest(wnd.wireguard_ip) AS ip + FROM unnest(wnd.wireguard_ips) AS ip ) \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs index a68a9876a0..9fd88e6135 100644 --- a/src/grpc/utils.rs +++ b/src/grpc/utils.rs @@ -113,7 +113,7 @@ pub(crate) async fn build_device_config_response( config: Device::create_config(&network, &wireguard_network_device), network_id: network.id, network_name: network.name, - assigned_ip: wireguard_network_device.wireguard_ip.comma_separated(), + assigned_ip: wireguard_network_device.wireguard_ips.comma_separated(), endpoint: format!("{}:{}", network.endpoint, network.port), pubkey: network.pubkey, allowed_ips: network.allowed_ips.comma_separated(), @@ -140,7 +140,7 @@ pub(crate) async fn build_device_config_response( config: Device::create_config(&network, &wireguard_network_device), network_id: network.id, network_name: network.name, - assigned_ip: wireguard_network_device.wireguard_ip.comma_separated(), + assigned_ip: wireguard_network_device.wireguard_ips.comma_separated(), endpoint: format!("{}:{}", network.endpoint, network.port), pubkey: network.pubkey, allowed_ips: network.allowed_ips.comma_separated(), diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index bc5a1ada4a..807c5a1756 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -72,7 +72,7 @@ impl NetworkDeviceInfo { )))?; let added_by = device.get_owner(&mut *transaction).await?; let split_ips: Vec = wireguard_device - .wireguard_ip + .wireguard_ips .iter() .copied() .map(|ip| { @@ -90,7 +90,7 @@ impl NetworkDeviceInfo { Ok(NetworkDeviceInfo { id: device.id, name: device.name, - assigned_ips: wireguard_device.wireguard_ip, + assigned_ips: wireguard_device.wireguard_ips, description: device.description, added_by: added_by.username, added_date: device.created, @@ -701,12 +701,12 @@ pub async fn modify_network_device( // IP address has changed, so remove device from network and add it again with new IP address. // TODO(jck) order-insensitive comparison - if new_ips != *wireguard_network_device.wireguard_ip { + if new_ips != *wireguard_network_device.wireguard_ips { device_network .can_assign_ips(&mut transaction, &new_ips, Some(device.id)) .await?; // TODO(jck) - wireguard_network_device.wireguard_ip = new_ips.clone(); + wireguard_network_device.wireguard_ips = new_ips.clone(); wireguard_network_device.update(&mut *transaction).await?; let device_info = DeviceInfo::from_device(&mut *transaction, device.clone()).await?; appstate.send_wireguard_event(GatewayEvent::DeviceModified(device_info)); @@ -729,7 +729,7 @@ pub async fn modify_network_device( session.user.username, device.name, // TODO(jck) - wireguard_network_device.wireguard_ip.comma_separated(), + wireguard_network_device.wireguard_ips.comma_separated(), device_network.name ); } diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 4e3063d26d..bf4ab86808 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -780,7 +780,7 @@ pub(crate) async fn modify_device( if let Some(wireguard_network_device) = wireguard_network_device { let device_network_info = DeviceNetworkInfo { network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ip, + device_wireguard_ips: wireguard_network_device.wireguard_ips, preshared_key: wireguard_network_device.preshared_key, is_authorized: wireguard_network_device.is_authorized, }; diff --git a/src/wireguard_peer_disconnect.rs b/src/wireguard_peer_disconnect.rs index 32d3740847..b4e50a514b 100644 --- a/src/wireguard_peer_disconnect.rs +++ b/src/wireguard_peer_disconnect.rs @@ -105,7 +105,7 @@ pub async fn run_periodic_peer_disconnect( device, network_info: vec![DeviceNetworkInfo { network_id: location.id, - device_wireguard_ips: device_network_config.wireguard_ip, + device_wireguard_ips: device_network_config.wireguard_ips, preshared_key: device_network_config.preshared_key, is_authorized: device_network_config.is_authorized, }], diff --git a/web/src/pages/wizard/components/WizardMapDevices/WizardMapDevices.tsx b/web/src/pages/wizard/components/WizardMapDevices/WizardMapDevices.tsx index f94cf2856e..a1c722ea16 100644 --- a/web/src/pages/wizard/components/WizardMapDevices/WizardMapDevices.tsx +++ b/web/src/pages/wizard/components/WizardMapDevices/WizardMapDevices.tsx @@ -49,7 +49,7 @@ export const WizardMapDevices = () => { z.object({ devices: z.array( z.object({ - wireguard_ip: z.array(z.string().min(1, LL.form.error.required())), + wireguard_ips: z.array(z.string().min(1, LL.form.error.required())), user_id: z .number({ invalid_type_error: LL.form.error.required(), diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 7ff8fae004..9e8294822b 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -329,7 +329,7 @@ export interface ImportNetworkResponse { export interface ImportedDevice { name: string; - wireguard_ip: string[]; + wireguard_ips: string[]; wireguard_pubkey: string; user_id?: number; } From 46df1e5e6a1ccf5575974b8d9a0525eeba061298 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 22 Apr 2025 18:14:41 +0200 Subject: [PATCH 48/71] Fix tests --- tests/integration/wireguard.rs | 12 ++--- .../integration/wireguard_network_devices.rs | 48 ++++++++----------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/tests/integration/wireguard.rs b/tests/integration/wireguard.rs index 7c32748d57..a91f20746d 100644 --- a/tests/integration/wireguard.rs +++ b/tests/integration/wireguard.rs @@ -295,7 +295,7 @@ async fn test_device_permissions(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(response.status(), StatusCode::CREATED); let device = json!({"devices": [{ "name": "device_2", - "wireguard_ip": "10.0.0.3", + "wireguard_ips": ["10.0.0.3"], "wireguard_pubkey": "TJgN9JzUF5zdZAPYD96G/Wys2M3TvaT5TIrErUl20nI=", "user_id": 1, "created": "2023-05-05T23:56:04" @@ -319,7 +319,7 @@ async fn test_device_permissions(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(response.status(), StatusCode::CREATED); let device = json!({"devices": [{ "name": "device_4", - "wireguard_ip": "10.0.0.5", + "wireguard_ips": ["10.0.0.5"], "wireguard_pubkey": "gTMFF29nNLkJR1UhoiO3ZJLF60h2hW+WxmIu5DGJ0B4=", "user_id": 2, "created": "2023-05-05T23:56:04" @@ -348,7 +348,7 @@ async fn test_device_permissions(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(response.status(), StatusCode::CREATED); let device = json!({"devices": [{ "name": "device_6", - "wireguard_ip": "10.0.0.7", + "wireguard_ips": ["10.0.0.7"], "wireguard_pubkey": "xGLqgxVAnmk9+tsj5X/wzwouwx3bF1b3W+VWAb4NLjM=", "user_id": 2, "created": "2023-05-05T23:56:04" @@ -372,7 +372,7 @@ async fn test_device_permissions(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(response.status(), StatusCode::FORBIDDEN); let device = json!({"devices": [{ "name": "device_8", - "wireguard_ip": "10.0.0.9", + "wireguard_ips": ["10.0.0.9"], "wireguard_pubkey": "A2cg4qMe+s0MSFlV6xyhz7XY6PrET6mli9GVSUshXAk=", "user_id": 1, "created": "2023-05-05T23:56:04" @@ -491,14 +491,14 @@ async fn test_device_pubkey(_: PgPoolOptions, options: PgConnectOptions) { // try to create multiple devices let devices = json!({"devices": [{ "name": "device_2", - "wireguard_ip": "10.0.0.9", + "wireguard_ips": ["10.0.0.9"], "wireguard_pubkey": "o/8q3kmv5nnbrcb/7aceQWGE44a0yI707wObXRyyWGU=", "user_id": 1, "created": "2023-05-05T23:56:04" }, { "name": "device_3", - "wireguard_ip": "10.0.0.10", + "wireguard_ips": ["10.0.0.10"], "wireguard_pubkey": "invalid_key", "user_id": 1, "created": "2023-05-05T23:56:04" diff --git a/tests/integration/wireguard_network_devices.rs b/tests/integration/wireguard_network_devices.rs index 4f43ddbcb1..d3e12a0cbc 100644 --- a/tests/integration/wireguard_network_devices.rs +++ b/tests/integration/wireguard_network_devices.rs @@ -90,21 +90,19 @@ async fn test_network_devices(_: PgPoolOptions, options: PgConnectOptions) { // ip suggestions let response = client.get("/api/v1/device/network/ip/1").send().await; assert_eq!(response.status(), StatusCode::OK); - let res = response.json::().await; - let ips = res["ip"].as_str().unwrap(); - let ips: Vec = ips.split(",").map(|ip| ip.parse().unwrap()).collect(); - let net_ip = IpAddr::from_str("10.1.1.1").unwrap(); - let network_range = IpNetwork::new(net_ip, 24).unwrap(); - for ip in &ips { - assert!(network_range.contains(*ip)); + #[derive(Deserialize)] + struct SplitIp { + ip: IpAddr, } + let ips: Vec = response.json().await; + assert_eq!(ips.len(), 1); + let network_range = IpNetwork::from_str("10.1.1.1/24").unwrap(); + assert!(network_range.contains(ips[0].ip)); // checking whether ip is valid/available - let ip_check = json!( - { - "ip": "10.1.1.2".to_string(), - } - ); + let ip_check = json!({ + "ips": ["10.1.1.2".to_string()], + }); let response = client .post("/api/v1/device/network/ip/1") .json(&ip_check) @@ -115,11 +113,9 @@ async fn test_network_devices(_: PgPoolOptions, options: PgConnectOptions) { assert!(res.available); assert!(res.valid); - let ip_check = json!( - { - "ip": "10.1.1.0".to_string(), - } - ); + let ip_check = json!({ + "ips": ["10.1.1.0".to_string()], + }); let response = client .post("/api/v1/device/network/ip/1") .json(&ip_check) @@ -130,11 +126,9 @@ async fn test_network_devices(_: PgPoolOptions, options: PgConnectOptions) { assert!(!res.available); assert!(res.valid); - let ip_check = json!( - { - "ip": "10.1.1.1".to_string(), - } - ); + let ip_check = json!({ + "ips": ["10.1.1.1".to_string()], + }); let response = client .post("/api/v1/device/network/ip/1") .json(&ip_check) @@ -145,11 +139,9 @@ async fn test_network_devices(_: PgPoolOptions, options: PgConnectOptions) { assert!(!res.available); assert!(res.valid); - let ip_check = json!( - { - "ip": "10.1.1.abc".to_string(), - } - ); + let ip_check = json!({ + "ips": ["10.1.1.abc".to_string()], + }); let response = client .post("/api/v1/device/network/ip/1") .json(&ip_check) @@ -164,7 +156,7 @@ async fn test_network_devices(_: PgPoolOptions, options: PgConnectOptions) { let network_device = AddNetworkDevice { name: "device-1".into(), wireguard_pubkey: "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=".into(), - assigned_ips: ips.iter().map(IpAddr::to_string).collect(), + assigned_ips: ips.iter().map(|ip| ip.ip.to_string()).collect(), location_id: 1, description: None, }; From a2dc607fb6590b7c0454e4b7eff81b459c43b44d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 22 Apr 2025 19:25:40 +0200 Subject: [PATCH 49/71] IP assignment tests --- src/db/models/wireguard.rs | 269 +++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 886dde57d4..62f5dbc66e 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -1332,7 +1332,10 @@ pub struct WireguardNetworkStats { #[cfg(test)] mod test { + use std::str::FromStr; + use chrono::{SubsecRound, TimeDelta}; + use matches::assert_matches; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; @@ -1890,4 +1893,270 @@ mod test { transaction.commit().await.unwrap(); } + + #[sqlx::test] + async fn test_can_assign_ips(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let network = WireguardNetwork::new( + "network".to_string(), + vec![IpNetwork::from_str("10.1.1.1/24").unwrap()], + 50051, + String::new(), + None, + vec![IpNetwork::from_str("10.1.1.0/24").unwrap()], + false, + 300, + 300, + false, + false, + ) + .unwrap() + .save(&pool) + .await + .unwrap(); + + // assign free address + let addrs = vec![IpAddr::from_str("10.1.1.2").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Ok(()) + ); + + // assign multiple free addresses + let addrs = vec![ + IpAddr::from_str("10.1.1.2").unwrap(), + IpAddr::from_str("10.1.1.3").unwrap(), + ]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Ok(()) + ); + + // try to assign address from another network + let addrs = vec![IpAddr::from_str("10.2.1.2").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::NoContainingNetwork(..)) + ); + + // try to assign already assigned address + let user = User::new( + "hpotter", + Some("pass123"), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device".to_string(), + String::new(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + let _wnd = WireguardNetworkDevice::new( + network.id, + device.id, + vec![IpAddr::from_str("10.1.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + let addrs = vec![IpAddr::from_str("10.1.1.2").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::AddressAlreadyAssigned(..)) + ); + + // assign with exception for the device + let addrs = vec![IpAddr::from_str("10.1.1.2").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, Some(device.id)) + .await, + Ok(()) + ); + + // try to assign gateway address + let addrs = vec![IpAddr::from_str("10.1.1.1").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::ReservedForGateway(..)) + ); + + // try to assign network address + let addrs = vec![IpAddr::from_str("10.1.1.0").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::IsNetworkAddress(..)) + ); + + // try to assign broadcast address + let addrs = vec![IpAddr::from_str("10.1.1.255").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::IsBroadcastAddress(..)) + ); + } + + #[sqlx::test] + async fn test_can_assign_ips_multiple_addresses(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let network = WireguardNetwork::new( + "network".to_string(), + vec![ + IpNetwork::from_str("10.1.1.1/24").unwrap(), + IpNetwork::from_str("fc00::1/112").unwrap(), + ], + 50051, + String::new(), + None, + vec![IpNetwork::from_str("10.1.1.0/24").unwrap()], + false, + 300, + 300, + false, + false, + ) + .unwrap() + .save(&pool) + .await + .unwrap(); + + // assign free addresses + let addrs = vec![ + IpAddr::from_str("10.1.1.2").unwrap(), + IpAddr::from_str("fc00::2").unwrap(), + ]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Ok(()) + ); + + // assign multiple free addresses + let addrs = vec![ + IpAddr::from_str("10.1.1.2").unwrap(), + IpAddr::from_str("10.1.1.3").unwrap(), + IpAddr::from_str("fc00::2").unwrap(), + IpAddr::from_str("fc00::3").unwrap(), + ]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Ok(()) + ); + + // try to assign address from another network + let addrs = vec![IpAddr::from_str("fa::2").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::NoContainingNetwork(..)) + ); + + // try to assign already assigned address + let user = User::new( + "hpotter", + Some("pass123"), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device".to_string(), + String::new(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + let _wnd = WireguardNetworkDevice::new( + network.id, + device.id, + vec![IpAddr::from_str("10.1.1.2").unwrap(), IpAddr::from_str("fc00::2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + let addrs = vec![IpAddr::from_str("fc00::2").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::AddressAlreadyAssigned(..)) + ); + + // assign with exception for the device + let addrs = vec![IpAddr::from_str("fc00::2").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, Some(device.id)) + .await, + Ok(()) + ); + + // try to assign gateway address + let addrs = vec![IpAddr::from_str("fc00::1").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::ReservedForGateway(..)) + ); + + // try to assign network address + let addrs = vec![IpAddr::from_str("fc00::0").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::IsNetworkAddress(..)) + ); + + // try to assign broadcast address + let addrs = vec![IpAddr::from_str("fc00::ffff").unwrap()]; + assert_matches!( + network + .can_assign_ips(&mut pool.acquire().await.unwrap(), &addrs, None) + .await, + Err(NetworkIpAssignmentError::IsBroadcastAddress(..)) + ); + } } From 96eefe8d5f9735cace7b54c77dea68ebdd04b5b8 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 23 Apr 2025 08:16:31 +0200 Subject: [PATCH 50/71] Fix network device update handler --- src/handlers/network_devices.rs | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/handlers/network_devices.rs b/src/handlers/network_devices.rs index 807c5a1756..552e55e355 100644 --- a/src/handlers/network_devices.rs +++ b/src/handlers/network_devices.rs @@ -651,7 +651,7 @@ pub(crate) async fn add_network_device( pub struct ModifyNetworkDevice { name: String, description: Option, - assigned_ips: Vec, + assigned_ips: Vec, } pub async fn modify_network_device( @@ -684,29 +684,17 @@ pub async fn modify_network_device( error!("Failed to update device {device_id}, device not found in any network"); WebError::ObjectNotFound(format!("Device {device_id} not found in any network")) })?; - let new_ips = data - .assigned_ips - .iter() - .map(|ip| IpAddr::from_str(ip)) - .collect::, AddrParseError>>() - .map_err(|e| { - let msg = format!("Failed to update device {device_id}, invalid IP address: {e}"); - error!(msg); - WebError::BadRequest(msg) - })?; - device.name = data.name; device.description = data.description; device.save(&mut *transaction).await?; // IP address has changed, so remove device from network and add it again with new IP address. - // TODO(jck) order-insensitive comparison - if new_ips != *wireguard_network_device.wireguard_ips { + if data.assigned_ips != *wireguard_network_device.wireguard_ips { device_network - .can_assign_ips(&mut transaction, &new_ips, Some(device.id)) + .can_assign_ips(&mut transaction, &data.assigned_ips, Some(device.id)) .await?; - // TODO(jck) - wireguard_network_device.wireguard_ips = new_ips.clone(); + let old_ips = wireguard_network_device.wireguard_ips.clone(); + wireguard_network_device.wireguard_ips = data.assigned_ips; wireguard_network_device.update(&mut *transaction).await?; let device_info = DeviceInfo::from_device(&mut *transaction, device.clone()).await?; appstate.send_wireguard_event(GatewayEvent::DeviceModified(device_info)); @@ -725,11 +713,11 @@ pub async fn modify_network_device( } info!( - "User {} changed IP addresses of network device {} from {} to {new_ips:?} in network {}", + "User {} changed IP addresses of network device {} from {:?} to {:?} in network {}", session.user.username, device.name, - // TODO(jck) - wireguard_network_device.wireguard_ips.comma_separated(), + old_ips, + wireguard_network_device.wireguard_ips, device_network.name ); } From a3ebef78b515d6631f4217f6daf007ad5e9c6be2 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 23 Apr 2025 08:43:28 +0200 Subject: [PATCH 51/71] Improve log --- src/db/models/device.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index efa456a732..038ac8e6d2 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -834,9 +834,9 @@ impl Device { ); return Err(ModelError::CannotCreate); } - info!("Assigned IP addresses {ips:?} for device: {}", self.name); - let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, ips); + let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, ips.clone()); wireguard_network_device.insert(&mut *transaction).await?; + info!("Assigned IP addresses {ips:?} for device: {}", self.name); Ok(wireguard_network_device) } From a39c7f6b63260fe09577ce4574f5889191f3d531 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 23 Apr 2025 09:22:14 +0200 Subject: [PATCH 52/71] Reuse can_assign_ips method in assign_next_network_ip --- src/db/models/device.rs | 75 ++++++++++++++++++++++++-------------- src/db/models/wireguard.rs | 5 ++- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 038ac8e6d2..fc7c6ba51a 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -795,7 +795,27 @@ impl Device { Ok((network_info, configs)) } - // Assign IPs to the device in a given network. + /// 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`. + /// 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. + /// - `network`: The `WireguardNetwork` whose subnets will be assigned. + /// - `reserved_ips`: Optional slice of IPs that must not be assigned, even if otherwise free. + /// + /// # Returns + /// + /// - `Ok(WireguardNetworkDevice)`: A new relation linking this device to its assigned IPs across all subnets. + /// - `Err(ModelError::CannotCreate)`: If any subnet lacks an available IP. pub(crate) async fn assign_next_network_ip( &self, transaction: &mut PgConnection, @@ -803,41 +823,42 @@ impl Device { reserved_ips: Option<&[IpAddr]>, ) -> Result { let mut ips = Vec::new(); - // Iterate over all network addresses and assign new IP for the device in each of them. + let reserved = reserved_ips.unwrap_or(&[]); + + // iterate over all network addresses and assign new IP for the device in each of them for address in &network.address { - let net_ip = address.ip(); - let net_network = address.network(); - let net_broadcast = address.broadcast(); + let mut picked = None; for ip in address { - if ip == net_ip || ip == net_network || ip == net_broadcast { - continue; - } - if let Some(reserved_ips) = reserved_ips { - if reserved_ips.contains(&ip) { - continue; - } - } - // Break the loop if IP is unassigned and push the IP into result vector - if Self::find_by_ip(&mut *transaction, ip, network.id) - .await? - .is_none() + if network + .can_assign_ips(transaction, &[ip], None) + .await + .is_ok() + && !reserved.contains(&ip) { - ips.push(ip); + picked = Some(ip); break; } } + + // return error if no address can be assigned + let ip = picked.ok_or_else(|| { + error!( + "Failed to assign address for device {} in network {:?}", + self.name, address + ); + ModelError::CannotCreate + })?; + + // store the ip otherwise + ips.push(ip); } - if ips.len() != network.address.len() { - error!( - "Failed to assign address for device {} in one of the networks: {:?}", - self.name, network.address - ); - return Err(ModelError::CannotCreate); - } - let wireguard_network_device = WireguardNetworkDevice::new(network.id, self.id, ips.clone()); + + // create relation record + let wireguard_network_device = + WireguardNetworkDevice::new(network.id, self.id, ips.clone()); wireguard_network_device.insert(&mut *transaction).await?; - info!("Assigned IP addresses {ips:?} for device: {}", self.name); + info!("Assigned IP addresses {ips:?} for device: {}", self.name); Ok(wireguard_network_device) } diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 62f5dbc66e..eaa36e7977 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -2110,7 +2110,10 @@ mod test { let _wnd = WireguardNetworkDevice::new( network.id, device.id, - vec![IpAddr::from_str("10.1.1.2").unwrap(), IpAddr::from_str("fc00::2").unwrap()], + vec![ + IpAddr::from_str("10.1.1.2").unwrap(), + IpAddr::from_str("fc00::2").unwrap(), + ], ) .insert(&pool) .await From 50b3e443ae1199caa52fda3b3d8731d3e6d0a5f0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 23 Apr 2025 09:39:28 +0200 Subject: [PATCH 53/71] Take device's own IP into account during readdressing --- src/db/models/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index fc7c6ba51a..5fa67f9d8e 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -830,7 +830,7 @@ impl Device { let mut picked = None; for ip in address { if network - .can_assign_ips(transaction, &[ip], None) + .can_assign_ips(transaction, &[ip], Some(self.id)) .await .is_ok() && !reserved.contains(&ip) From fd01940d290b06296434687a36b72defbdf0a619 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 23 Apr 2025 10:18:28 +0200 Subject: [PATCH 54/71] sqlx fixtures --- ...4c32c219b73f3afa358f838cb833d5544842.json} | 4 ++-- ...125ce288466a847cf58b3e9be7659e7360933.json | 19 +++++++++++++++ ...32c9ebb2231dd6da663cdeaaf29b93afc8ac.json} | 6 ++--- ...c1288db6952db20d0f1c6f82addd0ebfb93f9.json | 23 ------------------- ...7d8596d6499193933c58fa1d7ba0a8f445bd.json} | 4 ++-- ...3a3ac6c360be2c949e5567afb8383ec92475.json} | 6 ++--- ...7089d0a6773cebb138f77700cc052d1229aa.json} | 4 ++-- ...5e337d121ee56509b42b6e4f57e2575c9bce.json} | 4 ++-- ...cafa2c1e5161eef7c5f35a7705be21235087.json} | 6 ++--- ...eb14fbfe962f46ebf0b0e00cfad7c2955f598.json | 23 +++++++++++++++++++ ...9d6b74b3288be8d31dfcd32394685ba54e0f.json} | 6 ++--- ...a6bd455ce7059baf539d7b678996c2c92c8c.json} | 6 ++--- ...b09f39004776d14c0b37fc9d7ac636b2b58ec.json | 19 --------------- ...35c93e831d73c532d038fda7c831123600ce3.json | 23 +++++++++++++++++++ ...764ac4f8f0ec59d43608fc3c34dc2c500a657.json | 23 ------------------- ...985e4f0d3d88810b317244ac40d2d976b6f3.json} | 6 ++--- 16 files changed, 91 insertions(+), 91 deletions(-) rename .sqlx/{query-12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1.json => query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json} (52%) create mode 100644 .sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json rename .sqlx/{query-4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744.json => query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json} (76%) delete mode 100644 .sqlx/query-64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9.json rename .sqlx/{query-0e07264ba01168b07bea06e7dd722d1d825ca3f759a846126cd17ec808f7deb0.json => query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json} (77%) rename .sqlx/{query-d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873.json => query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json} (76%) rename .sqlx/{query-decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53.json => query-751bb201b4bbf3477c87f81a2fd77089d0a6773cebb138f77700cc052d1229aa.json} (63%) rename .sqlx/{query-97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821.json => query-7573a899178c8cb2ee743381b6445e337d121ee56509b42b6e4f57e2575c9bce.json} (89%) rename .sqlx/{query-10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80.json => query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json} (71%) create mode 100644 .sqlx/query-9338acd23dbb4f6b14d15482d86eb14fbfe962f46ebf0b0e00cfad7c2955f598.json rename .sqlx/{query-c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb.json => query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json} (77%) rename .sqlx/{query-e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df.json => query-a1ffe5a3d79b9fb9261b59067286a6bd455ce7059baf539d7b678996c2c92c8c.json} (71%) delete mode 100644 .sqlx/query-da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec.json create mode 100644 .sqlx/query-dca1b36b0c60d9dd643e0f8b86b35c93e831d73c532d038fda7c831123600ce3.json delete mode 100644 .sqlx/query-e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657.json rename .sqlx/{query-7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295.json => query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json} (76%) diff --git a/.sqlx/query-12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1.json b/.sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json similarity index 52% rename from .sqlx/query-12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1.json rename to .sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json index 48feaa9d52..032ed79111 100644 --- a/.sqlx/query-12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1.json +++ b/.sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ip, is_authorized, authorized_at, preshared_key) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ip = $3, is_authorized = $4", + "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ips, is_authorized, authorized_at, preshared_key) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ips = $3, is_authorized = $4", "describe": { "columns": [], "parameters": { @@ -15,5 +15,5 @@ }, "nullable": [] }, - "hash": "12d13e1b78576a8751ae2c219f865ee71d125c20933c6659b747cf5ccdc1d9a1" + "hash": "09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842" } diff --git a/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json b/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json new file mode 100644 index 0000000000..f8ce8a0666 --- /dev/null +++ b/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE wireguard_network_device SET wireguard_ips = $3, is_authorized = $4, authorized_at = $5, preshared_key = $6 WHERE device_id = $1 AND wireguard_network_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "InetArray", + "Bool", + "Timestamp", + "Text" + ] + }, + "nullable": [] + }, + "hash": "20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933" +} diff --git a/.sqlx/query-4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744.json b/.sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json similarity index 76% rename from .sqlx/query-4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744.json rename to .sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json index d6a83c6cb3..0ba89d9c7a 100644 --- a/.sqlx/query-4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744.json +++ b/.sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1", "describe": { "columns": [ { @@ -15,7 +15,7 @@ }, { "ordinal": 2, - "name": "wireguard_ip: Vec", + "name": "wireguard_ips: Vec", "type_info": "InetArray" }, { @@ -48,5 +48,5 @@ true ] }, - "hash": "4051cc65ecbe6df7adb35ad864f60c7e14736e5d5e0943df2e20c7ab07c95744" + "hash": "469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac" } diff --git a/.sqlx/query-64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9.json b/.sqlx/query-64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9.json deleted file mode 100644 index 8b92496c34..0000000000 --- a/.sqlx/query-64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT wireguard_ip \"wireguard_ip: Vec\" FROM wireguard_network_device wnd JOIN device d ON d.id = wnd.device_id WHERE wnd.wireguard_network_id = $1 AND d.device_type = 'user'::device_type AND d.user_id = ANY($2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "wireguard_ip: Vec", - "type_info": "InetArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8Array" - ] - }, - "nullable": [ - false - ] - }, - "hash": "64bb5f027be40090da2fa30ebd0c1288db6952db20d0f1c6f82addd0ebfb93f9" -} diff --git a/.sqlx/query-0e07264ba01168b07bea06e7dd722d1d825ca3f759a846126cd17ec808f7deb0.json b/.sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json similarity index 77% rename from .sqlx/query-0e07264ba01168b07bea06e7dd722d1d825ca3f759a846126cd17ec808f7deb0.json rename to .sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json index b1ba572241..5df6af55de 100644 --- a/.sqlx/query-0e07264ba01168b07bea06e7dd722d1d825ca3f759a846126cd17ec808f7deb0.json +++ b/.sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT wireguard_network_id network_id, wireguard_ip \"device_wireguard_ips: Vec\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT wireguard_network_id network_id, wireguard_ips \"device_wireguard_ips: Vec\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -36,5 +36,5 @@ false ] }, - "hash": "0e07264ba01168b07bea06e7dd722d1d825ca3f759a846126cd17ec808f7deb0" + "hash": "72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd" } diff --git a/.sqlx/query-d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873.json b/.sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json similarity index 76% rename from .sqlx/query-d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873.json rename to .sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json index 9c70740907..a3db757922 100644 --- a/.sqlx/query-d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873.json +++ b/.sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1", "describe": { "columns": [ { @@ -15,7 +15,7 @@ }, { "ordinal": 2, - "name": "wireguard_ip: Vec", + "name": "wireguard_ips: Vec", "type_info": "InetArray" }, { @@ -48,5 +48,5 @@ true ] }, - "hash": "d45cee7649abe75e836a2ba87b279c5f1109fe7b4ad1d8fe16a7898f38208873" + "hash": "748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475" } diff --git a/.sqlx/query-decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53.json b/.sqlx/query-751bb201b4bbf3477c87f81a2fd77089d0a6773cebb138f77700cc052d1229aa.json similarity index 63% rename from .sqlx/query-decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53.json rename to .sqlx/query-751bb201b4bbf3477c87f81a2fd77089d0a6773cebb138f77700cc052d1229aa.json index f0fe29a86e..71b6a42369 100644 --- a/.sqlx/query-decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53.json +++ b/.sqlx/query-751bb201b4bbf3477c87f81a2fd77089d0a6773cebb138f77700cc052d1229aa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ip) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) AND d.configured = true AND u.is_active = true ORDER BY d.id ASC", + "query": "SELECT d.wireguard_pubkey pubkey, preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) AND d.configured = true AND u.is_active = true ORDER BY d.id ASC", "describe": { "columns": [ { @@ -31,5 +31,5 @@ null ] }, - "hash": "decd487d56badf04671f519b78b7021d72d392f54aca70eb15522882eebc7d53" + "hash": "751bb201b4bbf3477c87f81a2fd77089d0a6773cebb138f77700cc052d1229aa" } diff --git a/.sqlx/query-97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821.json b/.sqlx/query-7573a899178c8cb2ee743381b6445e337d121ee56509b42b6e4f57e2575c9bce.json similarity index 89% rename from .sqlx/query-97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821.json rename to .sqlx/query-7573a899178c8cb2ee743381b6445e337d121ee56509b42b6e4f57e2575c9bce.json index 32b807723a..d798b5cc0d 100644 --- a/.sqlx/query-97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821.json +++ b/.sqlx/query-7573a899178c8cb2ee743381b6445e337d121ee56509b42b6e4f57e2575c9bce.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT d.id, d.name, d.wireguard_pubkey, d.user_id, d.created, d.description, d.device_type \"device_type: DeviceType\", configured FROM device d JOIN wireguard_network_device wnd ON d.id = wnd.device_id WHERE $1 = ANY(wnd.wireguard_ip) AND wnd.wireguard_network_id = $2", + "query": "SELECT d.id, d.name, d.wireguard_pubkey, d.user_id, d.created, d.description, d.device_type \"device_type: DeviceType\", configured FROM device d JOIN wireguard_network_device wnd ON d.id = wnd.device_id WHERE $1 = ANY(wnd.wireguard_ips) AND wnd.wireguard_network_id = $2", "describe": { "columns": [ { @@ -71,5 +71,5 @@ false ] }, - "hash": "97bd07fb7dbe6490013edc12e05c312fa1812c93b402f40b915e0d262f546821" + "hash": "7573a899178c8cb2ee743381b6445e337d121ee56509b42b6e4f57e2575c9bce" } diff --git a/.sqlx/query-10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80.json b/.sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json similarity index 71% rename from .sqlx/query-10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80.json rename to .sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json index 911d425ba0..f56db2e413 100644 --- a/.sqlx/query-10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80.json +++ b/.sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1 AND device_id IN (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1 AND device_id IN (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", "describe": { "columns": [ { @@ -15,7 +15,7 @@ }, { "ordinal": 2, - "name": "wireguard_ip: Vec", + "name": "wireguard_ips: Vec", "type_info": "InetArray" }, { @@ -49,5 +49,5 @@ true ] }, - "hash": "10945dde387682e461bce8df9a0e2d641a1bae1f10933c46ce68f14015fbdc80" + "hash": "812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087" } diff --git a/.sqlx/query-9338acd23dbb4f6b14d15482d86eb14fbfe962f46ebf0b0e00cfad7c2955f598.json b/.sqlx/query-9338acd23dbb4f6b14d15482d86eb14fbfe962f46ebf0b0e00cfad7c2955f598.json new file mode 100644 index 0000000000..0474caf420 --- /dev/null +++ b/.sqlx/query-9338acd23dbb4f6b14d15482d86eb14fbfe962f46ebf0b0e00cfad7c2955f598.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device wnd JOIN device d ON d.id = wnd.device_id WHERE wnd.wireguard_network_id = $1 AND d.device_type = 'user'::device_type AND d.user_id = ANY($2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wireguard_ips: Vec", + "type_info": "InetArray" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8Array" + ] + }, + "nullable": [ + false + ] + }, + "hash": "9338acd23dbb4f6b14d15482d86eb14fbfe962f46ebf0b0e00cfad7c2955f598" +} diff --git a/.sqlx/query-c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb.json b/.sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json similarity index 77% rename from .sqlx/query-c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb.json rename to .sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json index 89440a7fae..4341335409 100644 --- a/.sqlx/query-c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb.json +++ b/.sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -15,7 +15,7 @@ }, { "ordinal": 2, - "name": "wireguard_ip: Vec", + "name": "wireguard_ips: Vec", "type_info": "InetArray" }, { @@ -48,5 +48,5 @@ true ] }, - "hash": "c18c9730260b8e97ac03204b027d4c5b48514cf99e25c5b9430da06f77a736bb" + "hash": "98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f" } diff --git a/.sqlx/query-e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df.json b/.sqlx/query-a1ffe5a3d79b9fb9261b59067286a6bd455ce7059baf539d7b678996c2c92c8c.json similarity index 71% rename from .sqlx/query-e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df.json rename to .sqlx/query-a1ffe5a3d79b9fb9261b59067286a6bd455ce7059baf539d7b678996c2c92c8c.json index c61eff65ab..b6b7ea5bec 100644 --- a/.sqlx/query-e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df.json +++ b/.sqlx/query-a1ffe5a3d79b9fb9261b59067286a6bd455ce7059baf539d7b678996c2c92c8c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH stats AS ( SELECT DISTINCT ON (network) network, endpoint, latest_handshake FROM wireguard_peer_stats WHERE device_id = $2 ORDER BY network, collected_at DESC ) SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, wnd.wireguard_ip \"device_wireguard_ip: Vec\", stats.endpoint device_endpoint, stats.latest_handshake \"latest_handshake?\", COALESCE((NOW() - stats.latest_handshake) < $1, FALSE) \"is_active!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN stats ON n.id = stats.network WHERE wnd.device_id = $2", + "query": "WITH stats AS ( SELECT DISTINCT ON (network) network, endpoint, latest_handshake FROM wireguard_peer_stats WHERE device_id = $2 ORDER BY network, collected_at DESC ) SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, wnd.wireguard_ips \"device_wireguard_ips: Vec\", stats.endpoint device_endpoint, stats.latest_handshake \"latest_handshake?\", COALESCE((NOW() - stats.latest_handshake) < $1, FALSE) \"is_active!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN stats ON n.id = stats.network WHERE wnd.device_id = $2", "describe": { "columns": [ { @@ -20,7 +20,7 @@ }, { "ordinal": 3, - "name": "device_wireguard_ip: Vec", + "name": "device_wireguard_ips: Vec", "type_info": "InetArray" }, { @@ -55,5 +55,5 @@ null ] }, - "hash": "e83b820663063d2e5a12c3b80fee30a0efb8018bbdb19dbc97b42b94142749df" + "hash": "a1ffe5a3d79b9fb9261b59067286a6bd455ce7059baf539d7b678996c2c92c8c" } diff --git a/.sqlx/query-da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec.json b/.sqlx/query-da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec.json deleted file mode 100644 index 9603aafaba..0000000000 --- a/.sqlx/query-da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE wireguard_network_device SET wireguard_ip = $3, is_authorized = $4, authorized_at = $5, preshared_key = $6 WHERE device_id = $1 AND wireguard_network_id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "InetArray", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [] - }, - "hash": "da7a1669b1ba89003f6c507b5b2b09f39004776d14c0b37fc9d7ac636b2b58ec" -} diff --git a/.sqlx/query-dca1b36b0c60d9dd643e0f8b86b35c93e831d73c532d038fda7c831123600ce3.json b/.sqlx/query-dca1b36b0c60d9dd643e0f8b86b35c93e831d73c532d038fda7c831123600ce3.json new file mode 100644 index 0000000000..4827cb824d --- /dev/null +++ b/.sqlx/query-dca1b36b0c60d9dd643e0f8b86b35c93e831d73c532d038fda7c831123600ce3.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device wnd WHERE wnd.wireguard_network_id = $1 AND wnd.device_id = ANY($2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wireguard_ips: Vec", + "type_info": "InetArray" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8Array" + ] + }, + "nullable": [ + false + ] + }, + "hash": "dca1b36b0c60d9dd643e0f8b86b35c93e831d73c532d038fda7c831123600ce3" +} diff --git a/.sqlx/query-e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657.json b/.sqlx/query-e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657.json deleted file mode 100644 index 81955df3bf..0000000000 --- a/.sqlx/query-e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT wireguard_ip \"wireguard_ip: Vec\" FROM wireguard_network_device wnd WHERE wnd.wireguard_network_id = $1 AND wnd.device_id = ANY($2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "wireguard_ip: Vec", - "type_info": "InetArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8Array" - ] - }, - "nullable": [ - false - ] - }, - "hash": "e981969defba207776669d06c54764ac4f8f0ec59d43608fc3c34dc2c500a657" -} diff --git a/.sqlx/query-7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295.json b/.sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json similarity index 76% rename from .sqlx/query-7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295.json rename to .sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json index d75da12aa1..aebfd05555 100644 --- a/.sqlx/query-7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295.json +++ b/.sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip \"wireguard_ip: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", "describe": { "columns": [ { @@ -15,7 +15,7 @@ }, { "ordinal": 2, - "name": "wireguard_ip: Vec", + "name": "wireguard_ips: Vec", "type_info": "InetArray" }, { @@ -49,5 +49,5 @@ true ] }, - "hash": "7508f3f00189765016abc01165ecb0b3c39a6b8c56740c6a82a19b60d216e295" + "hash": "f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3" } From 8b1514dcb38fa9b1dd71ff6ff57113bdf6ee4d6f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 23 Apr 2025 14:12:28 +0200 Subject: [PATCH 55/71] Fix readdressing when adding new network address --- src/db/models/wireguard.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index eaa36e7977..48f16bfe0e 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -511,9 +511,10 @@ impl WireguardNetwork { for device_network_config in currently_configured_devices { // device is allowed and an IP was already assigned if let Some(device) = allowed_devices.remove(&device_network_config.device_id) { - // network address changed and IP needs to be updated - if !self.contains_all(&device_network_config.wireguard_ips) { - // TODO(jck) ensure we don't leak IP addresses here + // network address changed and IPs need to be updated + if !self.contains_all(&device_network_config.wireguard_ips) + || self.address.len() != device_network_config.wireguard_ips.len() + { let wireguard_network_device = device .assign_next_network_ip(&mut *transaction, self, reserved_ips) .await?; From e5e8c9ecbb3615d5f2c589899ef3b8cd1af3dbf7 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 24 Apr 2025 08:59:15 +0200 Subject: [PATCH 56/71] Fix ips in mapped devices during network import --- .../components/WizardMapDevices/components/MapDeviceRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/wizard/components/WizardMapDevices/components/MapDeviceRow.tsx b/web/src/pages/wizard/components/WizardMapDevices/components/MapDeviceRow.tsx index 38a0df4444..d472c82eff 100644 --- a/web/src/pages/wizard/components/WizardMapDevices/components/MapDeviceRow.tsx +++ b/web/src/pages/wizard/components/WizardMapDevices/components/MapDeviceRow.tsx @@ -31,7 +31,7 @@ export const MapDeviceRow = ({ options, control, index }: Props) => { const ipController = useController({ control, - name: `devices.${index}.wireguard_ip`, + name: `devices.${index}.wireguard_ips`, }); const hasErrors = useMemo(() => { @@ -63,7 +63,7 @@ export const MapDeviceRow = ({ options, control, index }: Props) => { return ( - {ipController.field.value} + {ipController.field.value.join(" ")} - {ipController.field.value.join(" ")} + {ipController.field.value.join(' ')}