diff --git a/Dockerfile b/Dockerfile index c76ecb2..2925698 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" ] \ No newline at end of file diff --git a/README.md b/README.md index 788c53c..8830e20 100644 --- a/README.md +++ b/README.md @@ -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 | | -| 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 | 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 diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go index eb7c237..f56a64c 100644 --- a/cmd/wg-portal/main.go +++ b/cmd/wg-portal/main.go @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml index 8322b1e..8b640a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/frontend/src/components/InterfaceEditModal.vue b/frontend/src/components/InterfaceEditModal.vue index ee7c9ba..ec64eac 100644 --- a/frontend/src/components/InterfaceEditModal.vue +++ b/frontend/src/components/InterfaceEditModal.vue @@ -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() { diff --git a/frontend/src/lang/translations/en.json b/frontend/src/lang/translations/en.json index 48ae478..fd362a6 100644 --- a/frontend/src/lang/translations/en.json +++ b/frontend/src/lang/translations/en.json @@ -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:", diff --git a/go.mod b/go.mod index 1c85db9..7786e4e 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 7a082ed..5490328 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/app/configfile/manager.go b/internal/app/configfile/manager.go index 892501a..b2037a9 100644 --- a/internal/app/configfile/manager.go +++ b/internal/app/configfile/manager.go @@ -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 { diff --git a/internal/app/eventbus.go b/internal/app/eventbus.go index da21b9d..5a5f732 100644 --- a/internal/app/eventbus.go +++ b/internal/app/eventbus.go @@ -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" diff --git a/internal/app/wireguard/wireguard_interfaces.go b/internal/app/wireguard/wireguard_interfaces.go index e8f93de..5cae330 100644 --- a/internal/app/wireguard/wireguard_interfaces.go +++ b/internal/app/wireguard/wireguard_interfaces.go @@ -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 } diff --git a/internal/app/wireguard/wireguard_peers.go b/internal/app/wireguard/wireguard_peers.go index 87b8783..5c3a20b 100644 --- a/internal/app/wireguard/wireguard_peers.go +++ b/internal/app/wireguard/wireguard_peers.go @@ -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 } diff --git a/internal/domain/ip.go b/internal/domain/ip.go index 1bc65b3..35b6e75 100644 --- a/internal/domain/ip.go +++ b/internal/domain/ip.go @@ -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(), } } diff --git a/screenshot.png b/screenshot.png index e337859..010c7b7 100644 Binary files a/screenshot.png and b/screenshot.png differ