diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 74f8db866..9f8796cb9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: container: rust:1-slim steps: - name: Install packages - run: apt-get update && apt install -y git protobuf-compiler libssl-dev + run: apt-get update && apt install -y git protobuf-compiler libssl-dev pkg-config curl - name: Checkout uses: actions/checkout@v4 diff --git a/README.md b/README.md index 61220a89f..81354e337 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,13 @@ GitHub commits since latest release

-[Website](https://defguard.net) | [Getting Started](https://docs.defguard.net/#what-is-defguard) | [Features](https://github.com/defguard/defguard#features) | [Roadmap](https://github.com/orgs/defguard/projects/5) | [Support ❤](https://github.com/defguard/defguard#support-) +[Website](https://defguard.net) | [Getting Started](https://docs.defguard.net/#what-is-defguard) | [Features](https://github.com/defguard/defguard#features) | [Roadmap](https://github.com/orgs/defguard/projects/5) | [Support ❤](https://github.com/defguard/defguard#support) -## Enterprise features are here! - -🛑 We encourge to test the [pre-release](https://docs.defguard.net/admin-and-features/setting-up-your-instance/pre-production-and-development-releases) of the new **Open Source Open Core** & **Enterprise features** (like external OpenID (Google/Microsoft/Custom), real time client sync and more!) published! 🛑 - -All currently available enterprise features are in [enterprise documentation section](https://docs.defguard.net/enterprise/all-enteprise-features) as well as information about [enterprise license](https://docs.defguard.net/enterprise/license). -### Unique value proposition +### Comprehensive Access Control -- **Comprehensive [WireGuard® 2FA/MFA](https://docs.defguard.net/admin-and-features/wireguard/multi-factor-authentication-mfa-2fa/architecture)** - not 2FA to "access application" like most solutions +- **[WireGuard® VPN with 2FA/MFA](https://docs.defguard.net/admin-and-features/wireguard/multi-factor-authentication-mfa-2fa/architecture)** - not 2FA to "access application" like most solutions - The only solution with [automatic and real-time synchronization](https://docs.defguard.net/enterprise/automatic-real-time-desktop-client-configuration) for users' desktop client settings (including all VPNs/locations). - Control users [ability to manage devices and VPN options](https://docs.defguard.net/enterprise/behavior-customization) - [Integrated SSO based on OpenID Connect](https://docs.defguard.net/admin-and-features/openid-connect): @@ -31,7 +26,9 @@ All currently available enterprise features are in [enterprise documentation sec - Built on WireGuard® protocol which is faster than IPSec, and significantly faster than OpenVPN - Built with Rust for speed and security -See below [full list of features](https://github.com/defguard/defguard#features) +See: +- [full list of features](https://github.com/defguard/defguard#features) +- [enterprise only features](https://docs.defguard.net/enterprise/all-enteprise-features) #### Video introduction @@ -65,6 +62,8 @@ Better quality video can [be viewed here](https://github.com/DefGuard/docs/raw/d [Desktop client](https://github.com/DefGuard/client): - **2FA / Multi-Factor Authentication** with TOTP or email based tokens & WireGuard PSK +- [automatic and real-time synchronization](https://docs.defguard.net/enterprise/automatic-real-time-desktop-client-configuration) for users' desktop client settings (including all VPNs/locations). +- Control users [ability to manage devices and VPN options](https://docs.defguard.net/enterprise/behavior-customization) - Defguard instances as well as **any WireGuard tunnel** - just import your tunnels - one client for all WireGuard connections - Secure and remote user enrollment - setting up password, automatically configuring the client for all VPN Locations/Networks - Onboarding - displaying custom onboarding messages, with templates, links ... @@ -117,7 +116,7 @@ The story and motivation behind defguard [can be found here: https://teonite.com ## Features -* [WireGuard®](https://www.wireguard.com/) VPN server with: +* Remote Access: [WireGuard® VPN](https://www.wireguard.com/) server with: - [Multi-Factor Authentication](https://docs.defguard.net/help/desktop-client/multi-factor-authentication-mfa-2fa) with TOTP/Email & Pre-Shared Session Keys - multiple VPN Locations (networks/sites) - with defined access (all users or only Admin group) - multiple [Gateways](https://github.com/DefGuard/gateway) for each VPN Location (**high availability/failover**) - supported on a cluster of routers/firewalls for Linux, FreeBSD/PFSense/OPNSense @@ -129,18 +128,20 @@ The story and motivation behind defguard [can be found here: https://teonite.com - kernel (Linux, FreeBSD/OPNSense/PFSense) & userspace WireGuard® support with [our Rust library](https://github.com/defguard/wireguard-rs) - dashboard and statistics overview of connected users/devices for admins - *defguard is not an official WireGuard® project, and WireGuard is a registered trademark of Jason A. Donenfeld.* -* Integrated SSO: [OpenID Connect provider](https://openid.net/developers/how-connect-works/) - with **unique features**: - - Secure remote (over the internet) [user enrollment](https://docs.defguard.net/help/remote-user-enrollment) - - User [onboarding after enrollment](https://docs.defguard.net/help/remote-user-enrollment/user-onboarding-after-enrollment) - - LDAP (tested on [OpenLDAP](https://www.openldap.org/)) synchronization - - [forward auth](https://docs.defguard.net/features/forward-auth) for reverse proxies (tested with Traefik and Caddy) - - nice UI to manage users - - Users **self-service** (besides typical data management, users can revoke access to granted apps, MFA, WireGuard®, etc.) +* Identity & Account Management: + - SSO based on OpenID Connect](https://openid.net/developers/how-connect-works/) + - Extenal SSO: [external OpenID provider support](https://docs.defguard.net/enterprise/external-openid-providers) - [Multi-Factor/2FA](https://en.wikipedia.org/wiki/Multi-factor_authentication) Authentication: - [Time-based One-Time Password Algorithm](https://en.wikipedia.org/wiki/Time-based_one-time_password) (TOTP - e.g. Google Authenticator) - WebAuthn / FIDO2 - for hardware key authentication support (eg. YubiKey, FaceID, TouchID, ...) - Email based TOTP -* Extenal SSO: [external OpenID provider support](https://docs.defguard.net/enterprise/external-openid-providers) + - LDAP (tested on [OpenLDAP](https://www.openldap.org/)) synchronization + - [forward auth](https://docs.defguard.net/features/forward-auth) for reverse proxies (tested with Traefik and Caddy) + - nice UI to manage users + - Users **self-service** (besides typical data management, users can revoke access to granted apps, MFA, WireGuard®, etc.) +* Account Lifecycle Management: + - Secure remote (over the Internet) [user enrollment](https://docs.defguard.net/help/remote-user-enrollment) - on public web / Desktop Client + - User [onboarding after enrollment](https://docs.defguard.net/help/remote-user-enrollment/user-onboarding-after-enrollment) * SSH & GPG public key management in user profile - with [SSH keys authentication for servers](https://docs.defguard.net/admin-and-features/ssh-authentication) * [Yubikey hardware keys](https://www.yubico.com/) provisioning for users by *one click* * [Email/SMTP support](https://docs.defguard.net/help/setting-up-smtp-for-email-notifications) for notifications, remote enrollment and onboarding diff --git a/migrations/20241108110157_add_on_delete.down.sql b/migrations/20241108110157_add_on_delete.down.sql new file mode 100644 index 000000000..f7680821b --- /dev/null +++ b/migrations/20241108110157_add_on_delete.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE token DROP CONSTRAINT enrollment_admin_id_fkey; +ALTER TABLE token ADD CONSTRAINT enrollment_admin_id_fkey FOREIGN KEY(admin_id) REFERENCES "user"(id); diff --git a/migrations/20241108110157_add_on_delete.up.sql b/migrations/20241108110157_add_on_delete.up.sql new file mode 100644 index 000000000..3ade0c10c --- /dev/null +++ b/migrations/20241108110157_add_on_delete.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE token DROP CONSTRAINT enrollment_admin_id_fkey; +ALTER TABLE token ADD CONSTRAINT enrollment_admin_id_fkey FOREIGN KEY(admin_id) REFERENCES "user"(id) ON DELETE CASCADE; diff --git a/web/package.json b/web/package.json index c7ed2738c..544013ef8 100644 --- a/web/package.json +++ b/web/package.json @@ -71,6 +71,7 @@ "get-text-width": "^1.0.3", "hex-rgb": "^5.0.0", "html-react-parser": "^5.1.1", + "ipaddr.js": "^2.2.0", "itertools": "^2.2.3", "lodash-es": "^4.17.21", "numbro": "^2.4.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index ba7fe4b1a..468706420 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: html-react-parser: specifier: ^5.1.1 version: 5.1.1(react@18.2.0) + ipaddr.js: + specifier: ^2.2.0 + version: 2.2.0 itertools: specifier: ^2.2.3 version: 2.2.3 @@ -3268,6 +3271,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -8814,6 +8821,8 @@ snapshots: internmap@2.0.3: {} + ipaddr.js@2.2.0: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index a94620912..ed010dc60 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -856,6 +856,7 @@ const en: BaseTranslation = { portMax: 'Maximum port is 65535.', endpoint: 'Enter a valid endpoint.', address: 'Enter a valid address.', + addressNetmask: 'Enter a valid address with a netmask.', validPort: 'Enter a valid port.', validCode: 'Code should have 6 digits.', allowedIps: 'Only valid IP or domain is allowed.', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 524f84697..c70aa7afe 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2101,6 +2101,10 @@ type RootTranslation = { * E​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​a​d​d​r​e​s​s​. */ address: string + /** + * E​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​a​d​d​r​e​s​s​ ​w​i​t​h​ ​a​ ​n​e​t​m​a​s​k​. + */ + addressNetmask: string /** * E​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​p​o​r​t​. */ @@ -6356,6 +6360,10 @@ export type TranslationFunctions = { * Enter a valid address. */ address: () => LocalizedString + /** + * Enter a valid address with a netmask. + */ + addressNetmask: () => LocalizedString /** * Enter a valid port. */ diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 17045129f..b367c0c91 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -841,8 +841,9 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe oneUppercase: 'Wymagana jedna duża litera.', oneLowercase: 'Wymagana jedna mała litera.', portMax: 'Maksymalny numer portu to 65535.', - endpoint: 'Wpisz prawidłowy punkt końcowy.', + endpoint: 'Wpisz poprawny adres.', address: 'Wprowadź poprawny adres.', + addressNetmask: 'Wprowadź poprawny adres IP oraz maskę sieci.', validPort: 'Wprowadź prawidłowy port.', validCode: 'Kod powinien mieć 6 cyfr.', allowedIps: 'Tylko poprawne adresy IP oraz domeny.', diff --git a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx index 32875bcc9..33c358940 100644 --- a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx +++ b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx @@ -2,6 +2,7 @@ import './style.scss'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import ipaddr from 'ipaddr.js'; import { isNull, omit, omitBy } from 'lodash-es'; import { useEffect, useMemo, useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; @@ -20,11 +21,7 @@ import { QueryKeys } from '../../../shared/queries'; import { Network } from '../../../shared/types'; import { titleCase } from '../../../shared/utils/titleCase'; import { trimObjectStrings } from '../../../shared/utils/trimObjectStrings.ts'; -import { - validateIp, - validateIpOrDomain, - validateIpOrDomainList, -} from '../../../shared/validators'; +import { validateIpOrDomain, validateIpOrDomainList } from '../../../shared/validators'; import { useNetworkPageStore } from '../hooks/useNetworkPageStore'; type FormFields = { @@ -155,17 +152,43 @@ export const NetworkEditForm = () => { if (!netmaskPresent) { return false; } - const ipValid = validateIp(value, true); - if (ipValid) { - const host = value.split('.')[3].split('/')[0]; - if (host === '0') return false; + const ipValid = ipaddr.isValidCIDR(value); + if (!ipValid) { + return false; + } + const [address] = ipaddr.parseCIDR(value); + if (address.kind() === 'ipv6') { + const networkAddress = ipaddr.IPv6.networkAddressFromCIDR(value); + const broadcastAddress = ipaddr.IPv6.broadcastAddressFromCIDR(value); + if ( + (address as ipaddr.IPv6).toNormalizedString() === + networkAddress.toNormalizedString() || + (address as ipaddr.IPv6).toNormalizedString() === + broadcastAddress.toNormalizedString() + ) { + return false; + } + } else { + const networkAddress = ipaddr.IPv4.networkAddressFromCIDR(value); + const broadcastAddress = ipaddr.IPv4.broadcastAddressFromCIDR(value); + if ( + (address as ipaddr.IPv4).toNormalizedString() === + networkAddress.toNormalizedString() || + (address as ipaddr.IPv4).toNormalizedString() === + broadcastAddress.toNormalizedString() + ) { + return false; + } } return ipValid; - }, LL.form.error.address()), + }, LL.form.error.addressNetmask()), endpoint: z .string() .min(1, LL.form.error.required()) - .refine((val) => validateIpOrDomain(val), LL.form.error.endpoint()), + .refine( + (val) => validateIpOrDomain(val, false, true), + LL.form.error.endpoint(), + ), port: z .number({ invalid_type_error: LL.form.error.required(), @@ -179,7 +202,7 @@ export const NetworkEditForm = () => { if (val === '' || !val) { return true; } - return validateIpOrDomainList(val, ',', true); + return validateIpOrDomainList(val, ',', false, true); }, LL.form.error.allowedIps()), allowed_groups: z.array(z.string().min(1, LL.form.error.minimumLength())), mfa_enabled: z.boolean(), diff --git a/web/src/pages/settings/components/SmtpSettings/components/SmtpSettingsForm/SmtpSettingsForm.tsx b/web/src/pages/settings/components/SmtpSettings/components/SmtpSettingsForm/SmtpSettingsForm.tsx index 13bf87296..ba22409d1 100644 --- a/web/src/pages/settings/components/SmtpSettings/components/SmtpSettingsForm/SmtpSettingsForm.tsx +++ b/web/src/pages/settings/components/SmtpSettings/components/SmtpSettingsForm/SmtpSettingsForm.tsx @@ -104,7 +104,7 @@ export const SmtpSettingsForm = () => { .string() .min(1, LL.form.error.required()) .refine( - (val) => (!val ? true : validateIpOrDomain(val)), + (val) => (!val ? true : validateIpOrDomain(val, false, true)), LL.form.error.endpoint(), ), smtp_port: z diff --git a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx index fbd12098e..cce80f6d5 100644 --- a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx +++ b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx @@ -20,7 +20,7 @@ import { QueryKeys } from '../../../../shared/queries'; import { ModifyNetworkRequest } from '../../../../shared/types'; import { titleCase } from '../../../../shared/utils/titleCase'; import { trimObjectStrings } from '../../../../shared/utils/trimObjectStrings.ts'; -import { validateIp, validateIpOrDomainList } from '../../../../shared/validators'; +import { validateIpOrDomainList, validateIPv4 } from '../../../../shared/validators'; import { useWizardStore } from '../../hooks/useWizardStore'; type FormInputs = ModifyNetworkRequest['network']; @@ -91,7 +91,7 @@ export const WizardNetworkConfiguration = () => { if (!netmaskPresent) { return false; } - const ipValid = validateIp(value, true); + const ipValid = validateIPv4(value, true); if (ipValid) { const host = value.split('.')[3].split('/')[0]; if (host === '0') return false; diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts index 1cb06ea8c..6c17b23c9 100644 --- a/web/src/shared/patterns.ts +++ b/web/src/shared/patterns.ts @@ -68,9 +68,6 @@ export const patternValidUrl = new RegExp( export const patternValidDomain = /^(?:(?:(?:[a-zA-z\-]+)\:\/{1,3})?(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-\.]){1,61}(?:\.[a-zA-Z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3}))(?:\:[0-9]{1,5})?$/; -export const patternValidIp = - /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - export const patternSafeUsernameCharacters = /^[a-zA-Z0-9]+[a-zA-Z0-9.\-_]+$/; export const patternSafePasswordCharacters = diff --git a/web/src/shared/validators.ts b/web/src/shared/validators.ts index 92e538863..c9aa3c3a4 100644 --- a/web/src/shared/validators.ts +++ b/web/src/shared/validators.ts @@ -1,8 +1,18 @@ -import { patternValidDomain, patternValidIp } from './patterns'; +import ipaddr from 'ipaddr.js'; + +import { patternValidDomain } from './patterns'; // Returns flase when invalid -export const validateIpOrDomain = (val: string, allowMask = false): boolean => { - return validateIp(val, allowMask) || patternValidDomain.test(val); +export const validateIpOrDomain = ( + val: string, + allowMask = false, + allowIPv6 = false, +): boolean => { + return ( + (allowIPv6 && validateIPv6(val, allowMask)) || + validateIPv4(val, allowMask) || + patternValidDomain.test(val) + ); }; // Returns flase when invalid @@ -14,7 +24,7 @@ export const validateIpList = ( const trimed = val.replace(' ', ''); const split = trimed.split(splitWith); for (const value of split) { - if (!validateIp(value, allowMasks)) { + if (!validateIPv4(value, allowMasks)) { return false; } } @@ -26,11 +36,17 @@ export const validateIpOrDomainList = ( val: string, splitWith = ',', allowMasks = false, + allowIPv6 = false, ): boolean => { const trimed = val.replace(' ', ''); const split = trimed.split(splitWith); for (const value of split) { - if (!validateIp(value, allowMasks) && !patternValidDomain.test(value)) { + console.log(allowIPv6 && !validateIPv6(value, allowMasks)); + if ( + !validateIPv4(value, allowMasks) && + !patternValidDomain.test(value) && + (!allowIPv6 || !validateIPv6(value, allowMasks)) + ) { return false; } } @@ -38,19 +54,22 @@ export const validateIpOrDomainList = ( }; // Returns flase when invalid -export const validateIp = (ip: string, allowMask = false): boolean => { +export const validateIPv4 = (ip: string, allowMask = false): boolean => { + if (allowMask) { + if (ip.includes('/')) { + ipaddr.IPv4.isValidCIDR(ip); + } + } + return ipaddr.IPv4.isValid(ip); +}; + +export const validateIPv6 = (ip: string, allowMask = false): boolean => { if (allowMask) { if (ip.includes('/')) { - const split = ip.split('/'); - if (split.length !== 2) return true; - const ipValid = patternValidIp.test(split[0]); - if (split[1] === '') return false; - const mask = Number(split[1]); - const maskValid = mask >= 0 && mask <= 32; - return ipValid && maskValid; + ipaddr.IPv6.isValidCIDR(ip); } } - return patternValidIp.test(ip); + return ipaddr.IPv6.isValid(ip); }; export const validatePort = (val: string) => {