Compare commits

...

6 Commits

Author SHA1 Message Date
Christoph Haas
d01d865b4d fix self provisioning feature (#272)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-26 11:35:24 +01:00
Christoph Haas
1b8cdc3417 automatically append listening port to endpoint address (#352) 2025-01-26 09:52:09 +01:00
Christoph Haas
d35889de73 remove external google fonts (#107)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-25 23:06:44 +01:00
Dmytro Bondar
0b18b5efd6 [chart] Fix default configurations (#350)
Some checks failed
Chart / lint-test (push) Has been cancelled
Chart / publish (push) Has been cancelled
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-24 12:48:36 +01:00
Dmytro Bondar
2cf2341e4c [chart] Update helm chart (#349)
Some checks are pending
Chart / lint-test (push) Waiting to run
Chart / publish (push) Waiting to run
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-23 13:42:51 +01:00
Dmytro Bondar
043d25a08f [docs] big bang update (#348)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
* [docs] big bang update

* Simplified polluted README.md by moving parts to the documentation
* Removed duplicates with `pymdownx.snippets` extension
* Enabled code copy
* Extended "Getting Started"
* Added "Monitoring" page
* Separated "Upgrade" page
* Added default config yaml to docs

Signed-off-by: Dmytro Bondar <git@bonddim.dev>

* Update sources.md

Co-authored-by: h44z <christoph.h@sprinternet.at>

---------

Signed-off-by: Dmytro Bondar <git@bonddim.dev>
Co-authored-by: h44z <christoph.h@sprinternet.at>
2025-01-23 08:06:55 +01:00
38 changed files with 1493 additions and 493 deletions

243
README.md
View File

@@ -1,211 +1,67 @@
# WireGuard Portal (v2 - testing)
[![Build Status](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/h44z/wg-portal/actions)
[![Build Status](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml/badge.svg?event=push)](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
![GitHub last commit](https://img.shields.io/github/last-commit/h44z/wg-portal)
![GitHub last commit](https://img.shields.io/github/last-commit/h44z/wg-portal/master)
[![Go Report Card](https://goreportcard.com/badge/github.com/h44z/wg-portal)](https://goreportcard.com/report/github.com/h44z/wg-portal)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/h44z/wg-portal)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal)
[![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/wgportal/wg-portal/)
> :warning: **IMPORTANT** Version 2 is currently under development and may contain bugs. It is currently not advised to use this version
in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
> [!CAUTION]
> Version 2 is currently under development and may contain bugs and breaking changes.
> It is not advised to use this version in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to: https://hub.docker.com/r/wgportal/wg-portal.
Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
> [!IMPORTANT]
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
A simple, web based configuration portal for [WireGuard](https://wireguard.com).
## Introduction
<!-- Text from this line # is included in docs/documentation/overview.md -->
**WireGuard Portal** is a simple, web-based configuration portal for [WireGuard](https://wireguard.com) server management.
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
interfaces. This allows for the seamless activation or deactivation of new users without disturbing existing VPN
connections.
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP (Active Directory or OpenLDAP) as a user source for authentication and profile data.
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
## Features
* Self-hosted - the whole application is a single binary
* Responsive web UI written in Vue.JS
* Automatically select IP from the network pool assigned to client
* Responsive multi-language web UI written in Vue.JS
* Automatically selects IP from the network pool assigned to the client
* QR-Code for convenient mobile client configuration
* Sent email to client with QR-code and client config
* Sends email to the client with QR-code and client config
* Enable / Disable clients seamlessly
* Generation of wg-quick configuration file (`wgX.conf`) if required
* User authentication (database, OAuth or LDAP)
* User authentication (database, OAuth, or LDAP)
* IPv6 ready
* Docker ready
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* Peer Expiry Feature
* Handle route and DNS settings like wg-quick does
* Exposes Prometheus [metrics](#metrics)
* Handles route and DNS settings like wg-quick does
* Exposes Prometheus metrics for monitoring and alertingt
* REST API for management and client deployment
![Screenshot](screenshot.png)
<!-- Text to this line # is included in docs/documentation/overview.md -->
![Screenshot](docs/assets/images/screenshot.png)
## Documentation
## Configuration
You can configure WireGuard Portal using a yaml configuration file.
The filepath of the yaml configuration file defaults to **config/config.yml** in the working directory of the executable.
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
For example: `WG_PORTAL_CONFIG=/home/test/config.yml ./wg-portal-amd64`.
Also, environment variable substitution in config file is supported. Refer to [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs)
By default, WireGuard Portal uses a SQLite database. The database is stored in **data/sqlite.db** in the working directory of the executable.
### Configuration Options
The following configuration options are available:
| configuration key | parent key | default_value | description |
|----------------------------------|------------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. |
| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. |
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
| log_json | advanced | false | Logs in JSON format. |
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
| use_ip_v6 | advanced | true | Enable IPv6 support. |
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
| api_admin_only | advanced | true | This flag specifies if the public REST API is available to administrators only. The API Swagger documentation is available under /api/v1/doc.html |
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
| host | mail | 127.0.0.1 | The mail-server address. |
| port | mail | 25 | The mail-server SMTP port. |
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
| username | mail | | The SMTP user name. |
| password | mail | | The SMTP password. |
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
| client_id | auth/oidc | | The OAuth client id. |
| client_secret | auth/oidc | | The OAuth client secret. |
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department, is_admin and user_groups. |
| admin_mapping | auth/oidc | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| log_user_info | auth/oidc | | If true, the user info retrieved from the OIDC provider will be logged in trace level. |
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
| client_id | auth/oauth | | The OAuth client id. |
| client_secret | auth/oauth | | The OAuth client secret. |
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
| token_url | auth/oauth | | The URL for the token endpoint. |
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
| scopes | auth/oauth | | OAuth scopes. |
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin and user_groups. |
| admin_mapping | auth/oauth | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| log_user_info | auth/oauth | | If true, the user info retrieved from the OAuth provider will be logged in trace level. |
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
| tls_key_path | auth/ldap | | A path to the TLS key. |
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
| bind_pass | auth/ldap | | The bind password. |
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
| auto_re_enable | auth/ldap | | If auto re-enable is true, users that where disabled because they were missing will be re-enabled once they are found again. |
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| log_user_info | auth/ldap | | If true, the user info retrieved from the LDAP provider will be logged in trace level. |
| debug | database | false | Debug database statements (log each statement). |
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local |
| request_logging | web | false | Log all HTTP requests. |
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
| listening_address | web | :8888 | The listening port of the web server. |
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
| session_secret | web | very_secret | The session secret for the web frontend. |
| csrf_secret | web | extremely_secret | The CSRF secret. |
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
| cert_file | web | | (Optional) Path to the TLS certificate file |
| key_file | web | | (Optional) Path to the TLS certificate key file |
A sample config file can be found in the repository: [config.yml.sample](config.yml.sample).
More detailed information about the configuration can be found in the [documentation](https://wgportal.org/master/documentation/overview/) on [wgportal.org](https://wgportal.org/master/documentation/overview/).
## Upgrading from V1
> :warning: Before upgrading from V1, make sure that you have a backup of your currently working configuration files and database!
To start the upgrade process, start the wg-portal binary with the **-migrateFrom** parameter.
The configuration (config.yml) for WireGuard Portal must be updated and valid before starting the upgrade.
To upgrade from a previous SQLite database, start wg-portal like:
```shell
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
```
You can also specify the database type using the parameter **-migrateFromType**, supported types: mysql, mssql, postgres or sqlite.
For example:
```shell
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom=user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
```
The upgrade will transform the old, existing database and store the values in the new database specified in config.yml.
Ensure that the new database does not contain any data!
For the complete documentation visit [wgportal.org](https://wgportal.org).
## V2 TODOs
* Audit UI
## Building
To build a standalone application, use the Makefile provided in the repository.
Go version 1.23 or higher has to be installed to build WireGuard Portal.
If you want to re-compile the frontend, NodeJS 18 and NPM >= 9 is required.
```shell
# build the frontend
make frontend
# build the binary
make build
```
## What is out of scope
* Automatic generation or application of any `iptables` or `nftables` rules.
* Support for operating systems other than linux.
* Automatic import of private keys of an existing WireGuard setup.
## Application stack
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
@@ -213,49 +69,6 @@ make build
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
* [Vue.JS](https://vuejs.org/), for the frontend
## Metrics
Metrics are available if interface/peer statistic data collection is enabled.
Add following scrape job to your Prometheus config file:
```yaml
# prometheus.yaml
scrape_configs:
- job_name: "wg-portal"
scrape_interval: 60s
static_configs:
- targets: ["wg-portal:8787"]
```
Exposed metrics:
```console
# HELP wireguard_interface_info Interface info.
# TYPE wireguard_interface_info gauge
# HELP wireguard_interface_received_bytes_total Bytes received througth the interface.
# TYPE wireguard_interface_received_bytes_total gauge
# HELP wireguard_interface_sent_bytes_total Bytes sent through the interface.
# TYPE wireguard_interface_sent_bytes_total gauge
# HELP wireguard_peer_info Peer info.
# TYPE wireguard_peer_info gauge
# HELP wireguard_peer_received_bytes_total Bytes received from the peer.
# TYPE wireguard_peer_received_bytes_total gauge
# HELP wireguard_peer_sent_bytes_total Bytes sent to the peer.
# TYPE wireguard_peer_sent_bytes_total gauge
# HELP wireguard_peer_up Peer connection state (boolean: 1/0).
# TYPE wireguard_peer_up gauge
# HELP wireguard_peer_last_handshake_seconds Seconds from the last handshake with the peer.
# TYPE wireguard_peer_last_handshake_seconds gauge
```
## License
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>

View File

@@ -2,10 +2,10 @@ apiVersion: v2
name: wg-portal
description: WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
# Version is set to ensure compatibility with the chart's Ingress resource.
kubeVersion: '>=1.19.0'
kubeVersion: ">=1.19.0"
type: application
home: https://wgportal.org
icon: https://wgportal.org/assets/images/logo.svg
icon: https://wgportal.org/latest/assets/images/logo.svg
sources:
- https://github.com/h44z/wg-portal
@@ -16,10 +16,10 @@ annotations:
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.5.0
version: 0.7.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: latest
appVersion: "v2"

View File

@@ -1,6 +1,6 @@
# wg-portal
![Version: 0.5.0](https://img.shields.io/badge/Version-0.5.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
![Version: 0.7.0](https://img.shields.io/badge/Version-0.7.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2](https://img.shields.io/badge/AppVersion-v2-informational?style=flat-square)
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
@@ -32,13 +32,13 @@ The [Values](#values) section lists the parameters that can be configured during
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
| fullnameOverride | string | `""` | Fully override resource names |
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
| config.advanced | tpl/object | `{}` | Advanced configuration options. |
| config.auth | tpl/object | `{}` | Auth configuration options. |
| config.core | tpl/object | `{}` | Core configuration options.<br> If external admins in `auth` are not defined and there are no `admin_user` and `admin_password` defined here, the default credentials will be generated. |
| config.database | tpl/object | `{}` | Database configuration options |
| config.mail | tpl/object | `{}` | Mail configuration options |
| config.statistics | tpl/object | `{}` | Statistics configuration options |
| config.web | tpl/object | `{}` | Web configuration options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
| config.advanced | tpl/object | `{}` | [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options. |
| config.auth | tpl/object | `{}` | [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options. |
| config.core | tpl/object | `{}` | [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br> If external admins in `auth` are defined and there are no `admin_user` and `admin_password` defined here, the default admin account will be disabled. |
| config.database | tpl/object | `{}` | [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options |
| config.mail | tpl/object | `{}` | [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options |
| config.statistics | tpl/object | `{}` | [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options |
| config.web | tpl/object | `{}` | [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
@@ -73,6 +73,7 @@ The [Values](#values) section lists the parameters that can be configured during
| service.web.annotations | object | `{}` | Annotations for the web service |
| service.web.type | string | `"ClusterIP"` | Web service type |
| service.web.port | int | `8888` | Web service port Used for the web interface listener |
| service.web.appProtocol | string | `"http"` | Web service appProtocol. Will be auto set to `https` if certificate is enabled. |
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
@@ -81,7 +82,7 @@ The [Values](#values) section lists the parameters that can be configured during
| ingress.className | string | `""` | Ingress class name |
| ingress.annotations | object | `{}` | Ingress annotations |
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created |
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created. If enabled, certificate will be used for the web. |
| certificate.issuer.name | string | `""` | Certificate issuer name |
| certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) |
| certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group |

View File

@@ -62,9 +62,9 @@ Create the name of the service account to use
{{- end }}
{{/*
Define default admin credentials
Disables default admin credentials
If external auth is enabled and has admin group mappings,
the admin_user and admin_password values are not used.
the admin_user will be set to blank (disabled).
*/}}
{{- define "wg-portal.admin" -}}
{{- $externalAdmin := false -}}
@@ -80,9 +80,8 @@ the admin_user and admin_password values are not used.
{{- end -}}
{{- end -}}
{{- end -}}
{{- if not $externalAdmin -}}
admin_user: admin@wgportal.local
admin_password: {{ printf "%s/%s" .Release.Name .Release.Namespace | b64enc }}
{{- if $externalAdmin -}}
admin_user: ""
{{- end -}}
{{- end -}}

View File

@@ -51,3 +51,16 @@ spec:
{{- end }}
selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
{{- end -}}
{{/*
Define the service port template for the web port
*/}}
{{- define "wg-portal.service.webPort" -}}
name: web
port: {{ .Values.service.web.port }}
protocol: TCP
targetPort: web
{{- if semverCompare ">=1.20-0" .Capabilities.KubeVersion.Version }}
appProtocol: {{ ternary "https" .Values.service.web.appProtocol .Values.certificate.enabled }}
{{- end -}}
{{- end -}}

View File

@@ -1,3 +1,11 @@
{{- $advanced := dict "start_listen_port" (.Values.service.wireguard.ports | sortAlpha | first | int) -}}
{{- $statistics := dict "listening_address" (printf ":%v" .Values.service.metrics.port) -}}
{{- $web:= dict "listening_address" (printf ":%v" .Values.service.web.port) -}}
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
{{- $_ := set $web "cert_file" "/app/certs/tls.crt" }}
{{- $_ := set $web "key_file" "/app/certs/tls.key" }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
@@ -5,10 +13,8 @@ metadata:
labels: {{- include "wg-portal.labels" . | nindent 4 }}
stringData:
config.yml: |
advanced:
start_listen_port: {{ .Values.service.wireguard.ports | sortAlpha | first }}
{{- with .Values.config.advanced }}
{{- tpl (toYaml (omit . "start_listen_port")) $ | nindent 6 }}
{{- with mustMerge $advanced .Values.config.advanced }}
advanced: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with .Values.config.auth }}
@@ -27,14 +33,10 @@ stringData:
mail: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
statistics:
listening_address: :{{ .Values.service.metrics.port }}
{{- with .Values.config.statistics }}
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
{{- with mustMerge $statistics .Values.config.statistics }}
statistics: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
web:
listening_address: :{{ .Values.service.web.port }}
{{- with .Values.config.web }}
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
{{- with mustMerge $web .Values.config.web }}
web: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}

View File

@@ -1,4 +1,4 @@
{{- $portsWeb := list (dict "name" "web" "port" .Values.service.web.port "protocol" "TCP" "targetPort" "web") -}}
{{- $portsWeb := list (include "wg-portal.service.webPort" . | fromYaml) -}}
{{- $ports := list -}}
{{- range $idx, $port := .Values.service.wireguard.ports -}}
{{- $name := printf "wg%d" $idx -}}

View File

@@ -3,37 +3,36 @@
# Declare variables to be passed into your templates.
# -- Partially override resource names (adds suffix)
nameOverride: ''
nameOverride: ""
# -- Fully override resource names
fullnameOverride: ''
fullnameOverride: ""
# -- Array of extra objects to deploy with the release
extraDeploy: []
# https://github.com/h44z/wg-portal/blob/master/README.md#configuration-options
config:
# -- (tpl/object) Advanced configuration options.
# -- (tpl/object) [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options.
advanced: {}
# -- (tpl/object) Auth configuration options.
# -- (tpl/object) [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options.
auth: {}
# -- (tpl/object) Core configuration options.<br>
# If external admins in `auth` are not defined and
# -- (tpl/object) [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br>
# If external admins in `auth` are defined and
# there are no `admin_user` and `admin_password` defined here,
# the default credentials will be generated.
# the default admin account will be disabled.
core: {}
# -- (tpl/object) Database configuration options
# -- (tpl/object) [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options
database: {}
# -- (tpl/object) Mail configuration options
# -- (tpl/object) [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options
mail: {}
# -- (tpl/object) Statistics configuration options
# -- (tpl/object) [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options
statistics: {}
# -- (tpl/object) Web configuration options.<br>
# -- (tpl/object) [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br>
# `listening_address` will be set automatically from `service.web.port`.
# `external_url` is required to enable ingress and certificate resources.
web: {}
# -- The number of old ReplicaSets to retain to allow rollback.
# @default -- `10`
revisionHistoryLimit: ''
revisionHistoryLimit: ""
# -- Workload type - `Deployment` or `StatefulSet`
workloadType: Deployment
# -- Update strategy for the workload
@@ -49,7 +48,7 @@ image:
# -- Image pull policy
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion
tag: ''
tag: ""
# -- Image pull secrets
imagePullSecrets: []
@@ -73,14 +72,14 @@ sidecarContainers: []
# -- Set DNS policy for the pod.
# Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`.
# @default -- `"ClusterFirst"`
dnsPolicy: ''
dnsPolicy: ""
# -- Restart policy for all containers within the pod.
# Valid values are `Always`, `OnFailure` or `Never`.
# @default -- `"Always"`
restartPolicy: ''
restartPolicy: ""
# -- Use the host's network namespace.
# @default -- `false`.
hostNetwork: ''
hostNetwork: ""
# -- Resources requests and limits
resources: {}
# -- Overwrite pod command
@@ -123,6 +122,8 @@ service:
# -- Web service port
# Used for the web interface listener
port: 8888
# -- Web service appProtocol. Will be auto set to `https` if certificate is enabled.
appProtocol: http
wireguard:
# -- Annotations for the WireGuard service
annotations: {}
@@ -141,7 +142,7 @@ ingress:
# -- Specifies whether an ingress resource should be created
enabled: false
# -- Ingress class name
className: ''
className: ""
# -- Ingress annotations
annotations: {}
# -- Ingress TLS configuration.
@@ -149,21 +150,22 @@ ingress:
tls: false
certificate:
# -- Specifies whether a certificate resource should be created
# -- Specifies whether a certificate resource should be created.
# If enabled, certificate will be used for the web.
enabled: false
issuer:
# -- Certificate issuer name
name: ''
name: ""
# -- Certificate issuer kind (ClusterIssuer or Issuer)
kind: ''
kind: ""
# -- Certificate issuer group
group: cert-manager.io
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
duration: ''
duration: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
renewBefore: ''
renewBefore: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
commonName: ''
commonName: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
emailAddresses: []
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
@@ -188,7 +190,7 @@ persistence:
annotations: {}
# -- Persistent Volume storage class.
# If undefined (the default) cluster's default provisioner will be used.
storageClass: ''
storageClass: ""
# -- Persistent Volume Access Mode
accessMode: ReadWriteOnce
# -- Persistent Volume size
@@ -203,7 +205,7 @@ serviceAccount:
automount: false
# -- The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ''
name: ""
monitoring:
# -- Enable Prometheus monitoring.
@@ -220,15 +222,15 @@ monitoring:
annotations: {}
# -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used.
# @default -- `1m`
interval: ''
interval: ""
# -- Relabelings to samples before ingestion.
metricRelabelings: []
# -- Relabelings to samples before scraping.
relabelings: []
# -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used.
scrapeTimeout: ''
scrapeTimeout: ""
# -- The label to use to retrieve the job name from.
jobLabel: ''
jobLabel: ""
# -- Transfers labels on the Kubernetes Pod onto the target.
podTargetLabels: {}
@@ -241,4 +243,4 @@ monitoring:
labels: {}
# -- Dashboard ConfigMap namespace
# Overrides the namespace for the dashboard ConfigMap.
namespace: ''
namespace: ""

BIN
docs/assets/images/dashboard.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -1,6 +1,7 @@
Below are some sample YAML configurations demonstrating how to override some default values.
## Basic Configuration
## Basic
```yaml
core:
admin_user: test@example.com
@@ -31,13 +32,13 @@ database:
dsn: data/sqlite.db
```
## LDAP Authentication and Synchronization Configuration
## LDAP Authentication and Synchronization
```yaml
# ... (basic configuration)
auth:
ldap:
# a sample LDAP provider with user sync enabled
- id: ldap
provider_name: Active Directory
@@ -63,12 +64,24 @@ auth:
log_user_info: true
```
## OpenID Connect (OIDC) Authentication Configuration
## OpenID Connect (OIDC) Authentication
```yaml
# ... (basic configuration)
auth:
oidc:
# a sample Entra ID provider with environment variable substitution
- id: azure
provider_name: azure
display_name: Login with</br>Entra ID
registration_enabled: true
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
client_id: "${AZURE_CLIENT_ID}"
client_secret: "${AZURE_CLIENT_SECRET}"
extra_scopes:
- profile
- email
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
- id: oidc-with-admin-attribute
@@ -117,13 +130,13 @@ auth:
log_user_info: true
```
## Plain OAuth2 Authentication Configuration
## Plain OAuth2 Authentication
```yaml
# ... (basic configuration)
auth:
oauth:
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
# are considered as admins
- id: google_plain_oauth-with-admin-attribute

View File

@@ -1,10 +1,99 @@
# WireGuard Portal Configuration
This page provides an overview of **all available configuration options** for WireGuard Portal.
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
Complete configuration examples are available in the [Configuration Examples](./examples.md) page.
Below you will find sections like `core`, `advanced`, `statistics`, `mail`, `auth`, `database`, and `web`.
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
The path of the configuration file defaults to **config/config.yml** in the working directory of the executable.
It is possible to override configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
For example: `WG_PORTAL_CONFIG=/etc/wg-portal/config.yaml ./wg-portal`.
Also, environment variable substitution in config file is supported. Refer to [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs).
Configuration examples are available on the [Examples](./examples.md) page.
<details>
<summary>Default configuration</summary>
```yaml
core:
admin_user: admin@wgportal.local
admin_password: wgportal
editable_keys: true
create_default_peer: false
create_default_peer_on_creation: false
re_enable_peer_after_user_enable: true
delete_peer_after_user_deleted: false
self_provisioning_allowed: false
import_existing: true
restore_state: true
advanced:
log_level: info
log_pretty: false
log_json: false
start_listen_port: 51820
start_cidr_v4: 10.11.12.0/24
start_cidr_v6: fdfd:d3ad:c0de:1234::0/64
use_ip_v6: true
config_storage_path: ""
expiry_check_interval: 15m
rule_prio_offset: 20000
api_admin_only: true
database:
debug: false
slow_query_threshold: 0
type: sqlite
dsn: data/sqlite.db
statistics:
use_ping_checks: true
ping_check_workers: 10
ping_unprivileged: false
ping_check_interval: 1m
data_collection_interval: 1m
collect_interface_data: true
collect_peer_data: true
collect_audit_data: true
listening_address: :8787
mail:
host: 127.0.0.1
port: 25
encryption: none
cert_validation: false
username: ""
password: ""
auth_type: plain
from: Wireguard Portal <noreply@wireguard.local>
link_only: false
auth:
oidc: []
oauth: []
ldap: []
web:
listening_address: :8888
external_url: http://localhost:8888
site_company_name: WireGuard Portal
site_title: WireGuard Portal
session_identifier: wgPortalSession
session_secret: very_secret
csrf_secret: extremely_secret
request_logging: false
cert_file: ""
key_File: ""
```
</details>
Below you will find sections like
[`core`](#core),
[`advanced`](#advanced),
[`database`](#database),
[`statistics`](#statistics),
[`mail`](#mail),
[`auth`](#auth) and
[`web`](#web).
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
---
@@ -108,7 +197,6 @@ Additional or more specialized configuration options for logging and interface c
- **Default:** `true`
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
---
## Database
@@ -229,7 +317,7 @@ Each can have multiple providers configured. Below are the relevant keys.
---
### OIDC Provider Properties
### OIDC
The `oidc` array contains a list of OpenID Connect providers.
Below are the properties for each OIDC provider entry inside `auth.oidc`:
@@ -264,7 +352,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical OIDC Claim** | **Explanation** |
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
@@ -290,7 +378,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
---
### OAuth Provider Properties
### OAuth
The `oauth` array contains a list of plain OAuth2 providers.
Below are the properties for each OAuth provider entry inside `auth.oauth`:
@@ -333,7 +421,7 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical Claim** | **Explanation** |
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
@@ -359,7 +447,7 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
---
### LDAP Provider Properties
### LDAP
The `ldap` array contains a list of LDAP authentication providers.
Below are the properties for each LDAP provider entry inside `auth.ldap`:
@@ -402,7 +490,7 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|----------------------------|----------------------------|--------------------------------------------------------------|
| -------------------------- | -------------------------- | ------------------------------------------------------------ |
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
| email | mail / userPrincipalName | Stores the user's primary email address. |
| firstname | givenName | Contains the user's first (given) name. |
@@ -455,3 +543,47 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
#### `log_user_info`
- **Default:** *(empty)*
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
---
## Web
### `listening_address`
- **Default:** `:8888`
- **Description:** The listening port of the web server.
### `external_url`
- **Default:** `http://localhost:8888`
- **Description:** The URL where a client can access WireGuard Portal.
### `site_company_name`
- **Default:** `WireGuard Portal`
- **Description:** The company name that is shown at the bottom of the web frontend.
### `site_title`
- **Default:** `WireGuard Portal`
- **Description:** The title that is shown in the web frontend.
### `session_identifier`
- **Default:** `wgPortalSession`
- **Description:** The session identifier for the web frontend.
### `session_secret`
- **Default:** `very_secret`
- **Description:** The session secret for the web frontend.
### `csrf_secret`
- **Default:** `extremely_secret`
- **Description:** The CSRF secret.
### `request_logging`
- **Default:** `false`
- **Description:** Log all HTTP requests.
### `cert_file`
- **Default:** *(empty)*
- **Description:** (Optional) Path to the TLS certificate file.
### `key_file`
- **Default:** *(empty)*
- **Description:** (Optional) Path to the TLS certificate key file.

View File

@@ -0,0 +1,34 @@
Starting from v2, each [release](https://github.com/h44z/wg-portal/releases) includes compiled binaries for supported platforms.
These binary versions can be manually downloaded and installed.
## Download
With `curl`:
```shell
curl -L -o wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
```
With `wget`:
```shell
wget -O wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
```
with `gh cli`:
```shell
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
```
## Install
```shell
sudo mkdir -p /opt/wg-portal
sudo install wg-portal /opt/wg-portal/
```
## Unreleased
Unreleased versions could be downloaded from
[GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster) artifacs also.

View File

@@ -1,11 +0,0 @@
To build a standalone application, use the Makefile provided in the repository.
Go version **1.23** or higher has to be installed to build WireGuard Portal.
If you want to re-compile the frontend, NodeJS **18** and NPM >= **9** is required.
```shell
# build the frontend (optional)
make frontend
# build the binary
make build
```

View File

@@ -5,20 +5,7 @@ The preferred way to start WireGuard Portal as Docker container is to use Docker
A sample docker-compose.yml:
```yaml
version: '3.6'
services:
wg-portal:
image: wgportal/wg-portal:latest
restart: unless-stopped
cap_add:
- NET_ADMIN
network_mode: "host"
ports:
- "8888:8888"
volumes:
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
- ./config:/app/config
--8<-- "docker-compose.yml::17"
```
By default, the webserver is listening on port **8888**.
@@ -31,6 +18,7 @@ All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-por
There are three types of tags in the repository:
#### Semantic versioned tags
For example, `1.0.19`.
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
@@ -44,16 +32,17 @@ If you only want to stay at the same major or major+minor version, use either `v
Version **1** is currently **stable**, version **2** is in **development**.
#### latest
This is the most recent build to master! It changes a lot and is very unstable.
We recommend that you don't use it except for development purposes.
#### Branch tags
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
## Configuration
You can configure WireGuard Portal using a yaml configuration file.
The filepath of the yaml configuration file defaults to `/app/config/config.yml`.
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
@@ -61,6 +50,7 @@ It is possible to override the configuration filepath using the environment vari
By default, WireGuard Portal uses a SQLite database. The database is stored in `/app/data/sqlite.db`.
You should mount those directories as a volume:
- /app/data
- /app/config

View File

@@ -0,0 +1 @@
--8<-- "./deploy/helm/README.md:16"

View File

@@ -0,0 +1,24 @@
To build the application from source files, use the Makefile provided in the repository.
## Requirements
- [Git](https://git-scm.com/downloads)
- [Make](https://www.gnu.org/software/make/)
- [Go](https://go.dev/dl/): `>=1.23.0`
- [NodeJS with npm](https://nodejs.org/en/download): `node>=18, npm>=9`
## Build
```shell
# Get source code
git clone https://github.com/h44z/wg-portal -b ${WG_PORTAL_VERSION:-master} --depth 1
cd wg-portal
# Build the frontend
make frontend
# Build the backend
make build
```
## Install
Compiled binary will be available in `./dist` directory.

View File

@@ -0,0 +1,32 @@
By default WG-Portal exposes Prometheus metrics on port `8787` if interface/peer statistic data collection is enabled.
## Exposed Metrics
| Metric | Type | Description |
|--------------------------------------------|-------|------------------------------------------------|
| `wireguard_interface_received_bytes_total` | gauge | Bytes received through the interface. |
| `wireguard_interface_sent_bytes_total` | gauge | Bytes sent through the interface. |
| `wireguard_peer_last_handshake_seconds` | gauge | Seconds from the last handshake with the peer. |
| `wireguard_peer_received_bytes_total` | gauge | Bytes received from the peer. |
| `wireguard_peer_sent_bytes_total` | gauge | Bytes sent to the peer. |
| `wireguard_peer_up` | gauge | Peer connection state (boolean: 1/0). |
## Prometheus Config
Add following scrape job to your Prometheus config file:
```yaml
# prometheus.yaml
scrape_configs:
- job_name: wg-portal
scrape_interval: 60s
static_configs:
- targets:
- localhost:8787 # Change localhost to IP Address or hostname with WG-Portal
```
# Grafana Dashboard
You may import [`dashboard.json`](https://github.com/h44z/wg-portal/blob/master/deploy/helm/files/dashboard.json) into your Grafana instance.
![Dashboard](../../assets/images/dashboard.png)

View File

@@ -1,29 +1 @@
**WireGuard Portal** is a simple, web based configuration portal for [WireGuard](https://wireguard.com).
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
connections.
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
## Features
* Self-hosted - the whole application is a single binary
* Responsive web UI written in Vue.JS
* Automatically select IP from the network pool assigned to client
* QR-Code for convenient mobile client configuration
* Sent email to client with QR-code and client config
* Enable / Disable clients seamlessly
* Generation of wg-quick configuration file (`wgX.conf`) if required
* User authentication (database, OAuth or LDAP)
* IPv6 ready
* Docker ready
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* Peer Expiry Feature
* Handle route and DNS settings like wg-quick does
* REST API for management and client deployment
## Quick-Start
The easiest way to get started is to use the provided [Docker image](./getting-started/docker.md).
--8<-- "README.md:20:47"

View File

@@ -18,7 +18,7 @@ You can also specify the database type using the parameter **-migrateFromType**,
For example:
```shell
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom=user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom='user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local'
```
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yml** configuration file.

View File

@@ -8,26 +8,28 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@kyvg/vue3-notification": "^3.1.3",
"@fontsource/nunito-sans": "^5.1.1",
"@fortawesome/fontawesome-free": "^6.7.2",
"@kyvg/vue3-notification": "^3.4.1",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2",
"bootswatch": "^5.3.2",
"flag-icons": "^7.1.0",
"ip-address": "^9.0.5",
"is-cidr": "^5.0.3",
"bootstrap": "^5.3.3",
"bootswatch": "^5.3.3",
"flag-icons": "^7.3.2",
"ip-address": "^10.0.1",
"is-cidr": "^5.1.0",
"is-ip": "^5.0.1",
"pinia": "^2.1.7",
"pinia": "^2.3.1",
"prismjs": "^1.29.0",
"vue": "^3.3.13",
"vue-i18n": "^9.14.2",
"vue": "^3.5.13",
"vue-i18n": "^11.0.1",
"vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.2.5",
"vue-router": "^4.5.0",
"vue3-tags-input": "^1.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"vite": "^5.0.10"
"@vitejs/plugin-vue": "^5.2.1",
"sass-embedded": "^1.83.4",
"vite": "^5.4.12"
}
},
"node_modules/@babel/helper-string-parser": {
@@ -76,6 +78,13 @@
"node": ">=6.9.0"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz",
"integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==",
"dev": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -467,23 +476,29 @@
"node": ">=12"
}
},
"node_modules/@fontsource/nunito-sans": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@fontsource/nunito-sans/-/nunito-sans-5.1.1.tgz",
"integrity": "sha512-84sV7nRYKFlzoY6FeLBAf1FF6+MebDXklVz28Phuh4L52t2juhjRmLXweehNN9pjgdvM0gXCe/kYsgI8WVELUQ==",
"license": "OFL-1.1"
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz",
"integrity": "sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA==",
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"engines": {
"node": ">=6"
}
},
"node_modules/@intlify/core-base": {
"version": "9.14.2",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.2.tgz",
"integrity": "sha512-DZyQ4Hk22sC81MP4qiCDuU+LdaYW91A6lCjq8AWPvY3+mGMzhGDfOCzvyR6YBQxtlPjFqMoFk9ylnNYRAQwXtQ==",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.0.1.tgz",
"integrity": "sha512-NAmhw1l/llM0HZRpagR/ChJTNymW4ll6/4EDSJML5c8L5Hl/+k6UyF8EIgE6DeHpfheQujkSRngauViHqq6jJQ==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "9.14.2",
"@intlify/shared": "9.14.2"
"@intlify/message-compiler": "11.0.1",
"@intlify/shared": "11.0.1"
},
"engines": {
"node": ">= 16"
@@ -493,12 +508,12 @@
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.14.2",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.2.tgz",
"integrity": "sha512-YsKKuV4Qv4wrLNsvgWbTf0E40uRv+Qiw1BeLQ0LAxifQuhiMe+hfTIzOMdWj/ZpnTDj4RSZtkXjJM7JDiiB5LQ==",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.0.1.tgz",
"integrity": "sha512-5RFH8x+Mn3mbjcHXnb6KCXGiczBdiQkWkv99iiA0JpKrNuTAQeW59Pjq/uObMB0eR0shnKYGTkIJxum+DbL3sw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "9.14.2",
"@intlify/shared": "11.0.1",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -509,9 +524,9 @@
}
},
"node_modules/@intlify/shared": {
"version": "9.14.2",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.2.tgz",
"integrity": "sha512-uRAHAxYPeF+G5DBIboKpPgC/Waecd4Jz8ihtkpJQD5ycb5PwXp0k/+hBGl5dAjwF7w+l74kz/PKA8r8OK//RUw==",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.0.1.tgz",
"integrity": "sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
@@ -805,16 +820,16 @@
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
"integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.0.0"
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^4.0.0 || ^5.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vue": "^3.2.25"
}
},
@@ -949,6 +964,13 @@
"integrity": "sha512-cJLhobnZsVCelU7zdH/L7wpcXAyUoTX4/5l2dWQ0JXgaVK80BdTQNU/ImWwoyIGBeyms4iQDAdNtOfPQZf0Atg==",
"license": "MIT"
},
"node_modules/buffer-builder": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
"dev": true,
"license": "MIT/X11"
},
"node_modules/cidr-regex": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-4.1.1.tgz",
@@ -985,6 +1007,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/colorjs.io": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-hrtime": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
@@ -1061,9 +1090,9 @@
"license": "MIT"
},
"node_modules/flag-icons": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.2.3.tgz",
"integrity": "sha512-X2gUdteNuqdNqob2KKTJTS+ZCvyWeLCtDz9Ty8uJP17Y4o82Y+U/Vd4JNrdwTAjagYsRznOn9DZ+E/Q52qbmqg==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.3.2.tgz",
"integrity": "sha512-QkaZ6Zvai8LIjx+UNAHUJ5Dhz9OLZpBDwCRWxF6YErxIcR16jTkIFm3bFu54EkvKQy4+wicW+Gm7/0631wVQyQ==",
"license": "MIT"
},
"node_modules/fsevents": {
@@ -1093,15 +1122,28 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
"engines": {
"node": ">=8"
}
},
"node_modules/immutable": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
"dev": true,
"license": "MIT"
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
@@ -1158,12 +1200,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.14",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
@@ -1198,9 +1234,9 @@
"license": "ISC"
},
"node_modules/pinia": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.6.tgz",
"integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
@@ -1210,14 +1246,10 @@
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.5.11"
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
@@ -1324,6 +1356,401 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/sass-embedded": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.4.tgz",
"integrity": "sha512-Hf2burRA/y5PGxsg6jB9UpoK/xZ6g/pgrkOcdl6j+rRg1Zj8XhGKZ1MTysZGtTPUUmiiErqzkP5+Kzp95yv9GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"buffer-builder": "^0.2.0",
"colorjs.io": "^0.5.0",
"immutable": "^5.0.2",
"rxjs": "^7.4.0",
"supports-color": "^8.1.1",
"sync-child-process": "^1.0.2",
"varint": "^6.0.0"
},
"bin": {
"sass": "dist/bin/sass.js"
},
"engines": {
"node": ">=16.0.0"
},
"optionalDependencies": {
"sass-embedded-android-arm": "1.83.4",
"sass-embedded-android-arm64": "1.83.4",
"sass-embedded-android-ia32": "1.83.4",
"sass-embedded-android-riscv64": "1.83.4",
"sass-embedded-android-x64": "1.83.4",
"sass-embedded-darwin-arm64": "1.83.4",
"sass-embedded-darwin-x64": "1.83.4",
"sass-embedded-linux-arm": "1.83.4",
"sass-embedded-linux-arm64": "1.83.4",
"sass-embedded-linux-ia32": "1.83.4",
"sass-embedded-linux-musl-arm": "1.83.4",
"sass-embedded-linux-musl-arm64": "1.83.4",
"sass-embedded-linux-musl-ia32": "1.83.4",
"sass-embedded-linux-musl-riscv64": "1.83.4",
"sass-embedded-linux-musl-x64": "1.83.4",
"sass-embedded-linux-riscv64": "1.83.4",
"sass-embedded-linux-x64": "1.83.4",
"sass-embedded-win32-arm64": "1.83.4",
"sass-embedded-win32-ia32": "1.83.4",
"sass-embedded-win32-x64": "1.83.4"
}
},
"node_modules/sass-embedded-android-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.83.4.tgz",
"integrity": "sha512-9Z4pJAOgEkXa3VDY/o+U6l5XvV0mZTJcSl0l/mSPHihjAHSpLYnOW6+KOWeM8dxqrsqTYcd6COzhanI/a++5Gw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.83.4.tgz",
"integrity": "sha512-tgX4FzmbVqnQmD67ZxQDvI+qFNABrboOQgwsG05E5bA/US42zGajW9AxpECJYiMXVOHmg+d81ICbjb0fsVHskw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.83.4.tgz",
"integrity": "sha512-RsFOziFqPcfZXdFRULC4Ayzy9aK6R6FwQ411broCjlOBX+b0gurjRadkue3cfUEUR5mmy0KeCbp7zVKPLTK+5Q==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.83.4.tgz",
"integrity": "sha512-EHwh0nmQarBBrMRU928eTZkFGx19k/XW2YwbPR4gBVdWLkbTgCA5aGe8hTE6/1zStyx++3nDGvTZ78+b/VvvLg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.83.4.tgz",
"integrity": "sha512-0PgQNuPWYy1jEOEPDVsV89KfqOsMLIp9CSbjBY7jRcwRhyVAcigqrUG6bDeNtojHUYKA1kU+Eh/85WxOHUOgBw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.83.4.tgz",
"integrity": "sha512-rp2ywymWc3nymnSnAFG5R/8hvxWCsuhK3wOnD10IDlmNB7o4rzKby1c+2ZfpQGowlYGWsWWTgz8FW2qzmZsQRw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.83.4.tgz",
"integrity": "sha512-kLkN2lXz9PCgGfDS8Ev5YVcl/V2173L6379en/CaFuJJi7WiyPgBymW7hOmfCt4uO4R1y7CP2Uc08DRtZsBlAA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.83.4.tgz",
"integrity": "sha512-nL90ryxX2lNmFucr9jYUyHHx21AoAgdCL1O5Ltx2rKg2xTdytAGHYo2MT5S0LIeKLa/yKP/hjuSvrbICYNDvtA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.83.4.tgz",
"integrity": "sha512-E0zjsZX2HgESwyqw31EHtI39DKa7RgK7nvIhIRco1d0QEw227WnoR9pjH3M/ZQy4gQj3GKilOFHM5Krs/omeIA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.83.4.tgz",
"integrity": "sha512-ew5HpchSzgAYbQoriRh8QhlWn5Kw2nQ2jHoV9YLwGKe3fwwOWA0KDedssvDv7FWnY/FCqXyymhLd6Bxae4Xquw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.83.4.tgz",
"integrity": "sha512-0RrJRwMrmm+gG0VOB5b5Cjs7Sd+lhqpQJa6EJNEaZHljJokEfpE5GejZsGMRMIQLxEvVphZnnxl6sonCGFE/QQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.83.4.tgz",
"integrity": "sha512-IzMgalf6MZOxgp4AVCgsaWAFDP/IVWOrgVXxkyhw29fyAEoSWBJH4k87wyPhEtxSuzVHLxKNbc8k3UzdWmlBFg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.83.4.tgz",
"integrity": "sha512-LLb4lYbcxPzX4UaJymYXC+WwokxUlfTJEFUv5VF0OTuSsHAGNRs/rslPtzVBTvMeG9TtlOQDhku1F7G6iaDotA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.83.4.tgz",
"integrity": "sha512-zoKlPzD5Z13HKin1UGR74QkEy+kZEk2AkGX5RelRG494mi+IWwRuWCppXIovor9+BQb9eDWPYPoMVahwN5F7VA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.83.4.tgz",
"integrity": "sha512-hB8+/PYhfEf2zTIcidO5Bpof9trK6WJjZ4T8g2MrxQh8REVtdPcgIkoxczRynqybf9+fbqbUwzXtiUao2GV+vQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.83.4.tgz",
"integrity": "sha512-83fL4n+oeDJ0Y4KjASmZ9jHS1Vl9ESVQYHMhJE0i4xDi/P3BNarm2rsKljq/QtrwGpbqwn8ujzOu7DsNCMDSHA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.83.4.tgz",
"integrity": "sha512-NlnGdvCmTD5PK+LKXlK3sAuxOgbRIEoZfnHvxd157imCm/s2SYF/R28D0DAAjEViyI8DovIWghgbcqwuertXsA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.83.4.tgz",
"integrity": "sha512-J2BFKrEaeSrVazU2qTjyQdAk+MvbzJeTuCET0uAJEXSKtvQ3AzxvzndS7LqkDPbF32eXAHLw8GVpwcBwKbB3Uw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.83.4.tgz",
"integrity": "sha512-uPAe9T/5sANFhJS5dcfAOhOJy8/l2TRYG4r+UO3Wp4yhqbN7bggPvY9c7zMYS0OC8tU/bCvfYUDFHYMCl91FgA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.83.4.tgz",
"integrity": "sha512-C9fkDY0jKITdJFij4UbfPFswxoXN9O/Dr79v17fJnstVwtUojzVJWKHUXvF0Zg2LIR7TCc4ju3adejKFxj7ueA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1333,12 +1760,6 @@
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/super-regex": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz",
@@ -1356,6 +1777,45 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/sync-child-process": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"sync-message-port": "^1.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/sync-message-port": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/time-span": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
@@ -1371,10 +1831,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1453,13 +1927,13 @@
}
},
"node_modules/vue-i18n": {
"version": "9.14.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.2.tgz",
"integrity": "sha512-JK9Pm80OqssGJU2Y6F7DcM8RFHqVG4WkuCqOZTVsXkEzZME7ABejAUqUdA931zEBedc4thBgSUWxeQh4uocJAQ==",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.0.1.tgz",
"integrity": "sha512-pWAT8CusK8q9/EpN7V3oxwHwxWm6+Kp2PeTZmRGvdZTkUzMQDpbbmHp0TwQ8xw04XKm23cr6B4GL72y3W8Yekg==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "9.14.2",
"@intlify/shared": "9.14.2",
"@intlify/core-base": "11.0.1",
"@intlify/shared": "11.0.1",
"@vue/devtools-api": "^6.5.0"
},
"engines": {

View File

@@ -8,25 +8,27 @@
"preview": "vite preview --port 5050"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@kyvg/vue3-notification": "^3.1.3",
"@fontsource/nunito-sans": "^5.1.1",
"@fortawesome/fontawesome-free": "^6.7.2",
"@kyvg/vue3-notification": "^3.4.1",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2",
"bootswatch": "^5.3.2",
"flag-icons": "^7.1.0",
"ip-address": "^9.0.5",
"is-cidr": "^5.0.3",
"bootstrap": "^5.3.3",
"bootswatch": "^5.3.3",
"flag-icons": "^7.3.2",
"ip-address": "^10.0.1",
"is-cidr": "^5.1.0",
"is-ip": "^5.0.1",
"pinia": "^2.1.7",
"pinia": "^2.3.1",
"prismjs": "^1.29.0",
"vue": "^3.3.13",
"vue-i18n": "^9.14.2",
"vue": "^3.5.13",
"vue-i18n": "^11.0.1",
"vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.2.5",
"vue-router": "^4.5.0",
"vue3-tags-input": "^1.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"vite": "^5.0.10"
"@vitejs/plugin-vue": "^5.2.1",
"sass-embedded": "^1.83.4",
"vite": "^5.4.12"
}
}

View File

@@ -0,0 +1,16 @@
// disable external web fonts
$web-font-path: false;
@import "bootswatch/dist/lux/variables";
@import "bootstrap/scss/bootstrap";
@import "bootswatch/dist/lux/bootswatch";
// for future use, once bootswatch supports @use
/*
@use "bootswatch/dist/lux/_variables.scss" as lux-vars with (
$web-font-path: false
);
@use "bootstrap/scss/bootstrap" as bs;
@use "bootswatch/dist/lux/_bootswatch.scss" as lux-theme;
*/

View File

@@ -0,0 +1,294 @@
<script setup>
import Modal from "./Modal.vue";
import { peerStore } from "@/stores/peers";
import { computed, ref, watch } from "vue";
import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification";
import { freshPeer, freshInterface } from '@/helpers/models';
import { profileStore } from "@/stores/profile";
const { t } = useI18n()
const peers = peerStore()
const profile = profileStore()
const props = defineProps({
peerId: String,
visible: Boolean,
})
const emit = defineEmits(['close'])
const selectedPeer = computed(() => {
let p = peers.Find(props.peerId)
if (!p) {
if (!!props.peerId || props.peerId.length) {
p = profile.peers.find((p) => p.Identifier === props.peerId)
} else {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
}
}
return p
})
const selectedInterface = computed(() => {
let iId = profile.selectedInterfaceId;
let i = freshInterface() // dummy interface to avoid 'undefined' exceptions
if (iId) {
i = profile.interfaces.find((i) => i.Identifier === iId)
}
return i
})
const title = computed(() => {
if (!props.visible) {
return ""
}
if (selectedPeer.value) {
return t("modals.peer-edit.headline-edit-peer") + " " + selectedPeer.value.Identifier
}
return t("modals.peer-edit.headline-new-peer")
})
const formData = ref(freshPeer())
// functions
watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown
if (!selectedPeer.value) {
await peers.PreparePeer(selectedInterface.value.Identifier)
formData.value.Identifier = peers.Prepared.Identifier
formData.value.DisplayName = peers.Prepared.DisplayName
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
formData.value.Disabled = peers.Prepared.Disabled
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
formData.value.Notes = peers.Prepared.Notes
formData.value.Endpoint = peers.Prepared.Endpoint
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
formData.value.PresharedKey = peers.Prepared.PresharedKey
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
formData.value.PrivateKey = peers.Prepared.PrivateKey
formData.value.PublicKey = peers.Prepared.PublicKey
formData.value.Mode = peers.Prepared.Mode
formData.value.Addresses = peers.Prepared.Addresses
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
formData.value.Dns = peers.Prepared.Dns
formData.value.DnsSearch = peers.Prepared.DnsSearch
formData.value.Mtu = peers.Prepared.Mtu
formData.value.FirewallMark = peers.Prepared.FirewallMark
formData.value.RoutingTable = peers.Prepared.RoutingTable
formData.value.PreUp = peers.Prepared.PreUp
formData.value.PostUp = peers.Prepared.PostUp
formData.value.PreDown = peers.Prepared.PreDown
formData.value.PostDown = peers.Prepared.PostDown
} else { // fill existing data
formData.value.Identifier = selectedPeer.value.Identifier
formData.value.DisplayName = selectedPeer.value.DisplayName
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
formData.value.Disabled = selectedPeer.value.Disabled
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
formData.value.Notes = selectedPeer.value.Notes
formData.value.Endpoint = selectedPeer.value.Endpoint
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
formData.value.PresharedKey = selectedPeer.value.PresharedKey
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
formData.value.PrivateKey = selectedPeer.value.PrivateKey
formData.value.PublicKey = selectedPeer.value.PublicKey
formData.value.Mode = selectedPeer.value.Mode
formData.value.Addresses = selectedPeer.value.Addresses
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
formData.value.Dns = selectedPeer.value.Dns
formData.value.DnsSearch = selectedPeer.value.DnsSearch
formData.value.Mtu = selectedPeer.value.Mtu
formData.value.FirewallMark = selectedPeer.value.FirewallMark
formData.value.RoutingTable = selectedPeer.value.RoutingTable
formData.value.PreUp = selectedPeer.value.PreUp
formData.value.PostUp = selectedPeer.value.PostUp
formData.value.PreDown = selectedPeer.value.PreDown
formData.value.PostDown = selectedPeer.value.PostDown
if (!formData.value.Endpoint.Overridable ||
!formData.value.EndpointPublicKey.Overridable ||
!formData.value.AllowedIPs.Overridable ||
!formData.value.PersistentKeepalive.Overridable ||
!formData.value.Dns.Overridable ||
!formData.value.DnsSearch.Overridable ||
!formData.value.Mtu.Overridable ||
!formData.value.FirewallMark.Overridable ||
!formData.value.RoutingTable.Overridable ||
!formData.value.PreUp.Overridable ||
!formData.value.PostUp.Overridable ||
!formData.value.PreDown.Overridable ||
!formData.value.PostDown.Overridable) {
formData.value.IgnoreGlobalSettings = true
}
}
}
}
)
watch(() => formData.value.Disabled, async (newValue, oldValue) => {
if (oldValue && !newValue && formData.value.ExpiresAt) {
formData.value.ExpiresAt = "" // reset expiry date
}
}
)
function close() {
formData.value = freshPeer()
emit('close')
}
async function save() {
try {
if (props.peerId !== '#NEW#') {
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
} else {
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
}
close()
} catch (e) {
// console.log(e)
notify({
title: "Failed to save peer!",
text: e.toString(),
type: 'error',
})
}
}
async function del() {
try {
await peers.DeletePeer(selectedPeer.value.Identifier)
close()
} catch (e) {
// console.log(e)
notify({
title: "Failed to delete peer!",
text: e.toString(),
type: 'error',
})
}
}
</script>
<template>
<Modal :title="title" :visible="visible" @close="close">
<template #default>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')"
v-model="formData.DisplayName">
</div>
</fieldset>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
v-model="formData.PrivateKey">
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
v-model="formData.PublicKey">
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
v-model="formData.PresharedKey">
</div>
</fieldset>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label>
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')"
v-model="formData.PersistentKeepalive.Value">
</div>
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label>
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')"
v-model="formData.Mtu.Value">
</div>
</div>
</fieldset>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label>
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label>
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label>
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label>
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
</div>
</fieldset>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-state') }}</legend>
<div class="row">
<div class="form-group col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
<label class="form-check-label">{{ $t('modals.peer-edit.disabled.label') }}</label>
</div>
</div>
<div class="form-group col-md-6">
<label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label>
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01"
v-model="formData.ExpiresAt">
</div>
</div>
</fieldset>
</template>
<template #footer>
<div class="flex-fill text-start">
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{
$t('general.delete') }}</button>
</div>
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template>
</Modal>
</template>
<style></style>

View File

@@ -354,7 +354,7 @@
"endpoint": {
"label": "Endpoint Address",
"placeholder": "Endpoint Address",
"description": "The endpoint address that peers will connect to."
"description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
},
"networks": {
"label": "IP Networks",

View File

@@ -355,7 +355,7 @@
"endpoint": {
"label": "Endpoint Address",
"placeholder": "Endpoint Address",
"description": "The endpoint address that peers will connect to."
"description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
},
"networks": {
"label": "IP Networks",

View File

@@ -9,13 +9,14 @@ import i18n from "./lang";
import Notifications from '@kyvg/vue3-notification'
// Bootstrap (and theme)
//import "bootstrap/dist/css/bootstrap.min.css"
import "bootswatch/dist/lux/bootstrap.min.css";
import "@/assets/custom.scss";
import "bootstrap";
import "./assets/base.css";
// Fontawesome
// Fonts
import "@fortawesome/fontawesome-free/js/all.js"
import "@fontsource/nunito-sans/400.css";
import "@fontsource/nunito-sans/600.css";
// Flags
import "flag-icons/css/flag-icons.min.css"

View File

@@ -12,6 +12,8 @@ export const profileStore = defineStore({
id: 'profile',
state: () => ({
peers: [],
interfaces: [],
selectedInterfaceId: "",
stats: {},
statsEnabled: false,
user: {},
@@ -71,6 +73,7 @@ export const profileStore = defineStore({
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
},
hasStatistics: (state) => state.statsEnabled,
CountInterfaces: (state) => state.interfaces.length,
},
actions: {
afterPageSizeChange() {
@@ -116,6 +119,11 @@ export const profileStore = defineStore({
this.stats = statsResponse.Stats
this.statsEnabled = statsResponse.Enabled
},
setInterfaces(interfaces) {
this.interfaces = interfaces
this.selectedInterfaceId = interfaces.length > 0 ? interfaces[0].Identifier : ""
this.fetching = false
},
async enableApi() {
this.fetching = true
let currentUser = authStore().user.Identifier
@@ -186,5 +194,19 @@ export const profileStore = defineStore({
})
})
},
async LoadInterfaces() {
this.fetching = true
let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/interfaces`)
.then(this.setInterfaces)
.catch(error => {
this.setInterfaces([])
console.log("Failed to load interfaces for ", currentUser, ": ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load interfaces!",
})
})
},
}
})

View File

@@ -3,7 +3,7 @@ import PeerViewModal from "../components/PeerViewModal.vue";
import { onMounted, ref } from "vue";
import { profileStore } from "@/stores/profile";
import PeerEditModal from "@/components/PeerEditModal.vue";
import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
import { settingsStore } from "@/stores/settings";
import { humanFileSize } from "@/helpers/utils";
@@ -27,10 +27,18 @@ function sortBy(key) {
profile.sortOrder = sortOrder.value;
}
function friendlyInterfaceName(id, name) {
if (name) {
return name
}
return id
}
onMounted(async () => {
await profile.LoadUser()
await profile.LoadPeers()
await profile.LoadStats()
await profile.LoadInterfaces()
await profile.calculatePages(); // Forces to show initial page number
})
@@ -38,7 +46,7 @@ onMounted(async () => {
<template>
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId !== ''" @close="viewedPeerId = ''"></PeerViewModal>
<PeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''"></PeerEditModal>
<UserPeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''; profile.LoadPeers()"></UserPeerEditModal>
<!-- Peer list -->
<div class="mt-4 row">
@@ -56,9 +64,17 @@ onMounted(async () => {
</div>
</div>
<div class="col-12 col-lg-3 text-lg-end">
<a v-if="settings.Setting('SelfProvisioning')" class="btn btn-primary ms-2" href="#"
:title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'"><i
class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
<div class="form-group" v-if="settings.Setting('SelfProvisioning')">
<div class="input-group mb-3">
<button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'">
<i class="fa fa-plus me-1"></i><i class="fa fa-user"></i>
</button>
<select v-model="profile.selectedInterfaceId" :disabled="profile.CountInterfaces===0" class="form-select">
<option v-if="profile.CountInterfaces===0" value="nothing">{{ $t('interfaces.no-interface.default-selection') }}</option>
<option v-for="iface in profile.interfaces" :key="iface.Identifier" :value="iface.Identifier">{{ friendlyInterfaceName(iface.Identifier,iface.DisplayName) }}</option>
</select>
</div>
</div>
</div>
</div>
<div class="mt-2 table-responsive">

View File

@@ -24,8 +24,8 @@ func (e peerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
apiGroup.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
apiGroup.GET("/iface/:iface/stats", e.authenticator.LoggedIn(ScopeAdmin), e.handleStatsGet())
apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(ScopeAdmin), e.handlePrepareGet())
apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(), e.handlePrepareGet())
apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(), e.handleCreatePost())
apiGroup.POST("/iface/:iface/multiplenew", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreateMultiplePost())
apiGroup.GET("/config-qr/:id", e.handleQrCodeGet())
apiGroup.POST("/config-mail", e.handleEmailPost())

View File

@@ -28,6 +28,7 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
apiGroup.GET("/:id/interfaces", e.authenticator.UserIdMatch("id"), e.handleInterfacesGet())
apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
}
@@ -170,6 +171,7 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
// @ID users_handlePeersGet
// @Tags Users
// @Summary Get peers for the given user.
// @Param id path string true "The user identifier"
// @Produce json
// @Success 200 {object} []model.Peer
// @Failure 400 {object} model.Error
@@ -179,14 +181,14 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
interfaceId := Base64UrlDecode(c.Param("id"))
if interfaceId == "" {
userId := Base64UrlDecode(c.Param("id"))
if userId == "" {
c.JSON(http.StatusBadRequest,
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
return
}
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(userId))
if err != nil {
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
@@ -202,6 +204,7 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
// @ID users_handleStatsGet
// @Tags Users
// @Summary Get peer stats for the given user.
// @Param id path string true "The user identifier"
// @Produce json
// @Success 200 {object} model.PeerStats
// @Failure 400 {object} model.Error
@@ -229,6 +232,39 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
}
}
// handleInterfacesGet returns a gorm handler function.
//
// @ID users_handleInterfacesGet
// @Tags Users
// @Summary Get interfaces for the given user. Returns an empty list if self provisioning is disabled.
// @Param id path string true "The user identifier"
// @Produce json
// @Success 200 {object} []model.Interface
// @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error
// @Router /user/{id}/interfaces [get]
func (e userEndpoint) handleInterfacesGet() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
userId := Base64UrlDecode(c.Param("id"))
if userId == "" {
c.JSON(http.StatusBadRequest,
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
return
}
peers, err := e.app.GetUserInterfaces(ctx, domain.UserIdentifier(userId))
if err != nil {
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
c.JSON(http.StatusOK, model.NewInterfaces(peers, nil))
}
}
// handleDelete returns a gorm handler function.
//
// @ID users_handleDelete

View File

@@ -109,8 +109,12 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
results := make([]Interface, len(src))
for i := range src {
if srcPeers == nil {
results[i] = *NewInterface(&src[i], nil)
} else {
results[i] = *NewInterface(&src[i], srcPeers[i])
}
}
return results
}

View File

@@ -39,6 +39,7 @@ type WireGuardManager interface {
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error)
PrepareInterface(ctx context.Context) (*domain.Interface, error)
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)

View File

@@ -68,6 +68,34 @@ func (m Manager) GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interfa
return interfaces, allPeers, nil
}
// GetUserInterfaces returns all interfaces that are available for users to create new peers.
// If self-provisioning is disabled, this function will return an empty list.
func (m Manager) GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error) {
if !m.cfg.Core.SelfProvisioningAllowed {
return nil, nil // self-provisioning is disabled - no interfaces for users
}
interfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil {
return nil, fmt.Errorf("unable to load all interfaces: %w", err)
}
// strip sensitive data, users only need very limited information
userInterfaces := make([]domain.Interface, 0, len(interfaces))
for _, iface := range interfaces {
if iface.IsDisabled() {
continue // skip disabled interfaces
}
if iface.Type != domain.InterfaceTypeServer {
continue // skip client interfaces
}
userInterfaces = append(userInterfaces, iface.PublicInfo())
}
return userInterfaces, nil
}
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return 0, err
@@ -456,6 +484,10 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
*domain.Interface,
error,
) {
if err := iface.Validate(); err != nil {
return nil, fmt.Errorf("interface validation failed: %w", err)
}
stateChanged := m.hasInterfaceStateChanged(ctx, iface)
if err := m.handleInterfacePreSaveHooks(stateChanged, iface); err != nil {

View File

@@ -62,8 +62,10 @@ func (m Manager) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]
}
func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) {
if !m.cfg.Core.SelfProvisioningAllowed {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err // TODO: self provisioning?
return nil, err
}
}
currentUser := domain.GetUserInfo(ctx)
@@ -73,6 +75,10 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
return nil, fmt.Errorf("unable to find interface %s: %w", id, err)
}
if m.cfg.Core.SelfProvisioningAllowed && iface.Type != domain.InterfaceTypeServer {
return nil, fmt.Errorf("self provisioning is only allowed for server interfaces: %w", domain.ErrNoPermission)
}
ips, err := m.getFreshPeerIpConfig(ctx, iface)
if err != nil {
return nil, fmt.Errorf("unable to get fresh ip addresses: %w", err)
@@ -149,9 +155,17 @@ func (m Manager) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain
}
func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
if !m.cfg.Core.SelfProvisioningAllowed {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
} else {
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
return nil, err
}
}
sessionUser := domain.GetUserInfo(ctx)
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
@@ -161,6 +175,18 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
}
// if a peer is self provisioned, ensure that only allowed fields are set from the request
if !sessionUser.IsAdmin {
preparedPeer, err := m.PreparePeer(ctx, peer.InterfaceIdentifier)
if err != nil {
return nil, fmt.Errorf("failed to prepare peer for interface %s: %w", peer.InterfaceIdentifier, err)
}
preparedPeer.OverwriteUserEditableFields(peer)
peer = preparedPeer
}
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
return nil, fmt.Errorf("creation not allowed: %w", err)
}
@@ -229,6 +255,19 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("update not allowed: %w", err)
}
sessionUser := domain.GetUserInfo(ctx)
// if a peer is self provisioned, ensure that only allowed fields are set from the request
if !sessionUser.IsAdmin {
originalPeer, err := m.db.GetPeer(ctx, peer.Identifier)
if err != nil {
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
}
originalPeer.OverwriteUserEditableFields(peer)
peer = originalPeer
}
// handle peer identifier change (new public key)
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
@@ -438,7 +477,7 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin {
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission
}
@@ -452,7 +491,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
}
if !currentUser.IsAdmin {
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission
}
@@ -467,7 +506,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin {
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission
}

View File

@@ -3,6 +3,7 @@ package domain
import (
"fmt"
"math"
"net"
"regexp"
"strconv"
"strings"
@@ -71,8 +72,35 @@ type Interface struct {
PeerDefPostDown string // default action that is executed after the device is down
}
func (i *Interface) IsValid() bool {
return true // TODO: implement check
// PublicInfo returns a copy of the interface with only the public information.
// Sensible information like keys are not included.
func (i *Interface) PublicInfo() Interface {
return Interface{
Identifier: i.Identifier,
DisplayName: i.DisplayName,
Type: i.Type,
Disabled: i.Disabled,
}
}
// Validate performs checks to ensure that the interface is valid.
func (i *Interface) Validate() error {
// validate peer default endpoint, add port if needed
if i.PeerDefEndpoint != "" {
host, port, err := net.SplitHostPort(i.PeerDefEndpoint)
switch {
case err != nil && !strings.Contains(err.Error(), "missing port in address"):
return fmt.Errorf("invalid default endpoint: %w", err)
case err != nil && strings.Contains(err.Error(), "missing port in address"):
// In this case, the entire string is the host, and there's no port.
host = i.PeerDefEndpoint
port = strconv.Itoa(i.ListenPort)
}
i.PeerDefEndpoint = net.JoinHostPort(host, port)
}
return nil
}
func (i *Interface) IsDisabled() bool {

View File

@@ -127,6 +127,19 @@ func (p *Peer) GenerateDisplayName(prefix string) {
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
}
// OverwriteUserEditableFields overwrites the user editable fields of the peer with the values from the userPeer
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer) {
p.DisplayName = userPeer.DisplayName
p.Interface.PublicKey = userPeer.Interface.PublicKey
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
p.Interface.Mtu = userPeer.Interface.Mtu
p.PersistentKeepalive = userPeer.PersistentKeepalive
p.ExpiresAt = userPeer.ExpiresAt
p.Disabled = userPeer.Disabled
p.DisabledReason = userPeer.DisabledReason
p.PresharedKey = userPeer.PresharedKey
}
type PeerInterfaceConfig struct {
KeyPair // private/public Key of the peer

View File

@@ -19,6 +19,7 @@ theme:
favicon: assets/images/favicon-large.png
language: en
features:
- content.code.copy
- navigation.instant
- navigation.tabs
- navigation.expand
@@ -46,6 +47,7 @@ markdown_extensions:
- admonition
- meta
- pymdownx.details
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
@@ -59,10 +61,13 @@ nav:
- Documentation:
- Overview: documentation/overview.md
- Getting Started:
- Building: documentation/getting-started/building.md
- Docker Container: documentation/getting-started/docker.md
- Upgrade from V1: documentation/getting-started/upgrade.md
- Binaries: documentation/getting-started/binaries.md
- Docker: documentation/getting-started/docker.md
- Helm: documentation/getting-started/helm.md
- Sources: documentation/getting-started/sources.md
- Configuration:
- Overview: documentation/configuration/overview.md
- Examples: documentation/configuration/examples.md
- Upgrade: documentation/upgrade/v1.md
- Monitoring: documentation/monitoring/prometheus.md
- REST API: documentation/rest-api/api-doc.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB