more cleanup

This commit is contained in:
Christoph Haas 2023-07-31 17:37:08 +02:00
parent 4b3225e293
commit 93f8633b2b
14 changed files with 365 additions and 182 deletions

View File

@ -42,12 +42,9 @@ COPY --from=builder /etc/group /etc/group
# Copy binaries
COPY --from=builder /build/dist/wg-portal /app/wg-portal
COPY --from=builder /build/dist/hc /app/hc
# Set the Current Working Directory inside the container
WORKDIR /app
# Command to run the executable
CMD [ "/app/wg-portal" ]
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ]
CMD [ "/app/wg-portal" ]

222
README.md
View File

@ -1,4 +1,4 @@
# WireGuard Portal
# WireGuard Portal (V2 - alpha testing)
[![Build Status](https://travis-ci.com/h44z/wg-portal.svg?token=q4pSqaqT58Jzpxdx62xk&branch=master)](https://travis-ci.com/h44z/wg-portal)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
@ -8,6 +8,9 @@
![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/h44z/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 [1.0.17](https://github.com/h44z/wg-portal/releases) instead.
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
@ -15,25 +18,27 @@ 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 and web based
* 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 `wgX.conf` if required
* Generation of wg-quick configuration file (`wgX.conf`) if required
* User authentication (database, OAuth or LDAP)
* IPv6 ready
* User authentication (database, OAuth or LDAP)
* Dockerized
* Responsive web UI written in Vue.JS
* One single binary
* Docker ready
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* Peer Expiry Feature
* REST API for management and client deployment (coming soon)
* Handle route and DNS settings like wg-quick does
* ~~REST API for management and client deployment~~ (coming soon)
![Screenshot](screenshot.png)
## Configuration
You can configure WireGuard Portal using a yaml configuration file.
The filepath of the yaml configuration file defaults to **config.yml** in the working directory of the executable.
@ -55,94 +60,129 @@ The following configuration options are available:
| log_level | advanced | warn | 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. |
| ldap_sync_interval | advanced | 15m | |
| start_listen_port | advanced | 51820 | |
| start_cidr_v4 | advanced | 10.11.12.0/24 | |
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | |
| use_ip_v6 | advanced | true | |
| config_storage_path | advanced | | |
| expiry_check_interval | advanced | 15m | |
| rule_prio_offset | advanced | 20000 | |
| route_table_offset | advanced | 20000 | |
| use_ping_checks | statistics | true | |
| ping_check_workers | statistics | 10 | |
| ping_unprivileged | statistics | false | |
| ping_check_interval | statistics | 1m | |
| data_collection_interval | statistics | 10m | |
| collect_interface_data | statistics | true | |
| collect_peer_data | statistics | true | |
| collect_audit_data | statistics | true | |
| host | mail | 127.0.0.1 | |
| port | mail | 25 | |
| encryption | mail | none | |
| cert_validation | mail | false | |
| username | mail | | |
| password | mail | | |
| auth_type | mail | plain | |
| from | mail | Wireguard Portal <noreply@wireguard.local> | |
| link_only | mail | false | |
| callback_url_prefix | auth | /api/v0 | |
| oidc | auth | Empty Array - no providers configured | |
| oauth | auth | Empty Array - no providers configured | |
| ldap | auth | Empty Array - no providers configured | |
| provider_name | auth/oidc | | |
| display_name | auth/oidc | | |
| base_url | auth/oidc | | |
| client_id | auth/oidc | | |
| client_secret | auth/oidc | | |
| extra_scopes | auth/oidc | | |
| field_map | auth/oidc | | |
| registration_enabled | auth/oidc | | |
| provider_name | auth/oidc | | |
| display_name | auth/oauth | | |
| base_url | auth/oauth | | |
| client_id | auth/oauth | | |
| client_secret | auth/oauth | | |
| auth_url | auth/oauth | | |
| token_url | auth/oauth | | |
| redirect_url | auth/oauth | | |
| user_info_url | auth/oauth | | |
| scopes | auth/oauth | | |
| field_map | auth/oauth | | |
| registration_enabled | auth/oauth | | |
| url | auth/ldap | | |
| start_tls | auth/ldap | | |
| cert_validation | auth/ldap | | |
| tls_certificate_path | auth/ldap | | |
| tls_key_path | auth/ldap | | |
| base_dn | auth/ldap | | |
| bind_user | auth/ldap | | |
| bind_pass | auth/ldap | | |
| field_map | auth/ldap | | |
| login_filter | auth/ldap | | |
| admin_group | auth/ldap | | |
| synchronize | auth/ldap | | |
| disable_missing | auth/ldap | | |
| sync_filter | auth/ldap | | |
| registration_enabled | auth/ldap | | |
| debug | database | false | |
| slow_query_threshold | database | | |
| type | database | sqlite | |
| dsn | database | sqlite.db | |
| request_logging | web | false | |
| external_url | web | http://localhost:8888 | |
| listening_address | web | :8888 | |
| session_identifier | web | wgPortalSession | |
| session_secret | web | very_secret | |
| csrf_secret | web | extremely_secret | |
| site_title | web | WireGuard Portal | |
| site_company_name | web | WireGuard Portal | |
| ldap_sync_interval | advanced | 15m | The time interval after which users will be synchronized from LDAP. |
| 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. |
| 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 | 10m | 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. |
| 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. |
| callback_url_prefix | auth | /api/v0 | OAuth callback URL prefix. The full callback URL will look like: https://wg.portal.local/callback_url_prefix/provider_name/callback |
| 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 and is_admin. |
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| 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). |
| base_url | auth/oauth | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
| 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. |
| redirect_url | auth/oauth | | The redirect URL. |
| 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. |
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| 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. |
| synchronize | auth/ldap | | Periodically synchronize users (name, department, phone, status, ...) to the WireGuard Portal database. |
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| 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 | 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. |
## 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!
## V2 TODOs
* Public REST API
* Translations
* Documentation
* Audit UI
## What is out of scope
* Generation or application of any `iptables` or `nftables` rules.
* Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux.
* Importing private keys of an existing WireGuard setup.
* 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
* [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin)
* [Bootstrap, for the HTML templates](https://getbootstrap.com/)
* [Vue.JS, for the frontend](https://vuejs.org/)
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
* [Gin](https://github.com/gin-gonic/gin), HTTP web framework written in Go
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
* [Vue.JS](https://vuejs.org/), for the frontend
## License

View File

@ -28,7 +28,8 @@ import (
func main() {
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
logrus.Infof("Starting WireGuard Portal...")
logrus.Infof("Starting WireGuard Portal V2...")
logrus.Infof("WireGuard Portal version: %s", internal.Version)
cfg, err := config.GetConfig()
internal.AssertNoError(err)
@ -77,7 +78,7 @@ func main() {
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, database, wireGuard)
internal.AssertNoError(err)
cfgFileManager, err := configfile.NewConfigFileManager(cfg, database, database, cfgFileSystem)
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
internal.AssertNoError(err)
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)

View File

@ -2,7 +2,7 @@
version: '3.6'
services:
wg-portal:
image: h44z/wg-portal:1.0.16
image: h44z/wg-portal:2.0.0-alpha1
container_name: wg-portal
restart: unless-stopped
logging:
@ -16,4 +16,4 @@ services:
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
environment:
- EXTERNAL_URL=http://localhost:8123
- EXTERNAL_URL=http://localhost:8888

View File

@ -33,9 +33,9 @@ const title = computed(() => {
}
if (selectedInterface.value) {
return t("interfaces.interface.edit") + ": " + selectedInterface.value.Identifier
return t("modals.interface-edit.headline-edit") + " " + selectedInterface.value.Identifier
}
return t("interfaces.interface.new")
return t("modals.interface-edit.headline-new")
})
const formData = ref(freshInterface())
@ -291,134 +291,135 @@ async function del() {
<template #default>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#interface">Interface</a>
<a class="nav-link active" data-bs-toggle="tab" href="#interface">{{ $t('modals.interface-edit.tab-interface') }}</a>
</li>
<li v-if="formData.Mode==='server'" class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#peerdefaults">Peer Defaults</a>
<a class="nav-link" data-bs-toggle="tab" href="#peerdefaults">{{ $t('modals.interface-edit.tab-peerdef') }}</a>
</li>
</ul>
<div id="interfaceTabs" class="tab-content">
<div id="interface" class="tab-pane fade active show">
<fieldset>
<legend class="mt-4">General</legend>
<legend class="mt-4">{{ $t('modals.interface-edit.header-general') }}</legend>
<div v-if="props.interfaceId==='#NEW#'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.identifier') }}</label>
<input v-model="formData.Identifier" class="form-control" placeholder="The device identifier" type="text">
<label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label>
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text">
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.displayname') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
<select v-model="formData.Mode" class="form-select">
<option value="server">Server Mode</option>
<option value="client">Client Mode</option>
<option value="any">Custom Mode</option>
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
</select>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.displayname') }}</label>
<input v-model="formData.DisplayName" class="form-control" placeholder="A descriptive name of the interface" type="text">
<label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label>
<input v-model="formData.DisplayName" class="form-control" :placeholder="$t('modals.interface-edit.display-name.placeholder')" type="text">
</div>
</fieldset>
<fieldset>
<legend class="mt-4">Cryptography</legend>
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.privatekey') }}</label>
<input v-model="formData.PrivateKey" class="form-control" placeholder="The private key" required type="email">
<label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label>
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="email">
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.publickey') }}</label>
<input v-model="formData.PublicKey" class="form-control" placeholder="The public key" required type="email">
<label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label>
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="email">
</div>
</fieldset>
<fieldset>
<legend class="mt-4">Networking</legend>
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.ips') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Addresses"
placeholder="IP Addresses (CIDR format)"
:placeholder="$t('modals.interface-edit.ip.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]"
:validate="validateCIDR"
@on-tags-changed="handleChangeAddresses"/>
</div>
<div v-if="formData.Mode==='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.listenport') }}</label>
<input v-model="formData.ListenPort" class="form-control" placeholder="Listen Port" type="number">
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
<input v-model="formData.ListenPort" class="form-control" :placeholder="$t('modals.interface-edit.listen-port.placeholder')" type="number">
</div>
<div v-if="formData.Mode!=='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.dns') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Dns"
placeholder="DNS Servers"
:placeholder="$t('modals.interface-edit.dns.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]"
:validate="validateIP"
@on-tags-changed="handleChangeDns"/>
</div>
<div v-if="formData.Mode!=='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.dnssearch') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
placeholder="DNS Search prefixes"
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]"
:validate="validateDomain"
@on-tags-changed="handleChangeDnsSearch"/>
</div>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.mtu') }}</label>
<input v-model="formData.Mtu" class="form-control" placeholder="Client MTU (0 = default)" type="number">
<label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label>
<input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number">
</div>
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.firewallmark') }}</label>
<input v-model="formData.FirewallMark" class="form-control" placeholder="Firewall Mark (0 = default)" type="number">
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
<input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
</div>
</div>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.routingtable') }}</label>
<input v-model="formData.RoutingTable" class="form-control" placeholder="Routing Table (0 = default)" type="number">
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
<input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text">
<small id="routingTableHelp" class="form-text text-muted">{{ $t('modals.interface-edit.routing-table.description') }}</small>
</div>
<div class="form-group col-md-6">
</div>
</div>
</fieldset>
<fieldset>
<legend class="mt-4">Hooks</legend>
<legend class="mt-4">{{ $t('modals.interface-edit.header-hooks') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.preup') }}</label>
<textarea v-model="formData.PreUp" class="form-control" rows="2"></textarea>
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-up.label') }}</label>
<textarea v-model="formData.PreUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-up.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.postup') }}</label>
<textarea v-model="formData.PostUp" class="form-control" rows="2"></textarea>
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-up.label') }}</label>
<textarea v-model="formData.PostUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-up.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.predown') }}</label>
<textarea v-model="formData.PreDown" class="form-control" rows="2"></textarea>
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-down.label') }}</label>
<textarea v-model="formData.PreDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-down.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.postdown') }}</label>
<textarea v-model="formData.PostDown" class="form-control" rows="2"></textarea>
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-down.label') }}</label>
<textarea v-model="formData.PostDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-down.placeholder')"></textarea>
</div>
</fieldset>
<fieldset>
<legend class="mt-4">State</legend>
<legend class="mt-4">{{ $t('modals.interface-edit.header-state') }}</legend>
<div class="form-check form-switch">
<input v-model="formData.Disabled" class="form-check-input" type="checkbox">
<label class="form-check-label" >Disabled</label>
<label class="form-check-label">{{ $t('modals.interface-edit.disabled.label') }}</label>
</div>
<div class="form-check form-switch">
<input v-model="formData.SaveConfig" checked="" class="form-check-input" type="checkbox">
<label class="form-check-label">Save Config to File</label>
<label class="form-check-label">{{ $t('modals.interface-edit.save-config.label') }}</label>
</div>
</fieldset>
</div>
<div id="peerdefaults" class="tab-pane fade">
<fieldset>
<legend class="mt-4">Networking</legend>
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.endpoint') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.endpoint') }}</label>
<input v-model="formData.PeerDefEndpoint" class="form-control" placeholder="Endpoint Addresses" type="text">
<small class="form-text text-muted">The endpoint address that peers will connect to.</small>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.networks') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefNetwork"
placeholder="Network Addresses"
:add-tag-on-keys="[13, 188, 32, 9]"
@ -427,7 +428,7 @@ async function del() {
<small class="form-text text-muted">Peers will get IP addresses from those subnets.</small>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.allowedips') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowedips') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefAllowedIPs"
placeholder="Default Allowed IP Addresses"
:add-tag-on-keys="[13, 188, 32, 9]"
@ -435,7 +436,7 @@ async function del() {
@on-tags-changed="handleChangePeerDefAllowedIPs"/>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.dns') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.dns') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefDns"
placeholder="DNS Servers"
:add-tag-on-keys="[13, 188, 32, 9]"
@ -443,7 +444,7 @@ async function del() {
@on-tags-changed="handleChangePeerDefDns"/>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.dnssearch') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.dnssearch') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefDnsSearch"
placeholder="DNS Search prefix"
:add-tag-on-keys="[13, 188, 32, 9]"
@ -452,41 +453,41 @@ async function del() {
</div>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.mtu') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.mtu') }}</label>
<input v-model="formData.PeerDefMtu" class="form-control" placeholder="Client MTU (0 = default)" type="number">
</div>
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.firewallmark') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.firewallmark') }}</label>
<input v-model="formData.PeerDefFirewallMark" class="form-control" placeholder="Firewall Mark (0 = default)" type="number">
</div>
</div>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.routingtable') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.routingtable') }}</label>
<input v-model="formData.PeerDefRoutingTable" class="form-control" placeholder="Routing Table (0 = default)" type="number">
</div>
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.keepalive') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.keepalive') }}</label>
<input v-model="formData.PeerDefPersistentKeepalive" class="form-control" placeholder="Persistent Keepalive (0 = default)" type="number">
</div>
</div>
</fieldset>
<fieldset>
<legend class="mt-4">Hooks</legend>
<legend class="mt-4">{{ $t('modals.interface-edit.header-peer-hooks') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.preup') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.preup') }}</label>
<textarea v-model="formData.PeerDefPreUp" class="form-control" rows="2"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.postup') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.postup') }}</label>
<textarea v-model="formData.PeerDefPostUp" class="form-control" rows="2"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.predown') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.predown') }}</label>
<textarea v-model="formData.PeerDefPreDown" class="form-control" rows="2"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.postdown') }}</label>
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.postdown') }}</label>
<textarea v-model="formData.PeerDefPostDown" class="form-control" rows="2"></textarea>
</div>
</fieldset>
@ -499,10 +500,10 @@ async function del() {
</template>
<template #footer>
<div class="flex-fill text-start">
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">Delete</button>
<button v-if="props.interfaceId!=='#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">Save</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">Discard</button>
<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>

View File

@ -243,7 +243,89 @@
"headline": "Config for Interface:"
},
"interface-edit": {
"privatekey": "Private Key"
"headline-edit": "Edit Interface:",
"headline-new": "New Interface",
"tab-interface": "Interface",
"tab-peerdef": "Peer Defaults",
"header-general": "General",
"header-network": "Network",
"header-crypto": "Cryptography",
"header-hooks": "Interface Hooks",
"header-peer-hooks": "Hooks",
"header-state": "State",
"identifier": {
"label": "Identifier",
"placeholder": "The unique interface identifier"
},
"mode": {
"label": "Interface Mode",
"server": "Server Mode",
"client": "Client Mode",
"any": "Unknown Mode"
},
"display-name": {
"label": "Display Name",
"placeholder": "The descriptive name for the interface"
},
"private-key": {
"label": "Private Key",
"placeholder": "The private key"
},
"public-key": {
"label": "Public Key",
"placeholder": "The public key"
},
"ip": {
"label": "IP Addresses",
"placeholder": "IP Addresses (CIDR format)"
},
"listen-port": {
"label": "Listen Port",
"placeholder": "The listening port"
},
"dns": {
"label": "DNS Server",
"placeholder": "The DNS servers that should be used"
},
"dns-search": {
"label": "DNS Search Domains",
"placeholder": "DNS search prefixes"
},
"mtu": {
"label": "MTU",
"placeholder": "The interface MTU (0 = keep default)"
},
"firewall-mark": {
"label": "Firewall Mark",
"placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
},
"routing-table": {
"label": "Routing Table",
"placeholder": "The routing table ID",
"description": "Special cases: off = do not manage routes, 0 = automatic"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "One or multiple bash commands"
},
"post-up": {
"label": "Post-Up",
"placeholder": "One or multiple bash commands"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "One or multiple bash commands"
},
"post-down": {
"label": "Post-Down",
"placeholder": "One or multiple bash commands"
},
"disabled": {
"label": "Interface Disabled"
},
"save-config": {
"label": "Automatically save wg-quick config"
}
},
"peer-view": {
"headline-peer": "Peer:",

12
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/gin-contrib/cors v1.4.0
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.8.0
github.com/glebarez/sqlite v1.9.0
github.com/go-ldap/ldap/v3 v3.4.5
github.com/prometheus-community/pro-bing v0.3.0
github.com/sirupsen/logrus v1.9.3
@ -18,9 +18,10 @@ require (
github.com/vardius/message-bus v1.1.5
github.com/vishvananda/netlink v1.1.0
github.com/xhit/go-simple-mail/v2 v2.15.0
github.com/yeqown/go-qrcode/v2 v2.2.1
github.com/yeqown/go-qrcode/v2 v2.2.2
golang.org/x/crypto v0.11.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sys v0.10.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.5.1
@ -39,7 +40,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
@ -91,16 +92,15 @@ require (
golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.9.3 // indirect
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.21.1 // indirect
modernc.org/sqlite v1.23.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

20
go.sum
View File

@ -45,10 +45,10 @@ github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.21.1 h1:7MZyUPh2XTrHS7xNEHQbrhfMZuPSzhkm2A1qgg0y5NY=
github.com/glebarez/go-sqlite v1.21.1/go.mod h1:ISs8MF6yk5cL4n/43rSOmVMGJJjHYr7L2MbZZ5Q4E2E=
github.com/glebarez/sqlite v1.8.0 h1:02X12E2I/4C1n+v90yTqrjRa8yuo7c3KeHI3FRznCvc=
github.com/glebarez/sqlite v1.8.0/go.mod h1:bpET16h1za2KOOMb8+jCp6UBP/iahDpfPQqSaYLTLx8=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
@ -246,8 +246,8 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xhit/go-simple-mail/v2 v2.15.0 h1:qMXeqcZErUW/Dw6EXxmPuxHzVI8MdxWnEnu2xcisohU=
github.com/xhit/go-simple-mail/v2 v2.15.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/yeqown/go-qrcode/v2 v2.2.1 h1:Jc1Q916fwC05R8C7mpWDbrT9tyLPaLLKDABoC5XBCe8=
github.com/yeqown/go-qrcode/v2 v2.2.1/go.mod h1:2Qsk2APUCPne0TsRo40DIkI5MYnbzYKCnKGEFWrxd24=
github.com/yeqown/go-qrcode/v2 v2.2.2 h1:0comk6jEwi0oWNhKEmzx4JI+Q7XIneAApmFSMKWmSVc=
github.com/yeqown/go-qrcode/v2 v2.2.2/go.mod h1:2Qsk2APUCPne0TsRo40DIkI5MYnbzYKCnKGEFWrxd24=
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@ -368,14 +368,14 @@ gorm.io/driver/sqlserver v1.5.1/go.mod h1:AYHzzte2msKTmYBYsSIq8ZUsznLJwBdkB2wpI+
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@ -5,8 +5,11 @@ import (
"bytes"
"context"
"fmt"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
evbus "github.com/vardius/message-bus"
"github.com/yeqown/go-qrcode/v2"
"io"
"os"
@ -15,6 +18,7 @@ import (
type Manager struct {
cfg *config.Config
bus evbus.MessageBus
tplHandler *TemplateHandler
fsRepo FileSystemRepo // can be nil if storing the configuration is disabled
@ -22,7 +26,7 @@ type Manager struct {
wg WireguardDatabaseRepo
}
func NewConfigFileManager(cfg *config.Config, users UserDatabaseRepo, wg WireguardDatabaseRepo, fsRepo FileSystemRepo) (*Manager, error) {
func NewConfigFileManager(cfg *config.Config, bus evbus.MessageBus, users UserDatabaseRepo, wg WireguardDatabaseRepo, fsRepo FileSystemRepo) (*Manager, error) {
tplHandler, err := newTemplateHandler()
if err != nil {
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
@ -30,6 +34,7 @@ func NewConfigFileManager(cfg *config.Config, users UserDatabaseRepo, wg Wiregua
m := &Manager{
cfg: cfg,
bus: bus,
tplHandler: tplHandler,
fsRepo: fsRepo,
@ -58,6 +63,51 @@ func (m Manager) createStorageDirectory() error {
return nil
}
func (m Manager) connectToMessageBus() {
if m.fsRepo == nil {
return // skip subscription
}
_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceUpdatedEvent)
_ = m.bus.Subscribe(app.TopicPeerInterfaceUpdated, m.handleInterfaceUpdatedEvent)
}
func (m Manager) handleInterfaceUpdatedEvent(iface *domain.Interface) {
logrus.Errorf("handling interface updated event for %s", iface.Identifier)
if !iface.SaveConfig || m.fsRepo == nil {
return
}
err := m.PersistInterfaceConfig(context.Background(), iface.Identifier)
if err != nil {
logrus.Errorf("failed to automatically persist interface config for %s: %v", iface.Identifier, err)
}
}
func (m Manager) handlePeerInterfaceUpdatedEvent(id domain.InterfaceIdentifier) {
logrus.Errorf("handling interface updated event for %s", id)
if m.fsRepo == nil {
return
}
peerInterface, err := m.wg.GetInterface(context.Background(), id)
if err != nil {
logrus.Errorf("failed to load interface %s: %v", id, err)
return
}
if !peerInterface.SaveConfig {
return
}
err = m.PersistInterfaceConfig(context.Background(), peerInterface.Identifier)
if err != nil {
logrus.Errorf("failed to automatically persist interface config for %s: %v", peerInterface.Identifier, err)
}
}
func (m Manager) GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error) {
iface, peers, err := m.wg.GetInterfaceAndPeers(ctx, id)
if err != nil {

View File

@ -7,3 +7,5 @@ const TopicUserDeleted = "user:deleted"
const TopicAuthLogin = "auth:login"
const TopicRouteUpdate = "route:update"
const TopicRouteRemove = "route:remove"
const TopicInterfaceUpdated = "interface:updated"
const TopicPeerInterfaceUpdated = "peer:interface:updated"

View File

@ -414,6 +414,8 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface, pee
return nil, fmt.Errorf("post-save hooks failed: %w", err)
}
m.bus.Publish(app.TopicInterfaceUpdated, iface)
return iface, nil
}

View File

@ -284,6 +284,10 @@ func (m Manager) savePeers(ctx context.Context, peers ...*domain.Peer) error {
m.bus.Publish(app.TopicRouteUpdate, "peers updated")
}
for iface := range interfaces {
m.bus.Publish(app.TopicPeerInterfaceUpdated, iface)
}
return nil
}

View File

@ -122,13 +122,17 @@ func (c Cidr) BroadcastAddr() Cidr {
a16[off+byteNum] |= 1 << uint(bitInByte)
}
if prefix.Addr().Is4() {
addr := netip.AddrFrom16(a16).Unmap()
return Cidr{
Addr: netip.AddrFrom16(a16).Unmap().String(),
Cidr: netip.PrefixFrom(addr, prefix.Bits()).String(),
Addr: addr.String(),
NetLength: prefix.Bits(),
}
} else {
addr := netip.AddrFrom16(a16) // doesn't unmap
return Cidr{
Addr: netip.AddrFrom16(a16).String(), // doesn't unmap
Cidr: netip.PrefixFrom(addr, prefix.Bits()).String(),
Addr: addr.String(), // doesn't unmap
NetLength: prefix.Bits(),
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 107 KiB