Compare commits

..

45 Commits

Author SHA1 Message Date
h44z
ad267ed0a8 Update README.md, fix build badge
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-05 13:53:28 +01:00
h44z
624988aef1 Create SECURITY.md 2025-01-05 13:49:35 +01:00
Christoph Haas
3020fbca4e fix change of peer identifier (public key) (#265) 2025-01-05 11:30:34 +01:00
Christoph Haas
6d86f15ff8 implement/fix peer and user disable event (#337, #273)
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-05 10:06:34 +01:00
Christoph Haas
62dbdfe0f9 fix plain oauth login (#317)
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-04 14:25:13 +01:00
Christoph Haas
378252ba2f sec: validate return url 2025-01-04 13:43:18 +01:00
Christoph
0664bd0ad0 chore: update dependencies
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2024-12-27 15:10:04 +01:00
dependabot[bot]
877cdae587 Bump github.com/go-ldap/ldap/v3 from 3.4.8 to 3.4.9 in the patch group (#335)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Bumps the patch group with 1 update: [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap).


Updates `github.com/go-ldap/ldap/v3` from 3.4.8 to 3.4.9
- [Release notes](https://github.com/go-ldap/ldap/releases)
- [Commits](https://github.com/go-ldap/ldap/compare/v3.4.8...v3.4.9)

---
updated-dependencies:
- dependency-name: github.com/go-ldap/ldap/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 18:15:22 +01:00
dependabot[bot]
edb5c82a66 Bump golang.org/x/crypto from 0.30.0 to 0.31.0 in the golang group (#334)
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
Bumps the golang group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.30.0 to 0.31.0
- [Commits](https://github.com/golang/crypto/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 15:53:48 +01:00
Dmytro Bondar
0ea24e313d feat: handle missing config file gracefully with a warning (#331)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-12-10 15:17:31 +01:00
dependabot[bot]
983568b36a Bump the golang group with 2 updates (#332)
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
Bumps the golang group with 2 updates: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/crypto` from 0.29.0 to 0.30.0
- [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.30.0)

Updates `golang.org/x/sys` from 0.27.0 to 0.28.0
- [Commits](https://github.com/golang/sys/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 19:25:44 +01:00
Dmytro Bondar
81ff0cde60 chore: update vue-i18n and related dependencies to version 9.14.2 (#330)
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
Resolved XSS vulnerability https://github.com/advisories/GHSA-9r9m-ffp6-9x4v
2024-12-04 12:22:24 +01:00
Dmytro Bondar
0f27443ffc chart: update monitoring resources (#329)
### Monitoring settings:
* Disabled rendering of Prometheus resources by default.
* Added a new relabeling configuration to replace the `instance` label based on the `app_kubernetes_io_name` label. Instance will be displayed as Helm release name instead of pod IP address.

### Grafana dashboard configuration:
* Changed the `interval` field for several metrics queries from `"$__rate_interval"` to `"$interval"`.
* Updated the `refresh` interval from `30s` to `1m`.
* Added a new variable for `Step Interval` with default value `2m`.

### Chart version update:
* Updated the chart version from `0.4.0` to `0.5.0`.

Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-12-04 12:22:03 +01:00
dependabot[bot]
ca6070689e Bump gorm.io/driver/postgres from 1.5.10 to 1.5.11 in the gorm group (#328)
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
Bumps the gorm group with 1 update: [gorm.io/driver/postgres](https://github.com/go-gorm/postgres).


Updates `gorm.io/driver/postgres` from 1.5.10 to 1.5.11
- [Commits](https://github.com/go-gorm/postgres/compare/v1.5.10...v1.5.11)

---
updated-dependencies:
- dependency-name: gorm.io/driver/postgres
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gorm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-04 09:56:49 +01:00
Dmytro Bondar
ba9b6c39e0 docs: build multi-version documentation (#327)
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
* Build multi-version docs with 'mike'

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

* Revert remote_branch option

---------

Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-12-03 19:04:43 +01:00
Dmytro Bondar
afcba8d43e chore: update dependencies (#325)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
* Update go version and dependencies

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

* Group dependabot updates

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

* Lock file maintenance

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

* Rename vite.config.js

Keep CJS as default, opt-in to ESM if needed
More: https://vite.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated

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

---------

Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-11-27 18:24:23 +01:00
Dmytro Bondar
90a570bd66 fix: enhance PrivateString Scan method to support []byte input (#324)
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
Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-11-26 21:09:39 +01:00
h44z
f7c3bdf456 Update docker-compose.yml (fixes #308)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2024-11-24 18:53:28 +01:00
ClarkQAQ
486a6ac038 feat(translations): chinese translations (#316)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
*  feat(translations): chinese translations

*  feat(App.vue): append dropdown item
2024-11-01 18:05:38 +01:00
Christoph
bf9183256a chore: update dependencies, refactor option types
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2024-10-15 15:44:47 +02:00
Christoph Haas
6bb683047e chore: update go dependencies
Some checks failed
Chart / lint-test (push) Has been cancelled
Chart / publish (push) Has been cancelled
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2024-10-09 22:41:01 +02:00
dependabot[bot]
5a289276f4 Bump docker/build-push-action from 5 to 6 in the actions group (#310)
Bumps the actions group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action).


Updates `docker/build-push-action` from 5 to 6
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 22:37:56 +02:00
Dmytro Bondar
d8eac37302 Updated metrics, added dashboard for Grafana (#311)
* Updated metrics, added dashboard for Grafana

* Remove unused interfce metric

* Set default scrape interval
2024-10-09 22:33:50 +02:00
Ryazanov Alexander Mihailovich
386597e057 UX: Config downloads without .txt extension (#314)
* refactor: change mime type in PeerViewModal.vue

* refactor: change download type in InterfaceView.vue
2024-10-09 22:32:32 +02:00
Dmytro Bondar
f22a7e4a2e feat: Metrics for Prometheus (#309)
* feat: prometheus metrics

* Added Prometheus resources support to helm chart
2024-09-29 22:10:50 +02:00
Christoph Haas
ae1be0e367 chore: update go dependencies 2024-09-23 22:04:24 +02:00
Dmytro Bondar
7a08c14de4 chore: CI files update (#306)
* Update Dockerfile

- Fix FromAsCasing
- Copy `cmd` and `internal` directories only for backend

* Export binaries from docker images

* Create release with assets from workflow

* Remove circleci config

* fix chart publishing
2024-09-23 21:54:22 +02:00
Dmytro Bondar
2c01f42369 feat: substitute environment variables in config file (#305)
* feat: use envsubst to substitute env variables in config file

* Remove output config to log

* Update readme
2024-09-23 21:48:11 +02:00
Dmytro Bondar
3196010a58 feat: Added peers sorting on views (#302)
* Added peers sorting for Interface and Profile views

* Use ip-address package to sort with IPv6 addresses only

* Add RX/TX column and fix add-peer button title
2024-09-23 21:44:43 +02:00
Dmytro Bondar
6ffe1a90ae feat: TLS support for web (#301)
* Added TLS support for web

- Added optional configurations `cert_file` and `key_file` to run web server with https

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

* Helm chart update

- Refactored Ingress to use one host only (`config.web.external_url` is required)
- Added Certificate resource template (secret is mounted to container into `/app/certs/`)
- Added support for service with mixed protocols (exposes UI and Wireguard ports on same IP)
- Added helm-docs target to makefile
- Changed pod labels to use selectorLabels
- Removed default probes (app runs without healthy web)
- Removed sections from README

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

* Fix chart workflow path filter

* Fix chart lint issue

* Skip clean-up tested chart

* Try k3d cluster

---------

Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-09-22 13:25:08 +02:00
dithmer
e3d05a4678 Fix not loading stats on interface change (#294)
To fix that, the method peers.LoadStats() is simply also called together
with peers.LoadPeers() when the @change handler is activated on the
interface selector element.

Co-authored-by: Tim Dithmer <tim.dithmer@suresecure.de>
2024-09-22 13:20:47 +02:00
dithmer
deff2334ac Fix the wrong default hooks for PreDown and PostDown on Peer Preparing by using the correct properties of the iface (#293)
Co-authored-by: Tim Dithmer <tim.dithmer@suresecure.de>
2024-09-22 11:55:41 +02:00
congnvp
4f1044a963 Add Vietnamese in to translations (#291) 2024-09-22 11:54:41 +02:00
Dmytro Bondar
2428dedc42 fix: autosave wireguard conf files (#303)
* fix: autosave wireguard conf files

- Fix subscription to Interface and Peer updates topics
- Remove admin permissions validation
- Update file on peer deletion
- Change save condition to configured storage path only, as initialized interface is not nil

* Added  comment to peer config for prometheus exporter
2024-09-22 11:53:42 +02:00
Dmytro Bondar
605841f2a0 fix: LDAP sync interval (#304)
Configurable LDAP sync interval for each LDAP provider
2024-09-22 11:49:23 +02:00
Christoph Haas
a46dabc1d3 #282: change default peer mask to /32 2024-08-13 22:49:58 +02:00
Christoph Haas
3f72de6af4 chore: update dependencies 2024-08-13 22:38:01 +02:00
dependabot[bot]
f1f5280cbc Bump the actions group across 1 directory with 3 updates (#279)
Bumps the actions group with 3 updates in the / directory: [actions/setup-python](https://github.com/actions/setup-python), [docker/login-action](https://github.com/docker/login-action) and [docker/build-push-action](https://github.com/docker/build-push-action).


Updates `actions/setup-python` from 4 to 5
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

Updates `docker/login-action` from 1 to 3
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1...v3)

Updates `docker/build-push-action` from 5 to 6
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-04 23:09:44 +02:00
Christoph Haas
48f4b6cb0e chore: update dependencies 2024-07-04 22:44:50 +02:00
Ryazanov Alexander Mihailovich
58294a3c2a QoL frontend improvements (#267)
* refactor: add space for language title

* feat: add overflow-break to text-wrap class

* feat: add public key text wrapping for interfaces

* refactor: get rid of getlang function and set locale during initialization

fix: show english flag when locale does not match

* refactor: bind header logo alt attribute to companyName

* refactor: add language name to german

* refactor: add language name to russian

* refactor: add language name to english
2024-07-04 22:40:16 +02:00
Dmytro Bondar
6f52cb2ada Init Helm chart (#255)
* Initial chart version

* Add CI/CD for chart

* Fix admin creds template

* Add command, args, env, envFrom

* Render volumes and volumeMounts with tpl

* Change persistance accessMode type

* Add update strategy config

* Use custom types in docs

* Add startup probe config

* Fix web.external_url config
2024-07-04 22:37:30 +02:00
Christoph Haas
85381121ee update to go 1.22 2024-04-29 23:28:32 +02:00
Dmytro Bondar
a6d985c2fe Remove CodeQL workflow file (#256) 2024-04-29 23:21:00 +02:00
Christoph Haas
aebf80bf68 update dependencies 2024-04-29 23:18:16 +02:00
muellpanda
e72ba87619 Remove 'hidden'-attribute from email button (#251) 2024-04-29 23:10:31 +02:00
88 changed files with 5425 additions and 1467 deletions

View File

@@ -1,67 +0,0 @@
version: 2.1
jobs:
build-latest:
steps:
- checkout
- restore_cache:
keys:
- go-mod-latest-v4-{{ checksum "go.sum" }}
- run:
name: Build Frontend
command: |
make frontend
- run:
name: Install Dependencies
command: |
make build-dependencies
- save_cache:
key: go-mod-latest-v4-{{ checksum "go.sum" }}
paths:
- "~/go/pkg/mod"
- run:
name: Build AMD64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-amd64
- run:
name: Install Cross-Platform Dependencies
command: |
sudo apt-get update
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
sudo ln -s /usr/include/asm-generic /usr/include/asm
- run:
name: Build ARM64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm64
- run:
name: Build ARM
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm
- store_artifacts:
path: ~/repo/dist
- run:
name: "Publish Release on GitHub"
command: |
if [ ! -z "${CIRCLE_TAG}" ]; then
go install github.com/tcnksm/ghr@latest
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace $CIRCLE_TAG ~/repo/dist
fi
working_directory: ~/repo
docker:
- image: cimg/go:1.21-node
workflows:
build-and-release:
jobs:
#--------------- BUILD ---------------#
- build-latest:
filters:
tags:
only: /^v.*/

View File

@@ -1,5 +1,14 @@
.github/
**/.vscode/
docs/
frontend/node_modules/
internal/app/api/core/frontend-dist
# Ignore everything
*
# Allow backend files
!cmd/
!internal/
!go.mod
!go.sum
# Allow frontend files
!frontend/
# Ignore node_modules
**/node_modules/

View File

@@ -18,3 +18,13 @@ updates:
directory: /
schedule:
interval: weekly
groups:
golang:
patterns:
- golang.org*
gorm:
patterns:
- gorm.io*
patch:
update-types:
- patch

75
.github/workflows/chart.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
# Publish chart to the GitHub Container Registry (GHCR) on push to master
# Run the following tests on PRs:
# - Check if chart's documentation is up to date
# - Check chart linting
# - Check chart installation in a Kind cluster
# - Check chart packaging
name: Chart
on:
pull_request:
branches: [master]
paths: ['deploy/helm/**']
push:
branches: [master]
paths: ['deploy/helm/**']
jobs:
lint-test:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check docs
run: |
make helm-docs
if ! git diff --exit-code; then
echo "error::Documentation is not up to date. Please run helm-docs and commit changes."
exit 1
fi
# ct lint requires Python 3.x to run following packages:
# - yamale (https://github.com/23andMe/Yamale)
# - yamllint (https://github.com/adrienverge/yamllint)
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- uses: helm/chart-testing-action@v2
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml
- uses: nolar/setup-k3d-k3s@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run chart-testing (install)
run: ct install --config ct.yaml
- name: Check chart packaging
run: helm package deploy/helm
publish:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Package helm chart
run: helm package deploy/helm
- name: Push chart to GHCR
run: helm push wg-portal-*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts

View File

@@ -1,70 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '35 15 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
# required for all workflows
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript-typescript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@@ -67,7 +67,7 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
@@ -77,3 +77,44 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: |
BUILD_VERSION=${{ env.BUILD_VERSION }}
- name: Export binaries from images
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
target: binaries
outputs: type=local,dest=./binaries
build-args: |
BUILD_VERSION=${{ env.BUILD_VERSION }}
- name: Rename binaries
run: |
for file in binaries/linux*/wg-portal; do
mv $file binaries/wg-portal_$(basename $(dirname $file))
done
- name: Upload binaries
uses: actions/upload-artifact@v4
with:
name: binaries
path: binaries/wg-portal_linux*
retention-days: 10
release:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: build-n-push
permissions:
contents: write
steps:
- name: Download binaries
uses: actions/download-artifact@v4
with:
name: binaries
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: 'wg-portal_linux*'
generate_release_notes: true

View File

@@ -1,22 +1,29 @@
name: github-pages
on:
push:
branches:
- master
branches: [master]
tags: ["v*"]
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: 3.x
- uses: actions/cache@v4
with:
key: ${{ github.ref }}
path: .cache
- run: pip install mkdocs-material
- run: pip install pillow cairosvg
- run: mkdocs gh-deploy --force
- name: Install dependencies
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin
- name: Publish documentation
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
env:
GIT_COMMITTER_NAME: "github-actions[bot]"
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"

View File

@@ -4,7 +4,7 @@
######
# Build frontend
######
FROM --platform=${BUILDPLATFORM} node:lts-alpine as frontend
FROM --platform=${BUILDPLATFORM} node:lts-alpine AS frontend
# Set the working directory
WORKDIR /build
# Download dependencies
@@ -20,14 +20,15 @@ RUN npm run build
######
# Build backend
######
FROM --platform=${BUILDPLATFORM} golang:1.21-alpine as builder
FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder
# Set the working directory
WORKDIR /build
# Download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the sources to the working directory
COPY . .
COPY ./cmd ./cmd
COPY ./internal ./internal
# Copy the frontend build result
COPY --from=frontend /build/dist/ ./internal/app/api/core/frontend-dist/
# Set the build version from arguments
@@ -40,6 +41,12 @@ RUN CGO_ENABLED=0 GOARCH=${TARGETARCH} go build -o /build/dist/wg-portal \
-tags netgo \
cmd/wg-portal/main.go
######
# Export binaries
######
FROM scratch AS binaries
COPY --from=builder /build/dist/wg-portal /
######
# Final image
######
@@ -47,13 +54,15 @@ FROM alpine:3.19
# Install OS-level dependencies
RUN apk add --no-cache bash curl iptables nftables openresolv
# Setup timezone
ENV TZ=Europe/Vienna
ENV TZ=UTC
# Copy binaries
COPY --from=builder /build/dist/wg-portal /app/wg-portal
# Set the Current Working Directory inside the container
WORKDIR /app
# by default, the web-portal is reachable on port 8888
# Expose default ports for metrics, web and wireguard
EXPOSE 8787/tcp
EXPOSE 8888/tcp
EXPOSE 51820/udp
# the database and config file can be mounted from the host
VOLUME [ "/app/data", "/app/config" ]
# Command to run the executable

View File

@@ -127,4 +127,9 @@ build-docker:
docker build --progress=plain \
--build-arg BUILD_IDENTIFIER=${ENV_BUILD_IDENTIFIER} --build-arg BUILD_VERSION=${ENV_BUILD_VERSION} \
--build-arg TARGETPLATFORM=unknown . \
-t h44z/wg-portal:local
-t h44z/wg-portal:local
#< helm-docs: Generate the helm chart documentation
.PHONY: helm-docs
helm-docs:
docker run --rm --volume "${PWD}/deploy:/helm-docs" -u "$$(id -u)" jnorwood/helm-docs -s file

236
README.md
View File

@@ -1,6 +1,6 @@
# WireGuard Portal (v2 - testing)
[![Build Status](https://travis-ci.com/h44z/wg-portal.svg?token=q4pSqaqT58Jzpxdx62xk&branch=master)](https://travis-ci.com/h44z/wg-portal)
[![Build Status](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/h44z/wg-portal/actions)
[![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)
[![Go Report Card](https://goreportcard.com/badge/github.com/h44z/wg-portal)](https://goreportcard.com/report/github.com/h44z/wg-portal)
@@ -37,113 +37,115 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post
* Support for multiple WireGuard interfaces
* Peer Expiry Feature
* Handle route and DNS settings like wg-quick does
* Exposes Prometheus [metrics](#metrics)
* ~~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/config.yml** in the working directory of the executable.
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`.
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. |
| 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 | 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 | 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 | 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. |
| 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. |
| 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 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). |
| 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. |
| 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. |
| 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. |
| 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. |
| 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 |
## Upgrading from V1
@@ -179,7 +181,7 @@ Ensure that the new database does not contain any data!
## Building
To build a standalone application, use the Makefile provided in the repository.
Go version 1.20 or higher has to be installed to build WireGuard Portal.
Go version 1.22 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
@@ -203,6 +205,48 @@ 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

33
SECURITY.md Normal file
View File

@@ -0,0 +1,33 @@
# Security Policy
If you believe you've found a security issue in one of the supported versions of *WireGuard Portal*, please report it to us as described below.
## Supported Versions
| Version | Supported |
| ------- | -------------------- |
| v2.x | :white_check_mark: |
| v1.x | :white_check_mark: |
## Reporting a Vulnerability
Please do not report security vulnerabilities through public GitHub issues.
Instead, we encourage you to submit a report through Github [private vulnerability reporting](https://github.com/h44z/wg-portal/security).
If you prefer to submit a report without logging in to Github, please email *info (at) wgportal.org*.
We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times.
We prefer all communications to be in English.
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
- Type of issue (e.g. SQL injection, cross-site scripting, ...)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
Thank you for helping keep *WireGuard Portal* and its users safe!

View File

@@ -2,6 +2,11 @@ package main
import (
"context"
"os"
"strings"
"syscall"
"time"
"github.com/h44z/wg-portal/internal/app/api/core"
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
"github.com/h44z/wg-portal/internal/app/audit"
@@ -11,10 +16,6 @@ import (
"github.com/h44z/wg-portal/internal/app/route"
"github.com/h44z/wg-portal/internal/app/users"
"github.com/h44z/wg-portal/internal/app/wireguard"
"os"
"strings"
"syscall"
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/adapters"
@@ -49,6 +50,8 @@ func main() {
mailer := adapters.NewSmtpMailRepo(cfg.Mail)
metricsServer := adapters.NewMetricsServer(cfg)
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
internal.AssertNoError(err)
@@ -69,13 +72,13 @@ func main() {
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
internal.AssertNoError(err)
authenticator, err := auth.NewAuthenticator(&cfg.Auth, eventBus, userManager)
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
internal.AssertNoError(err)
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
internal.AssertNoError(err)
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, database, wireGuard)
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, eventBus, database, wireGuard, metricsServer)
internal.AssertNoError(err)
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
@@ -103,6 +106,7 @@ func main() {
webSrv, err := core.NewServer(cfg, apiFrontend)
internal.AssertNoError(err)
go metricsServer.Run(ctx)
go webSrv.Run(ctx, cfg.Web.ListeningAddress)
// wait until context gets cancelled
@@ -128,7 +132,7 @@ func setupLogging(cfg *config.Config) {
case "error":
logrus.SetLevel(logrus.ErrorLevel)
default:
logrus.SetLevel(logrus.WarnLevel)
logrus.SetLevel(logrus.InfoLevel)
}
switch {

View File

@@ -12,7 +12,6 @@ web:
request_logging: true
auth:
callback_url_prefix: http://localhost:8888/api/v0
ldap:
- id: ldap1
provider_name: company ldap
@@ -46,4 +45,23 @@ auth:
extra_scopes:
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
registration_enabled: true
oauth:
- id: google_plain_oauth
provider_name: google3
display_name: Login with</br>Google3
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
auth_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- openid
- email
- profile
field_map:
email: email
firstname: name
user_identifier: sub
is_admin: roles
registration_enabled: true

5
ct.yaml Normal file
View File

@@ -0,0 +1,5 @@
# See https://github.com/helm/chart-testing#configuration
remote: origin
chart-dirs: deploy
target-branch: master
validate-maintainers: false

23
deploy/helm/.helmignore Normal file
View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

25
deploy/helm/Chart.yaml Normal file
View File

@@ -0,0 +1,25 @@
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'
type: application
home: https://wgportal.org
icon: https://wgportal.org/assets/images/logo.svg
sources:
- https://github.com/h44z/wg-portal
annotations:
artifacthub.io/category: networking
artifacthub.io/changes: ""
# 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
# 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

122
deploy/helm/README.md Normal file
View File

@@ -0,0 +1,122 @@
# 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)
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
**Homepage:** <https://wgportal.org>
## Source Code
* <https://github.com/h44z/wg-portal>
## Requirements
Kubernetes: `>=1.19.0`
## Installing the Chart
To install the chart with the release name `wg-portal`:
```console
helm install wg-portal oci://ghcr.io/h44z/charts/wg-portal
```
This command deploy wg-portal on the Kubernetes cluster in the default configuration.
The [Values](#values) section lists the parameters that can be configured during installation.
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| 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. |
| 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 |
| image.repository | string | `"ghcr.io/h44z/wg-portal"` | Image repository |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion |
| imagePullSecrets | list | `[]` | Image pull secrets |
| podAnnotations | tpl/object | `{}` | Extra annotations to add to the pod |
| podLabels | object | `{}` | Extra labels to add to the pod |
| podSecurityContext | object | `{}` | Pod Security Context |
| securityContext.capabilities.add | list | `["NET_ADMIN"]` | Add capabilities to the container |
| initContainers | tpl/list | `[]` | Pod init containers |
| sidecarContainers | tpl/list | `[]` | Pod sidecar containers |
| dnsPolicy | string | `"ClusterFirst"` | Set DNS policy for the pod. Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`. |
| restartPolicy | string | `"Always"` | Restart policy for all containers within the pod. Valid values are `Always`, `OnFailure` or `Never`. |
| hostNetwork | string | `false`. | Use the host's network namespace. |
| resources | object | `{}` | Resources requests and limits |
| command | list | `[]` | Overwrite pod command |
| args | list | `[]` | Additional pod arguments |
| env | tpl/list | `[]` | Additional environment variables |
| envFrom | tpl/list | `[]` | Additional environment variables from a secret or configMap |
| livenessProbe | object | `{}` | Liveness probe configuration |
| readinessProbe | object | `{}` | Readiness probe configuration |
| startupProbe | object | `{}` | Startup probe configuration |
| volumes | tpl/list | `[]` | Additional volumes |
| volumeMounts | tpl/list | `[]` | Additional volumeMounts |
| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | Node Selector configuration |
| tolerations | list | `[]` | Tolerations configuration |
| affinity | object | `{}` | Affinity configuration |
| service.mixed.enabled | bool | `false` | Whether to create a single service for the web and wireguard interfaces |
| service.mixed.type | string | `"LoadBalancer"` | Service type |
| 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.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. |
| service.metrics.port | int | `8787` | |
| ingress.enabled | bool | `false` | Specifies whether an ingress resource should be created |
| 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.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 |
| certificate.duration | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.renewBefore | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.commonName | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.emailAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.ipAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.keystores | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.privateKey | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.secretTemplate | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.subject | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.uris | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.usages | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| persistence.enabled | bool | `false` | Specifies whether an persistent volume should be created |
| persistence.annotations | object | `{}` | Persistent Volume Claim annotations |
| persistence.storageClass | string | `""` | Persistent Volume storage class. If undefined (the default) cluster's default provisioner will be used. |
| persistence.accessMode | string | `"ReadWriteOnce"` | Persistent Volume Access Mode |
| persistence.size | string | `"1Gi"` | Persistent Volume size |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
| serviceAccount.annotations | object | `{}` | Service account annotations |
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
| monitoring.enabled | bool | `false` | Enable Prometheus monitoring. |
| monitoring.apiVersion | string | `"monitoring.coreos.com/v1"` | API version of the Prometheus resource. Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus. |
| monitoring.kind | string | `"PodMonitor"` | Kind of the Prometheus resource. Could be `PodMonitor` or `ServiceMonitor`. |
| monitoring.labels | object | `{}` | Resource labels. |
| monitoring.annotations | object | `{}` | Resource annotations. |
| monitoring.interval | string | `1m` | Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used. |
| monitoring.metricRelabelings | list | `[]` | Relabelings to samples before ingestion. |
| monitoring.relabelings | list | `[]` | Relabelings to samples before scraping. |
| monitoring.scrapeTimeout | string | `""` | Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used. |
| monitoring.jobLabel | string | `""` | The label to use to retrieve the job name from. |
| monitoring.podTargetLabels | object | `{}` | Transfers labels on the Kubernetes Pod onto the target. |
| monitoring.dashboard.enabled | bool | `false` | Enable Grafana dashboard. |
| monitoring.dashboard.annotations | object | `{}` | Annotations for the dashboard ConfigMap. |
| monitoring.dashboard.labels | object | `{}` | Additional labels for the dashboard ConfigMap. |
| monitoring.dashboard.namespace | string | `""` | Dashboard ConfigMap namespace Overrides the namespace for the dashboard ConfigMap. |

View File

@@ -0,0 +1,27 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
## Installing the Chart
To install the chart with the release name `wg-portal`:
```console
helm install wg-portal oci://ghcr.io/h44z/charts/wg-portal
```
This command deploy wg-portal on the Kubernetes cluster in the default configuration.
The [Values](#values) section lists the parameters that can be configured during installation.
{{ template "chart.valuesSection" . }}

View File

@@ -0,0 +1,917 @@
{
"annotations": {},
"description": "WireGuard Portal Dashboard",
"panels": [
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": 3600000,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"disableTextWrap": false,
"editorMode": "code",
"exemplar": false,
"expr": "sum by (instance, interface) (wireguard_interface_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"})",
"fullMetaSearch": false,
"hide": false,
"includeNullMetadata": true,
"instant": false,
"interval": "",
"legendFormat": "Received {{interface}}",
"range": true,
"refId": "A",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (instance, interface) (wireguard_interface_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"})",
"hide": false,
"instant": false,
"legendFormat": "Sent {{interface}}",
"range": true,
"refId": "B"
}
],
"title": "Interface Bytes Total",
"type": "timeseries"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": 3600000,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 0
},
"id": 13,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (instance, interface) (rate(wireguard_interface_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
"hide": false,
"instant": false,
"interval": "",
"legendFormat": "Received {{interface}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (instance, interface) (rate(wireguard_interface_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
"hide": false,
"instant": false,
"interval": "",
"legendFormat": "Sent {{interface}}",
"range": true,
"refId": "B"
}
],
"title": "Interface Bandwidth",
"type": "timeseries"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": 3600000,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"id": 16,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (name, instance, interface) (rate(wireguard_peer_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
"hide": false,
"instant": false,
"interval": "$interval",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Peer Receive Bandwidth",
"type": "timeseries"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": 3600000,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 9
},
"id": 17,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (instance, interface, name) (rate(wireguard_peer_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
"hide": false,
"instant": false,
"interval": "$interval",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Peer Transmit Bandwidth",
"type": "timeseries"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"fillOpacity": 60,
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 1
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "bool_yes_no"
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 24,
"x": 0,
"y": 18
},
"id": 12,
"options": {
"colWidth": 0.85,
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false
},
"rowHeight": 0.85,
"showValue": "never",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(name) (wireguard_peer_up{instance=\"$instance\", interface=~\"$interface\"})",
"instant": false,
"interval": "$interval",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Peer Connection History",
"type": "status-history"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic-by-name"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto",
"wrapText": false
},
"filterable": false,
"inspect": false
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "dark-red",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": "/(Time|instance|interface|name)\\s\\d*/"
},
"properties": [
{
"id": "custom.hidden",
"value": true
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/Received|Transmitted/"
},
"properties": [
{
"id": "unit",
"value": "bytes"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Last Handshake"
},
"properties": [
{
"id": "unit",
"value": "s"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Connected"
},
"properties": [
{
"id": "mappings",
"value": [
{
"options": {
"0": {
"color": "red",
"index": 0,
"text": "No"
},
"1": {
"color": "green",
"index": 1,
"text": "Yes"
}
},
"type": "value"
}
]
},
{
"id": "custom.cellOptions",
"value": {
"type": "color-text"
}
}
]
}
]
},
"gridPos": {
"h": 14,
"w": 24,
"x": 0,
"y": 29
},
"id": 11,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"enablePagination": false,
"fields": [],
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Sent"
}
]
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"disableTextWrap": false,
"editorMode": "code",
"exemplar": false,
"expr": "sum by(id, instance, interface, name, addresses) (increase(wireguard_peer_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__range]))",
"format": "table",
"fullMetaSearch": false,
"hide": false,
"includeNullMetadata": true,
"instant": false,
"interval": "",
"legendFormat": "__auto",
"range": true,
"refId": "A",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"disableTextWrap": false,
"editorMode": "code",
"exemplar": false,
"expr": "sum by(id, instance, interface, name) (increase(wireguard_peer_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__range]))",
"format": "table",
"fullMetaSearch": false,
"includeNullMetadata": true,
"instant": false,
"interval": "",
"legendFormat": "__auto",
"range": true,
"refId": "B",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "time()-sum(wireguard_peer_last_handshake_seconds{instance=\"$instance\", interface=~\"$interface\"}) by(id, instance, interface, name) ",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "__auto",
"range": false,
"refId": "C"
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum(wireguard_peer_up{instance=\"$instance\", interface=~\"$interface\"}) by(id, instance, interface, name) ",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "__auto",
"range": false,
"refId": "D"
}
],
"title": "Peer Info",
"transformations": [
{
"id": "joinByField",
"options": {
"byField": "id",
"mode": "outer"
}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time 1": false,
"Time 2": false,
"Time 3": false,
"Time 4": false
},
"includeByName": {},
"indexByName": {
"Time 1": 8,
"Time 2": 9,
"Time 3": 10,
"Time 4": 11,
"Value #A": 4,
"Value #B": 5,
"Value #C": 6,
"Value #D": 7,
"addresses": 2,
"id": 3,
"instance 1": 12,
"instance 2": 13,
"instance 3": 16,
"instance 4": 19,
"interface 1": 0,
"interface 2": 14,
"interface 3": 17,
"interface 4": 20,
"name 1": 1,
"name 2": 15,
"name 3": 18,
"name 4": 21
},
"renameByName": {
"Value #A": "Received",
"Value #B": "Transmitted",
"Value #C": "Last Handshake",
"Value #D": "Connected",
"addresses": "IP Addresses",
"id": "Public Key",
"interface": "Interface",
"interface 1": "Interface",
"name": "Name",
"name 1": "Name"
}
}
}
],
"type": "table"
}
],
"refresh": "1m",
"tags": [
"wireguard",
"vpn"
],
"templating": {
"list": [
{
"current": {},
"hide": 0,
"includeAll": false,
"label": "Prometheus",
"multi": false,
"name": "datasource",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"definition": "label_values(wireguard_interface_sent_bytes_total,instance)",
"hide": 0,
"includeAll": false,
"label": "Instance",
"multi": false,
"name": "instance",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(wireguard_interface_sent_bytes_total,instance)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"definition": "label_values(wireguard_interface_sent_bytes_total{instance=\"$instance\"},interface)",
"hide": 0,
"includeAll": true,
"label": "Interface",
"multi": true,
"name": "interface",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(wireguard_interface_sent_bytes_total{instance=\"$instance\"},interface)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"current": {
"text": "2m",
"value": "2m"
},
"description": "",
"label": "Step Interval",
"name": "interval",
"options": [
{
"selected": false,
"text": "30s",
"value": "30s"
},
{
"selected": false,
"text": "1m",
"value": "1m"
},
{
"selected": true,
"text": "2m",
"value": "2m"
},
{
"selected": false,
"text": "5m",
"value": "5m"
},
{
"selected": false,
"text": "10m",
"value": "10m"
}
],
"query": "30s,1m,2m,5m,10m",
"type": "custom"
}
]
},
"time": {
"from": "now-12h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "WireGuard Portal",
"uid": "wireguard-portal",
"weekStart": ""
}

View File

@@ -0,0 +1,24 @@
{{- $serviceName := printf "%s-web" (include "wg-portal.fullname" .) -}}
{{- $servicePort := .Values.service.web.port }}
{{- if not .Values.ingress.enabled }}
Get the application URL by running these commands:
{{- if eq "ClusterIP" .Values.service.web.type }}
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ $serviceName }} {{ $servicePort }}:{{ $servicePort }}
Visit http://127.0.0.1:{{ $servicePort }} to use your application
{{- else if eq "LoadBalancer" .Values.service.web.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ $serviceName }}'
export SERVICE_IP=$(kubectl get --namespace {{ .Release.Namespace }} svc {{ $serviceName }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ $servicePort }}
{{- else if eq "NodePort" .Values.service.web.type }}
export NODE_IP=$(kubectl get --namespace {{ .Release.Namespace }} nodes -o jsonpath="{.items[0].status.addresses[0].address}")
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} svc {{ $serviceName }} -o jsonpath="{.spec.ports[0].nodePort}" )
echo http://$NODE_IP:$NODE_PORT
{{- end }}
{{- else }}
Visit http{{ if .Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.host }}{{ .Values.ingress.path }} to use your application
{{- end }}

View File

@@ -0,0 +1,129 @@
{{/*
Expand the name of the chart
*/}}
{{- define "wg-portal.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "wg-portal.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label
*/}}
{{- define "wg-portal.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "wg-portal.labels" -}}
helm.sh/chart: {{ include "wg-portal.chart" . }}
{{ include "wg-portal.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "wg-portal.selectorLabels" -}}
app.kubernetes.io/name: {{ include "wg-portal.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "wg-portal.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "wg-portal.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Define default admin credentials
If external auth is enabled and has admin group mappings,
the admin_user and admin_password values are not used.
*/}}
{{- define "wg-portal.admin" -}}
{{- $externalAdmin := false -}}
{{- with .Values.config.auth -}}
{{- range (default list .ldap) -}}
{{- if hasKey . "admin_group" -}}
{{- $externalAdmin = true -}}
{{- end -}}
{{- end }}
{{- range (concat (default list .oidc) (default list .oauth)) -}}
{{- if hasKey .field_map "is_admin" -}}
{{- $externalAdmin = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if not $externalAdmin -}}
admin_user: admin@wgportal.local
admin_password: {{ printf "%s/%s" .Release.Name .Release.Namespace | b64enc }}
{{- end -}}
{{- end -}}
{{/*
Define PersistentVolumeClaim spec
*/}}
{{- define "wg-portal.pvc" -}}
accessModes: [{{ .Values.persistence.accessMode }}]
{{- with .Values.persistence.storageClass }}
storageClassName: {{ . }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end -}}
{{/*
Define hostname
*/}}
{{- define "wg-portal.hostname" -}}
{{- if .Values.config.web.external_url -}}
{{- (urlParse (tpl .Values.config.web.external_url .)).hostname -}}
{{- end -}}
{{- end -}}
{{/*
wg-portal.util.merge will merge two YAML templates or dict with template and output the result.
This takes an array of three values:
- the top context
- the template name or dict of the overrides (destination)
- the template name of the base (source)
{{- include "wg-portal.util.merge" (list $ .Values.podLabels "wg-portal.selectorLabels") }}
{{- include "wg-portal.util.merge" (list $ "wg-portal.destTemplate" "wg-portal.sourceTemplate") }}
*/}}
{{- define "wg-portal.util.merge" -}}
{{- $top := first . -}}
{{- $overrides := index . 1 -}}
{{- $base := fromYaml (include (index . 2) $top) | default (dict) -}}
{{- if kindIs "string" $overrides -}}
{{- $overrides = fromYaml (include $overrides $top) | default (dict) -}}
{{- end -}}
{{- toYaml (merge $overrides $base) -}}
{{- end -}}

View File

@@ -0,0 +1,119 @@
{{- define "wg-portal.podTemplate" -}}
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
kubectl.kubernetes.io/default-container: {{ .Chart.Name }}
{{- with .Values.podAnnotations }}
{{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
labels: {{- include "wg-portal.util.merge" (list $ .Values.podLabels "wg-portal.selectorLabels") | nindent 4 }}
spec:
{{- with .Values.affinity }}
affinity: {{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
containers:
{{- with .Values.sidecarContainers }}
{{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag}}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.command }}
command: {{ . }}
{{- end }}
{{- with .Values.args }}
args: {{ . }}
{{- end }}
{{- with .Values.env }}
env: {{- tpl (toYaml .) $ | nindent 8 }}
{{- end }}
{{- with .Values.envFrom }}
envFrom: {{- tpl (toYaml .) $ | nindent 8 }}
{{- end }}
ports:
- name: metrics
containerPort: {{ .Values.service.metrics.port}}
protocol: TCP
- name: web
containerPort: {{ .Values.service.web.port }}
protocol: TCP
{{- range $index, $port := .Values.service.wireguard.ports }}
- name: wg{{ $index }}
containerPort: {{ $port }}
protocol: UDP
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.startupProbe }}
startupProbe: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.securityContext }}
securityContext: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.resources}}
resources: {{- toYaml . | nindent 8 }}
{{- end }}
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: data
mountPath: /app/data
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
- name: certs
mountPath: /app/certs
{{- end }}
{{- with .Values.volumeMounts }}
{{- tpl (toYaml .) $ | nindent 8 }}
{{- end }}
{{- with .Values.dnsPolicy }}
dnsPolicy: {{ . }}
{{- end }}
{{- with .Values.hostNetwork }}
hostNetwork: {{ . }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.initContainers }}
initContainers: {{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.restartPolicy }}
restartPolicy: {{ . }}
{{- end }}
serviceAccountName: {{ include "wg-portal.serviceAccountName" . }}
{{- with .Values.podSecurityContext }}
securityContext: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations: {{- toYaml . | nindent 4 }}
{{- end }}
volumes:
- name: config
secret:
secretName: {{ include "wg-portal.fullname" . }}
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
- name: certs
secret:
secretName: {{ include "wg-portal.fullname" . }}-tls
{{- end }}
{{- if not .Values.persistence.enabled }}
- name: data
emptyDir: {}
{{- else if eq .Values.workloadType "Deployment" }}
- name: data
persistentVolumeClaim:
claimName: {{ include "wg-portal.fullname" . }}
{{- end }}
{{- with .Values.volumes }}
{{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
{{- end -}}

View File

@@ -0,0 +1,53 @@
{{/*
Define the service template
{{- include "wg-portal.service" (dict "context" $ "scope" .Values.service.<name> "ports" list "name" "<name>") -}}
*/}}
{{- define "wg-portal.service.tpl" -}}
apiVersion: v1
kind: Service
metadata:
{{- with .scope.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
labels: {{- include "wg-portal.labels" .context | nindent 4 }}
name: {{ include "wg-portal.fullname" .context }}{{ ternary "" (printf "-%s" .name) (empty .name) }}
spec:
{{- with .scope.clusterIP }}
clusterIP: {{ . }}
{{- end }}
{{- with .scope.externalIPs }}
externalIPs: {{ toYaml . | nindent 4 }}
{{- end }}
{{- with .scope.externalName }}
externalName: {{ . }}
{{- end }}
{{- with .scope.externalTrafficPolicy }}
externalTrafficPolicy: {{ . }}
{{- end }}
{{- with .scope.healthCheckNodePort }}
healthCheckNodePort: {{ . }}
{{- end }}
{{- with .scope.loadBalancerIP }}
loadBalancerIP: {{ . }}
{{- end }}
{{- with .scope.loadBalancerSourceRanges }}
loadBalancerSourceRanges: {{ toYaml . | nindent 4 }}
{{- end }}
ports: {{- toYaml .ports | nindent 4 }}
{{- with .scope.publishNotReadyAddresses }}
publishNotReadyAddresses: {{ . }}
{{- end }}
{{- with .scope.sessionAffinity }}
sessionAffinity: {{ . }}
{{- end }}
{{- with .scope.sessionAffinityConfig }}
sessionAffinityConfig: {{ toYaml . | nindent 4 }}
{{- end }}
{{- with .scope.topologyKeys }}
topologyKeys: {{ toYaml . | nindent 4 }}
{{- end }}
{{- with .scope.type }}
type: {{ . }}
{{- end }}
selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
{{- end -}}

View File

@@ -0,0 +1,54 @@
{{/* https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources */}}
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) -}}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec:
secretName: {{ include "wg-portal.fullname" . }}-tls
{{- with .Values.certificate.secretTemplate }}
secretTemplate: {{ toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.privateKey }}
privateKey: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.keystores }}
keystores: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.duration }}
duration: {{ . }}
{{- end }}
{{- with .Values.certificate.renewBefore }}
renewBefore: {{ . }}
{{- end }}
{{- with .Values.certificate.usages }}
usages: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.subject }}
subject: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.commonName }}
commonName: {{ . }}
{{- end }}
dnsNames:
- {{ include "wg-portal.hostname" . }}
{{- with .Values.certificate.uris }}
uris: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.emailAddresses }}
emailAddresses: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.ipAddresses }}
ipAddresses: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.otherNames }}
otherNames: {{- toYaml . | nindent 4 }}
{{- end }}
issuerRef:
{{- with .Values.certificate.issuer.group }}
group: {{ . }}
{{- end }}
kind: {{ .Values.certificate.issuer.kind }}
name: {{ .Values.certificate.issuer.name }}
{{- end -}}

View File

@@ -0,0 +1,14 @@
{{- with .Values.monitoring.dashboard -}}
{{- if .enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
{{- with .annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
labels: {{- include "wg-portal.util.merge" (list $ .labels "wg-portal.labels") | nindent 4 }}
name: {{ printf "grafana-dashboards-%s" (include "wg-portal.fullname" $) }}
namespace: {{ default $.Release.Namespace .namespace }}
data: {{ ($.Files.Glob "files/dashboard.json").AsConfig | nindent 2 }}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,17 @@
{{- if eq .Values.workloadType "Deployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec:
{{- with .Values.revisionHistoryLimit }}
revisionHistoryLimit: {{ . }}
{{- end }}
{{- with .Values.strategy }}
strategy: {{- toYaml . | nindent 4 }}
{{- end }}
selector:
matchLabels: {{- include "wg-portal.selectorLabels" . | nindent 6 }}
template: {{- include "wg-portal.podTemplate" . | nindent 4 }}
{{- end -}}

View File

@@ -0,0 +1,4 @@
{{- range .Values.extraDeploy -}}
{{- tpl (toYaml .) $ }}
---
{{- end -}}

View File

@@ -0,0 +1,30 @@
{{- $hostname := include "wg-portal.hostname" . -}}
{{- if and .Values.ingress.enabled $hostname -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
{{- with .Values.ingress.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ $hostname }}
http:
paths:
- path: {{ default "/" (urlParse (tpl .Values.config.web.external_url .)).path }}
pathType: {{ default "ImplementationSpecific" .pathType }}
backend:
service:
name: {{ include "wg-portal.fullname" . }}
port:
name: web
{{- if .Values.ingress.tls }}
tls:
- hosts:
- {{ $hostname | quote }}
secretName: {{ include "wg-portal.fullname" . }}-tls
{{- end }}
{{- end }}

View File

@@ -0,0 +1,44 @@
{{- with .Values.monitoring -}}
{{- if and .enabled ($.Capabilities.APIVersions.Has .apiVersion) -}}
{{- $endpointsKey := (eq .kind "PodMonitor") | ternary "podMetricsEndpoints" "endpoints" -}}
apiVersion: {{ .apiVersion }}
kind: {{ .kind }}
metadata:
{{- with .annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
labels: {{- include "wg-portal.util.merge" (list $ .labels "wg-portal.labels") | nindent 4 }}
name: {{ include "wg-portal.fullname" $ }}
spec:
namespaceSelector:
matchNames:
- {{ $.Release.Namespace }}
selector:
matchLabels:
{{- include "wg-portal.selectorLabels" $ | nindent 6 }}
{{ $endpointsKey }}:
- port: metrics
path: /metrics
interval: {{ coalesce .interval ($.Values.config.statistics).data_collection_interval "1m" }}
{{- with .metricRelabelings }}
metricRelabelings: {{- toYaml . | nindent 8 }}
{{- end }}
relabelings:
- action: replace
sourceLabels:
- __meta_kubernetes_pod_label_app_kubernetes_io_name
targetLabel: instance
{{- with .relabelings }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .scrapeTimeout }}
scrapeTimeout: {{ . }}
{{- end }}
{{- with .jobLabel }}
jobLabel: {{ . }}
{{- end }}
{{- with .podTargetLabels }}
podTargetLabels: {{- toYaml . | nindent 2 }}
{{- end }}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,11 @@
{{- if and .Values.persistence.enabled (eq .Values.workloadType "Deployment") -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
{{- with .Values.persistence.annotations }}
annotations: {{- toYaml . | nindent 4}}
{{- end }}
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec: {{- include "wg-portal.pvc" . | nindent 2 }}
{{- end -}}

View File

@@ -0,0 +1,40 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "wg-portal.fullname" . }}
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 }}
{{- end }}
{{- with .Values.config.auth }}
auth: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with mustMerge .Values.config.core (include "wg-portal.admin" . | fromYaml) }}
core: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with .Values.config.database }}
database: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with .Values.config.mail }}
mail: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
statistics:
listening_address: :{{ .Values.service.metrics.port }}
{{- with .Values.config.statistics }}
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
{{- end }}
web:
listening_address: :{{ .Values.service.web.port }}
{{- with .Values.config.web }}
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
{{- end }}

View File

@@ -0,0 +1,20 @@
{{- $portsWeb := list (dict "name" "web" "port" .Values.service.web.port "protocol" "TCP" "targetPort" "web") -}}
{{- $ports := list -}}
{{- range $idx, $port := .Values.service.wireguard.ports -}}
{{- $name := printf "wg%d" $idx -}}
{{- $ports = append $ports (dict "name" $name "port" $port "protocol" "UDP" "targetPort" $name) -}}
{{- end -}}
{{- if .Values.service.mixed.enabled -}}
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.mixed "ports" (concat $portsWeb $ports)) }}
{{- else }}
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.web "ports" $portsWeb) }}
---
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.wireguard "ports" $ports "name" "wireguard") }}
{{- end -}}
{{- if and .Values.monitoring.enabled (eq .Values.monitoring.kind "ServiceMonitor") }}
---
{{- $portsMetrics := list (dict "name" "metrics" "port" .Values.service.metrics.port "protocol" "TCP" "targetPort" "metrics") -}}
{{- include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.metrics "ports" $portsWeb "name" "metrics") }}
{{- end -}}

View File

@@ -0,0 +1,10 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "wg-portal.serviceAccountName" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,24 @@
{{- if eq .Values.workloadType "StatefulSet" -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec:
{{- with .Values.revisionHistoryLimit }}
revisionHistoryLimit: {{ . }}
{{- end }}
{{- with .Values.strategy }}
updateStrategy: {{- toYaml . | nindent 4 }}
{{- end }}
serviceName: {{ template "wg-portal.fullname" . }}-web
selector:
matchLabels: {{- include "wg-portal.selectorLabels" . | nindent 6 }}
template: {{- include "wg-portal.podTemplate" . | nindent 4 }}
{{- if .Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec: {{- include "wg-portal.pvc" . | nindent 8 }}
{{- end -}}
{{- end -}}

244
deploy/helm/values.yaml Normal file
View File

@@ -0,0 +1,244 @@
# Default values for wg-portal.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# -- Partially override resource names (adds suffix)
nameOverride: ''
# -- Fully override resource names
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.
advanced: {}
# -- (tpl/object) Auth configuration options.
auth: {}
# -- (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.
core: {}
# -- (tpl/object) Database configuration options
database: {}
# -- (tpl/object) Mail configuration options
mail: {}
# -- (tpl/object) Statistics configuration options
statistics: {}
# -- (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.
web: {}
# -- The number of old ReplicaSets to retain to allow rollback.
# @default -- `10`
revisionHistoryLimit: ''
# -- Workload type - `Deployment` or `StatefulSet`
workloadType: Deployment
# -- Update strategy for the workload
# Valid values are:
# `RollingUpdate` or `Recreate` for Deployment,
# `RollingUpdate` or `OnDelete` for StatefulSet
strategy:
type: RollingUpdate
image:
# -- Image repository
repository: ghcr.io/h44z/wg-portal
# -- Image pull policy
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion
tag: ''
# -- Image pull secrets
imagePullSecrets: []
# -- (tpl/object) Extra annotations to add to the pod
podAnnotations: {}
# -- Extra labels to add to the pod
podLabels: {}
# -- Pod Security Context
podSecurityContext: {}
# Container Security Context
securityContext:
capabilities:
# -- Add capabilities to the container
add:
- NET_ADMIN
# -- (tpl/list) Pod init containers
initContainers: []
# -- (tpl/list) Pod sidecar containers
sidecarContainers: []
# -- Set DNS policy for the pod.
# Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`.
# @default -- `"ClusterFirst"`
dnsPolicy: ''
# -- Restart policy for all containers within the pod.
# Valid values are `Always`, `OnFailure` or `Never`.
# @default -- `"Always"`
restartPolicy: ''
# -- Use the host's network namespace.
# @default -- `false`.
hostNetwork: ''
# -- Resources requests and limits
resources: {}
# -- Overwrite pod command
command: []
# -- Additional pod arguments
args: []
# -- (tpl/list) Additional environment variables
env: []
# -- (tpl/list) Additional environment variables from a secret or configMap
envFrom: []
# -- Liveness probe configuration
livenessProbe: {}
# -- Readiness probe configuration
readinessProbe: {}
# -- Startup probe configuration
startupProbe: {}
# -- (tpl/list) Additional volumes
volumes: []
# -- (tpl/list) Additional volumeMounts
volumeMounts: []
# -- Node Selector configuration
nodeSelector:
kubernetes.io/os: linux
# -- Tolerations configuration
tolerations: []
# -- Affinity configuration
affinity: {}
service:
mixed:
# -- Whether to create a single service for the web and wireguard interfaces
enabled: false
# -- Service type
type: LoadBalancer
web:
# -- Annotations for the web service
annotations: {}
# -- Web service type
type: ClusterIP
# -- Web service port
# Used for the web interface listener
port: 8888
wireguard:
# -- Annotations for the WireGuard service
annotations: {}
# -- Wireguard service type
type: LoadBalancer
# -- 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.
ports:
- 51820
metrics:
port: 8787
ingress:
# -- Specifies whether an ingress resource should be created
enabled: false
# -- Ingress class name
className: ''
# -- Ingress annotations
annotations: {}
# -- Ingress TLS configuration.
# Enable certificate resource or add ingress annotation to create required secret
tls: false
certificate:
# -- Specifies whether a certificate resource should be created
enabled: false
issuer:
# -- Certificate issuer name
name: ''
# -- Certificate issuer kind (ClusterIssuer or Issuer)
kind: ''
# -- Certificate issuer group
group: cert-manager.io
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
duration: ''
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
renewBefore: ''
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
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)
ipAddresses: []
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
keystores: {}
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
privateKey: {}
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
secretTemplate: {}
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
subject: {}
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
uris: []
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
usages: []
persistence:
# -- Specifies whether an persistent volume should be created
enabled: false
# -- Persistent Volume Claim annotations
annotations: {}
# -- Persistent Volume storage class.
# If undefined (the default) cluster's default provisioner will be used.
storageClass: ''
# -- Persistent Volume Access Mode
accessMode: ReadWriteOnce
# -- Persistent Volume size
size: 1Gi
serviceAccount:
# -- Specifies whether a service account should be created
create: true
# -- Service account annotations
annotations: {}
# -- Automatically mount a ServiceAccount's API credentials
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: ''
monitoring:
# -- Enable Prometheus monitoring.
enabled: false
# -- API version of the Prometheus resource.
# Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus.
apiVersion: monitoring.coreos.com/v1
# -- Kind of the Prometheus resource.
# Could be `PodMonitor` or `ServiceMonitor`.
kind: PodMonitor
# -- Resource labels.
labels: {}
# -- Resource annotations.
annotations: {}
# -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used.
# @default -- `1m`
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: ''
# -- The label to use to retrieve the job name from.
jobLabel: ''
# -- Transfers labels on the Kubernetes Pod onto the target.
podTargetLabels: {}
dashboard:
# -- Enable Grafana dashboard.
enabled: false
# -- Annotations for the dashboard ConfigMap.
annotations: {}
# -- Additional labels for the dashboard ConfigMap.
labels: {}
# -- Dashboard ConfigMap namespace
# Overrides the namespace for the dashboard ConfigMap.
namespace: ''

View File

@@ -1,8 +1,7 @@
---
version: '3.6'
services:
wg-portal:
image: wgportal/wg-portal:v2
image: wgportal/wg-portal:latest
container_name: wg-portal
restart: unless-stopped
logging:

View File

@@ -1,5 +1,5 @@
To build a standalone application, use the Makefile provided in the repository.
Go version **1.21** or higher has to be installed to build WireGuard Portal.
Go version **1.22** 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

View File

@@ -327,7 +327,7 @@
<div class="md-container">
<div class="tx-hero__image">
<img
src="{{config.site_url}}assets/images/screenshot.png"
src="{{config.site_url}}/assets/images/screenshot.png"
alt=""
draggable="false"
>
@@ -356,7 +356,7 @@
<div class="second-column">
<div class="image-wrapper">
<img
src="{{config.site_url}}assets/images/wg-tool.png"
src="{{config.site_url}}/assets/images/wg-tool.png"
alt=""
draggable="false"
>

File diff suppressed because it is too large Load Diff

View File

@@ -14,12 +14,13 @@
"bootstrap": "^5.3.2",
"bootswatch": "^5.3.2",
"flag-icons": "^7.1.0",
"ip-address": "^9.0.5",
"is-cidr": "^5.0.3",
"is-ip": "^5.0.1",
"pinia": "^2.1.7",
"prismjs": "^1.29.0",
"vue": "^3.3.13",
"vue-i18n": "^9.8.0",
"vue-i18n": "^9.14.2",
"vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.2.5",
"vue3-tags-input": "^1.0.12"

View File

@@ -42,9 +42,15 @@ const switchLanguage = function (lang) {
const languageFlag = computed(() => {
// `this` points to the component instance
let lang = appGlobal.$i18n.locale.toLowerCase();
if (!appGlobal.$i18n.availableLocales.includes(lang)) {
lang = appGlobal.$i18n.fallbackLocale;
}
if (lang === "en") {
lang = "us";
}
if (lang === "zh") {
lang = "cn";
}
return "fi-" + lang;
})
@@ -59,7 +65,7 @@ const currentYear = ref(new Date().getFullYear())
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img alt="WireGuard Portal" src="/img/header-logo.png" /></a>
<a class="navbar-brand" href="/"><img :alt="companyName" src="/img/header-logo.png" /></a>
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
<span class="navbar-toggler-icon"></span>
@@ -110,9 +116,11 @@ const currentYear = ref(new Date().getFullYear())
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0"
data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span>English</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span>Deutsch</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span>Русский</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
</div>
</div>
</div>

View File

@@ -1,5 +1,17 @@
a.disabled {
pointer-events: none;
cursor: default;
color: #888888;
}
pointer-events: none;
cursor: default;
color: #888888;
}
.text-wrap {
overflow-break: anywhere;
}
.asc::after {
content: " ↑";
}
.desc::after {
content: " ↓";
}

View File

@@ -100,7 +100,7 @@ function download() {
let text = configString.value
let element = document.createElement('a')
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
element.setAttribute('download', filename)
element.style.display = 'none'
@@ -212,7 +212,7 @@ function ConfigQrUrl() {
<div class="flex-fill text-start">
<button @click.prevent="download" type="button" class="btn btn-primary me-1">{{
$t('modals.peer-view.button-download') }}</button>
<button @click.prevent="email" hidden type="button" class="btn btn-primary me-1">{{
<button @click.prevent="email" type="button" class="btn btn-primary me-1">{{
$t('modals.peer-view.button-email') }}</button>
</div>
<button @click.prevent="close" type="button" class="btn btn-secondary">{{ $t('general.close') }}</button>

View File

@@ -0,0 +1,20 @@
import { Address4, Address6 } from "ip-address"
export function ipToBigInt(ip) {
// Check if it's an IPv4 address
if (ip.includes(".")) {
const addr = new Address4(ip)
return addr.bigInteger()
}
// Otherwise, assume it's an IPv6 address
const addr = new Address6(ip)
return addr.bigInteger()
}
export function humanFileSize(size) {
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
if (size === 0) return "0B"
const i = parseInt(Math.floor(Math.log(size) / Math.log(1024)))
return Math.round(size / Math.pow(1024, i), 2) + sizes[i]
}

View File

@@ -2,28 +2,28 @@
import de from './translations/de.json';
import ru from './translations/ru.json';
import en from './translations/en.json';
import vi from './translations/vi.json';
import zh from './translations/zh.json';
import {createI18n} from "vue-i18n";
function getStoredLanguage() {
let initialLang = localStorage.getItem('wgLang');
if (!initialLang) {
initialLang = "en"
}
return initialLang
}
// Create i18n instance with options
const i18n = createI18n({
legacy: false,
globalInjection: true,
allowComposition: true,
locale: getStoredLanguage(), // set locale
locale: (
localStorage.getItem('wgLang')
|| (window && window.navigator && (window.navigator.userLanguage || window.navigator.language).split('-')[0])
|| 'en'
), // set locale
fallbackLocale: "en", // set fallback locale
messages: {
"de": de,
"ru": ru,
"en": en
"en": en,
"vi": vi,
"zh": zh
}
});
export default i18n
export default i18n

View File

@@ -1,4 +1,7 @@
{
"languages": {
"de": "Deutsch"
},
"general": {
"pagination": {
"size": "Anzahl an Elementen",

View File

@@ -1,4 +1,7 @@
{
"languages": {
"en": "English"
},
"general": {
"pagination": {
"size": "Number of Elements",

View File

@@ -1,4 +1,7 @@
{
"languages": {
"ru": "Русский"
},
"general": {
"pagination": {
"size": "Количество элементов",

View File

@@ -0,0 +1,492 @@
{
"languages": {
"vi": "Tiếng Việt"
},
"general": {
"pagination": {
"size": "Số mục",
"all": "Tất (chậm)"
},
"search": {
"placeholder": "Tìm...",
"button": "Tìm kiếm"
},
"select-all": "Chọn tất",
"yes": "Có",
"no": "Không",
"cancel": "Hủy",
"close": "Đóng",
"save": "Lưu",
"delete": "Xóa"
},
"login": {
"headline": "Vui lòng đăng nhập",
"username": {
"label": "Tài khoản",
"placeholder": "Vui lòng nhập tài khoản"
},
"password": {
"label": "Mật khẩu",
"placeholder": "Vui lòng nhập mật khẩu"
},
"button": "Đăng nhập"
},
"menu": {
"home": "Trang chủ",
"interfaces": "Giao diện",
"users": "Người dùng",
"lang": "Chuyển ngữ",
"profile": "Hồ sơ của tôi",
"login": "Đăng nhập",
"logout": "Đăng xuất"
},
"home": {
"headline": "Cổng VPN WireGuard®",
"info-headline": "Thêm thông tin",
"abstract": "WireGuard® là một VPN cực kỳ đơn giản nhưng nhanh chóng và hiện đại, sử dụng mật mã tiên tiến. Nó hướng đến mục tiêu nhanh hơn, đơn giản hơn, gọn nhẹ hơn và hữu ích hơn IPsec, cũng đỡ nhức đầu hơn. Nó có hiệu suất dự kiến là cao hơn đáng kể so với OpenVPN.",
"installation": {
"box-header": "Cài đặt WireGuard",
"headline": "Cài đặt",
"content": "Bạn có thể tìm thấy hướng dẫn cài đặt phần mềm máy khách trên trang web chính thức của WireGuard.",
"button": "Mở hướng dẫn"
},
"about-wg": {
"box-header": "Nói về WireGuard",
"headline": "Về",
"content": "WireGuard® là một VPN cực kỳ đơn giản nhưng nhanh chóng và hiện đại, sử dụng công nghệ mật mã tiên tiến.",
"button": "Thêm"
},
"about-portal": {
"box-header": "Giới thiệu về Cổng thông tin WireGuard",
"headline": "Cổng thông tin WireGuard",
"content": "Cổng thông tin WireGuard là một cổng cấu hình đơn giản, dựa trên web cho WireGuard.",
"button": "Tìm hiểu thêm"
},
"profiles": {
"headline": "Hồ sơ VPN",
"abstract": "Bạn có thể truy cập và tải xuống các cấu hình VPN cá nhân của mình qua hồ sơ người dùng của bạn.",
"content": "Để tìm tất cả các hồ sơ đã cấu hình của bạn, hãy nhấp vào nút dưới đây.",
"button": "Mở hồ sơ của tôi"
},
"admin": {
"headline": "Khu vực Quản trị",
"abstract": "Trong khu vực quản trị, bạn có thể quản lý các peer WireGuard và giao diện máy chủ cũng như người dùng được phép đăng nhập vào Cổng thông tin WireGuard.",
"content": "",
"button-admin": "Mở Quản trị Máy chủ",
"button-user": "Mở Quản trị Người dùng"
}
},
"interfaces": {
"headline": "Quản trị Giao diện",
"headline-peers": "Các Peer VPN Hiện tại",
"headline-endpoints": "Các Điểm cuối Hiện tại",
"no-interface": {
"default-selection": "Không có giao diện nào",
"headline": "Không tìm thấy giao diện...",
"abstract": "Nhấp vào nút cộng trên để tạo một giao diện WireGuard mới."
},
"no-peer": {
"headline": "Không có peer nào",
"abstract": "Hiện tại, không có peer nào khả dụng cho giao diện WireGuard đã chọn."
},
"table-heading": {
"name": "Tên",
"user": "Người dùng",
"ip": "Địa chỉ IP",
"endpoint": "Điểm cuối",
"status": "Trạng thái"
},
"interface": {
"headline": "Trạng thái giao diện cho",
"mode": "chế độ",
"key": "Khóa Công khai",
"endpoint": "Điểm cuối Công khai",
"port": "Cổng Nghe",
"peers": "Các Peer Được Kích hoạt",
"total-peers": "Tổng số Peer",
"endpoints": "Các Điểm cuối Được Kích hoạt",
"total-endpoints": "Tổng số Điểm cuối",
"ip": "Địa chỉ IP",
"default-allowed-ip": "IP được phép mặc định",
"dns": "Máy chủ DNS",
"mtu": "MTU",
"default-keep-alive": "Khoảng thời gian giữ kết nối mặc định",
"button-show-config": "Hiển thị cấu hình",
"button-download-config": "Tải xuống cấu hình",
"button-store-config": "Lưu cấu hình cho wg-quick",
"button-edit": "Chỉnh sửa giao diện"
},
"button-add-interface": "Thêm Giao diện",
"button-add-peer": "Thêm Peer",
"button-add-peers": "Thêm Nhiều Peer",
"button-show-peer": "Hiển thị Peer",
"button-edit-peer": "Chỉnh sửa Peer",
"peer-disabled": "Peer đã bị vô hiệu hóa, lý do:",
"peer-expiring": "Peer sẽ hết hạn vào",
"peer-connected": "Đã kết nối",
"peer-not-connected": "Chưa kết nối",
"peer-handshake": "Lần bắt tay cuối cùng:"
},
"users": {
"headline": "Quản trị Người dùng",
"table-heading": {
"id": "ID",
"email": "E-Mail",
"firstname": "Tên",
"lastname": "Họ",
"source": "Nguồn",
"peers": "Peers",
"admin": "Quản trị viên"
},
"no-user": {
"headline": "Không có người dùng nào",
"abstract": "Hiện tại, không có người dùng nào được đăng ký với WireGuard Portal."
},
"button-add-user": "Thêm Người dùng",
"button-show-user": "Hiển thị Người dùng",
"button-edit-user": "Chỉnh sửa Người dùng",
"user-disabled": "Người dùng đã bị vô hiệu hóa, lý do:",
"user-locked": "Tài khoản bị khóa, lý do:",
"admin": "Người dùng có quyền quản trị",
"no-admin": "Người dùng không có quyền quản trị"
},
"profile": {
"headline": "Các Peer VPN của Tôi",
"table-heading": {
"name": "Tên",
"ip": "Địa chỉ IP",
"stats": "Trạng thái",
"interface": "Giao diện Máy chủ"
},
"no-peer": {
"headline": "Không có peer nào",
"abstract": "Hiện tại, không có peer nào liên kết với hồ sơ người dùng của bạn."
},
"peer-connected": "Đã kết nối",
"button-add-peer": "Thêm Peer",
"button-show-peer": "Hiển thị Peer",
"button-edit-peer": "Chỉnh sửa Peer"
},
"modals": {
"user-view": {
"headline": "Tài khoản Người dùng:",
"tab-user": "Thông tin",
"tab-peers": "Peers",
"headline-info": "Thông tin Người dùng:",
"headline-notes": "Ghi chú:",
"email": "E-Mail",
"firstname": "Tên",
"lastname": "Họ",
"phone": "Số điện thoại",
"department": "Phòng ban",
"disabled": "Tài khoản bị vô hiệu hóa",
"locked": "Tài khoản bị khóa",
"no-peers": "Người dùng không có peers liên kết.",
"peers": {
"name": "Tên",
"interface": "Giao diện",
"ip": "Địa chỉ IP"
}
},
"user-edit": {
"headline-edit": "Chỉnh sửa người dùng:",
"headline-new": "Người dùng mới",
"header-general": "Chung",
"header-personal": "Thông tin Người dùng",
"header-notes": "Ghi chú",
"header-state": "Trạng thái",
"identifier": {
"label": "Mã định danh",
"placeholder": "Mã định danh người dùng duy nhất"
},
"source": {
"label": "Nguồn",
"placeholder": "Nguồn gốc của người dùng"
},
"password": {
"label": "Mật khẩu",
"placeholder": "Mật khẩu siêu bí mật",
"description": "Để trống trường này để giữ nguyên mật khẩu hiện tại."
},
"email": {
"label": "Email",
"placeholder": "Địa chỉ email"
},
"phone": {
"label": "Điện thoại",
"placeholder": "Số điện thoại"
},
"department": {
"label": "Phòng ban",
"placeholder": "Phòng ban"
},
"firstname": {
"label": "Tên",
"placeholder": "Tên"
},
"lastname": {
"label": "Họ",
"placeholder": "Họ"
},
"notes": {
"label": "Ghi chú",
"placeholder": "Chú thích thêm"
},
"disabled": {
"label": "Vô hiệu hóa (không thể kết nối WireGuard và không thể đăng nhập)"
},
"locked": {
"label": "Khóa (không thể đăng nhập, kết nối WireGuard vẫn hoạt động)"
},
"admin": {
"label": "Là Quản trị viên"
}
},
"interface-view": {
"headline": "Cấu hình cho Giao diện:"
},
"interface-edit": {
"headline-edit": "Chỉnh sửa Giao diện:",
"headline-new": "Giao diện Mới",
"tab-interface": "Giao diện",
"tab-peerdef": "Cài đặt Mặc định của Peer",
"header-general": "Chung",
"header-network": "Mạng",
"header-crypto": "Mã hóa",
"header-hooks": "Kẹp Giao diện",
"header-peer-hooks": "Kẹp Peer",
"header-state": "Trạng thái",
"identifier": {
"label": "Mã định danh",
"placeholder": "Mã định danh giao diện duy nhất"
},
"mode": {
"label": "Chế độ Giao diện",
"server": "Chế độ Máy chủ",
"client": "Chế độ Khách hàng",
"any": "Chế độ Không xác định"
},
"display-name": {
"label": "Tên Hiển thị",
"placeholder": "Tên mô tả cho giao diện"
},
"private-key": {
"label": "Khóa Riêng",
"placeholder": "Khóa riêng"
},
"public-key": {
"label": "Khóa Công khai",
"placeholder": "Khóa công khai"
},
"ip": {
"label": "Địa chỉ IP",
"placeholder": "Địa chỉ IP (định dạng CIDR)"
},
"listen-port": {
"label": "Cổng Nghe",
"placeholder": "Cổng nghe"
},
"dns": {
"label": "Máy chủ DNS",
"placeholder": "Các máy chủ DNS sẽ được sử dụng"
},
"dns-search": {
"label": "Tên miền Tìm kiếm DNS",
"placeholder": "Tiền tố tìm kiếm DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU của giao diện (0 = giữ mặc định)"
},
"firewall-mark": {
"label": "Đánh dấu Tường lửa",
"placeholder": "Đánh dấu tường lửa áp dụng cho lưu lượng đi. (0 = tự động)"
},
"routing-table": {
"label": "Bảng Định tuyến",
"placeholder": "ID bảng định tuyến",
"description": "Các trường hợp đặc biệt: off = không quản lý các tuyến đường, 0 = tự động"
},
"pre-up": {
"label": "Trước khi Bật",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"post-up": {
"label": "Sau khi Bật",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"pre-down": {
"label": "Trước khi Tắt",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"post-down": {
"label": "Sau khi Tắt",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"disabled": {
"label": "Giao diện Bị vô hiệu hóa"
},
"save-config": {
"label": "Tự động lưu cấu hình wg-quick"
},
"defaults": {
"endpoint": {
"label": "Địa chỉ Endpoint",
"placeholder": "Địa chỉ Endpoint",
"description": "Địa chỉ endpoint mà các peer sẽ kết nối tới."
},
"networks": {
"label": "Mạng IP",
"placeholder": "Địa chỉ Mạng",
"description": "Các peer sẽ nhận địa chỉ IP từ những mạng con này."
},
"allowed-ip": {
"label": "Địa chỉ IP Được phép",
"placeholder": "Địa chỉ IP Được phép mặc định"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU của client (0 = giữ mặc định)"
},
"keep-alive": {
"label": "Khoảng thời gian Giữ kết nối",
"placeholder": "Giữ kết nối liên tục (0 = mặc định)"
}
},
"button-apply-defaults": "Áp dụng Cài đặt Mặc định của Peer"
},
"peer-view": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"section-info": "Thông tin Peer",
"section-status": "Trạng thái Hiện tại",
"section-config": "Cấu hình",
"identifier": "Mã định danh",
"ip": "Địa chỉ IP",
"user": "Người dùng Liên kết",
"notes": "Ghi chú",
"expiry-status": "Hết hạn vào",
"disabled-status": "Bị Vô hiệu hóa vào",
"traffic": "Lưu lượng",
"connection-status": "Thông tin Kết nối",
"upload": "Số Byte Tải lên (từ Máy chủ đến Peer)",
"download": "Số Byte Tải xuống (từ Peer đến Máy chủ)",
"pingable": "Có thể Ping",
"handshake": "Lần bắt tay cuối cùng",
"connected-since": "Kết nối từ",
"endpoint": "Endpoint",
"button-download": "Tải cấu hình",
"button-email": "Gửi cấu hình qua Email"
},
"peer-edit": {
"headline-edit-peer": "Chỉnh sửa Peer:",
"headline-edit-endpoint": "Chỉnh sửa Endpoint:",
"headline-new-peer": "Tạo Peer mới",
"headline-new-endpoint": "Tạo Endpoint mới",
"header-general": "Chung",
"header-network": "Mạng",
"header-crypto": "Mã hóa",
"header-hooks": "Kẹp (Thực thi trên Peer)",
"header-state": "Trạng thái",
"display-name": {
"label": "Tên Hiển thị",
"placeholder": "Tên mô tả cho peer"
},
"linked-user": {
"label": "Người dùng Liên kết",
"placeholder": "Tài khoản người dùng sở hữu peer này"
},
"private-key": {
"label": "Khóa Riêng",
"placeholder": "Khóa riêng"
},
"public-key": {
"label": "Khóa Công khai",
"placeholder": "Khóa công khai"
},
"preshared-key": {
"label": "Khóa Preshared",
"placeholder": "Khóa chia sẻ trước (tuỳ chọn)"
},
"endpoint-public-key": {
"label": "Khóa Công khai của Endpoint",
"placeholder": "Khóa công khai của endpoint từ xa"
},
"endpoint": {
"label": "Địa chỉ Endpoint",
"placeholder": "Địa chỉ của endpoint từ xa"
},
"ip": {
"label": "Địa chỉ IP",
"placeholder": "Địa chỉ IP (định dạng CIDR)"
},
"allowed-ip": {
"label": "Địa chỉ IP Được phép",
"placeholder": "Địa chỉ IP Được phép (định dạng CIDR)"
},
"extra-allowed-ip": {
"label": "Địa chỉ IP Được phép Thêm",
"placeholder": "Địa chỉ IP Thêm (Phía Máy chủ)",
"description": "Những địa chỉ IP này sẽ được thêm vào giao diện WireGuard từ xa dưới dạng địa chỉ IP được phép."
},
"dns": {
"label": "Máy chủ DNS",
"placeholder": "Các máy chủ DNS sẽ được sử dụng"
},
"dns-search": {
"label": "Tên miền Tìm kiếm DNS",
"placeholder": "Tiền tố tìm kiếm DNS"
},
"keep-alive": {
"label": "Khoảng thời gian Giữ kết nối",
"placeholder": "Giữ kết nối liên tục (0 = mặc định)"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU của client (0 = giữ mặc định)"
},
"pre-up": {
"label": "Trước khi Bật",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"post-up": {
"label": "Sau khi Bật",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"pre-down": {
"label": "Trước khi Tắt",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"post-down": {
"label": "Sau khi Tắt",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"disabled": {
"label": "Peer Bị Vô hiệu hóa"
},
"ignore-global": {
"label": "Bỏ qua cài đặt toàn cầu"
},
"expires-at": {
"label": "Ngày hết hạn"
}
},
"peer-multi-create": {
"headline-peer": "Tạo nhiều peer",
"headline-endpoint": "Tạo nhiều endpoint",
"identifiers": {
"label": "Mã định danh Người dùng",
"placeholder": "Mã định danh Người dùng",
"description": "Một mã định danh người dùng (tên người dùng) cho mà một peer sẽ được tạo ra."
},
"prefix": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"label": "Tiền tố Tên Hiển thị",
"placeholder": "Tiền tố",
"description": "Một tiền tố được thêm vào tên hiển thị của các peer."
}
}
}
}

View File

@@ -0,0 +1,492 @@
{
"languages": {
"zh": "中文"
},
"general": {
"pagination": {
"size": "每页显示数量",
"all": "全部 (较慢)"
},
"search": {
"placeholder": "搜索...",
"button": "搜索"
},
"select-all": "全选",
"yes": "是",
"no": "否",
"cancel": "取消",
"close": "关闭",
"save": "保存",
"delete": "删除"
},
"login": {
"headline": "请登录",
"username": {
"label": "用户名",
"placeholder": "请输入用户名"
},
"password": {
"label": "密码",
"placeholder": "请输入密码"
},
"button": "登录"
},
"menu": {
"home": "首页",
"interfaces": "接口",
"users": "用户",
"lang": "切换语言",
"profile": "个人资料",
"login": "登录",
"logout": "注销"
},
"home": {
"headline": "WireGuard® VPN Portal",
"info-headline": "更多信息",
"abstract": "WireGuard® 是一种极其简单但又快速现代的 VPN采用先进的加密技术。它旨在比 IPsec 更快、更简单、更轻量、更实用,同时避免了大量的麻烦。它的性能显著优于 OpenVPN。",
"installation": {
"box-header": "WireGuard 安装",
"headline": "安装",
"content": "客户端软件的安装说明可在官方 WireGuard 网站上找到。",
"button": "打开说明"
},
"about-wg": {
"box-header": "关于 WireGuard",
"headline": "关于",
"content": "WireGuard® 是一种极其简单但又快速现代的 VPN采用先进的加密技术。",
"button": "更多"
},
"about-portal": {
"box-header": "关于 WireGuard Portal",
"headline": "WireGuard Portal",
"content": "WireGuard Portal 是一个简单的基于网页的 WireGuard 配置平台。",
"button": "更多"
},
"profiles": {
"headline": "VPN 配置文件",
"abstract": "您可以通过您的用户个人资料访问并下载个人 VPN 配置。",
"content": "要查找您所有的配置文件,请点击下面的按钮。",
"button": "打开我的个人资料"
},
"admin": {
"headline": "后台管理",
"abstract": "在后台管理,您可以管理 WireGuard 节点和服务器接口,以及允许登录 WireGuard Portal 的用户。",
"content": "",
"button-admin": "打开服务器管理",
"button-user": "打开用户管理"
}
},
"interfaces": {
"headline": "接口管理",
"headline-peers": "当前 VPN 节点",
"headline-endpoints": "当前节点",
"no-interface": {
"default-selection": "没有可用接口",
"headline": "未找到接口...",
"abstract": "点击上面的加号按钮以创建新的 WireGuard 接口。"
},
"no-peer": {
"headline": "没有可用节点",
"abstract": "当前没有可用的节点与所选的 WireGuard 接口关联。"
},
"table-heading": {
"name": "名称",
"user": "用户",
"ip": "IP 地址",
"endpoint": "节点",
"status": "状态"
},
"interface": {
"headline": "接口状态",
"mode": "模式",
"key": "公钥",
"endpoint": "公开节点",
"port": "监听端口",
"peers": "启用节点",
"total-peers": "节点总数",
"endpoints": "启用节点",
"total-endpoints": "节点总数",
"ip": "IP 地址",
"default-allowed-ip": "默认允许的 IP",
"dns": "DNS 服务器",
"mtu": "MTU",
"default-keep-alive": "默认心跳包间隔",
"button-show-config": "显示配置",
"button-download-config": "下载配置",
"button-store-config": "为 wg-quick 保存配置",
"button-edit": "编辑接口"
},
"button-add-interface": "添加接口",
"button-add-peer": "添加节点",
"button-add-peers": "添加多个节点",
"button-show-peer": "显示节点",
"button-edit-peer": "编辑节点",
"peer-disabled": "节点已禁用,原因: ",
"peer-expiring": "节点将在以下时间过期: ",
"peer-connected": "已连接",
"peer-not-connected": "未连接",
"peer-handshake": "最后一次握手: "
},
"users": {
"headline": "用户管理",
"table-heading": {
"id": "ID",
"email": "电子邮件",
"firstname": "名",
"lastname": "姓",
"source": "来源",
"peers": "节点",
"admin": "管理员"
},
"no-user": {
"headline": "没有可用用户",
"abstract": "当前没有用户注册 WireGuard 门户。"
},
"button-add-user": "添加用户",
"button-show-user": "显示用户",
"button-edit-user": "编辑用户",
"user-disabled": "用户已禁用, 原因: ",
"user-locked": "账户已锁定, 原因: ",
"admin": "用户具有管理员权限",
"no-admin": "用户没有管理员权限"
},
"profile": {
"headline": "我的 VPN 节点列表",
"table-heading": {
"name": "名称",
"ip": "IP 地址",
"stats": "状态",
"interface": "服务器接口"
},
"no-peer": {
"headline": "没有可用节点",
"abstract": "当前没有与您的用户个人资料关联的节点。"
},
"peer-connected": "已连接",
"button-add-peer": "添加节点",
"button-show-peer": "显示节点",
"button-edit-peer": "编辑节点"
},
"modals": {
"user-view": {
"headline": "用户账户: ",
"tab-user": "信息",
"tab-peers": "节点",
"headline-info": "用户信息: ",
"headline-notes": "备注: ",
"email": "电子邮件",
"firstname": "名",
"lastname": "姓",
"phone": "电话号码",
"department": "部门",
"disabled": "账户已禁用",
"locked": "账户已锁定",
"no-peers": "用户没有关联的节点。",
"peers": {
"name": "名称",
"interface": "接口",
"ip": "IP 地址"
}
},
"user-edit": {
"headline-edit": "编辑用户: ",
"headline-new": "新用户",
"header-general": "常规",
"header-personal": "用户信息",
"header-notes": "备注",
"header-state": "状态",
"identifier": {
"label": "标识符",
"placeholder": "唯一用户标识符"
},
"source": {
"label": "来源",
"placeholder": "用户来源"
},
"password": {
"label": "密码",
"placeholder": "一个复杂的密码",
"description": "留空保持当前密码不变。"
},
"email": {
"label": "电子邮件",
"placeholder": "电子邮件地址"
},
"phone": {
"label": "电话",
"placeholder": "电话号码"
},
"department": {
"label": "部门",
"placeholder": "部门"
},
"firstname": {
"label": "名",
"placeholder": "名"
},
"lastname": {
"label": "姓",
"placeholder": "姓"
},
"notes": {
"label": "备注",
"placeholder": ""
},
"disabled": {
"label": "禁用 (无法连接 WireGuard 和登录)"
},
"locked": {
"label": "锁定 (无法登录,但 WireGuard 仍然可以连接)"
},
"admin": {
"label": "管理员"
}
},
"interface-view": {
"headline": "接口配置: "
},
"interface-edit": {
"headline-edit": "编辑接口: ",
"headline-new": "新接口",
"tab-interface": "接口",
"tab-peerdef": "节点默认值",
"header-general": "常规",
"header-network": "网络",
"header-crypto": "加密",
"header-hooks": "接口 Hooks",
"header-peer-hooks": "Hooks",
"header-state": "状态",
"identifier": {
"label": "标识符",
"placeholder": "唯一接口标识符"
},
"mode": {
"label": "接口模式",
"server": "服务器模式",
"client": "客户端模式",
"any": "未知模式"
},
"display-name": {
"label": "显示名称",
"placeholder": "接口的描述性名称"
},
"private-key": {
"label": "私钥",
"placeholder": "私钥"
},
"public-key": {
"label": "公钥",
"placeholder": "公钥"
},
"ip": {
"label": "IP 地址",
"placeholder": "IP 地址 (CIDR 格式)"
},
"listen-port": {
"label": "监听端口",
"placeholder": "监听端口"
},
"dns": {
"label": "DNS 服务器",
"placeholder": "应使用的 DNS 服务器"
},
"dns-search": {
"label": "DNS 搜索域",
"placeholder": "DNS 搜索前缀"
},
"mtu": {
"label": "MTU",
"placeholder": "接口 MTU (0 = 保持默认)"
},
"firewall-mark": {
"label": "防火墙掩码",
"placeholder": "应用于出站流量的防火墙掩码 (0 = 自动)"
},
"routing-table": {
"label": "路由表",
"placeholder": "路由表 ID",
"description": "特殊情况: off = 不管理路由, 0 = 自动"
},
"pre-up": {
"label": "启动前脚本",
"placeholder": "一个或多个用分号分隔的 bash 命令"
},
"post-up": {
"label": "启动后脚本",
"placeholder": "一个或多个用分号分隔的 bash 命令"
},
"pre-down": {
"label": "停止前脚本",
"placeholder": "一个或多个用分号分隔的 bash 命令"
},
"post-down": {
"label": "停止后脚本",
"placeholder": "一个或多个用分号分隔的 bash 命令"
},
"disabled": {
"label": "接口已禁用"
},
"save-config": {
"label": "自动保存 wg-quick 配置"
},
"defaults": {
"endpoint": {
"label": "服务器地址",
"placeholder": "服务器地址",
"description": "节点将连接到服务器的地址。"
},
"networks": {
"label": "IP 地址",
"placeholder": "IP 地址",
"description": "节点将从这些子网获取 IP 地址。"
},
"allowed-ip": {
"label": "允许的 IP 地址",
"placeholder": "默认允许的 IP 地址"
},
"mtu": {
"label": "MTU",
"placeholder": "客户端 MTU (0 = 保持默认)"
},
"keep-alive": {
"label": "心跳包间隔",
"placeholder": "持久保持连接 (0 = 默认)"
}
},
"button-apply-defaults": "应用节点默认值"
},
"peer-view": {
"headline-peer": "节点: ",
"headline-endpoint": "节点: ",
"section-info": "节点信息",
"section-status": "当前状态",
"section-config": "配置",
"identifier": "标识符",
"ip": "IP 地址",
"user": "关联用户",
"notes": "备注",
"expiry-status": "过期时间",
"disabled-status": "禁用时间",
"traffic": "流量",
"connection-status": "连接状态",
"upload": "上传字节 (服务器到节点)",
"download": "下载字节 (节点到服务器)",
"pingable": "连通状态",
"handshake": "最后握手",
"connected-since": "首次成功连接",
"endpoint": "节点地址",
"button-download": "下载配置",
"button-email": "通过电子邮件发送配置"
},
"peer-edit": {
"headline-edit-peer": "编辑节点: ",
"headline-edit-endpoint": "编辑节点: ",
"headline-new-peer": "创建节点",
"headline-new-endpoint": "创建节点",
"header-general": "常规",
"header-network": "网络",
"header-crypto": "加密",
"header-hooks": "Hooks (在节点执行)",
"header-state": "状态",
"display-name": {
"label": "显示名称",
"placeholder": "节点的描述性名称"
},
"linked-user": {
"label": "关联用户",
"placeholder": "拥有此节点的用户账户"
},
"private-key": {
"label": "私钥",
"placeholder": "私钥"
},
"public-key": {
"label": "公钥",
"placeholder": "公钥"
},
"preshared-key": {
"label": "预共享密钥",
"placeholder": "可选的预共享密钥"
},
"endpoint-public-key": {
"label": "节点公钥",
"placeholder": "远程节点的公钥"
},
"endpoint": {
"label": "节点地址",
"placeholder": "远程节点的地址"
},
"ip": {
"label": "IP 地址",
"placeholder": "IP 地址(CIDR 格式)"
},
"allowed-ip": {
"label": "允许的 IP 地址",
"placeholder": "允许的 IP 地址(CIDR 格式)"
},
"extra-allowed-ip": {
"label": "额外允许的 IP 地址",
"placeholder": "额外允许的 IP 地址(服务器端)",
"description": "这些 IP 将作为允许的 IP 添加到远程 WireGuard 接口。"
},
"dns": {
"label": "DNS 服务器",
"placeholder": "要使用的 DNS 服务器"
},
"dns-search": {
"label": "DNS 搜索域",
"placeholder": "DNS 搜索前缀"
},
"keep-alive": {
"label": "心跳包间隔",
"placeholder": "持久保持连接 (0 = 默认)"
},
"mtu": {
"label": "MTU",
"placeholder": "客户端 MTU (0 = 保持默认)"
},
"pre-up": {
"label": "启动前脚本",
"placeholder": "一个或多个用分号分隔的 bash 命令"
},
"post-up": {
"label": "启动后脚本",
"placeholder": "一个或多个用分号分隔的 bash 命令"
},
"pre-down": {
"label": "停止前脚本",
"placeholder": "一个或多个用分号分隔的 bash 命令"
},
"post-down": {
"label": "停止后脚本",
"placeholder": "一个或多个用分号分隔的 bash 命令"
},
"disabled": {
"label": "节点已禁用"
},
"ignore-global": {
"label": "忽略全局设置"
},
"expires-at": {
"label": "过期日期"
}
},
"peer-multi-create": {
"headline-peer": "创建多个节点",
"headline-endpoint": "创建多个节点",
"identifiers": {
"label": "用户标识符",
"placeholder": "用户标识符",
"description": "要为其创建节点 的用户标识符(用户名)。"
},
"prefix": {
"headline-peer": "节点: ",
"headline-endpoint": "节点: ",
"label": "显示名称前缀",
"placeholder": "前缀",
"description": "添加到节点 显示名称的前缀。"
}
}
}
}

View File

@@ -4,6 +4,7 @@ import {notify} from "@kyvg/vue3-notification";
import {interfaceStore} from "./interfaces";
import {freshPeer, freshStats} from '@/helpers/models';
import { base64_url_encode } from '@/helpers/encoding';
import { ipToBigInt } from '@/helpers/utils';
const baseUrl = `/peer`
@@ -21,6 +22,8 @@ export const peerStore = defineStore({
pageOffset: 0,
pages: [],
fetching: false,
sortKey: 'IsConnected', // Default sort key
sortOrder: -1, // 1 for ascending, -1 for descending
}),
getters: {
Find: (state) => {
@@ -39,8 +42,30 @@ export const peerStore = defineStore({
return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
})
},
Sorted: (state) => {
return state.Filtered.slice().sort((a, b) => {
let aValue = a[state.sortKey];
let bValue = b[state.sortKey];
if (state.sortKey === 'Addresses') {
aValue = aValue.length > 0 ? ipToBigInt(aValue[0]) : 0;
bValue = bValue.length > 0 ? ipToBigInt(bValue[0]) : 0;
}
if (state.sortKey === 'IsConnected') {
aValue = state.statsEnabled && state.stats[a.Identifier]?.IsConnected ? 1 : 0;
bValue = state.statsEnabled && state.stats[b.Identifier]?.IsConnected ? 1 : 0;
}
if (state.sortKey === 'Traffic') {
aValue = state.statsEnabled ? (state.stats[a.Identifier].BytesReceived + state.stats[a.Identifier].BytesTransmitted) : 0;
bValue = state.statsEnabled ? (state.stats[b.Identifier].BytesReceived + state.stats[b.Identifier].BytesTransmitted) : 0;
}
let result = 0;
if (aValue > bValue) result = 1;
if (aValue < bValue) result = -1;
return state.sortOrder === 1 ? result : -result;
});
},
FilteredAndPaged: (state) => {
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
return state.Sorted.slice(state.pageOffset, state.pageOffset + state.pageSize);
},
ConfigQrUrl: (state) => {
return (id) => state.peers.find((p) => p.Identifier === id) ? apiWrapper.url(`${baseUrl}/config-qr/${base64_url_encode(id)}`) : ''

View File

@@ -4,6 +4,7 @@ import {notify} from "@kyvg/vue3-notification";
import {authStore} from "@/stores/auth";
import { base64_url_encode } from '@/helpers/encoding';
import {freshStats} from "@/helpers/models";
import { ipToBigInt } from '@/helpers/utils';
const baseUrl = `/user`
@@ -19,6 +20,8 @@ export const profileStore = defineStore({
pageOffset: 0,
pages: [],
fetching: false,
sortKey: 'IsConnected', // Default sort key
sortOrder: -1, // 1 for ascending, -1 for descending
}),
getters: {
FindPeers: (state) => {
@@ -35,8 +38,30 @@ export const profileStore = defineStore({
return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
})
},
Sorted: (state) => {
return state.FilteredPeers.slice().sort((a, b) => {
let aValue = a[state.sortKey];
let bValue = b[state.sortKey];
if (state.sortKey === 'Addresses') {
aValue = aValue.length > 0 ? ipToBigInt(aValue[0]) : 0;
bValue = bValue.length > 0 ? ipToBigInt(bValue[0]) : 0;
}
if (state.sortKey === 'IsConnected') {
aValue = state.statsEnabled && state.stats[a.Identifier]?.IsConnected ? 1 : 0;
bValue = state.statsEnabled && state.stats[b.Identifier]?.IsConnected ? 1 : 0;
}
if (state.sortKey === 'Traffic') {
aValue = state.statsEnabled ? (state.stats[a.Identifier].BytesReceived + state.stats[a.Identifier].BytesTransmitted) : 0;
bValue = state.statsEnabled ? (state.stats[b.Identifier].BytesReceived + state.stats[b.Identifier].BytesTransmitted) : 0;
}
let result = 0;
if (aValue > bValue) result = 1;
if (aValue < bValue) result = -1;
return state.sortOrder === 1 ? result : -result;
});
},
FilteredAndPagedPeers: (state) => {
return state.FilteredPeers.slice(state.pageOffset, state.pageOffset + state.pageSize)
return state.Sorted.slice(state.pageOffset, state.pageOffset + state.pageSize);
},
isFetching: (state) => state.fetching,
hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize),

View File

@@ -10,6 +10,7 @@ import {peerStore} from "@/stores/peers";
import {interfaceStore} from "@/stores/interfaces";
import {notify} from "@kyvg/vue3-notification";
import {settingsStore} from "@/stores/settings";
import {humanFileSize} from '@/helpers/utils';
const settings = settingsStore()
const interfaces = interfaceStore()
@@ -21,6 +22,20 @@ const multiCreatePeerId = ref("")
const editInterfaceId = ref("")
const viewedInterfaceId = ref("")
const sortKey = ref("");
const sortOrder = ref(1);
function sortBy(key) {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value * -1; // Toggle sort order
} else {
sortKey.value = key;
sortOrder.value = 1; // Default to ascending
}
peers.sortKey = sortKey.value;
peers.sortOrder = sortOrder.value;
}
function calculateInterfaceName(id, name) {
let result = id
if (name) {
@@ -37,7 +52,7 @@ async function download() {
let text = interfaces.configuration
let element = document.createElement('a')
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
element.setAttribute('download', filename)
element.style.display = 'none'
@@ -94,7 +109,7 @@ onMounted(async () => {
<button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-interface')" @click.prevent="editInterfaceId='#NEW#'">
<i class="fa-solid fa-plus-circle"></i>
</button>
<select v-model="interfaces.selected" :disabled="interfaces.Count===0" class="form-select" @change="peers.LoadPeers()">
<select v-model="interfaces.selected" :disabled="interfaces.Count===0" class="form-select" @change="() => { peers.LoadPeers(); peers.LoadStats() }">
<option v-if="interfaces.Count===0" value="nothing">{{ $t('interfaces.no-interface.default-selection') }}</option>
<option v-for="iface in interfaces.All" :key="iface.Identifier" :value="iface.Identifier">{{ calculateInterfaceName(iface.Identifier,iface.DisplayName) }}</option>
</select>
@@ -138,7 +153,7 @@ onMounted(async () => {
<tbody>
<tr>
<td>{{ $t('interfaces.interface.key') }}:</td>
<td>{{interfaces.GetSelected.PublicKey}}</td>
<td class="text-wrap">{{interfaces.GetSelected.PublicKey}}</td>
</tr>
<tr>
<td>{{ $t('interfaces.interface.endpoint') }}:</td>
@@ -192,7 +207,7 @@ onMounted(async () => {
<tbody>
<tr>
<td>{{ $t('interfaces.interface.key') }}:</td>
<td>{{interfaces.GetSelected.PublicKey}}</td>
<td class="text-wrap">{{interfaces.GetSelected.PublicKey}}</td>
</tr>
<tr>
<td>{{ $t('interfaces.interface.endpoints') }}:</td>
@@ -230,7 +245,7 @@ onMounted(async () => {
<tbody>
<tr>
<td>{{ $t('interfaces.interface.key') }}:</td>
<td>{{interfaces.GetSelected.PublicKey}}</td>
<td class="text-wrap">{{interfaces.GetSelected.PublicKey}}</td>
</tr>
<tr>
<td>{{ $t('interfaces.interface.endpoint') }}:</td>
@@ -314,11 +329,28 @@ onMounted(async () => {
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox" value="">
</th><!-- select -->
<th scope="col"></th><!-- status -->
<th scope="col">{{ $t('interfaces.table-heading.name') }}</th>
<th scope="col">{{ $t('interfaces.table-heading.user') }}</th>
<th scope="col">{{ $t('interfaces.table-heading.ip') }}</th>
<th v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.table-heading.endpoint') }}</th>
<th v-if="peers.hasStatistics" scope="col">{{ $t('interfaces.table-heading.status') }}</th>
<th scope="col" @click="sortBy('DisplayName')">
{{ $t("interfaces.table-heading.name") }}
<i v-if="sortKey === 'DisplayName'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th scope="col" @click="sortBy('UserIdentifier')">
{{ $t("interfaces.table-heading.user") }}
<i v-if="sortKey === 'UserIdentifier'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th scope="col" @click="sortBy('Addresses')">
{{ $t("interfaces.table-heading.ip") }}
<i v-if="sortKey === 'Addresses'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th v-if="interfaces.GetSelected.Mode === 'client'" scope="col">
{{ $t("interfaces.table-heading.endpoint") }}
</th>
<th v-if="peers.hasStatistics" scope="col" @click="sortBy('IsConnected')">
{{ $t("interfaces.table-heading.status") }}
<i v-if="sortKey === 'IsConnected'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th v-if="peers.hasStatistics" scope="col" @click="sortBy('Traffic')">RX/TX
<i v-if="sortKey === 'Traffic'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th scope="col"></th><!-- Actions -->
</tr>
</thead>
@@ -345,6 +377,9 @@ onMounted(async () => {
<span class="badge rounded-pill bg-light" :title="$t('interfaces.peer-not-connected')"><i class="fa-solid fa-link-slash"></i></span>
</div>
</td>
<td v-if="peers.hasStatistics" >
<span class="text-center" >{{ humanFileSize(peers.Statistics(peer.Identifier).BytesReceived) }} / {{ humanFileSize(peers.Statistics(peer.Identifier).BytesTransmitted) }}</span>
</td>
<td class="text-center">
<a href="#" :title="$t('interfaces.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
<a href="#" :title="$t('interfaces.button-edit-peer')" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>

View File

@@ -5,6 +5,7 @@ import { onMounted, ref } from "vue";
import { profileStore } from "@/stores/profile";
import PeerEditModal from "@/components/PeerEditModal.vue";
import { settingsStore } from "@/stores/settings";
import { humanFileSize } from "@/helpers/utils";
const settings = settingsStore()
const profile = profileStore()
@@ -12,10 +13,25 @@ const profile = profileStore()
const viewedPeerId = ref("")
const editPeerId = ref("")
const sortKey = ref("");
const sortOrder = ref(1);
function sortBy(key) {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value * -1; // Toggle sort order
} else {
sortKey.value = key;
sortOrder.value = 1; // Default to ascending
}
profile.sortKey = sortKey.value;
profile.sortOrder = sortOrder.value;
}
onMounted(async () => {
await profile.LoadUser()
await profile.LoadPeers()
await profile.LoadStats()
await profile.calculatePages(); // Forces to show initial page number
})
</script>
@@ -41,7 +57,7 @@ onMounted(async () => {
</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('general.search.button-add-peer')" @click.prevent="editPeerId = '#NEW#'"><i
: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>
</div>
@@ -58,9 +74,21 @@ onMounted(async () => {
value="">
</th><!-- select -->
<th scope="col"></th><!-- status -->
<th scope="col">{{ $t('profile.table-heading.name') }}</th>
<th scope="col">{{ $t('profile.table-heading.ip') }}</th>
<th v-if="profile.hasStatistics" scope="col">{{ $t('profile.table-heading.stats') }}</th>
<th scope="col" @click="sortBy('DisplayName')">
{{ $t("profile.table-heading.name") }}
<i v-if="sortKey === 'DisplayName'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th scope="col" @click="sortBy('Addresses')">
{{ $t("profile.table-heading.ip") }}
<i v-if="sortKey === 'Addresses'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th v-if="profile.hasStatistics" scope="col" @click="sortBy('IsConnected')">
{{ $t("profile.table-heading.stats") }}
<i v-if="sortKey === 'IsConnected'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th v-if="profile.hasStatistics" scope="col" @click="sortBy('Traffic')">RX/TX
<i v-if="sortKey === 'Traffic'" :class="sortOrder === 1 ? 'asc' : 'desc'"></i>
</th>
<th scope="col">{{ $t('profile.table-heading.interface') }}</th>
<th scope="col"></th><!-- Actions -->
</tr>
@@ -90,6 +118,9 @@ onMounted(async () => {
<span class="badge rounded-pill bg-light"><i class="fa-solid fa-link-slash"></i></span>
</div>
</td>
<td v-if="profile.hasStatistics" >
<span class="text-center" >{{ humanFileSize(profile.Statistics(peer.Identifier).BytesReceived) }} / {{ humanFileSize(profile.Statistics(peer.Identifier).BytesTransmitted) }}</span>
</td>
<td>{{ peer.InterfaceIdentifier }}</td>
<td class="text-center">
<a href="#" :title="$t('profile.button-show-peer')" @click.prevent="viewedPeerId = peer.Identifier"><i

109
go.mod
View File

@@ -1,110 +1,119 @@
module github.com/h44z/wg-portal
go 1.21
go 1.23
require (
github.com/coreos/go-oidc/v3 v3.10.0
github.com/gin-contrib/cors v1.7.1
github.com/gin-contrib/sessions v1.0.0
github.com/gin-gonic/gin v1.9.1
github.com/a8m/envsubst v1.4.2
github.com/coreos/go-oidc/v3 v3.12.0
github.com/gin-contrib/cors v1.7.3
github.com/gin-contrib/sessions v1.0.2
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.6
github.com/prometheus-community/pro-bing v0.4.0
github.com/go-ldap/ldap/v3 v3.4.10
github.com/prometheus-community/pro-bing v0.5.0
github.com/prometheus/client_golang v1.20.5
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/swaggo/swag v1.16.3
github.com/stretchr/testify v1.10.0
github.com/swaggo/swag v1.16.4
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
github.com/vardius/message-bus v1.1.5
github.com/vishvananda/netlink v1.1.0
github.com/vishvananda/netlink v1.3.0
github.com/xhit/go-simple-mail/v2 v2.16.0
github.com/yeqown/go-qrcode/v2 v2.2.2
golang.org/x/crypto v0.21.0
golang.org/x/oauth2 v0.18.0
golang.org/x/sys v0.18.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
github.com/yeqown/go-qrcode/v2 v2.2.4
golang.org/x/crypto v0.31.0
golang.org/x/oauth2 v0.25.0
golang.org/x/sys v0.29.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.5.6
gorm.io/driver/postgres v1.5.7
gorm.io/driver/sqlserver v1.5.3
gorm.io/gorm v1.25.9
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlserver v1.5.4
gorm.io/gorm v1.25.12
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.11.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.12.6 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dchest/uniuri v1.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/go-test/deep v1.1.1 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.2 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/microsoft/go-mssqldb v1.7.0 // indirect
github.com/microsoft/go-mssqldb v1.8.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.61.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.19.0 // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.49.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.29.5 // indirect
modernc.org/libc v1.61.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.1 // indirect
modernc.org/sqlite v1.34.4 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

341
go.sum
View File

@@ -1,51 +1,52 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg=
github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -56,31 +57,31 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/gin-contrib/cors v1.7.1 h1:s9SIppU/rk8enVvkzwiC2VK3UZ/0NNGsWfUKvV55rqs=
github.com/gin-contrib/cors v1.7.1/go.mod h1:n/Zj7B4xyrgk/cX1WCX2dkzFfaNm/xJb6oIUk7WTtps=
github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963/go.mod h1:4lkInX8nHSR62NSmhXM3xtPeMSyfiR58NaEz+om1lHM=
github.com/gin-contrib/sessions v1.0.0 h1:r5GLta4Oy5xo9rAwMHx8B4wLpeRGHMdz9NafzJAdP8Y=
github.com/gin-contrib/sessions v1.0.0/go.mod h1:DN0f4bvpqMQElDdi+gNGScrP2QEI04IErRyMFyorUOI=
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
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/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@@ -95,41 +96,35 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -141,23 +136,30 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -171,9 +173,11 @@ github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -186,8 +190,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -198,9 +202,9 @@ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs=
github.com/microsoft/go-mssqldb v1.7.0/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -211,17 +215,27 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus-community/pro-bing v0.5.0 h1:Fq+4BUXKIvsPtXUY8K+04ud9dkAuFozqGmRAyNUpffY=
github.com/prometheus-community/pro-bing v0.5.0/go.mod h1:1joR9oXdMEAcAJJvhs+8vNDvTg5thfAZcRFhcUozG2g=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
@@ -243,12 +257,11 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY=
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
@@ -263,111 +276,140 @@ github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca h1:lpvAjPK+PcxnbcB
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca/go.mod h1:XXKxNbpoLihvvT7orUZbs/iZayg1n4ip7iJakJPAwA8=
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
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/go-qrcode/v2 v2.2.4 h1:cXdYlrhzHzVAnJHiwr/T6lAUmS9MtEStjEZBjArrvnc=
github.com/yeqown/go-qrcode/v2 v2.2.4/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
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=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -383,41 +425,40 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0=
gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/cc/v4 v4.19.5 h1:QlsZyQ1zf78DGeqnQ9ILi9hXyMdoC5e1qoGNUyBjHQw=
modernc.org/cc/v4 v4.19.5/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.13.1 h1:qBttaSxEHNze36VBivw1/vkHuyjMDN3RY5wQX+p1Oxg=
modernc.org/ccgo/v4 v4.13.1/go.mod h1:Td6RI9W9G2ZpKHaJ7UeGEiB2aIpoDqLBnm4wtkbJTbQ=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.24.2 h1:uektamHbSXU7egelXcyVpMaaAsrRH4/+uMKUQAQUdOw=
modernc.org/cc/v4 v4.24.2/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.49.0 h1:/kkNBuCXvlTbOGwrQdgR67eK1Y9+kR+fhdBd89C64VM=
modernc.org/libc v1.49.0/go.mod h1:DNz0lgQgT6FPIPm8rHtjFj0FL5/YOr/NYFXWYBcSxMw=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.61.6 h1:L2jW0wxHPCyHK0YSHaGaVlY0WxjpG/TTVdg6gRJOPqw=
modernc.org/libc v1.61.6/go.mod h1:G+DzuaCcReUYYg4nNSfigIfTDCENdj9EByglvaRx53A=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A=
modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@@ -4,15 +4,16 @@ import (
"context"
"errors"
"fmt"
"github.com/sirupsen/logrus"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
"gorm.io/gorm/utils"
"os"
"path/filepath"
"strings"
"time"
"github.com/sirupsen/logrus"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
"gorm.io/gorm/utils"
"github.com/glebarez/sqlite"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
@@ -204,7 +205,8 @@ func (r *SqlRepo) preCheck() error {
return nil // we probably don't have a V1 database =)
}
return fmt.Errorf("detected a WireGuard Portal V1 database (version: %s) - please migrate first", lastVersion.Version)
return fmt.Errorf("detected a WireGuard Portal V1 database (version: %s) - please migrate first",
lastVersion.Version)
}
func (r *SqlRepo) migrate() error {
@@ -249,7 +251,11 @@ func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifie
return &in, nil
}
func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) {
func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.Interface,
[]domain.Peer,
error,
) {
in, err := r.GetInterface(ctx, id)
if err != nil {
return nil, nil, fmt.Errorf("failed to load interface: %w", err)
@@ -305,7 +311,11 @@ func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.I
return users, nil
}
func (r *SqlRepo) SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.Interface) (*domain.Interface, error)) error {
func (r *SqlRepo) SaveInterface(
ctx context.Context,
id domain.InterfaceIdentifier,
updateFunc func(in *domain.Interface) (*domain.Interface, error),
) error {
userInfo := domain.GetUserInfo(ctx)
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
in, err := r.getOrCreateInterface(userInfo, tx, id)
@@ -333,7 +343,11 @@ func (r *SqlRepo) SaveInterface(ctx context.Context, id domain.InterfaceIdentifi
return nil
}
func (r *SqlRepo) getOrCreateInterface(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.InterfaceIdentifier) (*domain.Interface, error) {
func (r *SqlRepo) getOrCreateInterface(
ui *domain.ContextUserInfo,
tx *gorm.DB,
id domain.InterfaceIdentifier,
) (*domain.Interface, error) {
var in domain.Interface
// interfaceDefaults will be applied to newly created interface records
@@ -449,7 +463,10 @@ func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIden
return peers, nil
}
func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) ([]domain.Peer, error) {
func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) (
[]domain.Peer,
error,
) {
var peers []domain.Peer
searchValue := "%" + strings.ToLower(search) + "%"
@@ -492,7 +509,11 @@ func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, s
return peers, nil
}
func (r *SqlRepo) SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error {
func (r *SqlRepo) SavePeer(
ctx context.Context,
id domain.PeerIdentifier,
updateFunc func(in *domain.Peer) (*domain.Peer, error),
) error {
userInfo := domain.GetUserInfo(ctx)
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
peer, err := r.getOrCreatePeer(userInfo, tx, id)
@@ -520,7 +541,10 @@ func (r *SqlRepo) SavePeer(ctx context.Context, id domain.PeerIdentifier, update
return nil
}
func (r *SqlRepo) getOrCreatePeer(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.PeerIdentifier) (*domain.Peer, error) {
func (r *SqlRepo) getOrCreatePeer(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.PeerIdentifier) (
*domain.Peer,
error,
) {
var peer domain.Peer
// interfaceDefaults will be applied to newly created interface records
@@ -601,7 +625,10 @@ func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]d
return result, nil
}
func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error) {
func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (
map[domain.Cidr][]domain.Cidr,
error,
) {
var peerIps []struct {
domain.Cidr
PeerId domain.PeerIdentifier `gorm:"column:peer_identifier"`
@@ -699,7 +726,11 @@ func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User,
return users, nil
}
func (r *SqlRepo) SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error {
func (r *SqlRepo) SaveUser(
ctx context.Context,
id domain.UserIdentifier,
updateFunc func(u *domain.User) (*domain.User, error),
) error {
userInfo := domain.GetUserInfo(ctx)
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@@ -737,7 +768,10 @@ func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) erro
return nil
}
func (r *SqlRepo) getOrCreateUser(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.UserIdentifier) (*domain.User, error) {
func (r *SqlRepo) getOrCreateUser(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.UserIdentifier) (
*domain.User,
error,
) {
var user domain.User
// userDefaults will be applied to newly created user records
@@ -777,7 +811,11 @@ func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *doma
// region statistics
func (r *SqlRepo) UpdateInterfaceStatus(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.InterfaceStatus) (*domain.InterfaceStatus, error)) error {
func (r *SqlRepo) UpdateInterfaceStatus(
ctx context.Context,
id domain.InterfaceIdentifier,
updateFunc func(in *domain.InterfaceStatus) (*domain.InterfaceStatus, error),
) error {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
in, err := r.getOrCreateInterfaceStatus(tx, id)
if err != nil {
@@ -804,7 +842,10 @@ func (r *SqlRepo) UpdateInterfaceStatus(ctx context.Context, id domain.Interface
return nil
}
func (r *SqlRepo) getOrCreateInterfaceStatus(tx *gorm.DB, id domain.InterfaceIdentifier) (*domain.InterfaceStatus, error) {
func (r *SqlRepo) getOrCreateInterfaceStatus(tx *gorm.DB, id domain.InterfaceIdentifier) (
*domain.InterfaceStatus,
error,
) {
var in domain.InterfaceStatus
// defaults will be applied to newly created record
@@ -830,7 +871,11 @@ func (r *SqlRepo) upsertInterfaceStatus(tx *gorm.DB, in *domain.InterfaceStatus)
return nil
}
func (r *SqlRepo) UpdatePeerStatus(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.PeerStatus) (*domain.PeerStatus, error)) error {
func (r *SqlRepo) UpdatePeerStatus(
ctx context.Context,
id domain.PeerIdentifier,
updateFunc func(in *domain.PeerStatus) (*domain.PeerStatus, error),
) error {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
in, err := r.getOrCreatePeerStatus(tx, id)
if err != nil {
@@ -883,6 +928,15 @@ func (r *SqlRepo) upsertPeerStatus(tx *gorm.DB, in *domain.PeerStatus) error {
return nil
}
func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error {
err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{}, id).Error
if err != nil {
return err
}
return nil
}
// endregion statistics
// region audit

View File

@@ -0,0 +1,135 @@
package adapters
import (
"context"
"net/http"
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
)
type MetricsServer struct {
*http.Server
ifaceReceivedBytesTotal *prometheus.GaugeVec
ifaceSendBytesTotal *prometheus.GaugeVec
peerIsConnected *prometheus.GaugeVec
peerLastHandshakeSeconds *prometheus.GaugeVec
peerReceivedBytesTotal *prometheus.GaugeVec
peerSendBytesTotal *prometheus.GaugeVec
}
// Wireguard metrics labels
var (
ifaceLabels = []string{"interface"}
peerLabels = []string{"interface", "addresses", "id", "name"}
)
// NewMetricsServer returns a new prometheus server
func NewMetricsServer(cfg *config.Config) *MetricsServer {
reg := prometheus.NewRegistry()
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}))
return &MetricsServer{
Server: &http.Server{
Addr: cfg.Statistics.ListeningAddress,
Handler: mux,
},
ifaceReceivedBytesTotal: promauto.With(reg).NewGaugeVec(
prometheus.GaugeOpts{
Name: "wireguard_interface_received_bytes_total",
Help: "Bytes received througth the interface.",
}, ifaceLabels,
),
ifaceSendBytesTotal: promauto.With(reg).NewGaugeVec(
prometheus.GaugeOpts{
Name: "wireguard_interface_sent_bytes_total",
Help: "Bytes sent through the interface.",
}, ifaceLabels,
),
peerIsConnected: promauto.With(reg).NewGaugeVec(
prometheus.GaugeOpts{
Name: "wireguard_peer_up",
Help: "Peer connection state (boolean: 1/0).",
}, peerLabels,
),
peerLastHandshakeSeconds: promauto.With(reg).NewGaugeVec(
prometheus.GaugeOpts{
Name: "wireguard_peer_last_handshake_seconds",
Help: "Seconds from the last handshake with the peer.",
}, peerLabels,
),
peerReceivedBytesTotal: promauto.With(reg).NewGaugeVec(
prometheus.GaugeOpts{
Name: "wireguard_peer_received_bytes_total",
Help: "Bytes received from the peer.",
}, peerLabels,
),
peerSendBytesTotal: promauto.With(reg).NewGaugeVec(
prometheus.GaugeOpts{
Name: "wireguard_peer_sent_bytes_total",
Help: "Bytes sent to the peer.",
}, peerLabels,
),
}
}
// Run starts the metrics server
func (m *MetricsServer) Run(ctx context.Context) {
// Run the metrics server in a goroutine
go func() {
if err := m.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logrus.Errorf("metrics service on %s exited: %v", m.Addr, err)
}
}()
logrus.Infof("started metrics service on %s", m.Addr)
// Wait for the context to be done
<-ctx.Done()
// Create a context with timeout for the shutdown process
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Attempt to gracefully shutdown the metrics server
if err := m.Shutdown(shutdownCtx); err != nil {
logrus.Errorf("metrics service on %s shutdown failed: %v", m.Addr, err)
} else {
logrus.Infof("metrics service on %s shutdown gracefully", m.Addr)
}
}
// UpdateInterfaceMetrics updates the metrics for the given interface
func (m *MetricsServer) UpdateInterfaceMetrics(status domain.InterfaceStatus) {
labels := []string{string(status.InterfaceId)}
m.ifaceReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
m.ifaceSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
}
// UpdatePeerMetrics updates the metrics for the given peer
func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) {
labels := []string{
string(peer.InterfaceIdentifier),
string(peer.Interface.AddressStr()),
string(status.PeerId),
string(peer.DisplayName),
}
if status.LastHandshake != nil {
m.peerLastHandshakeSeconds.WithLabelValues(labels...).Set(float64(status.LastHandshake.Unix()))
}
m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected()))
}

View File

@@ -3,11 +3,12 @@ package adapters
import (
"bytes"
"fmt"
"os/exec"
"strings"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
"os/exec"
"strings"
)
// WgQuickRepo implements higher level wg-quick like interactions like setting DNS, routing tables or interface hooks.
@@ -57,7 +58,10 @@ func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr
err := r.exec(dnsCommand, id, dnsCommandInput...)
if err != nil {
return fmt.Errorf("failed to set dns settings: %w", err)
return fmt.Errorf(
"failed to set dns settings (is resolvconf available?, for systemd create this symlink: ln -s /usr/bin/resolvectl /usr/local/bin/resolvconf): %w",
err,
)
}
return nil

View File

@@ -4,12 +4,13 @@ import (
"context"
"errors"
"fmt"
"os"
"github.com/h44z/wg-portal/internal/domain"
"github.com/h44z/wg-portal/internal/lowlevel"
"github.com/vishvananda/netlink"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"os"
)
// WgRepo implements all low-level WireGuard interactions.
@@ -74,7 +75,11 @@ func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier
return peers, nil
}
func (r *WgRepo) GetPeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error) {
func (r *WgRepo) GetPeer(
_ context.Context,
deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier,
) (*domain.PhysicalPeer, error) {
return r.getPeer(deviceId, id)
}
@@ -90,7 +95,7 @@ func (r *WgRepo) convertWireGuardInterface(device *wgtypes.Device) (domain.Physi
ListenPort: device.ListenPort,
Addresses: nil,
Mtu: 0,
FirewallMark: int32(device.FirewallMark),
FirewallMark: uint32(device.FirewallMark),
DeviceUp: false,
ImportSource: "wgctrl",
DeviceType: device.Type.String(),
@@ -151,7 +156,11 @@ func (r *WgRepo) convertWireGuardPeer(peer *wgtypes.Peer) (domain.PhysicalPeer,
return peerModel, nil
}
func (r *WgRepo) SaveInterface(_ context.Context, id domain.InterfaceIdentifier, updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error)) error {
func (r *WgRepo) SaveInterface(
_ context.Context,
id domain.InterfaceIdentifier,
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
) error {
physicalInterface, err := r.getOrCreateInterface(id)
if err != nil {
return err
@@ -324,7 +333,12 @@ func (r *WgRepo) deleteLowLevelInterface(id domain.InterfaceIdentifier) error {
return nil
}
func (r *WgRepo) SavePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier, updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error)) error {
func (r *WgRepo) SavePeer(
_ context.Context,
deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier,
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
) error {
physicalPeer, err := r.getOrCreatePeer(deviceId, id)
if err != nil {
return err
@@ -342,7 +356,10 @@ func (r *WgRepo) SavePeer(_ context.Context, deviceId domain.InterfaceIdentifier
return nil
}
func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error) {
func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (
*domain.PhysicalPeer,
error,
) {
peer, err := r.getPeer(deviceId, id)
if err == nil {
return peer, nil
@@ -352,9 +369,13 @@ func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.
}
// create new peer
err = r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{Peers: []wgtypes.PeerConfig{{
PublicKey: id.ToPublicKey(),
}}})
err = r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{
Peers: []wgtypes.PeerConfig{
{
PublicKey: id.ToPublicKey(),
},
},
})
peer, err = r.getPeer(deviceId, id)
return peer, nil

View File

@@ -68,8 +68,7 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
c.Writer.Header().Set("X-Served-By", hostname)
c.Next()
}).Use(func(c *gin.Context) {
var xRequestID string
xRequestID = uuid(16)
xRequestID := uuid(16)
c.Request.Header.Set(RequestIDKey, xRequestID)
c.Set(RequestIDKey, xRequestID)
@@ -96,8 +95,6 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
}
func (s *Server) Run(ctx context.Context, listenAddress string) {
logrus.Infof("starting web service on %s", listenAddress)
// Run web service
srv := &http.Server{
Addr: listenAddress,
@@ -106,11 +103,18 @@ func (s *Server) Run(ctx context.Context, listenAddress string) {
srvContext, cancelFn := context.WithCancel(ctx)
go func() {
if err := srv.ListenAndServe(); err != nil {
var err error
if s.cfg.Web.CertFile != "" && s.cfg.Web.KeyFile != "" {
err = srv.ListenAndServeTLS(s.cfg.Web.CertFile, s.cfg.Web.KeyFile)
} else {
err = srv.ListenAndServe()
}
if err != nil {
logrus.Infof("web service on %s exited: %v", listenAddress, err)
cancelFn()
}
}()
logrus.Infof("started web service on %s", listenAddress)
// Wait for the main context to end
<-srvContext.Done()

View File

@@ -2,14 +2,16 @@ package handlers
import (
"context"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/app/api/v0/model"
"github.com/h44z/wg-portal/internal/domain"
"net/http"
"net/url"
"strconv"
"time"
)
type authEndpoint struct {
@@ -114,6 +116,10 @@ func (e authEndpoint) handleOauthInitiateGet() gin.HandlerFunc {
}
if returnTo != "" {
if !e.isValidReturnUrl(returnTo) {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "invalid return URL"})
return
}
if u, err := url.Parse(returnTo); err == nil {
returnUrl = u
}
@@ -124,7 +130,7 @@ func (e authEndpoint) handleOauthInitiateGet() gin.HandlerFunc {
}
if currentSession.LoggedIn {
if autoRedirect {
if autoRedirect && e.isValidReturnUrl(returnTo) {
queryParams := returnUrl.Query()
queryParams.Set("wgLoginState", "success")
returnParams = queryParams.Encode()
@@ -138,10 +144,11 @@ func (e authEndpoint) handleOauthInitiateGet() gin.HandlerFunc {
ctx := domain.SetUserInfoFromGin(c)
authCodeUrl, state, nonce, err := e.app.Authenticator.OauthLoginStep1(ctx, provider)
if err != nil {
if autoRedirect {
if autoRedirect && e.isValidReturnUrl(returnTo) {
redirectToReturn()
} else {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
}
return
}
@@ -193,7 +200,7 @@ func (e authEndpoint) handleOauthCallbackGet() gin.HandlerFunc {
}
if currentSession.LoggedIn {
if returnUrl != nil {
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
queryParams := returnUrl.Query()
queryParams.Set("wgLoginState", "success")
returnParams = queryParams.Encode()
@@ -209,15 +216,16 @@ func (e authEndpoint) handleOauthCallbackGet() gin.HandlerFunc {
oauthState := c.Query("state")
if provider != currentSession.OauthProvider {
if returnUrl != nil {
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
redirectToReturn()
} else {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "invalid oauth provider"})
c.JSON(http.StatusBadRequest,
model.Error{Code: http.StatusBadRequest, Message: "invalid oauth provider"})
}
return
}
if oauthState != currentSession.OauthState {
if returnUrl != nil {
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
redirectToReturn()
} else {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "invalid oauth state"})
@@ -229,7 +237,7 @@ func (e authEndpoint) handleOauthCallbackGet() gin.HandlerFunc {
user, err := e.app.Authenticator.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce, oauthCode)
cancel()
if err != nil {
if returnUrl != nil {
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
redirectToReturn()
} else {
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: err.Error()})
@@ -239,7 +247,7 @@ func (e authEndpoint) handleOauthCallbackGet() gin.HandlerFunc {
e.setAuthenticatedUser(c, user)
if returnUrl != nil {
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
queryParams := returnUrl.Query()
queryParams.Set("wgLoginState", "success")
returnParams = queryParams.Encode()
@@ -328,3 +336,12 @@ func (e authEndpoint) handleLogoutPost() gin.HandlerFunc {
c.JSON(http.StatusOK, model.Error{Code: http.StatusOK, Message: "logout ok"})
}
}
// isValidReturnUrl checks if the given return URL matches the configured external URL of the application.
func (e authEndpoint) isValidReturnUrl(returnUrl string) bool {
if !strings.HasPrefix(returnUrl, e.app.Config.Web.ExternalUrl) {
return false
}
return true
}

View File

@@ -5,132 +5,42 @@ import (
"github.com/h44z/wg-portal/internal/domain"
)
type StringConfigOption struct {
Value string `json:"Value"`
Overridable bool `json:"Overridable"`
type ConfigOption[T any] struct {
Value T `json:"Value"`
Overridable bool `json:"Overridable"`
}
func NewStringConfigOption(value string, overridable bool) StringConfigOption {
return StringConfigOption{
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
return ConfigOption[T]{
Value: value,
Overridable: overridable,
}
}
func StringConfigOptionFromDomain(opt domain.StringConfigOption) StringConfigOption {
return StringConfigOption{
func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] {
return ConfigOption[T]{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
func StringConfigOptionToDomain(opt StringConfigOption) domain.StringConfigOption {
return domain.StringConfigOption{
func ConfigOptionToDomain[T any](opt ConfigOption[T]) domain.ConfigOption[T] {
return domain.ConfigOption[T]{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
type StringSliceConfigOption struct {
Value []string `json:"Value"`
Overridable bool `json:"Overridable"`
}
func NewStringSliceConfigOption(value []string, overridable bool) StringSliceConfigOption {
return StringSliceConfigOption{
Value: value,
Overridable: overridable,
}
}
func StringSliceConfigOptionFromDomain(opt domain.StringConfigOption) StringSliceConfigOption {
return StringSliceConfigOption{
func StringSliceConfigOptionFromDomain(opt domain.ConfigOption[string]) ConfigOption[[]string] {
return ConfigOption[[]string]{
Value: internal.SliceString(opt.Value),
Overridable: opt.Overridable,
}
}
func StringSliceConfigOptionToDomain(opt StringSliceConfigOption) domain.StringConfigOption {
return domain.StringConfigOption{
func StringSliceConfigOptionToDomain(opt ConfigOption[[]string]) domain.ConfigOption[string] {
return domain.ConfigOption[string]{
Value: internal.SliceToString(opt.Value),
Overridable: opt.Overridable,
}
}
type IntConfigOption struct {
Value int `json:"Value"`
Overridable bool `json:"Overridable"`
}
func NewIntConfigOption(value int, overridable bool) IntConfigOption {
return IntConfigOption{
Value: value,
Overridable: overridable,
}
}
func IntConfigOptionFromDomain(opt domain.IntConfigOption) IntConfigOption {
return IntConfigOption{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
func IntConfigOptionToDomain(opt IntConfigOption) domain.IntConfigOption {
return domain.IntConfigOption{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
type Int32ConfigOption struct {
Value int32 `json:"Value"`
Overridable bool `json:"Overridable"`
}
func NewInt32ConfigOption(value int32, overridable bool) Int32ConfigOption {
return Int32ConfigOption{
Value: value,
Overridable: overridable,
}
}
func Int32ConfigOptionFromDomain(opt domain.Int32ConfigOption) Int32ConfigOption {
return Int32ConfigOption{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
func Int32ConfigOptionToDomain(opt Int32ConfigOption) domain.Int32ConfigOption {
return domain.Int32ConfigOption{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
type BoolConfigOption struct {
Value bool `json:"Value"`
Overridable bool `json:"Overridable"`
}
func NewBoolConfigOption(value bool, overridable bool) BoolConfigOption {
return BoolConfigOption{
Value: value,
Overridable: overridable,
}
}
func BoolConfigOptionFromDomain(opt domain.BoolConfigOption) BoolConfigOption {
return BoolConfigOption{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
func BoolConfigOptionToDomain(opt BoolConfigOption) domain.BoolConfigOption {
return domain.BoolConfigOption{
Value: opt.Value,
Overridable: opt.Overridable,
}
}

View File

@@ -1,9 +1,10 @@
package model
import (
"github.com/h44z/wg-portal/internal"
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain"
)
@@ -22,7 +23,7 @@ type Interface struct {
Dns []string `json:"Dns"` // the dns server that should be set if the interface is up, comma separated
DnsSearch []string `json:"DnsSearch"` // the dns search option string that should be set if the interface is up, will be appended to DnsStr
Mtu int `json:"Mtu"` // the device MTU
FirewallMark int32 `json:"FirewallMark"` // a firewall mark
FirewallMark uint32 `json:"FirewallMark"` // a firewall mark
RoutingTable string `json:"RoutingTable"` // the routing table
PreUp string `json:"PreUp"` // action that is executed before the device is up
@@ -37,7 +38,7 @@ type Interface struct {
PeerDefAllowedIPs []string `json:"PeerDefAllowedIPs"` // the default allowed IP string for the peer
PeerDefMtu int `json:"PeerDefMtu"` // the default device MTU
PeerDefPersistentKeepalive int `json:"PeerDefPersistentKeepalive"` // the default persistent keep-alive Value
PeerDefFirewallMark int32 `json:"PeerDefFirewallMark"` // default firewall mark
PeerDefFirewallMark uint32 `json:"PeerDefFirewallMark"` // default firewall mark
PeerDefRoutingTable string `json:"PeerDefRoutingTable"` // the default routing table
PeerDefPreUp string `json:"PeerDefPreUp"` // default action that is executed before the device is up

View File

@@ -1,9 +1,10 @@
package model
import (
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain"
"time"
)
const ExpiryDateTimeLayout = "\"2006-01-02\""
@@ -48,30 +49,30 @@ type Peer struct {
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty"` // expiry dates for peers
Notes string `json:"Notes"` // a note field for peers
Endpoint StringConfigOption `json:"Endpoint"` // the endpoint address
EndpointPublicKey StringConfigOption `json:"EndpointPublicKey"` // the endpoint public key
AllowedIPs StringSliceConfigOption `json:"AllowedIPs"` // all allowed ip subnets, comma seperated
ExtraAllowedIPs []string `json:"ExtraAllowedIPs"` // all allowed ip subnets on the server side, comma seperated
PresharedKey string `json:"PresharedKey"` // the pre-shared Key of the peer
PersistentKeepalive IntConfigOption `json:"PersistentKeepalive"` // the persistent keep-alive interval
Endpoint ConfigOption[string] `json:"Endpoint"` // the endpoint address
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"` // the endpoint public key
AllowedIPs ConfigOption[[]string] `json:"AllowedIPs"` // all allowed ip subnets, comma seperated
ExtraAllowedIPs []string `json:"ExtraAllowedIPs"` // all allowed ip subnets on the server side, comma seperated
PresharedKey string `json:"PresharedKey"` // the pre-shared Key of the peer
PersistentKeepalive ConfigOption[int] `json:"PersistentKeepalive"` // the persistent keep-alive interval
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server peer
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server peer
Mode string // the peer interface type (server, client, any)
Addresses []string `json:"Addresses"` // the interface ip addresses
CheckAliveAddress string `json:"CheckAliveAddress"` // optional ip address or DNS name that is used for ping checks
Dns StringSliceConfigOption `json:"Dns"` // the dns server that should be set if the interface is up, comma separated
DnsSearch StringSliceConfigOption `json:"DnsSearch"` // the dns search option string that should be set if the interface is up, will be appended to DnsStr
Mtu IntConfigOption `json:"Mtu"` // the device MTU
FirewallMark Int32ConfigOption `json:"FirewallMark"` // a firewall mark
RoutingTable StringConfigOption `json:"RoutingTable"` // the routing table
Addresses []string `json:"Addresses"` // the interface ip addresses
CheckAliveAddress string `json:"CheckAliveAddress"` // optional ip address or DNS name that is used for ping checks
Dns ConfigOption[[]string] `json:"Dns"` // the dns server that should be set if the interface is up, comma separated
DnsSearch ConfigOption[[]string] `json:"DnsSearch"` // the dns search option string that should be set if the interface is up, will be appended to DnsStr
Mtu ConfigOption[int] `json:"Mtu"` // the device MTU
FirewallMark ConfigOption[uint32] `json:"FirewallMark"` // a firewall mark
RoutingTable ConfigOption[string] `json:"RoutingTable"` // the routing table
PreUp StringConfigOption `json:"PreUp"` // action that is executed before the device is up
PostUp StringConfigOption `json:"PostUp"` // action that is executed after the device is up
PreDown StringConfigOption `json:"PreDown"` // action that is executed before the device is down
PostDown StringConfigOption `json:"PostDown"` // action that is executed after the device is down
PreUp ConfigOption[string] `json:"PreUp"` // action that is executed before the device is up
PostUp ConfigOption[string] `json:"PostUp"` // action that is executed after the device is up
PreDown ConfigOption[string] `json:"PreDown"` // action that is executed before the device is down
PostDown ConfigOption[string] `json:"PostDown"` // action that is executed after the device is down
}
func NewPeer(src *domain.Peer) *Peer {
@@ -84,12 +85,12 @@ func NewPeer(src *domain.Peer) *Peer {
DisabledReason: src.DisabledReason,
ExpiresAt: ExpiryDate{src.ExpiresAt},
Notes: src.Notes,
Endpoint: StringConfigOptionFromDomain(src.Endpoint),
EndpointPublicKey: StringConfigOptionFromDomain(src.EndpointPublicKey),
Endpoint: ConfigOptionFromDomain(src.Endpoint),
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr),
PresharedKey: string(src.PresharedKey),
PersistentKeepalive: IntConfigOptionFromDomain(src.PersistentKeepalive),
PersistentKeepalive: ConfigOptionFromDomain(src.PersistentKeepalive),
PrivateKey: src.Interface.PrivateKey,
PublicKey: src.Interface.PublicKey,
Mode: string(src.Interface.Type),
@@ -97,13 +98,13 @@ func NewPeer(src *domain.Peer) *Peer {
CheckAliveAddress: src.Interface.CheckAliveAddress,
Dns: StringSliceConfigOptionFromDomain(src.Interface.DnsStr),
DnsSearch: StringSliceConfigOptionFromDomain(src.Interface.DnsSearchStr),
Mtu: IntConfigOptionFromDomain(src.Interface.Mtu),
FirewallMark: Int32ConfigOptionFromDomain(src.Interface.FirewallMark),
RoutingTable: StringConfigOptionFromDomain(src.Interface.RoutingTable),
PreUp: StringConfigOptionFromDomain(src.Interface.PreUp),
PostUp: StringConfigOptionFromDomain(src.Interface.PostUp),
PreDown: StringConfigOptionFromDomain(src.Interface.PreDown),
PostDown: StringConfigOptionFromDomain(src.Interface.PostDown),
Mtu: ConfigOptionFromDomain(src.Interface.Mtu),
FirewallMark: ConfigOptionFromDomain(src.Interface.FirewallMark),
RoutingTable: ConfigOptionFromDomain(src.Interface.RoutingTable),
PreUp: ConfigOptionFromDomain(src.Interface.PreUp),
PostUp: ConfigOptionFromDomain(src.Interface.PostUp),
PreDown: ConfigOptionFromDomain(src.Interface.PreDown),
PostDown: ConfigOptionFromDomain(src.Interface.PostDown),
}
}
@@ -123,12 +124,12 @@ func NewDomainPeer(src *Peer) *domain.Peer {
res := &domain.Peer{
BaseModel: domain.BaseModel{},
Endpoint: StringConfigOptionToDomain(src.Endpoint),
EndpointPublicKey: StringConfigOptionToDomain(src.EndpointPublicKey),
Endpoint: ConfigOptionToDomain(src.Endpoint),
EndpointPublicKey: ConfigOptionToDomain(src.EndpointPublicKey),
AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs),
ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs),
PresharedKey: domain.PreSharedKey(src.PresharedKey),
PersistentKeepalive: IntConfigOptionToDomain(src.PersistentKeepalive),
PersistentKeepalive: ConfigOptionToDomain(src.PersistentKeepalive),
DisplayName: src.DisplayName,
Identifier: domain.PeerIdentifier(src.Identifier),
UserIdentifier: domain.UserIdentifier(src.UserIdentifier),
@@ -147,13 +148,13 @@ func NewDomainPeer(src *Peer) *domain.Peer {
CheckAliveAddress: src.CheckAliveAddress,
DnsStr: StringSliceConfigOptionToDomain(src.Dns),
DnsSearchStr: StringSliceConfigOptionToDomain(src.DnsSearch),
Mtu: IntConfigOptionToDomain(src.Mtu),
FirewallMark: Int32ConfigOptionToDomain(src.FirewallMark),
RoutingTable: StringConfigOptionToDomain(src.RoutingTable),
PreUp: StringConfigOptionToDomain(src.PreUp),
PostUp: StringConfigOptionToDomain(src.PostUp),
PreDown: StringConfigOptionToDomain(src.PreDown),
PostDown: StringConfigOptionToDomain(src.PostDown),
Mtu: ConfigOptionToDomain(src.Mtu),
FirewallMark: ConfigOptionToDomain(src.FirewallMark),
RoutingTable: ConfigOptionToDomain(src.RoutingTable),
PreUp: ConfigOptionToDomain(src.PreUp),
PostUp: ConfigOptionToDomain(src.PostUp),
PreDown: ConfigOptionToDomain(src.PreDown),
PostDown: ConfigOptionToDomain(src.PostDown),
},
}

View File

@@ -6,8 +6,6 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/h44z/wg-portal/internal/app"
"github.com/sirupsen/logrus"
"io"
"net/url"
"path"
@@ -15,10 +13,11 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
evbus "github.com/vardius/message-bus"
"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"
)
type UserManager interface {
@@ -33,14 +32,21 @@ type Authenticator struct {
oauthAuthenticators map[string]domain.OauthAuthenticator
ldapAuthenticators map[string]domain.LdapAuthenticator
// URL prefix for the callback endpoints, this is a combination of the external URL and the API prefix
callbackUrlPrefix string
users UserManager
}
func NewAuthenticator(cfg *config.Auth, bus evbus.MessageBus, users UserManager) (*Authenticator, error) {
func NewAuthenticator(cfg *config.Auth, extUrl string, bus evbus.MessageBus, users UserManager) (
*Authenticator,
error,
) {
a := &Authenticator{
cfg: cfg,
bus: bus,
users: users,
cfg: cfg,
bus: bus,
users: users,
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -55,7 +61,7 @@ func NewAuthenticator(cfg *config.Auth, bus evbus.MessageBus, users UserManager)
}
func (a *Authenticator) setupExternalAuthProviders(ctx context.Context) error {
extUrl, err := url.Parse(a.cfg.CallbackUrlPrefix)
extUrl, err := url.Parse(a.callbackUrlPrefix)
if err != nil {
return fmt.Errorf("failed to parse external url: %w", err)
}
@@ -141,8 +147,8 @@ func (a *Authenticator) GetExternalLoginProviders(_ context.Context) []domain.Lo
authProviders = append(authProviders, domain.LoginProviderInfo{
Identifier: providerId,
Name: providerName,
ProviderUrl: fmt.Sprintf("%s/%s/init", a.cfg.CallbackUrlPrefix, providerId),
CallbackUrl: fmt.Sprintf("%s/%s/callback", a.cfg.CallbackUrlPrefix, providerId),
ProviderUrl: fmt.Sprintf("/auth/login/%s/init", providerId),
CallbackUrl: fmt.Sprintf("/auth/login/%s/callback", providerId),
})
}
@@ -187,8 +193,13 @@ func (a *Authenticator) PlainLogin(ctx context.Context, username, password strin
return user, nil
}
func (a *Authenticator) passwordAuthentication(ctx context.Context, identifier domain.UserIdentifier, password string) (*domain.User, error) {
ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
func (a *Authenticator) passwordAuthentication(
ctx context.Context,
identifier domain.UserIdentifier,
password string,
) (*domain.User, error) {
ctx = domain.SetUserInfo(ctx,
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
var ldapUserInfo *domain.AuthenticatorUserInfo
var ldapProvider domain.LdapAuthenticator
@@ -235,6 +246,10 @@ func (a *Authenticator) passwordAuthentication(ctx context.Context, identifier d
return nil, errors.New("user not found")
}
if userSource == domain.UserSourceLdap && ldapProvider == nil {
return nil, errors.New("ldap provider not found")
}
switch userSource {
case domain.UserSourceDatabase:
err = existingUser.CheckPassword(password)
@@ -248,7 +263,8 @@ func (a *Authenticator) passwordAuthentication(ctx context.Context, identifier d
}
if !userInDatabase {
user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapProvider.GetName(), ldapProvider.RegistrationEnabled())
user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapProvider.GetName(),
ldapProvider.RegistrationEnabled())
if err != nil {
return nil, fmt.Errorf("unable to process user information: %w", err)
}
@@ -262,7 +278,10 @@ func (a *Authenticator) passwordAuthentication(ctx context.Context, identifier d
// region oauth authentication
func (a *Authenticator) OauthLoginStep1(_ context.Context, providerId string) (authCodeUrl, state, nonce string, err error) {
func (a *Authenticator) OauthLoginStep1(_ context.Context, providerId string) (
authCodeUrl, state, nonce string,
err error,
) {
oauthProvider, ok := a.oauthAuthenticators[providerId]
if !ok {
return "", "", "", fmt.Errorf("missing oauth provider %s", providerId)
@@ -318,8 +337,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
return nil, fmt.Errorf("unable to parse user information: %w", err)
}
ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(), oauthProvider.RegistrationEnabled())
ctx = domain.SetUserInfo(ctx,
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(),
oauthProvider.RegistrationEnabled())
if err != nil {
return nil, fmt.Errorf("unable to process user information: %w", err)
}
@@ -333,7 +354,13 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
return user, nil
}
func (a *Authenticator) processUserInfo(ctx context.Context, userInfo *domain.AuthenticatorUserInfo, source domain.UserSource, provider string, withReg bool) (*domain.User, error) {
func (a *Authenticator) processUserInfo(
ctx context.Context,
userInfo *domain.AuthenticatorUserInfo,
source domain.UserSource,
provider string,
withReg bool,
) (*domain.User, error) {
// Search user in backend
user, err := a.users.GetUser(ctx, userInfo.Identifier)
switch {
@@ -349,7 +376,12 @@ func (a *Authenticator) processUserInfo(ctx context.Context, userInfo *domain.Au
return user, nil
}
func (a *Authenticator) registerNewUser(ctx context.Context, userInfo *domain.AuthenticatorUserInfo, source domain.UserSource, provider string) (*domain.User, error) {
func (a *Authenticator) registerNewUser(
ctx context.Context,
userInfo *domain.AuthenticatorUserInfo,
source domain.UserSource,
provider string,
) (*domain.User, error) {
// convert user info to domain.User
user := &domain.User{
Identifier: userInfo.Identifier,

View File

@@ -5,15 +5,16 @@ import (
"bytes"
"context"
"fmt"
"io"
"os"
"strings"
"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"
"strings"
)
type Manager struct {
@@ -21,7 +22,7 @@ type Manager struct {
bus evbus.MessageBus
tplHandler *TemplateHandler
fsRepo FileSystemRepo // can be nil if storing the configuration is disabled
fsRepo FileSystemRepo
users UserDatabaseRepo
wg WireguardDatabaseRepo
}
@@ -42,18 +43,18 @@ func NewConfigFileManager(cfg *config.Config, bus evbus.MessageBus, users UserDa
wg: wg,
}
if err := m.createStorageDirectory(); err != nil {
return nil, err
if m.cfg.Advanced.ConfigStoragePath != "" {
if err := m.createStorageDirectory(); err != nil {
return nil, err
}
m.connectToMessageBus()
}
return m, nil
}
func (m Manager) createStorageDirectory() error {
if m.cfg.Advanced.ConfigStoragePath == "" {
return nil // no storage path configured, skip initialization step
}
err := os.MkdirAll(m.cfg.Advanced.ConfigStoragePath, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create configuration storage path %s: %w",
@@ -64,21 +65,17 @@ func (m Manager) createStorageDirectory() error {
}
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)
_ = m.bus.Subscribe(app.TopicPeerInterfaceUpdated, m.handlePeerInterfaceUpdatedEvent)
}
func (m Manager) handleInterfaceUpdatedEvent(iface *domain.Interface) {
logrus.Errorf("handling interface updated event for %s", iface.Identifier)
if !iface.SaveConfig || m.fsRepo == nil {
if !iface.SaveConfig {
return
}
logrus.Debugf("handling interface updated event for %s", iface.Identifier)
err := m.PersistInterfaceConfig(context.Background(), iface.Identifier)
if err != nil {
logrus.Errorf("failed to automatically persist interface config for %s: %v", iface.Identifier, err)
@@ -86,12 +83,6 @@ func (m Manager) handleInterfaceUpdatedEvent(iface *domain.Interface) {
}
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)
@@ -102,6 +93,8 @@ func (m Manager) handlePeerInterfaceUpdatedEvent(id domain.InterfaceIdentifier)
return
}
logrus.Debugf("handling peer interface updated event for %s", id)
err = m.PersistInterfaceConfig(context.Background(), peerInterface.Identifier)
if err != nil {
logrus.Errorf("failed to automatically persist interface config for %s: %v", peerInterface.Identifier, err)
@@ -184,14 +177,6 @@ func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifi
}
func (m Manager) PersistInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) error {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return err
}
if m.fsRepo == nil {
return fmt.Errorf("peristing configuration is not supported")
}
iface, peers, err := m.wg.GetInterfaceAndPeers(ctx, id)
if err != nil {
return fmt.Errorf("failed to fetch interface %s: %w", id, err)

View File

@@ -60,6 +60,8 @@ PostDown = {{ .Interface.PostDown }}
{{range .Peers}}
{{- if not .IsDisabled}}
[Peer]
{{/* `friendly_name` used by https://github.com/MindFlavor/prometheus_wireguard_exporter */ -}}
# friendly_name = {{ .DisplayName }}
# -WGP- Peer: {{.Identifier}}
# -WGP- Created: {{.CreatedAt}}
# -WGP- Updated: {{.UpdatedAt}}
@@ -86,4 +88,4 @@ Endpoint = {{ .Endpoint.GetValue }}
PersistentKeepalive = {{ .PersistentKeepalive.GetValue }}
{{- end}}
{{- end}}
{{end}}
{{end}}

View File

@@ -3,9 +3,11 @@ package app
const TopicUserCreated = "user:created"
const TopicUserRegistered = "user:registered"
const TopicUserDisabled = "user:disabled"
const TopicUserEnabled = "user:enabled"
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"
const TopicPeerIdentifierUpdated = "peer:identifier:updated"

View File

@@ -3,13 +3,14 @@ package app
import (
"errors"
"fmt"
"os"
"time"
"github.com/h44z/wg-portal/internal/adapters"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"os"
"time"
)
func migrateFromV1(cfg *config.Config, db *gorm.DB, source, typ string) error {
@@ -137,7 +138,7 @@ func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
DisplayName string
PrivateKey string
ListenPort int
FirewallMark int32
FirewallMark uint32
PublicKey string
Mtu int
IPsStr string
@@ -288,7 +289,8 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
ifaceType = domain.InterfaceTypeAny
}
var user domain.User
err = newDb.First(&user, "identifier = ?", oldPeer.Email).Error // migrated users use the email address as identifier
err = newDb.First(&user, "identifier = ?",
oldPeer.Email).Error // migrated users use the email address as identifier
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to find user %s for peer %s: %w", oldPeer.Email, oldPeer.PublicKey, err)
}
@@ -325,20 +327,12 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
CreatedAt: oldPeer.CreatedAt,
UpdatedAt: oldPeer.UpdatedAt,
},
Endpoint: domain.StringConfigOption{
Value: oldPeer.Endpoint, Overridable: !oldPeer.IgnoreGlobalSettings,
},
EndpointPublicKey: domain.StringConfigOption{
Value: iface.PublicKey, Overridable: !oldPeer.IgnoreGlobalSettings,
},
AllowedIPsStr: domain.StringConfigOption{
Value: oldPeer.AllowedIPsStr, Overridable: !oldPeer.IgnoreGlobalSettings,
},
ExtraAllowedIPsStr: oldPeer.AllowedIPsSrvStr,
PresharedKey: domain.PreSharedKey(oldPeer.PresharedKey),
PersistentKeepalive: domain.IntConfigOption{
Value: oldPeer.PersistentKeepalive, Overridable: !oldPeer.IgnoreGlobalSettings,
},
Endpoint: domain.NewConfigOption(oldPeer.Endpoint, !oldPeer.IgnoreGlobalSettings),
EndpointPublicKey: domain.NewConfigOption(iface.PublicKey, !oldPeer.IgnoreGlobalSettings),
AllowedIPsStr: domain.NewConfigOption(oldPeer.AllowedIPsStr, !oldPeer.IgnoreGlobalSettings),
ExtraAllowedIPsStr: oldPeer.AllowedIPsSrvStr,
PresharedKey: domain.PreSharedKey(oldPeer.PresharedKey),
PersistentKeepalive: domain.NewConfigOption(oldPeer.PersistentKeepalive, !oldPeer.IgnoreGlobalSettings),
DisplayName: oldPeer.Identifier,
Identifier: domain.PeerIdentifier(oldPeer.PublicKey),
UserIdentifier: user.Identifier,
@@ -352,35 +346,17 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
PrivateKey: oldPeer.PrivateKey,
PublicKey: oldPeer.PublicKey,
},
Type: ifaceType,
Addresses: ips,
DnsStr: domain.StringConfigOption{
Value: oldPeer.DNSStr, Overridable: !oldPeer.IgnoreGlobalSettings,
},
DnsSearchStr: domain.StringConfigOption{
Value: iface.PeerDefDnsSearchStr, Overridable: !oldPeer.IgnoreGlobalSettings,
},
Mtu: domain.IntConfigOption{
Value: oldPeer.Mtu, Overridable: !oldPeer.IgnoreGlobalSettings,
},
FirewallMark: domain.Int32ConfigOption{
Value: iface.PeerDefFirewallMark, Overridable: !oldPeer.IgnoreGlobalSettings,
},
RoutingTable: domain.StringConfigOption{
Value: iface.PeerDefRoutingTable, Overridable: !oldPeer.IgnoreGlobalSettings,
},
PreUp: domain.StringConfigOption{
Value: iface.PeerDefPreUp, Overridable: !oldPeer.IgnoreGlobalSettings,
},
PostUp: domain.StringConfigOption{
Value: iface.PeerDefPostUp, Overridable: !oldPeer.IgnoreGlobalSettings,
},
PreDown: domain.StringConfigOption{
Value: iface.PeerDefPreDown, Overridable: !oldPeer.IgnoreGlobalSettings,
},
PostDown: domain.StringConfigOption{
Value: iface.PeerDefPostDown, Overridable: !oldPeer.IgnoreGlobalSettings,
},
Type: ifaceType,
Addresses: ips,
DnsStr: domain.NewConfigOption(oldPeer.DNSStr, !oldPeer.IgnoreGlobalSettings),
DnsSearchStr: domain.NewConfigOption(iface.PeerDefDnsSearchStr, !oldPeer.IgnoreGlobalSettings),
Mtu: domain.NewConfigOption(oldPeer.Mtu, !oldPeer.IgnoreGlobalSettings),
FirewallMark: domain.NewConfigOption(iface.PeerDefFirewallMark, !oldPeer.IgnoreGlobalSettings),
RoutingTable: domain.NewConfigOption(iface.PeerDefRoutingTable, !oldPeer.IgnoreGlobalSettings),
PreUp: domain.NewConfigOption(iface.PeerDefPreUp, !oldPeer.IgnoreGlobalSettings),
PostUp: domain.NewConfigOption(iface.PeerDefPostUp, !oldPeer.IgnoreGlobalSettings),
PreDown: domain.NewConfigOption(iface.PeerDefPreDown, !oldPeer.IgnoreGlobalSettings),
PostDown: domain.NewConfigOption(iface.PeerDefPostDown, !oldPeer.IgnoreGlobalSettings),
},
}

View File

@@ -3,6 +3,7 @@ package route
import (
"context"
"fmt"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
@@ -17,7 +18,7 @@ import (
type routeRuleInfo struct {
ifaceId domain.InterfaceIdentifier
fwMark int
fwMark uint32
table int
family int
hasDefault bool
@@ -210,7 +211,7 @@ func (m Manager) setFwMarkRules(rules []routeRuleInfo, family int) error {
SuppressIfgroup: -1,
SuppressPrefixlen: -1,
Priority: m.getRulePriority(existingRules),
Mask: -1,
Mask: nil,
Goto: -1,
Flow: -1,
}); err != nil {
@@ -220,7 +221,7 @@ func (m Manager) setFwMarkRules(rules []routeRuleInfo, family int) error {
return nil
}
func (m Manager) removeFwMarkRules(fwmark, table int, family int) error {
func (m Manager) removeFwMarkRules(fwmark uint32, table int, family int) error {
existingRules, err := m.nl.RuleList(family)
if err != nil {
return fmt.Errorf("failed to get existing rules for family %d: %w", family, err)
@@ -272,8 +273,8 @@ func (m Manager) setMainRule(rules []routeRuleInfo, family int) error {
SuppressIfgroup: -1,
SuppressPrefixlen: 0,
Priority: m.getMainRulePriority(existingRules),
Mark: -1,
Mask: -1,
Mark: 0,
Mask: nil,
Goto: -1,
Flow: -1,
}); err != nil {
@@ -424,20 +425,25 @@ func (m Manager) removeDeprecatedRoutes(link netlink.Link, family int, allowedIP
return nil
}
func (m Manager) getRoutingTableAndFwMark(iface *domain.Interface, allowedIPs []domain.Cidr, link netlink.Link) (table, fwmark int, err error) {
func (m Manager) getRoutingTableAndFwMark(
iface *domain.Interface,
allowedIPs []domain.Cidr,
link netlink.Link,
) (table int, fwmark uint32, err error) {
table = iface.GetRoutingTable()
fwmark = int(iface.FirewallMark)
fwmark = iface.FirewallMark
if fwmark == 0 {
fwmark = m.cfg.Advanced.RouteTableOffset + link.Attrs().Index // generate a new (temporary) firewall mark based on the interface index
logrus.Debugf("using fwmark %d to handle routes", table)
// generate a new (temporary) firewall mark based on the interface index
fwmark = uint32(m.cfg.Advanced.RouteTableOffset + link.Attrs().Index)
logrus.Debugf("%s: using fwmark %d to handle routes", iface.Identifier, table)
// apply the temporary fwmark to the wireguard interface
err = m.setFwMark(iface.Identifier, fwmark)
err = m.setFwMark(iface.Identifier, int(fwmark))
}
if table == 0 {
table = fwmark // generate a new routing table base on interface index
logrus.Debugf("using routing table %d to handle default routes", table)
table = int(fwmark) // generate a new routing table base on interface index
logrus.Debugf("%s: using routing table %d to handle default routes", iface.Identifier, table)
}
return
}

View File

@@ -26,19 +26,20 @@ type Manager struct {
cfg *config.Config
bus evbus.MessageBus
syncInterval time.Duration
users UserDatabaseRepo
peers PeerDatabaseRepo
users UserDatabaseRepo
peers PeerDatabaseRepo
}
func NewUserManager(cfg *config.Config, bus evbus.MessageBus, users UserDatabaseRepo, peers PeerDatabaseRepo) (*Manager, error) {
func NewUserManager(cfg *config.Config, bus evbus.MessageBus, users UserDatabaseRepo, peers PeerDatabaseRepo) (
*Manager,
error,
) {
m := &Manager{
cfg: cfg,
bus: bus,
syncInterval: 10 * time.Second,
users: users,
peers: peers,
users: users,
peers: peers,
}
return m, nil
}
@@ -172,6 +173,13 @@ func (m Manager) UpdateUser(ctx context.Context, user *domain.User) (*domain.Use
return nil, fmt.Errorf("update failure: %w", err)
}
switch {
case !existingUser.IsDisabled() && user.IsDisabled():
m.bus.Publish(app.TopicUserDisabled, *user)
case existingUser.IsDisabled() && !user.IsDisabled():
m.bus.Publish(app.TopicUserEnabled, *user)
}
return user, nil
}
@@ -227,7 +235,7 @@ func (m Manager) DeleteUser(ctx context.Context, id domain.UserIdentifier) error
return fmt.Errorf("deletion failure: %w", err)
}
m.bus.Publish(app.TopicUserDeleted, existingUser)
m.bus.Publish(app.TopicUserDeleted, *existingUser)
return nil
}
@@ -311,26 +319,29 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error {
}
func (m Manager) runLdapSynchronizationService(ctx context.Context) {
running := true
for running {
select {
case <-ctx.Done():
running = false
continue
case <-time.After(m.syncInterval):
// select blocks until one of the cases evaluate to true
}
for _, ldapCfg := range m.cfg.Auth.Ldap { // LDAP Auth providers
go func(cfg config.LdapProvider) {
syncInterval := cfg.SyncInterval
if syncInterval == 0 {
logrus.Debugf("sync disabled for LDAP server: %s", cfg.ProviderName)
return
}
running := true
for running {
select {
case <-ctx.Done():
running = false
continue
case <-time.After(syncInterval * time.Second):
// select blocks until one of the cases evaluate to true
}
for _, ldapCfg := range m.cfg.Auth.Ldap { // LDAP Auth providers
if !ldapCfg.Synchronize {
continue // sync disabled
err := m.synchronizeLdapUsers(ctx, &cfg)
if err != nil {
logrus.Errorf("failed to synchronize LDAP users for %s: %v", cfg.ProviderName, err)
}
}
//logrus.Tracef(&ldapCfg)
err := m.synchronizeLdapUsers(ctx, &ldapCfg)
if err != nil {
logrus.Errorf("failed to synchronize LDAP users for %s: %v", ldapCfg.ProviderName, err)
}
}
}(ldapCfg)
}
}
@@ -373,7 +384,13 @@ func (m Manager) synchronizeLdapUsers(ctx context.Context, provider *config.Ldap
return nil
}
func (m Manager) updateLdapUsers(ctx context.Context, providerName string, rawUsers []internal.RawLdapUser, fields *config.LdapFields, adminGroupDN *ldap.DN) error {
func (m Manager) updateLdapUsers(
ctx context.Context,
providerName string,
rawUsers []internal.RawLdapUser,
fields *config.LdapFields,
adminGroupDN *ldap.DN,
) error {
for _, rawUser := range rawUsers {
user, err := convertRawLdapUser(providerName, rawUser, fields, adminGroupDN)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
@@ -388,7 +405,7 @@ func (m Manager) updateLdapUsers(ctx context.Context, providerName string, rawUs
tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
tctx = domain.SetUserInfo(tctx, domain.SystemAdminContextUserInfo())
if existingUser == nil {
err := m.NewUser(tctx, user)
if err != nil {
@@ -396,8 +413,9 @@ func (m Manager) updateLdapUsers(ctx context.Context, providerName string, rawUs
}
}
if existingUser != nil && existingUser.Source == domain.UserSourceLdap && userChangedInLdap(existingUser, user) {
if existingUser != nil && existingUser.Source == domain.UserSourceLdap && userChangedInLdap(existingUser,
user) {
err := m.users.SaveUser(tctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
u.UpdatedAt = time.Now()
u.UpdatedBy = "ldap_sync"
@@ -420,7 +438,12 @@ func (m Manager) updateLdapUsers(ctx context.Context, providerName string, rawUs
return nil
}
func (m Manager) disableMissingLdapUsers(ctx context.Context, providerName string, rawUsers []internal.RawLdapUser, fields *config.LdapFields) error {
func (m Manager) disableMissingLdapUsers(
ctx context.Context,
providerName string,
rawUsers []internal.RawLdapUser,
fields *config.LdapFields,
) error {
allUsers, err := m.users.GetAllUsers(ctx)
if err != nil {
return err

View File

@@ -2,6 +2,7 @@ package wireguard
import (
"context"
"github.com/h44z/wg-portal/internal/domain"
)
@@ -12,13 +13,21 @@ type InterfaceAndPeerDatabaseRepo interface {
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error)
GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error)
SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.Interface) (*domain.Interface, error)) error
SaveInterface(
ctx context.Context,
id domain.InterfaceIdentifier,
updateFunc func(in *domain.Interface) (*domain.Interface, error),
) error
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error)
FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) ([]domain.Peer, error)
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error)
SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error
SavePeer(
ctx context.Context,
id domain.PeerIdentifier,
updateFunc func(in *domain.Peer) (*domain.Peer, error),
) error
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error)
@@ -27,19 +36,42 @@ type InterfaceAndPeerDatabaseRepo interface {
type StatisticsDatabaseRepo interface {
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error)
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
UpdatePeerStatus(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.PeerStatus) (*domain.PeerStatus, error)) error
UpdateInterfaceStatus(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.InterfaceStatus) (*domain.InterfaceStatus, error)) error
UpdatePeerStatus(
ctx context.Context,
id domain.PeerIdentifier,
updateFunc func(in *domain.PeerStatus) (*domain.PeerStatus, error),
) error
UpdateInterfaceStatus(
ctx context.Context,
id domain.InterfaceIdentifier,
updateFunc func(in *domain.InterfaceStatus) (*domain.InterfaceStatus, error),
) error
DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error
}
type InterfaceController interface {
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
GetPeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error)
SaveInterface(_ context.Context, id domain.InterfaceIdentifier, updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error)) error
GetPeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (
*domain.PhysicalPeer,
error,
)
SaveInterface(
_ context.Context,
id domain.InterfaceIdentifier,
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
) error
DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error
SavePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier, updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error)) error
SavePeer(
_ context.Context,
deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier,
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
) error
DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error
}
@@ -48,3 +80,8 @@ type WgQuickController interface {
SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error
UnsetDNS(id domain.InterfaceIdentifier) error
}
type MetricsServer interface {
UpdateInterfaceMetrics(status domain.InterfaceStatus)
UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus)
}

View File

@@ -2,31 +2,48 @@ package wireguard
import (
"context"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/prometheus-community/pro-bing"
"github.com/sirupsen/logrus"
"sync"
"time"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
probing "github.com/prometheus-community/pro-bing"
"github.com/sirupsen/logrus"
evbus "github.com/vardius/message-bus"
)
type StatisticsCollector struct {
cfg *config.Config
bus evbus.MessageBus
pingWaitGroup sync.WaitGroup
pingJobs chan domain.Peer
db StatisticsDatabaseRepo
wg InterfaceController
ms MetricsServer
}
func NewStatisticsCollector(cfg *config.Config, db StatisticsDatabaseRepo, wg InterfaceController) (*StatisticsCollector, error) {
return &StatisticsCollector{
func NewStatisticsCollector(
cfg *config.Config,
bus evbus.MessageBus,
db StatisticsDatabaseRepo,
wg InterfaceController,
ms MetricsServer,
) (*StatisticsCollector, error) {
c := &StatisticsCollector{
cfg: cfg,
bus: bus,
db: db,
wg: wg,
}, nil
ms: ms,
}
c.connectToMessageBus()
return c, nil
}
func (c *StatisticsCollector) StartBackgroundJobs(ctx context.Context) {
@@ -66,15 +83,21 @@ func (c *StatisticsCollector) collectInterfaceData(ctx context.Context) {
logrus.Warnf("failed to load physical interface %s for data collection: %v", in.Identifier, err)
continue
}
err = c.db.UpdateInterfaceStatus(ctx, in.Identifier, func(i *domain.InterfaceStatus) (*domain.InterfaceStatus, error) {
i.UpdatedAt = time.Now()
i.BytesReceived = physicalInterface.BytesDownload
i.BytesTransmitted = physicalInterface.BytesUpload
return i, nil
})
err = c.db.UpdateInterfaceStatus(ctx, in.Identifier,
func(i *domain.InterfaceStatus) (*domain.InterfaceStatus, error) {
i.UpdatedAt = time.Now()
i.BytesReceived = physicalInterface.BytesDownload
i.BytesTransmitted = physicalInterface.BytesUpload
// Update prometheus metrics
go c.updateInterfaceMetrics(*i)
return i, nil
})
if err != nil {
logrus.Warnf("failed to update interface status for %s: %v", in.Identifier, err)
}
logrus.Tracef("updated interface status for %s", in.Identifier)
}
}
}
@@ -112,24 +135,31 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
continue
}
for _, peer := range peers {
err = c.db.UpdatePeerStatus(ctx, peer.Identifier, func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
var lastHandshake *time.Time
if !peer.LastHandshake.IsZero() {
lastHandshake = &peer.LastHandshake
}
err = c.db.UpdatePeerStatus(ctx, peer.Identifier,
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
var lastHandshake *time.Time
if !peer.LastHandshake.IsZero() {
lastHandshake = &peer.LastHandshake
}
// calculate if session was restarted
p.UpdatedAt = time.Now()
p.LastSessionStart = getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload, lastHandshake)
p.BytesReceived = peer.BytesUpload // store bytes that where uploaded from the peer and received by the server
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
p.Endpoint = peer.Endpoint
p.LastHandshake = lastHandshake
// calculate if session was restarted
p.UpdatedAt = time.Now()
p.LastSessionStart = getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
lastHandshake)
p.BytesReceived = peer.BytesUpload // store bytes that where uploaded from the peer and received by the server
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
p.Endpoint = peer.Endpoint
p.LastHandshake = lastHandshake
return p, nil
})
// Update prometheus metrics
go c.updatePeerMetrics(ctx, *p)
return p, nil
})
if err != nil {
logrus.Warnf("failed to update interface status for %s: %v", in.Identifier, err)
logrus.Warnf("failed to update peer status for %s: %v", peer.Identifier, err)
} else {
logrus.Tracef("updated peer status for %s", peer.Identifier)
}
}
}
@@ -137,7 +167,11 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
}
}
func getSessionStartTime(oldStats domain.PeerStatus, newReceived, newTransmitted uint64, latestHandshake *time.Time) *time.Time {
func getSessionStartTime(
oldStats domain.PeerStatus,
newReceived, newTransmitted uint64,
latestHandshake *time.Time,
) *time.Time {
if latestHandshake == nil {
return nil // currently not connected
}
@@ -230,11 +264,33 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
for peer := range c.pingJobs {
peerPingable := c.isPeerPingable(ctx, peer)
logrus.Tracef("peer %s pingable: %t", peer.Identifier, peerPingable)
now := time.Now()
err := c.db.UpdatePeerStatus(ctx, peer.Identifier,
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
if peerPingable {
p.IsPingable = true
p.LastPing = &now
} else {
p.IsPingable = false
p.LastPing = nil
}
// Update prometheus metrics
go c.updatePeerMetrics(ctx, *p)
return p, nil
})
if err != nil {
logrus.Warnf("failed to update peer ping status for %s: %v", peer.Identifier, err)
} else {
logrus.Tracef("updated peer ping status for %s", peer.Identifier)
}
}
}
func (c *StatisticsCollector) isPeerPingable(ctx context.Context, peer domain.Peer) bool {
if c.cfg.Statistics.UsePingChecks == false {
if !c.cfg.Statistics.UsePingChecks {
return false
}
@@ -245,7 +301,7 @@ func (c *StatisticsCollector) isPeerPingable(ctx context.Context, peer domain.Pe
pinger, err := probing.NewPinger(checkAddr)
if err != nil {
logrus.Tracef("failed to instatiate pinger for %s: %v", checkAddr, err)
logrus.Tracef("failed to instatiate pinger for %s (%s): %v", peer.Identifier, checkAddr, err)
return false
}
@@ -255,9 +311,38 @@ func (c *StatisticsCollector) isPeerPingable(ctx context.Context, peer domain.Pe
pinger.Timeout = 2 * time.Second
err = pinger.RunWithContext(ctx) // Blocks until finished.
if err != nil {
logrus.Tracef("pinger for %s exited unexpectedly: %v", checkAddr, err)
logrus.Tracef("pinger for peer %s (%s) exited unexpectedly: %v", peer.Identifier, checkAddr, err)
return false
}
stats := pinger.Statistics()
return stats.PacketsRecv == checkCount
}
func (c *StatisticsCollector) updateInterfaceMetrics(status domain.InterfaceStatus) {
c.ms.UpdateInterfaceMetrics(status)
}
func (c *StatisticsCollector) updatePeerMetrics(ctx context.Context, status domain.PeerStatus) {
// Fetch peer data from the database
peer, err := c.db.GetPeer(ctx, status.PeerId)
if err != nil {
logrus.Warnf("failed to fetch peer data for metrics %s: %v", status.PeerId, err)
return
}
c.ms.UpdatePeerMetrics(peer, status)
}
func (c *StatisticsCollector) connectToMessageBus() {
_ = c.bus.Subscribe(app.TopicPeerIdentifierUpdated, c.handlePeerIdentifierChangeEvent)
}
func (c *StatisticsCollector) handlePeerIdentifierChangeEvent(oldIdentifier, newIdentifier domain.PeerIdentifier) {
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
// remove potential left-over status data
err := c.db.DeletePeerStatus(ctx, oldIdentifier)
if err != nil {
logrus.Errorf("failed to delete old peer status for migrated peer, %s -> %s: %v",
oldIdentifier, newIdentifier, err)
}
}

View File

@@ -1,10 +1,11 @@
package wireguard
import (
"github.com/h44z/wg-portal/internal/domain"
"reflect"
"testing"
"time"
"github.com/h44z/wg-portal/internal/domain"
)
func Test_getSessionStartTime(t *testing.T) {
@@ -66,7 +67,9 @@ func Test_getSessionStartTime(t *testing.T) {
{
name: "still connected",
args: args{
oldStats: domain.PeerStatus{LastSessionStart: &nowMinus1, BytesReceived: 10, BytesTransmitted: 10},
oldStats: domain.PeerStatus{
LastSessionStart: &nowMinus1, BytesReceived: 10, BytesTransmitted: 10,
},
newReceived: 100,
newTransmitted: 100,
lastHandshake: &now,
@@ -76,7 +79,9 @@ func Test_getSessionStartTime(t *testing.T) {
{
name: "no longer connected",
args: args{
oldStats: domain.PeerStatus{LastSessionStart: &nowMinus5, BytesReceived: 100, BytesTransmitted: 100},
oldStats: domain.PeerStatus{
LastSessionStart: &nowMinus5, BytesReceived: 100, BytesTransmitted: 100,
},
newReceived: 100,
newTransmitted: 100,
lastHandshake: &nowMinus3,
@@ -116,7 +121,9 @@ func Test_getSessionStartTime(t *testing.T) {
{
name: "reconnect (sent)",
args: args{
oldStats: domain.PeerStatus{LastSessionStart: &nowMinus1, BytesReceived: 100, BytesTransmitted: 100},
oldStats: domain.PeerStatus{
LastSessionStart: &nowMinus1, BytesReceived: 100, BytesTransmitted: 100,
},
newReceived: 100,
newTransmitted: 10,
lastHandshake: &now,
@@ -126,7 +133,8 @@ func Test_getSessionStartTime(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getSessionStartTime(tt.args.oldStats, tt.args.newReceived, tt.args.newTransmitted, tt.args.lastHandshake); !reflect.DeepEqual(got, tt.want) {
if got := getSessionStartTime(tt.args.oldStats, tt.args.newReceived, tt.args.newTransmitted,
tt.args.lastHandshake); !reflect.DeepEqual(got, tt.want) {
t.Errorf("getSessionStartTime() = %v, want %v", got, tt.want)
}
})

View File

@@ -2,9 +2,10 @@ package wireguard
import (
"context"
"time"
"github.com/h44z/wg-portal/internal/app"
"github.com/sirupsen/logrus"
"time"
evbus "github.com/vardius/message-bus"
@@ -21,7 +22,13 @@ type Manager struct {
quick WgQuickController
}
func NewWireGuardManager(cfg *config.Config, bus evbus.MessageBus, wg InterfaceController, quick WgQuickController, db InterfaceAndPeerDatabaseRepo) (*Manager, error) {
func NewWireGuardManager(
cfg *config.Config,
bus evbus.MessageBus,
wg InterfaceController,
quick WgQuickController,
db InterfaceAndPeerDatabaseRepo,
) (*Manager, error) {
m := &Manager{
cfg: cfg,
bus: bus,
@@ -42,6 +49,9 @@ func (m Manager) StartBackgroundJobs(ctx context.Context) {
func (m Manager) connectToMessageBus() {
_ = m.bus.Subscribe(app.TopicUserCreated, m.handleUserCreationEvent)
_ = m.bus.Subscribe(app.TopicAuthLogin, m.handleUserLoginEvent)
_ = m.bus.Subscribe(app.TopicUserDisabled, m.handleUserDisabledEvent)
_ = m.bus.Subscribe(app.TopicUserEnabled, m.handleUserEnabledEvent)
_ = m.bus.Subscribe(app.TopicUserDeleted, m.handleUserDeletedEvent)
}
func (m Manager) handleUserCreationEvent(user *domain.User) {
@@ -84,6 +94,104 @@ func (m Manager) handleUserLoginEvent(userId domain.UserIdentifier) {
}
}
func (m Manager) handleUserDisabledEvent(user domain.User) {
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
userPeers, err := m.db.GetUserPeers(ctx, user.Identifier)
if err != nil {
logrus.Errorf("failed to retrieve peers for disabled user %s: %v", user.Identifier, err)
return
}
for _, peer := range userPeers {
if peer.IsDisabled() {
continue // peer is already disabled
}
logrus.Debugf("disabling peer %s due to user %s being disabled", peer.Identifier, user.Identifier)
peer.Disabled = user.Disabled // set to user disabled timestamp
peer.DisabledReason = domain.DisabledReasonUserDisabled
_, err := m.UpdatePeer(ctx, &peer)
if err != nil {
logrus.Errorf("failed to disable peer %s for disabled user %s: %v",
peer.Identifier, user.Identifier, err)
}
}
}
func (m Manager) handleUserEnabledEvent(user domain.User) {
if !m.cfg.Core.ReEnablePeerAfterUserEnable {
return
}
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
userPeers, err := m.db.GetUserPeers(ctx, user.Identifier)
if err != nil {
logrus.Errorf("failed to retrieve peers for re-enabled user %s: %v", user.Identifier, err)
return
}
for _, peer := range userPeers {
if !peer.IsDisabled() {
continue // peer is already active
}
if peer.DisabledReason != domain.DisabledReasonUserDisabled {
continue // peer was disabled for another reason
}
logrus.Debugf("enabling peer %s due to user %s being enabled", peer.Identifier, user.Identifier)
peer.Disabled = nil
peer.DisabledReason = ""
_, err := m.UpdatePeer(ctx, &peer)
if err != nil {
logrus.Errorf("failed to enable peer %s for enabled user %s: %v",
peer.Identifier, user.Identifier, err)
}
}
return
}
func (m Manager) handleUserDeletedEvent(user domain.User) {
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
userPeers, err := m.db.GetUserPeers(ctx, user.Identifier)
if err != nil {
logrus.Errorf("failed to retrieve peers for deleted user %s: %v", user.Identifier, err)
return
}
deletionTime := time.Now()
for _, peer := range userPeers {
if peer.IsDisabled() {
continue // peer is already disabled
}
if m.cfg.Core.DeletePeerAfterUserDeleted {
logrus.Debugf("deleting peer %s due to user %s being deleted", peer.Identifier, user.Identifier)
if err := m.DeletePeer(ctx, peer.Identifier); err != nil {
logrus.Errorf("failed to delete peer %s for deleted user %s: %v",
peer.Identifier, user.Identifier, err)
}
} else {
logrus.Debugf("disabling peer %s due to user %s being deleted", peer.Identifier, user.Identifier)
peer.UserIdentifier = "" // remove user reference
peer.Disabled = &deletionTime
peer.DisabledReason = domain.DisabledReasonUserDeleted
_, err := m.UpdatePeer(ctx, &peer)
if err != nil {
logrus.Errorf("failed to disable peer %s for deleted user %s: %v",
peer.Identifier, user.Identifier, err)
}
}
}
}
func (m Manager) runExpiredPeersCheck(ctx context.Context) {
ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo())

View File

@@ -4,12 +4,13 @@ import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
"os"
"time"
)
func (m Manager) GetImportableInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
@@ -25,7 +26,11 @@ func (m Manager) GetImportableInterfaces(ctx context.Context) ([]domain.Physical
return physicalInterfaces, nil
}
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) {
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.Interface,
[]domain.Peer,
error,
) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, nil, err
}
@@ -145,7 +150,11 @@ func (m Manager) ApplyPeerDefaults(ctx context.Context, in *domain.Interface) er
return nil
}
func (m Manager) RestoreInterfaceState(ctx context.Context, updateDbOnError bool, filter ...domain.InterfaceIdentifier) error {
func (m Manager) RestoreInterfaceState(
ctx context.Context,
updateDbOnError bool,
filter ...domain.InterfaceIdentifier,
) error {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return err
}
@@ -166,60 +175,93 @@ func (m Manager) RestoreInterfaceState(ctx context.Context, updateDbOnError bool
}
_, err = m.wg.GetInterface(ctx, iface.Identifier)
if err != nil {
if err != nil && !iface.IsDisabled() {
logrus.Debugf("creating missing interface %s...", iface.Identifier)
// try to create a new interface
_, err = m.saveInterface(ctx, &iface, peers)
if err != nil {
return err
}
_, err = m.saveInterface(ctx, &iface)
if err != nil {
if updateDbOnError {
// disable interface in database as no physical interface exists
_ = m.db.SaveInterface(ctx, iface.Identifier, func(in *domain.Interface) (*domain.Interface, error) {
now := time.Now()
in.Disabled = &now // set
in.DisabledReason = domain.DisabledReasonInterfaceMissing
return in, nil
})
_ = m.db.SaveInterface(ctx, iface.Identifier,
func(in *domain.Interface) (*domain.Interface, error) {
now := time.Now()
in.Disabled = &now // set
in.DisabledReason = domain.DisabledReasonInterfaceMissing
return in, nil
})
}
return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err)
}
// restore peers
for _, peer := range peers {
err := m.wg.SavePeer(ctx, iface.Identifier, peer.Identifier, func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, &peer)
return pp, nil
})
if err != nil {
return fmt.Errorf("failed to create physical peer %s: %w", peer.Identifier, err)
}
}
} else {
logrus.Debugf("restoring interface state for %s to disabled=%t", iface.Identifier, iface.IsDisabled())
// try to move interface to stored state
_, err = m.saveInterface(ctx, &iface, peers)
_, err = m.saveInterface(ctx, &iface)
if err != nil {
if updateDbOnError {
// disable interface in database as no physical interface is available
_ = m.db.SaveInterface(ctx, iface.Identifier, func(in *domain.Interface) (*domain.Interface, error) {
if iface.IsDisabled() {
now := time.Now()
in.Disabled = &now // set
in.DisabledReason = domain.DisabledReasonInterfaceMissing
} else {
in.Disabled = nil
in.DisabledReason = ""
}
return in, nil
})
_ = m.db.SaveInterface(ctx, iface.Identifier,
func(in *domain.Interface) (*domain.Interface, error) {
if iface.IsDisabled() {
now := time.Now()
in.Disabled = &now // set
in.DisabledReason = domain.DisabledReasonInterfaceMissing
} else {
in.Disabled = nil
in.DisabledReason = ""
}
return in, nil
})
}
return fmt.Errorf("failed to change physical interface state for %s: %w", iface.Identifier, err)
}
}
// restore peers
for _, peer := range peers {
switch {
case iface.IsDisabled(): // if interface is disabled, delete all peers
if err := m.wg.DeletePeer(ctx, iface.Identifier, peer.Identifier); err != nil {
return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w",
peer.Identifier, iface.Identifier, err)
}
case peer.IsDisabled(): // if peer is disabled, delete it
if err := m.wg.DeletePeer(ctx, iface.Identifier, peer.Identifier); err != nil {
return fmt.Errorf("failed to remove disbaled peer %s from interface %s: %w",
peer.Identifier, iface.Identifier, err)
}
default: // update peer
err := m.wg.SavePeer(ctx, iface.Identifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, &peer)
return pp, nil
})
if err != nil {
return fmt.Errorf("failed to create/update physical peer %s for interface %s: %w",
peer.Identifier, iface.Identifier, err)
}
}
}
// remove non-wgportal peers
physicalPeers, _ := m.wg.GetPeers(ctx, iface.Identifier)
for _, physicalPeer := range physicalPeers {
isWgPortalPeer := false
for _, peer := range peers {
if peer.Identifier == domain.PeerIdentifier(physicalPeer.PublicKey) {
isWgPortalPeer = true
break
}
}
if !isWgPortalPeer {
err := m.wg.DeletePeer(ctx, iface.Identifier, domain.PeerIdentifier(physicalPeer.PublicKey))
if err != nil {
return fmt.Errorf("failed to remove non-wgportal peer %s from interface %s: %w",
physicalPeer.PublicKey, iface.Identifier, err)
}
}
}
}
return nil
@@ -322,7 +364,7 @@ func (m Manager) CreateInterface(ctx context.Context, in *domain.Interface) (*do
return nil, fmt.Errorf("creation not allowed: %w", err)
}
in, err = m.saveInterface(ctx, in, nil)
in, err = m.saveInterface(ctx, in)
if err != nil {
return nil, fmt.Errorf("creation failure: %w", err)
}
@@ -344,7 +386,7 @@ func (m Manager) UpdateInterface(ctx context.Context, in *domain.Interface) (*do
return nil, nil, fmt.Errorf("update not allowed: %w", err)
}
in, err = m.saveInterface(ctx, in, existingPeers)
in, err = m.saveInterface(ctx, in)
if err != nil {
return nil, nil, fmt.Errorf("update failure: %w", err)
}
@@ -392,9 +434,9 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
return fmt.Errorf("deletion failure: %w", err)
}
fwMark := int(existingInterface.FirewallMark)
fwMark := existingInterface.FirewallMark
if physicalInterface != nil && fwMark == 0 {
fwMark = int(physicalInterface.FirewallMark)
fwMark = physicalInterface.FirewallMark
}
m.bus.Publish(app.TopicRouteRemove, domain.RoutingTableInfo{
FwMark: fwMark,
@@ -410,7 +452,10 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
// region helper-functions
func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface, peers []domain.Peer) (*domain.Interface, error) {
func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
*domain.Interface,
error,
) {
stateChanged := m.hasInterfaceStateChanged(ctx, iface)
if err := m.handleInterfacePreSaveHooks(stateChanged, iface); err != nil {
@@ -424,10 +469,11 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface, pee
err := m.db.SaveInterface(ctx, iface.Identifier, func(i *domain.Interface) (*domain.Interface, error) {
iface.CopyCalculatedAttributes(i)
err := m.wg.SaveInterface(ctx, iface.Identifier, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
domain.MergeToPhysicalInterface(pi, iface)
return pi, nil
})
err := m.wg.SaveInterface(ctx, iface.Identifier,
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
domain.MergeToPhysicalInterface(pi, iface)
return pi, nil
})
if err != nil {
return nil, fmt.Errorf("failed to save physical interface %s: %w", iface.Identifier, err)
}
@@ -438,17 +484,18 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface, pee
return nil, fmt.Errorf("failed to save interface: %w", err)
}
m.bus.Publish(app.TopicRouteUpdate, "interface updated: "+string(iface.Identifier))
if iface.IsDisabled() {
physicalInterface, _ := m.wg.GetInterface(ctx, iface.Identifier)
fwMark := int(iface.FirewallMark)
fwMark := iface.FirewallMark
if physicalInterface != nil && fwMark == 0 {
fwMark = int(physicalInterface.FirewallMark)
fwMark = physicalInterface.FirewallMark
}
m.bus.Publish(app.TopicRouteRemove, domain.RoutingTableInfo{
FwMark: fwMark,
Table: iface.GetRoutingTable(),
})
} else {
m.bus.Publish(app.TopicRouteUpdate, "interface updated: "+string(iface.Identifier))
}
if err := m.handleInterfacePostSaveHooks(stateChanged, iface); err != nil {
@@ -702,18 +749,18 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
}
peer.InterfaceIdentifier = in.Identifier
peer.EndpointPublicKey = domain.StringConfigOption{Value: in.PublicKey, Overridable: true}
peer.AllowedIPsStr = domain.StringConfigOption{Value: in.PeerDefAllowedIPsStr, Overridable: true}
peer.EndpointPublicKey = domain.NewConfigOption(in.PublicKey, true)
peer.AllowedIPsStr = domain.NewConfigOption(in.PeerDefAllowedIPsStr, true)
peer.Interface.Addresses = p.AllowedIPs // use allowed IP's as the peer IP's TODO: Should this also match server interface address' prefix length?
peer.Interface.DnsStr = domain.StringConfigOption{Value: in.PeerDefDnsStr, Overridable: true}
peer.Interface.DnsSearchStr = domain.StringConfigOption{Value: in.PeerDefDnsSearchStr, Overridable: true}
peer.Interface.Mtu = domain.IntConfigOption{Value: in.PeerDefMtu, Overridable: true}
peer.Interface.FirewallMark = domain.Int32ConfigOption{Value: in.PeerDefFirewallMark, Overridable: true}
peer.Interface.RoutingTable = domain.StringConfigOption{Value: in.PeerDefRoutingTable, Overridable: true}
peer.Interface.PreUp = domain.StringConfigOption{Value: in.PeerDefPreUp, Overridable: true}
peer.Interface.PostUp = domain.StringConfigOption{Value: in.PeerDefPostUp, Overridable: true}
peer.Interface.PreDown = domain.StringConfigOption{Value: in.PeerDefPreDown, Overridable: true}
peer.Interface.PostDown = domain.StringConfigOption{Value: in.PeerDefPostDown, Overridable: true}
peer.Interface.DnsStr = domain.NewConfigOption(in.PeerDefDnsStr, true)
peer.Interface.DnsSearchStr = domain.NewConfigOption(in.PeerDefDnsSearchStr, true)
peer.Interface.Mtu = domain.NewConfigOption(in.PeerDefMtu, true)
peer.Interface.FirewallMark = domain.NewConfigOption(in.PeerDefFirewallMark, true)
peer.Interface.RoutingTable = domain.NewConfigOption(in.PeerDefRoutingTable, true)
peer.Interface.PreUp = domain.NewConfigOption(in.PeerDefPreUp, true)
peer.Interface.PostUp = domain.NewConfigOption(in.PeerDefPostUp, true)
peer.Interface.PreDown = domain.NewConfigOption(in.PeerDefPreDown, true)
peer.Interface.PostDown = domain.NewConfigOption(in.PeerDefPostDown, true)
switch in.Type {
case domain.InterfaceTypeAny:

View File

@@ -4,11 +4,12 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
"time"
)
func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdentifier) error {
@@ -101,12 +102,12 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Endpoint: domain.NewStringConfigOption(iface.PeerDefEndpoint, true),
EndpointPublicKey: domain.NewStringConfigOption(iface.PublicKey, true),
AllowedIPsStr: domain.NewStringConfigOption(iface.PeerDefAllowedIPsStr, true),
Endpoint: domain.NewConfigOption(iface.PeerDefEndpoint, true),
EndpointPublicKey: domain.NewConfigOption(iface.PublicKey, true),
AllowedIPsStr: domain.NewConfigOption(iface.PeerDefAllowedIPsStr, true),
ExtraAllowedIPsStr: "",
PresharedKey: pk,
PersistentKeepalive: domain.NewIntConfigOption(iface.PeerDefPersistentKeepalive, true),
PersistentKeepalive: domain.NewConfigOption(iface.PeerDefPersistentKeepalive, true),
DisplayName: fmt.Sprintf("Peer %s", internal.TruncateString(string(peerId), 8)),
Identifier: peerId,
UserIdentifier: currentUser.Id,
@@ -120,15 +121,15 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
Type: peerMode,
Addresses: ips,
CheckAliveAddress: "",
DnsStr: domain.NewStringConfigOption(iface.PeerDefDnsStr, true),
DnsSearchStr: domain.NewStringConfigOption(iface.PeerDefDnsSearchStr, true),
Mtu: domain.NewIntConfigOption(iface.PeerDefMtu, true),
FirewallMark: domain.NewInt32ConfigOption(iface.PeerDefFirewallMark, true),
RoutingTable: domain.NewStringConfigOption(iface.PeerDefRoutingTable, true),
PreUp: domain.NewStringConfigOption(iface.PeerDefPreUp, true),
PostUp: domain.NewStringConfigOption(iface.PeerDefPostUp, true),
PreDown: domain.NewStringConfigOption(iface.PeerDefPreUp, true),
PostDown: domain.NewStringConfigOption(iface.PeerDefPostUp, true),
DnsStr: domain.NewConfigOption(iface.PeerDefDnsStr, true),
DnsSearchStr: domain.NewConfigOption(iface.PeerDefDnsSearchStr, true),
Mtu: domain.NewConfigOption(iface.PeerDefMtu, true),
FirewallMark: domain.NewConfigOption(iface.PeerDefFirewallMark, true),
RoutingTable: domain.NewConfigOption(iface.PeerDefRoutingTable, true),
PreUp: domain.NewConfigOption(iface.PeerDefPreUp, true),
PostUp: domain.NewConfigOption(iface.PeerDefPostUp, true),
PreDown: domain.NewConfigOption(iface.PeerDefPreDown, true),
PostDown: domain.NewConfigOption(iface.PeerDefPostDown, true),
},
}
@@ -173,7 +174,11 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return peer, nil
}
func (m Manager) CreateMultiplePeers(ctx context.Context, interfaceId domain.InterfaceIdentifier, r *domain.PeerCreationRequest) ([]domain.Peer, error) {
func (m Manager) CreateMultiplePeers(
ctx context.Context,
interfaceId domain.InterfaceIdentifier,
r *domain.PeerCreationRequest,
) ([]domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
@@ -225,9 +230,31 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("update not allowed: %w", err)
}
err = m.savePeers(ctx, peer)
if err != nil {
return nil, fmt.Errorf("update failure: %w", err)
// 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
// delete old peer
err = m.DeletePeer(ctx, existingPeer.Identifier)
if err != nil {
return nil, fmt.Errorf("failed to delete old peer %s for %s: %w",
existingPeer.Identifier, peer.Identifier, err)
}
// save new peer
err = m.savePeers(ctx, peer)
if err != nil {
return nil, fmt.Errorf("update failure for re-identified peer %s (was %s): %w",
peer.Identifier, existingPeer.Identifier, err)
}
// publish event
m.bus.Publish(app.TopicPeerIdentifierUpdated, existingPeer.Identifier, peer.Identifier)
} else { // normal update
err = m.savePeers(ctx, peer)
if err != nil {
return nil, fmt.Errorf("update failure: %w", err)
}
}
return peer, nil
@@ -243,6 +270,10 @@ func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
return err
}
if err := m.validatePeerDeletion(ctx, peer); err != nil {
return fmt.Errorf("delete not allowed: %w", err)
}
err = m.wg.DeletePeer(ctx, peer.InterfaceIdentifier, id)
if err != nil {
return fmt.Errorf("wireguard failed to delete peer %s: %w", id, err)
@@ -253,6 +284,11 @@ func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
return fmt.Errorf("failed to delete peer %s: %w", id, err)
}
// Update routes after peers have changed
m.bus.Publish(app.TopicRouteUpdate, "peers updated")
// Update interface after peers have changed
m.bus.Publish(app.TopicPeerInterfaceUpdated, peer.InterfaceIdentifier)
return nil
}
@@ -299,20 +335,33 @@ func (m Manager) savePeers(ctx context.Context, peers ...*domain.Peer) error {
for i := range peers {
peer := peers[i]
err := m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
peer.CopyCalculatedAttributes(p)
var err error
if peer.IsDisabled() || peer.IsExpired() {
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
peer.CopyCalculatedAttributes(p)
err := m.wg.SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, peer)
return pp, nil
})
if err != nil {
return nil, fmt.Errorf("failed to save wireguard peer %s: %w", peer.Identifier, err)
}
if err := m.wg.DeletePeer(ctx, peer.InterfaceIdentifier, peer.Identifier); err != nil {
return nil, fmt.Errorf("failed to delete wireguard peer %s: %w", peer.Identifier, err)
}
return peer, nil
})
return peer, nil
})
} else {
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
peer.CopyCalculatedAttributes(p)
err := m.wg.SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, peer)
return pp, nil
})
if err != nil {
return nil, fmt.Errorf("failed to save wireguard peer %s: %w", peer.Identifier, err)
}
return peer, nil
})
}
if err != nil {
return fmt.Errorf("save failure for peer %s: %w", peer.Identifier, err)
}
@@ -372,7 +421,7 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
}
}
ips = append(ips, ip)
ips = append(ips, ip.HostAddr())
}
return

View File

@@ -1,14 +1,15 @@
package config
import (
"time"
"github.com/go-ldap/ldap/v3"
)
type Auth struct {
OpenIDConnect []OpenIDConnectProvider `yaml:"oidc"`
OAuth []OAuthProvider `yaml:"oauth"`
Ldap []LdapProvider `yaml:"ldap"`
CallbackUrlPrefix string `yaml:"callback_url_prefix"`
OpenIDConnect []OpenIDConnectProvider `yaml:"oidc"`
OAuth []OAuthProvider `yaml:"oauth"`
Ldap []LdapProvider `yaml:"ldap"`
}
type BaseFields struct {
@@ -22,7 +23,7 @@ type BaseFields struct {
type OauthFields struct {
BaseFields `yaml:",inline"`
IsAdmin string `yaml:"is_admin"`
IsAdmin string `yaml:"is_admin"` // If the value is "true", the user is an admin.
}
type LdapFields struct {
@@ -50,10 +51,10 @@ type LdapProvider struct {
AdminGroupDN string `yaml:"admin_group"` // Members of this group receive admin rights in WG-Portal
ParsedAdminGroupDN *ldap.DN `yaml:"-"`
Synchronize bool `yaml:"synchronize"`
// If DisableMissing is true, missing users will be deactivated
DisableMissing bool `yaml:"disable_missing"`
SyncFilter string `yaml:"sync_filter"`
DisableMissing bool `yaml:"disable_missing"`
SyncFilter string `yaml:"sync_filter"`
SyncInterval time.Duration `yaml:"sync_interval"`
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
RegistrationEnabled bool `yaml:"registration_enabled"`
@@ -91,8 +92,6 @@ type OAuthProvider struct {
// DisplayName is shown to the user on the login page. If it is empty, ProviderName will be displayed.
DisplayName string `yaml:"display_name"`
BaseUrl string `yaml:"base_url"`
// ClientID is the application's ID.
ClientID string `yaml:"client_id"`
@@ -103,10 +102,6 @@ type OAuthProvider struct {
TokenURL string `yaml:"token_url"`
UserInfoURL string `yaml:"user_info_url"`
// RedirectURL is the URL to redirect users going through
// the OAuth flow, after the resource owner's URLs.
RedirectURL string `yaml:"redirect_url"`
// Scope specifies optional requested permissions.
Scopes []string `yaml:"scopes"`

View File

@@ -2,10 +2,12 @@ package config
import (
"fmt"
"github.com/sirupsen/logrus"
"os"
"time"
"github.com/a8m/envsubst"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
@@ -18,6 +20,8 @@ type Config struct {
EditableKeys bool `yaml:"editable_keys"`
CreateDefaultPeer bool `yaml:"create_default_peer"`
CreateDefaultPeerOnCreation bool `yaml:"create_default_peer_on_creation"`
ReEnablePeerAfterUserEnable bool `yaml:"re_enable_peer_after_user_enable"`
DeletePeerAfterUserDeleted bool `yaml:"delete_peer_after_user_deleted"`
SelfProvisioningAllowed bool `yaml:"self_provisioning_allowed"`
ImportExisting bool `yaml:"import_existing"`
RestoreState bool `yaml:"restore_state"`
@@ -27,7 +31,6 @@ type Config struct {
LogLevel string `yaml:"log_level"`
LogPretty bool `yaml:"log_pretty"`
LogJson bool `yaml:"log_json"`
LdapSyncInterval time.Duration `yaml:"ldap_sync_interval"`
StartListenPort int `yaml:"start_listen_port"`
StartCidrV4 string `yaml:"start_cidr_v4"`
StartCidrV6 string `yaml:"start_cidr_v6"`
@@ -47,6 +50,7 @@ type Config struct {
CollectInterfaceData bool `yaml:"collect_interface_data"`
CollectPeerData bool `yaml:"collect_peer_data"`
CollectAuditData bool `yaml:"collect_audit_data"`
ListeningAddress string `yaml:"listening_address"`
} `yaml:"statistics"`
Mail MailConfig `yaml:"mail"`
@@ -59,9 +63,13 @@ type Config struct {
}
func (c *Config) LogStartupValues() {
logrus.Infof("Log Level: %s", c.Advanced.LogLevel)
logrus.Debug("WireGuard Portal Features:")
logrus.Debugf(" - EditableKeys: %t", c.Core.EditableKeys)
logrus.Debugf(" - CreateDefaultPeerOnCreation: %t", c.Core.CreateDefaultPeerOnCreation)
logrus.Debugf(" - ReEnablePeerAfterUserEnable: %t", c.Core.ReEnablePeerAfterUserEnable)
logrus.Debugf(" - DeletePeerAfterUserDeleted: %t", c.Core.DeletePeerAfterUserDeleted)
logrus.Debugf(" - SelfProvisioningAllowed: %t", c.Core.SelfProvisioningAllowed)
logrus.Debugf(" - ImportExisting: %t", c.Core.ImportExisting)
logrus.Debugf(" - RestoreState: %t", c.Core.RestoreState)
@@ -83,8 +91,16 @@ func (c *Config) LogStartupValues() {
func defaultConfig() *Config {
cfg := &Config{}
cfg.Core.AdminUser = "admin@wgportal.local"
cfg.Core.AdminPassword = "wgportal"
cfg.Core.ImportExisting = true
cfg.Core.RestoreState = true
cfg.Core.CreateDefaultPeer = false
cfg.Core.CreateDefaultPeerOnCreation = false
cfg.Core.EditableKeys = true
cfg.Core.SelfProvisioningAllowed = false
cfg.Core.ReEnablePeerAfterUserEnable = true
cfg.Core.DeletePeerAfterUserDeleted = false
cfg.Database = DatabaseConfig{
Type: "sqlite",
@@ -102,8 +118,7 @@ func defaultConfig() *Config {
SiteCompanyName: "WireGuard Portal",
}
cfg.Auth.CallbackUrlPrefix = "/api/v0"
cfg.Advanced.LogLevel = "info"
cfg.Advanced.StartListenPort = 51820
cfg.Advanced.StartCidrV4 = "10.11.12.0/24"
cfg.Advanced.StartCidrV6 = "fdfd:d3ad:c0de:1234::0/64"
@@ -116,10 +131,11 @@ func defaultConfig() *Config {
cfg.Statistics.PingCheckWorkers = 10
cfg.Statistics.PingUnprivileged = false
cfg.Statistics.PingCheckInterval = 1 * time.Minute
cfg.Statistics.DataCollectionInterval = 10 * time.Second
cfg.Statistics.DataCollectionInterval = 1 * time.Minute
cfg.Statistics.CollectInterfaceData = true
cfg.Statistics.CollectPeerData = true
cfg.Statistics.CollectAuditData = true
cfg.Statistics.ListeningAddress = ":8787"
cfg.Mail = MailConfig{
Host: "127.0.0.1",
@@ -154,20 +170,18 @@ func GetConfig() (*Config, error) {
}
func loadConfigFile(cfg any, filename string) error {
f, err := os.Open(filename)
data, err := envsubst.ReadFile(filename)
if err != nil {
return err
}
defer func(f *os.File) {
if err := f.Close(); err != nil {
logrus.Errorf("failed to close configuration file %s: %v", filename, err)
if os.IsNotExist(err) {
logrus.Warnf("Config file %s not found, using default values", filename)
return nil
}
}(f)
return fmt.Errorf("envsubst error: %v", err)
}
decoder := yaml.NewDecoder(f)
err = decoder.Decode(cfg)
err = yaml.Unmarshal(data, cfg)
if err != nil {
return err
return fmt.Errorf("yaml error: %v", err)
}
return nil

View File

@@ -9,4 +9,6 @@ type WebConfig struct {
CsrfSecret string `yaml:"csrf_secret"`
SiteTitle string `yaml:"site_title"`
SiteCompanyName string `yaml:"site_company_name"`
CertFile string `yaml:"cert_file"`
KeyFile string `yaml:"key_file"`
}

View File

@@ -1,10 +1,9 @@
package domain
import (
"database/sql/driver"
"errors"
"time"
"database/sql/driver"
)
type BaseModel struct {
@@ -26,27 +25,32 @@ func (PrivateString) String() string {
func (ps PrivateString) Value() (driver.Value, error) {
if len(ps) == 0 {
return nil, nil
}
return nil, nil
}
return string(ps), nil
}
func (ps *PrivateString) Scan(value interface{}) error {
if value == nil {
*ps = ""
return nil
}
strValue, ok := value.(string)
if !ok {
return errors.New("invalid type for PrivateString")
}
*ps = PrivateString(strValue)
return nil
if value == nil {
*ps = ""
return nil
}
switch v := value.(type) {
case string:
*ps = PrivateString(v)
case []byte:
*ps = PrivateString(string(v))
default:
return errors.New("invalid type for PrivateString")
}
return nil
}
const (
DisabledReasonExpired = "expired"
DisabledReasonDeleted = "deleted"
DisabledReasonUserDisabled = "user disabled"
DisabledReasonUserDeleted = "user deleted"
DisabledReasonUserEdit = "user edit action"
DisabledReasonUserCreate = "user create action"
DisabledReasonAdminEdit = "admin edit action"

View File

@@ -2,13 +2,14 @@ package domain
import (
"fmt"
"github.com/h44z/wg-portal/internal"
"github.com/sirupsen/logrus"
"math"
"regexp"
"strconv"
"strings"
"time"
"github.com/h44z/wg-portal/internal"
"github.com/sirupsen/logrus"
)
const (
@@ -34,7 +35,7 @@ type Interface struct {
DnsSearchStr string // the dns search option string that should be set if the interface is up, will be appended to DnsStr
Mtu int // the device MTU
FirewallMark int32 // a firewall mark
FirewallMark uint32 // a firewall mark
RoutingTable string // the routing table number or "off" if the routing table should not be managed
PreUp string // action that is executed before the device is up
@@ -61,7 +62,7 @@ type Interface struct {
PeerDefAllowedIPsStr string // the default allowed IP string for the peer
PeerDefMtu int // the default device MTU
PeerDefPersistentKeepalive int // the default persistent keep-alive Value
PeerDefFirewallMark int32 // default firewall mark
PeerDefFirewallMark uint32 // default firewall mark
PeerDefRoutingTable string // the default routing table
PeerDefPreUp string // default action that is executed before the device is up
@@ -161,8 +162,8 @@ type PhysicalInterface struct {
Addresses []Cidr // the interface ip addresses
Mtu int // the device MTU
FirewallMark int32 // a firewall mark
Mtu int // the device MTU
FirewallMark uint32 // a firewall mark
DeviceUp bool // device status
@@ -223,7 +224,7 @@ func MergeToPhysicalInterface(pi *PhysicalInterface, i *Interface) {
}
type RoutingTableInfo struct {
FwMark int
FwMark uint32
Table int
}
@@ -241,7 +242,7 @@ func (r RoutingTableInfo) ManagementEnabled() bool {
func (r RoutingTableInfo) GetRoutingTable() int {
if r.Table <= 0 {
return r.FwMark // use the dynamic routing table which has the same number as the firewall mark
return int(r.FwMark) // use the dynamic routing table which has the same number as the firewall mark
}
return r.Table

View File

@@ -1,47 +1,19 @@
package domain
type StringConfigOption struct {
Value string `gorm:"column:v"`
Overridable bool `gorm:"column:o"`
}
func (o StringConfigOption) GetValue() string {
return o.Value
}
func (o *StringConfigOption) SetValue(value string) {
o.Value = value
}
func (o *StringConfigOption) TrySetValue(value string) bool {
if o.Overridable {
o.Value = value
return true
}
return false
}
func NewStringConfigOption(value string, overridable bool) StringConfigOption {
return StringConfigOption{
Value: value,
Overridable: overridable,
}
}
type IntConfigOption struct {
Value int `gorm:"column:v"`
type ConfigOption[T any] struct {
Value T `gorm:"column:v"`
Overridable bool `gorm:"column:o"`
}
func (o IntConfigOption) GetValue() int {
func (o *ConfigOption[T]) GetValue() T {
return o.Value
}
func (o *IntConfigOption) SetValue(value int) {
func (o *ConfigOption[T]) SetValue(value T) {
o.Value = value
}
func (o *IntConfigOption) TrySetValue(value int) bool {
func (o *ConfigOption[T]) TrySetValue(value T) bool {
if o.Overridable {
o.Value = value
return true
@@ -49,64 +21,8 @@ func (o *IntConfigOption) TrySetValue(value int) bool {
return false
}
func NewIntConfigOption(value int, overridable bool) IntConfigOption {
return IntConfigOption{
Value: value,
Overridable: overridable,
}
}
type Int32ConfigOption struct {
Value int32 `gorm:"column:v"`
Overridable bool `gorm:"column:o"`
}
func (o Int32ConfigOption) GetValue() int32 {
return o.Value
}
func (o *Int32ConfigOption) SetValue(value int32) {
o.Value = value
}
func (o *Int32ConfigOption) TrySetValue(value int32) bool {
if o.Overridable {
o.Value = value
return true
}
return false
}
func NewInt32ConfigOption(value int32, overridable bool) Int32ConfigOption {
return Int32ConfigOption{
Value: value,
Overridable: overridable,
}
}
type BoolConfigOption struct {
Value bool `gorm:"column:v"`
Overridable bool `gorm:"column:o"`
}
func (o BoolConfigOption) GetValue() bool {
return o.Value
}
func (o *BoolConfigOption) SetValue(value bool) {
o.Value = value
}
func (o *BoolConfigOption) TrySetValue(value bool) bool {
if o.Overridable {
o.Value = value
return true
}
return false
}
func NewBoolConfigOption(value bool, overridable bool) BoolConfigOption {
return BoolConfigOption{
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
return ConfigOption[T]{
Value: value,
Overridable: overridable,
}

View File

@@ -2,12 +2,13 @@ package domain
import (
"fmt"
"github.com/h44z/wg-portal/internal"
"net"
"regexp"
"strings"
"time"
"github.com/h44z/wg-portal/internal"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
@@ -31,12 +32,12 @@ type Peer struct {
// WireGuard specific (for the [peer] section of the config file)
Endpoint StringConfigOption `gorm:"embedded;embeddedPrefix:endpoint_"` // the endpoint address
EndpointPublicKey StringConfigOption `gorm:"embedded;embeddedPrefix:endpoint_pubkey_"` // the endpoint public key
AllowedIPsStr StringConfigOption `gorm:"embedded;embeddedPrefix:allowed_ips_str_"` // all allowed ip subnets, comma seperated
ExtraAllowedIPsStr string // all allowed ip subnets on the server side, comma seperated
PresharedKey PreSharedKey // the pre-shared Key of the peer
PersistentKeepalive IntConfigOption `gorm:"embedded;embeddedPrefix:persistent_keep_alive_"` // the persistent keep-alive interval
Endpoint ConfigOption[string] `gorm:"embedded;embeddedPrefix:endpoint_"` // the endpoint address
EndpointPublicKey ConfigOption[string] `gorm:"embedded;embeddedPrefix:endpoint_pubkey_"` // the endpoint public key
AllowedIPsStr ConfigOption[string] `gorm:"embedded;embeddedPrefix:allowed_ips_str_"` // all allowed ip subnets, comma seperated
ExtraAllowedIPsStr string // all allowed ip subnets on the server side, comma seperated
PresharedKey PreSharedKey // the pre-shared Key of the peer
PersistentKeepalive ConfigOption[int] `gorm:"embedded;embeddedPrefix:persistent_keep_alive_"` // the persistent keep-alive interval
// WG Portal specific
@@ -124,18 +125,18 @@ type PeerInterfaceConfig struct {
Type InterfaceType `gorm:"column:iface_type"` // the interface type (server, client, any)
Addresses []Cidr `gorm:"many2many:peer_addresses;"` // the interface ip addresses
CheckAliveAddress string `gorm:"column:check_alive_address"` // optional ip address or DNS name that is used for ping checks
DnsStr StringConfigOption `gorm:"embedded;embeddedPrefix:iface_dns_str_"` // the dns server that should be set if the interface is up, comma separated
DnsSearchStr StringConfigOption `gorm:"embedded;embeddedPrefix:iface_dns_search_str_"` // the dns search option string that should be set if the interface is up, will be appended to DnsStr
Mtu IntConfigOption `gorm:"embedded;embeddedPrefix:iface_mtu_"` // the device MTU
FirewallMark Int32ConfigOption `gorm:"embedded;embeddedPrefix:iface_firewall_mark_"` // a firewall mark
RoutingTable StringConfigOption `gorm:"embedded;embeddedPrefix:iface_routing_table_"` // the routing table
Addresses []Cidr `gorm:"many2many:peer_addresses;"` // the interface ip addresses
CheckAliveAddress string `gorm:"column:check_alive_address"` // optional ip address or DNS name that is used for ping checks
DnsStr ConfigOption[string] `gorm:"embedded;embeddedPrefix:iface_dns_str_"` // the dns server that should be set if the interface is up, comma separated
DnsSearchStr ConfigOption[string] `gorm:"embedded;embeddedPrefix:iface_dns_search_str_"` // the dns search option string that should be set if the interface is up, will be appended to DnsStr
Mtu ConfigOption[int] `gorm:"embedded;embeddedPrefix:iface_mtu_"` // the device MTU
FirewallMark ConfigOption[uint32] `gorm:"embedded;embeddedPrefix:iface_firewall_mark_"` // a firewall mark
RoutingTable ConfigOption[string] `gorm:"embedded;embeddedPrefix:iface_routing_table_"` // the routing table
PreUp StringConfigOption `gorm:"embedded;embeddedPrefix:iface_pre_up_"` // action that is executed before the device is up
PostUp StringConfigOption `gorm:"embedded;embeddedPrefix:iface_post_up_"` // action that is executed after the device is up
PreDown StringConfigOption `gorm:"embedded;embeddedPrefix:iface_pre_down_"` // action that is executed before the device is down
PostDown StringConfigOption `gorm:"embedded;embeddedPrefix:iface_post_down_"` // action that is executed after the device is down
PreUp ConfigOption[string] `gorm:"embedded;embeddedPrefix:iface_pre_up_"` // action that is executed before the device is up
PostUp ConfigOption[string] `gorm:"embedded;embeddedPrefix:iface_post_up_"` // action that is executed after the device is up
PreDown ConfigOption[string] `gorm:"embedded;embeddedPrefix:iface_pre_down_"` // action that is executed before the device is down
PostDown ConfigOption[string] `gorm:"embedded;embeddedPrefix:iface_post_down_"` // action that is executed after the device is down
}
func (p *PeerInterfaceConfig) AddressStr() string {
@@ -202,12 +203,12 @@ func (p PhysicalPeer) GetAllowedIPs() []net.IPNet {
func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
peer := &Peer{
Endpoint: StringConfigOption{Value: pp.Endpoint, Overridable: true},
EndpointPublicKey: StringConfigOption{Value: "", Overridable: true},
AllowedIPsStr: StringConfigOption{Value: "", Overridable: true},
Endpoint: NewConfigOption(pp.Endpoint, true),
EndpointPublicKey: NewConfigOption("", true),
AllowedIPsStr: NewConfigOption("", true),
ExtraAllowedIPsStr: "",
PresharedKey: pp.PresharedKey,
PersistentKeepalive: IntConfigOption{Value: pp.PersistentKeepalive, Overridable: true},
PersistentKeepalive: NewConfigOption(pp.PersistentKeepalive, true),
DisplayName: string(pp.Identifier),
Identifier: pp.Identifier,
UserIdentifier: "",

View File

@@ -126,3 +126,10 @@ func TruncateString(s string, max int) string {
}
return s[:max]
}
func BoolToFloat64(b bool) float64 {
if b {
return 1.0
}
return 0.0
}

View File

@@ -1,13 +1,14 @@
# build with MkDocs 1.5.3, mkdocs-material-9.4.7
---
site_name: WireGuard Portal
site_description: 'Manage WireGuard Peers and Interface using a beautiful and simple web UI.'
site_url: https://wgportal.org
repo_name: 'h44z/wg-portal'
repo_url: 'https://github.com/h44z/wg-portal'
copyright: 'Copyright &copy; 2023-2024 WireGuard Portal Project'
site_description: Manage WireGuard Peers and Interface using a beautiful and simple web UI.
site_url: https://wgportal.org/
repo_name: h44z/wg-portal
repo_url: https://github.com/h44z/wg-portal
copyright: Copyright &copy; 2023-2024 WireGuard Portal Project
extra_css:
- stylesheets/extra.css
theme:
name: material
custom_dir: docs/theme-overrides
@@ -18,15 +19,19 @@ theme:
favicon: assets/images/favicon-large.png
language: en
features:
- tabs
- instant
- navigation.instant
- navigation.tabs
plugins:
- search
#- social # disable until https://github.com/squidfunk/mkdocs-material/issues/6983 is resolved
- social
- minify:
minify_html: true
extra:
version:
provider: mike
default: latest
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/h44z/wg-portal