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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
397 changes: 256 additions & 141 deletions Cargo.lock

Large diffs are not rendered by default.

26 changes: 15 additions & 11 deletions crates/defguard_core/src/db/models/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,18 +407,22 @@ impl WireguardNetwork<Id> {
&self,
device_count: usize,
) -> Result<(), WireguardNetworkError> {
debug!("Checking if {device_count} devices can fit in network {self}");
let network_size = self.address[0].size();
// include address, network, and broadcast in the calculation
match network_size {
NetworkSize::V4(size) => {
if device_count as u32 > size {
return Err(WireguardNetworkError::NetworkTooSmall);
debug!("Checking if {device_count} devices can fit in networks used by location {self}");
// if given location uses multiple subnets validate devices can fit them all
for subnet in &self.address {
debug!("Checking if {device_count} devices can fit in network {subnet}");
let network_size = subnet.size();
// include address, network, and broadcast in the calculation
match network_size {
NetworkSize::V4(size) => {
if device_count as u32 > size {
return Err(WireguardNetworkError::NetworkTooSmall);
}
}
}
NetworkSize::V6(size) => {
if device_count as u128 > size {
return Err(WireguardNetworkError::NetworkTooSmall);
NetworkSize::V6(size) => {
if device_count as u128 > size {
return Err(WireguardNetworkError::NetworkTooSmall);
}
}
}
}
Expand Down
26 changes: 25 additions & 1 deletion crates/defguard_core/src/handlers/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,29 @@ impl WireguardNetworkData {
.map_or(Vec::new(), |ips| parse_network_address_list(ips))
}

pub(crate) fn parse_addresses(&self) -> Result<Vec<IpNetwork>, WebError> {
// first parse the addresses
let subnets = parse_address_list(self.address.as_ref());

// check if address list is not empty
if subnets.is_empty() {
return Err(WebError::BadRequest(
"Must provide at least one valid network address".to_owned(),
));
}

// check if any subnet has an invalid /0 netmask
for subnet in &subnets {
if subnet.prefix() == 0 {
return Err(WebError::BadRequest(format!(
"{subnet} is not a valid address"
)));
}
}

Ok(subnets)
}

pub(crate) async fn validate_location_mfa_mode<'e, E: sqlx::PgExecutor<'e>>(
&self,
executor: E,
Expand Down Expand Up @@ -281,6 +304,8 @@ pub(crate) async fn modify_network(
let mut network = find_network(network_id, &appstate.pool).await?;
// store network before mods
let before = network.clone();
network.address = data.parse_addresses()?;

network.allowed_ips = data.parse_allowed_ips();
network.name = data.name;

Expand All @@ -290,7 +315,6 @@ pub(crate) async fn modify_network(
network.endpoint = data.endpoint;
network.port = data.port;
network.dns = data.dns;
network.address = parse_address_list(&data.address);
network.keepalive_interval = data.keepalive_interval;
network.peer_disconnect_threshold = data.peer_disconnect_threshold;
network.acl_enabled = data.acl_enabled;
Expand Down
139 changes: 139 additions & 0 deletions crates/defguard_core/tests/integration/api/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,3 +840,142 @@ async fn test_device_pubkey(_: PgPoolOptions, options: PgConnectOptions) {
let devices: Vec<Device<Id>> = response.json().await;
assert_eq!(devices.len(), 1);
}

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

let (client, _client_state) = make_test_client(pool).await;

let auth = Auth::new("admin", "pass123");
let response = &client.post("/api/v1/auth").json(&auth).send().await;
assert_eq!(response.status(), StatusCode::OK);

// create network
let network = json!({
"name": "network",
"address": "10.1.1.1/24",
"port": 55555,
"endpoint": "192.168.4.14",
"allowed_ips": "10.1.1.0/24",
"dns": "1.1.1.1",
"allowed_groups": [],
"keepalive_interval": 25,
"peer_disconnect_threshold": 300,
"acl_enabled": false,
"acl_default_allow": false,
"location_mfa_mode": "disabled",
"service_location_mode": "disabled"
});
let response = client.post("/api/v1/network").json(&network).send().await;
assert_eq!(response.status(), StatusCode::CREATED);

// network details
let response = client.get("/api/v1/network/1").send().await;
assert_eq!(response.status(), StatusCode::OK);
let network_from_details: WireguardNetwork<Id> = response.json().await;

// create devices
let device = json!({
"name": "device1",
"wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=",
});
let response = client
.post("/api/v1/device/admin")
.json(&device)
.send()
.await;
assert_eq!(response.status(), StatusCode::CREATED);
let device = json!({
"name": "device2",
"wireguard_pubkey": "ZqDlG4LQZRO9v57Sd27AHdtTLxegbMp5oVThjYrg21I=",
});
let response = client
.post("/api/v1/device/admin")
.json(&device)
.send()
.await;
assert_eq!(response.status(), StatusCode::CREATED);
let device = json!({
"name": "device3",
"wireguard_pubkey": "o/8q3kmv5nnbrcb/7aceQWGE44a0yI707wObXRyyWGU=",
});
let response = client
.post("/api/v1/device/admin")
.json(&device)
.send()
.await;
assert_eq!(response.status(), StatusCode::CREATED);

// try to add subnet with not enough IPs
let network = json!({
"id": network_from_details.id,
"name": "network",
"address": "10.1.1.1/24,10.2.1.1/30",
"port": 55555,
"endpoint": "192.168.4.14",
"allowed_ips": "10.1.1.0/24",
"dns": "1.1.1.1",
"allowed_groups": [],
"keepalive_interval": 25,
"peer_disconnect_threshold": 300,
"acl_enabled": false,
"acl_default_allow": false,
"location_mfa_mode": "disabled",
"service_location_mode": "disabled"
});
let response = client
.put(format!("/api/v1/network/{}", network_from_details.id))
.json(&network)
.send()
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);

// try to add subnet with invalid mask
let network = json!({
"id": network_from_details.id,
"name": "network",
"address": "10.2.0.1/24,10.1.1.1/0",
"port": 55555,
"endpoint": "192.168.4.14",
"allowed_ips": "10.1.1.0/24",
"dns": "1.1.1.1",
"allowed_groups": [],
"keepalive_interval": 25,
"peer_disconnect_threshold": 300,
"acl_enabled": false,
"acl_default_allow": false,
"location_mfa_mode": "disabled",
"service_location_mode": "disabled"
});
let response = client
.put(format!("/api/v1/network/{}", network_from_details.id))
.json(&network)
.send()
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);

// try to add no network
let network = json!({
"id": network_from_details.id,
"name": "network",
"address": "",
"port": 55555,
"endpoint": "192.168.4.14",
"allowed_ips": "10.1.1.0/24",
"dns": "1.1.1.1",
"allowed_groups": [],
"keepalive_interval": 25,
"peer_disconnect_threshold": 300,
"acl_enabled": false,
"acl_default_allow": false,
"location_mfa_mode": "disabled",
"service_location_mode": "disabled"
});
let response = client
.put(format!("/api/v1/network/{}", network_from_details.id))
.json(&network)
.send()
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
12 changes: 6 additions & 6 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 14 additions & 14 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@
"@react-rxjs/core": "^0.10.8",
"@stablelib/base64": "^2.0.1",
"@stablelib/x25519": "^2.0.1",
"@tanstack/query-core": "^5.90.6",
"@tanstack/react-query": "^5.90.6",
"@tanstack/query-core": "^5.90.9",
"@tanstack/react-query": "^5.90.9",
"@tanstack/react-virtual": "3.13.12",
"@tanstack/virtual-core": "3.13.12",
"@use-gesture/react": "^10.3.1",
"axios": "^1.13.1",
"axios": "^1.13.2",
"byte-size": "^9.0.1",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
Expand All @@ -70,7 +70,7 @@
"fuse.js": "^7.1.0",
"get-text-width": "^1.0.3",
"hex-rgb": "^5.0.0",
"html-react-parser": "^5.2.7",
"html-react-parser": "^5.2.8",
"humanize-duration": "^3.33.1",
"ipaddr.js": "^2.2.0",
"itertools": "^2.5.0",
Expand All @@ -85,7 +85,7 @@
"radash": "^12.1.1",
"react": "^19.2.0",
"react-click-away-listener": "^2.4.0",
"react-datepicker": "^8.8.0",
"react-datepicker": "^8.9.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.66.0",
"react-idle-timer": "^5.7.2",
Expand All @@ -95,11 +95,11 @@
"react-markdown": "^10.1.0",
"react-qr-code": "^2.0.18",
"react-resize-detector": "^12.3.0",
"react-router": "^6.30.1",
"react-router-dom": "^6.30.1",
"react-router": "^6.30.2",
"react-router-dom": "^6.30.2",
"react-tracked": "^2.0.1",
"react-virtualized-auto-sizer": "^1.0.26",
"recharts": "^3.3.0",
"recharts": "^3.4.1",
"rehype-external-links": "^3.0.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
Expand All @@ -122,13 +122,13 @@
"@types/file-saver": "^2.0.7",
"@types/humanize-duration": "^3.27.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.0",
"@types/node": "^24.10.1",
"@types/qs": "^6.14.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/react": "^19.2.4",
"@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^4.2.0",
"autoprefixer": "^10.4.21",
"@vitejs/plugin-react-swc": "^4.2.2",
"autoprefixer": "^10.4.22",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"esbuild": "^0.25.12",
Expand All @@ -139,7 +139,7 @@
"standard-version": "^9.5.0",
"type-fest": "^4.41.0",
"typescript": "~5.9.3",
"vite": "^7.1.12",
"vite": "^7.2.2",
"vite-plugin-package-version": "^1.1.0"
}
}
Loading