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
37 changes: 27 additions & 10 deletions crates/defguard_common/src/db/models/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ use crate::{
ModelError, WireguardNetwork,
user::User,
vpn_client_session::VpnClientSessionState,
wireguard::{LocationMfaMode, NetworkAddressError, ServiceLocationMode},
wireguard::{
LocationMfaMode, NetworkAddressError, ServiceLocationMode, WireguardNetworkError,
},
},
},
};
Expand Down Expand Up @@ -539,9 +541,13 @@ pub enum DeviceError {
#[error("Database error")]
DatabaseError(#[from] sqlx::Error),
#[error(transparent)]
ModelError(#[from] ModelError),
#[error(transparent)]
NetworkIpAssignmentError(#[from] NetworkAddressError),
#[error("Unexpected error: {0}")]
Unexpected(String),
#[error("Network {0} is full, no IP addresses available for device")]
NetworkFull(String),
}

impl Device {
Expand Down Expand Up @@ -780,12 +786,23 @@ impl Device<Id> {
continue;
}

// FIXME: don't ignore the error.
let Ok(wireguard_network_device) =
network.add_device_to_network(&mut *conn, self, None).await
else {
warn!("Failed to add device {self} to network {network}");
continue;
let wireguard_network_device = match network
.add_device_to_network(&mut *conn, self, None)
.await
{
Ok(device) => device,
Err(WireguardNetworkError::DeviceNotAllowed(_)) => {
debug!("Device {self} is not allowed in network {network}, skipping");
continue;
}
Err(WireguardNetworkError::DeviceError(DeviceError::NetworkFull(_))) => {
warn!("Network {network} is full, no IP addresses available for device {self}");
return Err(DeviceError::NetworkFull(network.name.clone()));
}
Err(err) => {
warn!("Failed to add device {self} to network {network}: {err}");
return Err(DeviceError::Unexpected(err.to_string()));
}
};

debug!(
Expand Down Expand Up @@ -846,15 +863,15 @@ impl Device<Id> {
/// # 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.
/// - `Err(DeviceError::NetworkFull)`: If any subnet lacks an available IP.
pub async fn assign_next_network_ip(
&self,
transaction: &mut PgConnection,
network: &WireguardNetwork<Id>,
used_ips: &HashSet<IpAddr>,
reserved_ips: Option<&[IpAddr]>,
current_ips: Option<&[IpAddr]>,
) -> Result<WireguardNetworkDevice, ModelError> {
) -> Result<WireguardNetworkDevice, DeviceError> {
debug!(
"Assiging IP addresses for device: {} in network {}",
self.name, network.name
Expand Down Expand Up @@ -899,7 +916,7 @@ impl Device<Id> {
"Failed to assign address for device {} in network {address:?}",
self.name,
);
ModelError::CannotCreate
DeviceError::NetworkFull(address.to_string())
})?;

// Otherwise, store the IP address
Expand Down
4 changes: 2 additions & 2 deletions crates/defguard_common/src/db/models/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ impl WireguardNetwork<Id> {
&self,
transaction: &mut PgConnection,
user_id: Id,
) -> Result<Vec<Device<Id>>, ModelError> {
) -> Result<Vec<Device<Id>>, DeviceError> {
debug!("Fetching all allowed devices for network {self}, user ID {user_id}");
let devices =
match self.get_allowed_groups(&mut *transaction).await? {
Expand Down Expand Up @@ -426,7 +426,7 @@ impl WireguardNetwork<Id> {
pub async fn add_all_allowed_devices(
&self,
transaction: &mut PgConnection,
) -> Result<(), ModelError> {
) -> Result<(), DeviceError> {
info!(
"Assigning IPs in network {} for all existing devices ",
self
Expand Down
4 changes: 4 additions & 0 deletions crates/defguard_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub enum WebError {
#[error(transparent)]
#[schema(value_type=Object)]
StaticIpError(#[from] StaticIpError),
#[error("Network full: {0}")]
NetworkFull(String),
}

impl From<tonic::Status> for WebError {
Expand Down Expand Up @@ -122,6 +124,8 @@ impl From<DeviceError> for WebError {
DeviceError::DatabaseError(_) => Self::DbError(error.to_string()),
DeviceError::NetworkIpAssignmentError(_) => Self::ModelError(error.to_string()),
DeviceError::Unexpected(_) => Self::Http(StatusCode::INTERNAL_SERVER_ERROR),
DeviceError::NetworkFull(_) => Self::NetworkFull(error.to_string()),
DeviceError::ModelError(_) => Self::ModelError(error.to_string()),
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions crates/defguard_core/src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ pub mod wireguard;
pub mod worker;
pub(crate) mod yubikey;

#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WebErrorCode {
NetworkFull,
}

pub static SESSION_COOKIE_NAME: &str = "defguard_session";
pub(crate) static SIGN_IN_COOKIE_NAME: &str = "defguard_sign_in";
pub(crate) const SIGN_IN_COOKIE_MAX_AGE: time::Duration = time::Duration::minutes(10);
Expand Down Expand Up @@ -224,6 +230,13 @@ impl From<WebError> for ApiResponse {
error!(msg);
ApiResponse::new(json!({"msg": msg}), StatusCode::BAD_REQUEST)
}
WebError::NetworkFull(msg) => {
warn!(msg);
ApiResponse::new(
json!({"msg": msg, "code": WebErrorCode::NetworkFull}),
StatusCode::BAD_REQUEST,
)
}
WebError::TemplateError(err) => {
error!("Template error: {err}");
ApiResponse::new(
Expand Down
18 changes: 10 additions & 8 deletions crates/defguard_core/src/handlers/network_devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,19 +349,21 @@ pub(crate) async fn find_available_ips(
}

transaction.commit().await?;
if split_ips.len() == network.address.len() {
debug!(
"Found addresses {:?} for new device i network {} ({:?})",
split_ips, network.name, network.address
);
Ok(ApiResponse::json(split_ips, StatusCode::OK))
} else {
if split_ips.len() != network.address.len() {
warn!(
"Failed to find available IPs for new device in network {} ({:?})",
network.name, network.address
);
Ok(ApiResponse::with_status(StatusCode::NOT_FOUND))
return Err(WebError::NetworkFull(format!(
"Network {} is full, no IP addresses available",
network.name
)));
}
debug!(
"Found addresses {:?} for new device in network {} ({:?})",
split_ips, network.name, network.address
);
Ok(ApiResponse::json(split_ips, StatusCode::OK))
}

#[derive(Serialize, Deserialize, Debug)]
Expand Down
4 changes: 4 additions & 0 deletions web/messages/en/api-error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"api_error_network_full": "Network is full, no IP addresses available"
}
1 change: 1 addition & 0 deletions web/messages/en/location.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"location_delete_failed": "Failed to delete location",
"network_device_delete_success": "Network device deleted",
"network_device_delete_failed": "Failed to delete network device",
"network_device_add_error": "Failed to load network data for adding device",
"location_edit_failed": "Failed to update location",
"location_edit_addresses_rewrite_warning": "Changing the Gateway VPN IP address or netmask will automatically reassign any device addresses that fall outside the new network range to a randomly selected available IP address."
}
3 changes: 2 additions & 1 deletion web/project.inlang/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"./messages/{locale}/settings.json",
"./messages/{locale}/gateway_wizard.json",
"./messages/{locale}/initial_wizard.json",
"./messages/{locale}/migration_wizard.json"
"./messages/{locale}/migration_wizard.json",
"./messages/{locale}/api-error.json"
]
}
}
18 changes: 17 additions & 1 deletion web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import type { AxiosError } from 'axios';
import { orderBy } from 'lodash-es';
import { useMemo } from 'react';
import { m } from '../../paraglide/messages';
import api from '../../shared/api/api';
import { LocationMfaMode, type NetworkDevice } from '../../shared/api/types';
import { getApiErrorMessage } from '../../shared/api/apiErrorMessages';
import {
type ApiError,
LocationMfaMode,
type NetworkDevice,
} from '../../shared/api/types';
import { Badge } from '../../shared/defguard-ui/components/Badge/Badge';
import { Button } from '../../shared/defguard-ui/components/Button/Button';
import type { ButtonProps } from '../../shared/defguard-ui/components/Button/types';
Expand All @@ -24,6 +30,7 @@ import { TableBody } from '../../shared/defguard-ui/components/table/TableBody/T
import { TableCell } from '../../shared/defguard-ui/components/table/TableCell/TableCell';
import { TableEditCell } from '../../shared/defguard-ui/components/table/TableEditCell/TableEditCell';
import { TableTop } from '../../shared/defguard-ui/components/table/TableTop/TableTop';
import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar';
import { ThemeSize } from '../../shared/defguard-ui/types';
import { openModal } from '../../shared/hooks/modalControls/modalsSubjects';
import { ModalName } from '../../shared/hooks/modalControls/modalTypes';
Expand Down Expand Up @@ -63,6 +70,15 @@ export const NetworkDevicesTable = ({ networkDevices }: Props) => {
reservedNames,
});
},
onError: (e) => {
console.error(e);
const code = (e as AxiosError<ApiError>).response?.data?.code;
if (code) {
Snackbar.error(getApiErrorMessage(code));
} else {
Snackbar.error(m.network_device_add_error());
}
},
});

const addButtonProps = useMemo(
Expand Down
7 changes: 7 additions & 0 deletions web/src/shared/api/apiErrorMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { m } from '../../paraglide/messages';
import type { ApiErrorMessageKey, WebErrorCode } from './types';

export function getApiErrorMessage(code: WebErrorCode): string {
const key: ApiErrorMessageKey = `api_error_${code}`;
return (m as Record<ApiErrorMessageKey, () => string>)[key]();
}
9 changes: 9 additions & 0 deletions web/src/shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,18 @@ export interface MfaFinishResponse {
user?: User;
}

export const WebErrorCode = {
NetworkFull: 'network_full',
} as const;

export type WebErrorCode = (typeof WebErrorCode)[keyof typeof WebErrorCode];

export type ApiErrorMessageKey = `api_error_${WebErrorCode}`;

export interface ApiError {
msg?: string;
message?: string;
code?: WebErrorCode;
}

export interface AppInfoExceededLimits {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { useMutation } from '@tanstack/react-query';
import type { AxiosError } from 'axios';
import { useMemo } from 'react';
import api from '../../../../../api/api';
import { getApiErrorMessage } from '../../../../../api/apiErrorMessages';
import type { ApiError } from '../../../../../api/types';
import { SizedBox } from '../../../../../defguard-ui/components/SizedBox/SizedBox';
import { Snackbar } from '../../../../../defguard-ui/providers/snackbar/snackbar';
import { ThemeSpacing } from '../../../../../defguard-ui/types';
import { patternValidWireguardKey } from '../../../../../patterns';
import { generateWGKeys } from '../../../../../utils/generateWGKeys';
Expand Down Expand Up @@ -107,13 +109,19 @@ export const AddDeviceModalManualSetupStep = () => {
},
},
});
} else {
const code = (e as AxiosError<ApiError>).response?.data?.code;
if (code) {
Snackbar.error(getApiErrorMessage(code));
}
}
});

if (!createResponse) return;

if (!createResponse.data.configs.length) {
useAddUserDeviceModal.getState().close();
return;
}

useAddUserDeviceModal.setState({
Expand Down
Loading