Compare commits

...

19 Commits

Author SHA1 Message Date
Christoph Haas
1fc7e352ab mikrotik: allow to set DNS, wip: handle routes in wg-controller 2025-10-06 22:17:39 +02:00
Christoph Haas
4d19f1d8bb fix a small visual glitch in dialogs
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-10-06 21:10:39 +02:00
Christoph Haas
3f539a1615 improve interface view: show correct DNS entries 2025-10-06 19:26:51 +02:00
PROLICHT GmbH
0305911467 Merge pull request #539 from h44z/dependabot/go_modules/github.com/go-playground/validator/v10-10.28.0
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
chore(deps): bump github.com/go-playground/validator/v10 from 10.27.0 to 10.28.0
2025-10-06 16:05:21 +02:00
dependabot[bot]
85f7a5a9a6 chore(deps): bump github.com/go-playground/validator/v10
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.27.0 to 10.28.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.27.0...v10.28.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-version: 10.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 13:48:44 +00:00
Christoph Haas
fb509a39b8 Merge remote-tracking branch 'origin/master'
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-10-04 14:24:45 +02:00
h44z
9e6ad98c4e Doc improvements (#538)
* add dark/light image to doc

* add dark/light image to doc

* add funding info, prepare release v2.1
2025-10-04 14:17:29 +02:00
Christoph Haas
05fbcccc9c chore: update dependencies 2025-10-04 13:22:51 +02:00
Christoph Haas
97b6c398e8 fix incorrect handling of client mode (#537)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-10-03 17:30:14 +02:00
Christoph Haas
cc2d1f53c4 improve logging of LDAP login process even more (#529)
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-09-24 18:39:45 +02:00
Christoph Haas
b122e1ae60 add tzdata to docker image (#531)
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-09-22 18:45:41 +02:00
Christoph Haas
ea26e56994 fix delayed setup of external auth providers (#529)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-09-21 21:16:12 +02:00
h44z
61bf349813 add user's display-name to peer view (#525) (#534)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-09-21 13:02:12 +02:00
Osvaldo-Net
80693400be Soporte para el idioma español. (#530) (#532)
* Add files via upload

* Update index.js

* Update App.vue

* Update es.json
2025-09-21 12:44:59 +02:00
Christoph Haas
afb38b685c improve logging of LDAP login process (#529)
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-09-17 22:33:54 +02:00
h44z
7cd7d13dc7 fix peer creation if custom public key is set (#523) (#528)
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-09-15 22:54:34 +02:00
dependabot[bot]
d945e313b2 chore(deps): bump github.com/go-webauthn/webauthn from 0.13.4 to 0.14.0 (#526)
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.13.4 to 0.14.0.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.13.4...v0.14.0)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 22:50:10 +02:00
dependabot[bot]
c5fe82ab11 chore(deps): bump gorm.io/gorm from 1.30.5 to 1.31.0 in the gorm group (#527)
Bumps the gorm group with 1 update: [gorm.io/gorm](https://github.com/go-gorm/gorm).


Updates `gorm.io/gorm` from 1.30.5 to 1.31.0
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.30.5...v1.31.0)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-version: 1.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gorm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 22:48:44 +02:00
h44z
765fb09770 Mikrotik improvements (#521)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
* allow to specify ignored interfaces (#514)

* only set endpoint info for "responder" peers (#516)
2025-09-09 21:43:16 +02:00
49 changed files with 2727 additions and 1559 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: h44z # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]

View File

@@ -52,7 +52,7 @@ COPY --from=builder /build/dist/wg-portal /
###### ######
FROM alpine:3.22 FROM alpine:3.22
# Install OS-level dependencies # Install OS-level dependencies
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools tzdata
# Setup timezone # Setup timezone
ENV TZ=UTC ENV TZ=UTC
# Copy binaries # Copy binaries

View File

@@ -1,4 +1,4 @@
Copyright (c) 2020-2023 Christoph Haas Copyright (c) 2020-2025 Christoph Haas
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View File

@@ -21,7 +21,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Pos
## Features ## Features
* Self-hosted - the whole application is a single binary * Self-hosted - the whole application is a single binary
* Responsive multi-language web UI written in Vue.js * Responsive multi-language web UI with dark-mode written in Vue.js
* Automatically selects IP from the network pool assigned to the client * Automatically selects IP from the network pool assigned to the client
* QR-Code for convenient mobile client configuration * QR-Code for convenient mobile client configuration
* Sends email to the client with QR-code and client config * Sends email to the client with QR-code and client config
@@ -32,7 +32,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Pos
* Docker ready * Docker ready
* Can be used with existing WireGuard setups * Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces * Support for multiple WireGuard interfaces
* Supports multiple WireGuard backends (wgctrl or MikroTik [BETA]) * Supports multiple WireGuard backends (wgctrl or MikroTik)
* Peer Expiry Feature * Peer Expiry Feature
* Handles route and DNS settings like wg-quick does * Handles route and DNS settings like wg-quick does
* Exposes Prometheus metrics for monitoring and alerting * Exposes Prometheus metrics for monitoring and alerting
@@ -62,6 +62,17 @@ For the complete documentation visit [wgportal.org](https://wgportal.org).
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT> * MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
## Contributors and Sponsors
Thanks so much for all your contributions! Theyre truly appreciated and help keep WireGuard Portal moving ahead.
<a href="https://github.com/h44z/wg-portal/graphs/contributors">
<img src="https://contrib.rocks/image?repo=h44z/wg-portal" />
</a>
Want to support the project? You can buy me a coffee or join as a contributor - every bit of support helps!
[Become a sponsor!](https://github.com/sponsors/h44z)
> [!IMPORTANT] > [!IMPORTANT]
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal). > Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).

View File

@@ -7,7 +7,7 @@ If you believe you've found a security issue in one of the supported versions of
| Version | Supported | | Version | Supported |
|---------|--------------------| |---------|--------------------|
| v2.x | :white_check_mark: | | v2.x | :white_check_mark: |
| v1.x | :white_check_mark: | | v1.x | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@@ -53,8 +53,6 @@ func main() {
wireGuard, err := wireguard.NewControllerManager(cfg) wireGuard, err := wireguard.NewControllerManager(cfg)
internal.AssertNoError(err) internal.AssertNoError(err)
wgQuick := adapters.NewWgQuickRepo()
mailer := adapters.NewSmtpMailRepo(cfg.Mail) mailer := adapters.NewSmtpMailRepo(cfg.Mail)
metricsServer := adapters.NewMetricsServer(cfg) metricsServer := adapters.NewMetricsServer(cfg)
@@ -93,7 +91,7 @@ func main() {
webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, eventBus, userManager) webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, eventBus, userManager)
internal.AssertNoError(err) internal.AssertNoError(err)
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database) wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, database)
internal.AssertNoError(err) internal.AssertNoError(err)
wireGuardManager.StartBackgroundJobs(ctx) wireGuardManager.StartBackgroundJobs(ctx)
@@ -107,7 +105,7 @@ func main() {
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database) mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
internal.AssertNoError(err) internal.AssertNoError(err)
routeManager, err := route.NewRouteManager(cfg, eventBus, database) routeManager, err := route.NewRouteManager(cfg, eventBus, database, wireGuard)
internal.AssertNoError(err) internal.AssertNoError(err)
routeManager.StartBackgroundJobs(ctx) routeManager.StartBackgroundJobs(ctx)

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@@ -11,6 +11,27 @@ core:
create_default_peer: true create_default_peer: true
self_provisioning_allowed: true self_provisioning_allowed: true
backend:
# default backend decides where new interfaces are created
default: mikrotik
# A prefix for resolvconf. Usually it is "tun.". If you are using systemd, the prefix should be empty.
local_resolvconf_prefix: "tun."
mikrotik:
- id: mikrotik # unique id, not "local"
display_name: RouterOS RB5009 # optional nice name
api_url: https://10.10.10.10/rest
api_user: wgportal
api_password: a-super-secret-password
api_verify_tls: false # set to false only if using self-signed during testing
api_timeout: 30s # maximum request duration
concurrency: 5 # limit parallel REST calls to device
debug: false # verbose logging for this backend
ignored_interfaces: # ignore these interfaces during import
- wgTest1
- wgTest2
web: web:
site_title: My WireGuard Server site_title: My WireGuard Server
site_company_name: My Company site_company_name: My Company
@@ -195,3 +216,5 @@ auth:
registration_enabled: true registration_enabled: true
log_user_info: true log_user_info: true
``` ```
For more information, check out the usage documentation (e.g. [General Configuration](../usage/general.md) or [Backends Configuration](../usage/backends.md)).

View File

@@ -28,6 +28,7 @@ core:
backend: backend:
default: local default: local
local_resolvconf_prefix: tun.
advanced: advanced:
log_level: info log_level: info
@@ -184,6 +185,16 @@ The current MikroTik backend is in **BETA** and may not support all features.
- **Description:** The default backend to use for managing WireGuard interfaces. - **Description:** The default backend to use for managing WireGuard interfaces.
Valid options are: `local`, or other backend id's configured in the `mikrotik` section. Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
### `local_resolvconf_prefix`
- **Default:** `tun.`
- **Description:** Interface name prefix for WireGuard interfaces on the local system which is used to configure DNS servers with *resolvconf*.
It depends on the *resolvconf* implementation you are using, most use a prefix of `tun.`, but some have an empty prefix (e.g., systemd).
### `ignored_local_interfaces`
- **Default:** *(empty)*
- **Description:** A list of interface names to exclude when enumerating local interfaces.
This is useful if you want to prevent certain interfaces from being imported from the local system.
### Mikrotik ### Mikrotik
The `mikrotik` array contains a list of MikroTik backend definitions. Each entry describes how to connect to a MikroTik RouterOS instance that hosts WireGuard interfaces. The `mikrotik` array contains a list of MikroTik backend definitions. Each entry describes how to connect to a MikroTik RouterOS instance that hosts WireGuard interfaces.
@@ -225,6 +236,11 @@ Below are the properties for each entry inside `backend.mikrotik`:
- **Default:** `5` - **Default:** `5`
- **Description:** Maximum number of concurrent API requests the backend will issue when enumerating interfaces and their details. If `0` or negative, a sane default of `5` is used. - **Description:** Maximum number of concurrent API requests the backend will issue when enumerating interfaces and their details. If `0` or negative, a sane default of `5` is used.
#### `ignored_interfaces`
- **Default:** *(empty)*
- **Description:** A list of interface names to exclude during interface enumeration.
This is useful if you want to prevent specific interfaces from being imported from the MikroTik device.
#### `debug` #### `debug`
- **Default:** `false` - **Default:** `false`
- **Description:** Enable verbose debug logging for the MikroTik backend. - **Description:** Enable verbose debug logging for the MikroTik backend.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
img-comparison-slider {
visibility: hidden;
}
img-comparison-slider [slot='second'] {
display: none;
}
img-comparison-slider.rendered {
visibility: inherit;
}
img-comparison-slider.rendered [slot='second'] {
display: unset;
}

View File

@@ -300,6 +300,59 @@
background: var(--md-accent-fg-color--transparent); background: var(--md-accent-fg-color--transparent);
} }
.before,
.after {
margin: 0;
}
.after figcaption {
background: #fff;
font-weight: bold;
border: 1px solid #c0c0c0;
color: #000000;
opacity: 0.9;
padding: 9px;
position: absolute;
top: 100%;
transform: translateY(-100%);
line-height: 100%;
}
.before figcaption {
background: #000;
font-weight: bold;
border: 1px solid #c0c0c0;
color: #ffffff;
opacity: 0.9;
padding: 9px;
position: absolute;
top: 100%;
transform: translateY(-100%);
line-height: 100%;
}
.before figcaption {
left: 0px;
}
.after figcaption {
right: 0px;
}
.custom-animated-handle {
transition: transform 0.2s;
}
.slider-with-animated-handle:hover .custom-animated-handle {
transform: scale(1.2);
}
.md-typeset img-comparison-slider figure {
margin: initial;
}
.first-overlay {
color: #000;
}
</style> </style>
<!-- Hero for landing page --> <!-- Hero for landing page -->
@@ -326,11 +379,34 @@
<div class="md-container"> <div class="md-container">
<div class="tx-hero__image"> <div class="tx-hero__image">
<img <div>
src="{{config.site_url}}/assets/images/screenshot.png" <img-comparison-slider hover="hover">
alt="" <figure slot="first" class="before">
draggable="false" <img src="{{config.site_url}}/assets/images/wgportal_light.png" alt="Light Mode"/>
> <figcaption>Light Mode</figcaption>
</figure>
<figure slot="second" class="after">
<img src="{{config.site_url}}/assets/images/wgportal_dark.png" alt="Dark Mode"/>
<figcaption>Dark Mode</figcaption>
</figure>
<svg slot="handle" class="custom-animated-handle" xmlns="http://www.w3.org/2000/svg" width="100" viewBox="-8 -3 16 6">
<!-- Left arrow (dark) -->
<path d="M -5 -2 L -7 0 L -5 2 M -5 -2 L -5 2"
stroke="#1a1a1a"
fill="#1a1a1a"
stroke-width="1"
vector-effect="non-scaling-stroke">
</path>
<!-- Right arrow (white) -->
<path d="M 5 -2 L 7 0 L 5 2 M 5 -2 L 5 2"
stroke="#fff"
fill="#fff"
stroke-width="1"
vector-effect="non-scaling-stroke">
</path>
</svg>
</img-comparison-slider>
</div>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,6 @@
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"sass-embedded": "^1.86.3", "sass-embedded": "^1.86.3",
"vite": "6.3.4" "vite": "^6.3.6"
} }
} }

View File

@@ -63,6 +63,7 @@ const languageFlag = computed(() => {
uk: "ua", uk: "ua",
zh: "cn", zh: "cn",
ko: "kr", ko: "kr",
es: "es",
}; };
return "fi-" + (langMap[lang] || lang); return "fi-" + (langMap[lang] || lang);
@@ -182,6 +183,7 @@ const userDisplayName = computed(() => {
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></span> Українська</a> <a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></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('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> <a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('es')"><span class="fi fi-es"></span> Español</a>
</div> </div>
</div> </div>

View File

@@ -104,4 +104,8 @@ a.disabled {
.vue-tags-input .ti-deletion-mark:after { .vue-tags-input .ti-deletion-mark:after {
transform: scaleX(1); transform: scaleX(1);
}
.modal-dialog {
box-shadow: none;
} }

View File

@@ -358,7 +358,7 @@ async function del() {
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint.placeholder')" <input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint.placeholder')"
v-model="formData.Endpoint.Value"> v-model="formData.Endpoint.Value">
</div> </div>
<div class="form-group"> <div class="form-group" v-if="selectedInterface.Mode !== 'client'">
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
<vue-tags-input class="form-control" v-model="currentTags.Addresses" <vue-tags-input class="form-control" v-model="currentTags.Addresses"
:tags="formData.Addresses.map(str => ({ text: str }))" :tags="formData.Addresses.map(str => ({ text: str }))"

View File

@@ -130,7 +130,7 @@ function ConfigQrUrl() {
<template> <template>
<Modal :title="title" :visible="visible" @close="close"> <Modal :title="title" :visible="visible" @close="close">
<template #default> <template #default>
<div class="d-flex justify-content-end align-items-center mb-1"> <div class="d-flex justify-content-end align-items-center mb-1" v-if="selectedInterface.Mode !== 'client'">
<span class="me-2">{{ $t('modals.peer-view.style-label') }}: </span> <span class="me-2">{{ $t('modals.peer-view.style-label') }}: </span>
<div class="btn-group btn-switch-group" role="group" aria-label="Configuration Style"> <div class="btn-group btn-switch-group" role="group" aria-label="Configuration Style">
<input type="radio" class="btn-check" name="configstyle" id="raw" value="raw" autocomplete="off" checked="" v-model="configStyle"> <input type="radio" class="btn-check" name="configstyle" id="raw" value="raw" autocomplete="off" checked="" v-model="configStyle">
@@ -151,20 +151,28 @@ function ConfigQrUrl() {
data-bs-parent="#peerInformation" style=""> data-bs-parent="#peerInformation" style="">
<div class="accordion-body"> <div class="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-8"> <div :class="{ 'col-md-8': selectedInterface.Mode !== 'client', 'col-md-12': selectedInterface.Mode !== 'server' }" class="col-md-8">
<ul> <ul>
<li>{{ $t('modals.peer-view.identifier') }}: {{ selectedPeer.PublicKey }}</li> <li v-if="selectedInterface.Mode !== 'client'"><strong>{{ $t('modals.peer-view.identifier') }}</strong>: {{ selectedPeer.PublicKey }}</li>
<li>{{ $t('modals.peer-view.ip') }}: <span v-for="ip in selectedPeer.Addresses" :key="ip" <li v-if="selectedInterface.Mode !== 'server'"><strong>{{ $t('modals.peer-view.endpoint-key') }}</strong>: {{ selectedPeer.PublicKey }}</li>
<li v-if="selectedInterface.Mode !== 'server'"><strong>{{ $t('modals.peer-view.endpoint') }}</strong>: {{ selectedPeer.Endpoint.Value }}</li>
<li v-if="selectedInterface.Mode !== 'client'"><strong>{{ $t('modals.peer-view.ip') }}</strong>: <span v-for="ip in selectedPeer.Addresses" :key="ip"
class="badge rounded-pill bg-light">{{ ip }}</span></li> class="badge rounded-pill bg-light">{{ ip }}</span></li>
<li>{{ $t('modals.peer-view.user') }}: {{ selectedPeer.UserIdentifier }}</li> <li v-if="selectedInterface.Mode === 'server'"><strong>{{ $t('modals.peer-view.extra-allowed-ip') }}</strong>: <span v-for="ip in selectedPeer.ExtraAllowedIPs" :key="ip"
<li v-if="selectedPeer.Notes">{{ $t('modals.peer-view.notes') }}: {{ selectedPeer.Notes }}</li> class="badge rounded-pill bg-light">{{ ip }}</span></li>
<li v-if="selectedPeer.ExpiresAt">{{ $t('modals.peer-view.expiry-status') }}: {{ <li v-if="selectedInterface.Mode !== 'server' && selectedPeer.AllowedIPs.Value"><strong>{{ $t('modals.peer-view.allowed-ip') }}</strong>: <span v-for="ip in selectedPeer.AllowedIPs.Value" :key="ip"
class="badge rounded-pill bg-light">{{ ip }}</span></li>
<li v-if="selectedInterface.Mode !== 'server'"><strong>{{ $t('modals.peer-view.keepalive') }}</strong>: {{ selectedPeer.PersistentKeepalive.Value }}</li>
<li v-if="selectedPeer.UserDisplayName"><strong>{{ $t('modals.peer-view.user') }}</strong>: {{ selectedPeer.UserDisplayName }} ({{ selectedPeer.UserIdentifier }})</li>
<li v-else><strong>{{ $t('modals.peer-view.user') }}</strong>: {{ selectedPeer.UserIdentifier }}</li>
<li v-if="selectedPeer.Notes"><strong>{{ $t('modals.peer-view.notes') }}</strong>: {{ selectedPeer.Notes }}</li>
<li v-if="selectedPeer.ExpiresAt"><strong>{{ $t('modals.peer-view.expiry-status') }}</strong>: {{
selectedPeer.ExpiresAt }}</li> selectedPeer.ExpiresAt }}</li>
<li v-if="selectedPeer.Disabled">{{ $t('modals.peer-view.disabled-status') }}: {{ <li v-if="selectedPeer.Disabled"><strong>{{ $t('modals.peer-view.disabled-status') }}</strong>: {{
selectedPeer.DisabledReason }}</li> selectedPeer.DisabledReason }}</li>
</ul> </ul>
</div> </div>
<div class="col-md-4"> <div class="col-md-4" v-if="selectedInterface.Mode !== 'client'">
<img class="config-qr-img" :src="ConfigQrUrl()" loading="lazy" alt="Configuration QR Code"> <img class="config-qr-img" :src="ConfigQrUrl()" loading="lazy" alt="Configuration QR Code">
</div> </div>
</div> </div>
@@ -199,7 +207,7 @@ function ConfigQrUrl() {
</div> </div>
</div> </div>
</div> </div>
<div v-if="selectedInterface.Mode === 'server'" class="accordion-item"> <div v-if="selectedInterface.Mode !== 'client'" class="accordion-item">
<h2 class="accordion-header" id="headingConfig"> <h2 class="accordion-header" id="headingConfig">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig"> data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
@@ -217,9 +225,9 @@ function ConfigQrUrl() {
</template> </template>
<template #footer> <template #footer>
<div class="flex-fill text-start"> <div class="flex-fill text-start">
<button @click.prevent="download" type="button" class="btn btn-primary me-1">{{ <button v-if="selectedInterface.Mode !== 'client'" @click.prevent="download" type="button" class="btn btn-primary me-1">{{
$t('modals.peer-view.button-download') }}</button> $t('modals.peer-view.button-download') }}</button>
<button @click.prevent="email" type="button" class="btn btn-primary me-1">{{ <button v-if="selectedInterface.Mode !== 'client'" @click.prevent="email" type="button" class="btn btn-primary me-1">{{
$t('modals.peer-view.button-email') }}</button> $t('modals.peer-view.button-email') }}</button>
</div> </div>
<button @click.prevent="close" type="button" class="btn btn-secondary">{{ $t('general.close') }}</button> <button @click.prevent="close" type="button" class="btn btn-secondary">{{ $t('general.close') }}</button>

View File

@@ -53,6 +53,7 @@ export function freshPeer() {
Identifier: "", Identifier: "",
DisplayName: "", DisplayName: "",
UserIdentifier: "", UserIdentifier: "",
UserDisplayName: "",
InterfaceIdentifier: "", InterfaceIdentifier: "",
Disabled: false, Disabled: false,
ExpiresAt: null, ExpiresAt: null,

View File

@@ -8,6 +8,7 @@ import ru from './translations/ru.json';
import uk from './translations/uk.json'; import uk from './translations/uk.json';
import vi from './translations/vi.json'; import vi from './translations/vi.json';
import zh from './translations/zh.json'; import zh from './translations/zh.json';
import es from './translations/es.json';
import {createI18n} from "vue-i18n"; import {createI18n} from "vue-i18n";
@@ -32,6 +33,7 @@ const i18n = createI18n({
"uk": uk, "uk": uk,
"vi": vi, "vi": vi,
"zh": zh, "zh": zh,
"es": es,
} }
}); });

View File

@@ -117,6 +117,7 @@
"dns": "DNS-Server", "dns": "DNS-Server",
"mtu": "MTU", "mtu": "MTU",
"default-keep-alive": "Standard Keepalive-Intervall", "default-keep-alive": "Standard Keepalive-Intervall",
"default-dns": "Standard DNS-Server",
"button-show-config": "Konfiguration anzeigen", "button-show-config": "Konfiguration anzeigen",
"button-download-config": "Konfiguration herunterladen", "button-download-config": "Konfiguration herunterladen",
"button-store-config": "Konfiguration für wg-quick speichern", "button-store-config": "Konfiguration für wg-quick speichern",
@@ -461,6 +462,8 @@
"section-config": "Konfiguration", "section-config": "Konfiguration",
"identifier": "Kennung", "identifier": "Kennung",
"ip": "IP-Adressen", "ip": "IP-Adressen",
"allowed-ip": "Erlaubte IP-Adressen",
"extra-allowed-ip": "Serverseitig erlaubte IP-Adressen",
"user": "Zugeordneter Benutzer", "user": "Zugeordneter Benutzer",
"notes": "Notizen", "notes": "Notizen",
"expiry-status": "Läuft ab am", "expiry-status": "Läuft ab am",
@@ -473,6 +476,8 @@
"handshake": "Letzter Handshake", "handshake": "Letzter Handshake",
"connected-since": "Verbunden seit", "connected-since": "Verbunden seit",
"endpoint": "Endpunkt", "endpoint": "Endpunkt",
"endpoint-key": "Öffentlicher Endpunkt-Schlüssel",
"keepalive": "Persistentes Keepalive",
"button-download": "Konfiguration herunterladen", "button-download": "Konfiguration herunterladen",
"button-email": "Konfiguration per E-Mail senden", "button-email": "Konfiguration per E-Mail senden",
"style-label": "Konfigurationsformat" "style-label": "Konfigurationsformat"

View File

@@ -117,6 +117,7 @@
"dns": "DNS Servers", "dns": "DNS Servers",
"mtu": "MTU", "mtu": "MTU",
"default-keep-alive": "Default Keepalive Interval", "default-keep-alive": "Default Keepalive Interval",
"default-dns": "Default DNS Servers",
"button-show-config": "Show configuration", "button-show-config": "Show configuration",
"button-download-config": "Download configuration", "button-download-config": "Download configuration",
"button-store-config": "Store configuration for wg-quick", "button-store-config": "Store configuration for wg-quick",
@@ -462,6 +463,8 @@
"section-config": "Configuration", "section-config": "Configuration",
"identifier": "Identifier", "identifier": "Identifier",
"ip": "IP Addresses", "ip": "IP Addresses",
"allowed-ip": "Allowed IP Addresses",
"extra-allowed-ip": "Server Side Allowed IP Addresses",
"user": "Associated User", "user": "Associated User",
"notes": "Notes", "notes": "Notes",
"expiry-status": "Expires At", "expiry-status": "Expires At",
@@ -474,6 +477,8 @@
"handshake": "Last Handshake", "handshake": "Last Handshake",
"connected-since": "Connected since", "connected-since": "Connected since",
"endpoint": "Endpoint", "endpoint": "Endpoint",
"endpoint-key": "Endpoint Public Key",
"keepalive": "Persistent Keepalive",
"button-download": "Download configuration", "button-download": "Download configuration",
"button-email": "Send configuration via E-Mail", "button-email": "Send configuration via E-Mail",
"style-label": "Configuration Style" "style-label": "Configuration Style"

View File

@@ -0,0 +1,587 @@
{
"languages": {
"es": "Español"
},
"general": {
"pagination": {
"size": "Numero de elementos",
"all": "Todos (Lento)"
},
"search": {
"placeholder": "Buscar...",
"button": "Buscar"
},
"select-all": "Buscar todos",
"yes": "Sí",
"no": "No",
"cancel": "Cancelar",
"close": "Cerrar",
"save": "Guardar",
"delete": "Eliminar"
},
"login": {
"headline": "Por favor inicie sesión",
"username": {
"label": "Usuario",
"placeholder": "Por favor ingrese su usuario"
},
"password": {
"label": "Contraseña",
"placeholder": "Por favor ingrese su contraseña"
},
"button": "Ingresar",
"button-webauthn": "Usar clave de acceso"
},
"menu": {
"home": "Inicio",
"interfaces": "Interfaces",
"users": "Usuarios",
"lang": "Cambiar idioma",
"profile": "Mi perfil",
"settings": "Configuración",
"audit": "Registro de auditoría",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"keygen": "Generador de claves"
},
"home": {
"headline": "Portal VPN WireGuard®",
"info-headline": "Más información",
"abstract": "WireGuard® es una VPN extremadamente simple pero rápida y moderna que utiliza criptografía de última generación. Su objetivo es ser más rápida, simple, ligera y útil que IPsec, a la vez que evita los enormes problemas que supone. Su objetivo es ofrecer un rendimiento considerablemente superior al de OpenVPN.",
"installation": {
"box-header": "Instalación de WireGuard",
"headline": "Instalación",
"content": "Las instrucciones de instalación del cliente se pueden encontrar en el sitio web oficial de WireGuard.",
"button": "Abrir instrucciones"
},
"about-wg": {
"box-header": "Acerca de WireGuard",
"headline": "Acerca de",
"content": "WireGuard® es una VPN extremadamente simple pero rápida y moderna que utiliza criptografía de última generación.",
"button": "Más"
},
"about-portal": {
"box-header": "Acerca del Portal WireGuard",
"headline": "Portal WireGuard",
"content": "WireGuard Portal es un portal web simple para la configuración de WireGuard.",
"button": "Más"
},
"profiles": {
"headline": "Perfiles VPN",
"abstract": "Puedes acceder y descargar tus configuraciones personales de VPN desde tu perfil de usuario.",
"content": "para ver todos tus perfiles configurados, haz clic en el botón de abajo.",
"button": "Abrir mi perfil"
},
"admin": {
"headline": "Área de administración",
"abstract": "En el área de administración puedes gestionar los peers de WireGuard, la interfaz del servidor, así como los usuarios que tienen acceso al Portal WireGuard.",
"content": "",
"button-admin": "Abrir administración del servidor",
"button-user": "Abrir administración de usuarios"
}
},
"interfaces": {
"headline": "Administración de interfaces",
"headline-peers": "Peers VPN actuales",
"headline-endpoints": "Extremos actuales",
"no-interface": {
"default-selection": "No hay interfaces disponibles",
"headline": "No se encontraron interfaces...",
"abstract": "Haz clic en el botón + para crear una nueva interfaz WireGuard."
},
"no-peer": {
"headline": "No hay peers disponibles",
"abstract": "Actualmente no hay peers disponibles para la interfaz WireGuard seleccionada."
},
"table-heading": {
"name": "Nombre",
"user": "Usuario",
"ip": "IP's",
"endpoint": "Endpoint",
"status": "Estado"
},
"interface": {
"headline": "Estado de la interfaz para",
"backend": "Backend",
"unknown-backend": "Desconocido",
"wrong-backend": "Backend inválido, usando backend local de WireGuard en su lugar.",
"key": "Clave pública",
"endpoint": "Endpoint público",
"port": "Puerto de escucha",
"peers": "Peers habilitados",
"total-peers": "Peers totales",
"endpoints": "Endpoints habilitados",
"total-endpoints": "Endpoints totales",
"ip": "Dirección IP",
"default-allowed-ip": "IPs permitidas por defecto",
"dns": "Servidores DNS",
"mtu": "MTU",
"default-keep-alive": "Intervalo Keepalive por defecto",
"button-show-config": "Mostrar configuración",
"button-download-config": "Descargar configuración",
"button-store-config": "Guardar configuración para wg-quick",
"button-edit": "Editar interfaz"
},
"button-add-interface": "Agregar interfaz",
"button-add-peer": "Agregar peer",
"button-add-peers": "Agregar múltiples peers",
"button-show-peer": "Mostrar peer",
"button-edit-peer": "Editar peer",
"peer-disabled": "Peer deshabilitado, motivo:",
"peer-expiring": "El peer expira en",
"peer-connected": "Conectado",
"peer-not-connected": "No conectado",
"peer-handshake": "Último handshake:"
},
"users": {
"headline": "Administración de usuarios",
"table-heading": {
"id": "ID",
"email": "Correo electrónico",
"firstname": "Nombre",
"lastname": "Apellido",
"source": "Origen",
"peers": "Peers",
"admin": "Administrador"
},
"no-user": {
"headline": "No hay usuarios disponibles",
"abstract": "Actualmente no hay usuarios registrados en el Portal WireGuard."
},
"button-add-user": "Agregar usuario",
"button-show-user": "Mostrar usuario",
"button-edit-user": "Editar usuario",
"user-disabled": "Usuario deshabilitado, motivo:",
"user-locked": "Cuenta bloqueada, motivo:",
"admin": "El usuario tiene privilegios de administrador",
"no-admin": "El usuario no tiene privilegios de administrador"
},
"profile": {
"headline": "Mis peers VPN",
"table-heading": {
"name": "Nombre",
"ip": "IP's",
"stats": "Estado",
"interface": "Interfaz del servidor"
},
"no-peer": {
"headline": "No hay peers disponibles",
"abstract": "Actualmente no hay peers asociados a tu perfil de usuario."
},
"peer-connected": "Conectado",
"button-add-peer": "Agregar peer",
"button-show-peer": "Mostrar peer",
"button-edit-peer": "Editar peer"
},
"settings": {
"headline": "Configuración",
"abstract": "Aquí puedes cambiar tu configuración personal.",
"api": {
"headline": "Configuración de API",
"abstract": "Aquí puedes configurar los ajustes de la API RESTful.",
"active-description": "La API está actualmente activa para tu cuenta. Todas las solicitudes están autenticadas con Basic Auth. Usa las siguientes credenciales.",
"inactive-description": "La API está actualmente inactiva. Presiona el botón de abajo para activarla.",
"user-label": "Usuario de la API:",
"user-placeholder": "Usuario de la API",
"token-label": "Contraseña de la API:",
"token-placeholder": "Token de la API",
"token-created-label": "Acceso API concedido en: ",
"button-disable-title": "Desactivar API, invalidará el token actual.",
"button-disable-text": "Desactivar API",
"button-enable-title": "Activar API, generará un nuevo token.",
"button-enable-text": "Activar API",
"api-link": "Documentación de API"
},
"webauthn": {
"headline": "Configuración de llave de acceso",
"abstract": "Las llaves de acceso son una forma moderna de autenticar usuarios sin necesidad de contraseñas. Se almacenan de forma segura en tu navegador y pueden usarse para iniciar sesión en el Portal WireGuard.",
"active-description": "Al menos una llave de acceso está activa en tu cuenta.",
"inactive-description": "Actualmente no hay llaves de acceso registradas. Presiona el botón de abajo para registrar una.",
"table": {
"name": "Nombre",
"created": "Creada",
"actions": ""
},
"credentials-list": "Llaves de acceso registradas actualmente",
"modal-delete": {
"headline": "Eliminar llaves de acceso",
"abstract": "¿Seguro que deseas eliminar esta llave de acceso? Ya no podrás usarla para iniciar sesión.",
"created": "Creada:",
"button-delete": "Eliminar",
"button-cancel": "Cancelar"
},
"button-rename-title": "Renombrar",
"button-rename-text": "Renombrar la llave de acceso.",
"button-save-title": "Guardar",
"button-save-text": "Guardar el nuevo nombre de la llave de acceso.",
"button-cancel-title": "Cancelar",
"button-cancel-text": "Cancelar el renombrado de la llave de acceso.",
"button-delete-title": "Eliminar",
"button-delete-text": "Eliminar la llave de acceso. Ya no podrás iniciar sesión con ella.",
"button-register-title": "Registrar llave de acceso",
"button-register-text": "Registrar una nueva llave de acceso para proteger tu cuenta."
}
},
"audit": {
"headline": "Registro de Auditoría",
"abstract": "Aquí puedes encontrar el registro de auditoría de todas las acciones realizadas en el Portal WireGuard.",
"no-entries": {
"headline": "No hay entradas en el registro",
"abstract": "Actualmente no se han registrado auditorías."
},
"entries-headline": "Entradas del Registro",
"table-heading": {
"id": "#",
"time": "Hora",
"user": "Usuario",
"severity": "Severidad",
"origin": "Origen",
"message": "Mensaje"
}
},
"keygen": {
"headline": "Generador de claves WireGuard",
"abstract": "Genera nuevas claves de WireGuard. Las claves se generan en tu navegador local y nunca se envían al servidor.",
"headline-keypair": "Nuevo par de claves",
"headline-preshared-key": "Nueva clave pre-compartida",
"button-generate": "Generar",
"private-key": {
"label": "Clave privada",
"placeholder": "La clave privada"
},
"public-key": {
"label": "Clave pública",
"placeholder": "La clave pública"
},
"preshared-key": {
"label": "Clave pre-compartida",
"placeholder": "La clave pre-compartida"
}
},
"modals": {
"user-view": {
"headline": "Cuenta de Usuario:",
"tab-user": "Información",
"tab-peers": "Peers",
"headline-info": "Información del Usuario:",
"headline-notes": "Notas:",
"email": "Correo Electrónico",
"firstname": "Nombre",
"lastname": "Apellido",
"phone": "Número de Teléfono",
"depeertment": "Departamento",
"api-enabled": "Acceso API",
"disabled": "Cuenta Deshabilitada",
"locked": "Cuenta Bloqueada",
"no-peers": "El usuario no tiene peers asociados.",
"peers": {
"name": "Nombre",
"interface": "Interfaz",
"ip": "IP's"
}
},
"user-edit": {
"headline-edit": "Editar usuario:",
"headline-new": "Nuevo usuario",
"header-general": "General",
"header-personal": "Información del Usuario",
"header-notes": "Notas",
"header-state": "Estado",
"identifier": {
"label": "Identificador",
"placeholder": "El identificador único del usuario"
},
"source": {
"label": "Origen",
"placeholder": "El origen del usuario"
},
"password": {
"label": "Contraseña",
"placeholder": "Una contraseña súper segura",
"description": "Deja este campo en blanco para mantener la contraseña actual.",
"too-weak": "La contraseña es demasiado débil. Por favor usa una más fuerte."
},
"email": {
"label": "Correo",
"placeholder": "La dirección de correo"
},
"phone": {
"label": "Teléfono",
"placeholder": "El número de teléfono"
},
"depeertment": {
"label": "Departamento",
"placeholder": "El departamento"
},
"firstname": {
"label": "Nombre",
"placeholder": "Nombre"
},
"lastname": {
"label": "Apellido",
"placeholder": "Apellido"
},
"notes": {
"label": "Notas",
"placeholder": ""
},
"disabled": {
"label": "Deshabilitado (sin conexión WireGuard y sin posibilidad de inicio de sesión)"
},
"locked": {
"label": "Bloqueado (no es posible iniciar sesión, las conexiones WireGuard aún funcionan)"
},
"admin": {
"label": "Es administrador"
}
},
"interface-view": {
"headline": "Configuración de la interfaz:"
},
"interface-edit": {
"headline-edit": "Editar interfaz:",
"headline-new": "Nueva interfaz",
"tab-interface": "Interfaz",
"tab-peerdef": "Valores predeterminados del peer",
"header-general": "General",
"header-network": "Red",
"header-crypto": "Criptografía",
"header-hooks": "Hooks de interfaz",
"header-peer-hooks": "Hooks",
"header-state": "Estado",
"identifier": {
"label": "Identificador",
"placeholder": "El identificador único de la interfaz"
},
"mode": {
"label": "Modo de Interfaz",
"server": "Modo Servidor",
"client": "Modo Cliente",
"any": "Modo Desconocido"
},
"backend": {
"label": "Backend de la Interfaz",
"invalid-label": "El backend original ya no está disponible, usando el backend local de WireGuard en su lugar.",
"local": "Backend local de WireGuard"
},
"display-name": {
"label": "Nombre para Mostrar",
"placeholder": "El nombre descriptivo de la interfaz"
},
"private-key": {
"label": "La clave Privada",
"placeholder": "La clave privada"
},
"public-key": {
"label": "La clave pública",
"placeholder": "La clave pública"
},
"ip": {
"label": "Direcciones IP",
"placeholder": "Direcciones IP (formato CIDR)"
},
"listen-port": {
"label": "Puerto de Escucha",
"placeholder": "El puerto de escucha"
},
"dns": {
"label": "Servidor DNS",
"placeholder": "Los servidores DNS que deben usarse"
},
"dns-search": {
"label": "Dominios de Búsqueda DNS",
"placeholder": "Prefijos de búsqueda DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "La MTU de la interfaz (0 = mantener por defecto)"
},
"firewall-mark": {
"label": "Marca de Firewall",
"placeholder": "Marca de firewall que se aplica al tráfico saliente. (0 = automático)"
},
"routing-table": {
"label": "Tabla de Enrutamiento",
"placeholder": "El ID de la tabla de enrutamiento",
"description": "Casos especiales: off = no administrar rutas, 0 = automático"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"disabled": {
"label": "Interfaz Deshabilitada"
},
"save-config": {
"label": "Guardar automáticamente la configuración de wg-quick"
},
"defaults": {
"endpoint": {
"label": "Dirección del Endpoint",
"placeholder": "Dirección del Endpoint",
"description": "La dirección del endpoint al que los peers se conectarán. (ej: wg.ejemplo.com o wg.ejemplo.com:51820)"
},
"networks": {
"label": "Redes IP",
"placeholder": "Direcciones de Red",
"description": "Los peers obtendrán direcciones IP de esas subredes."
},
"allowed-ip": {
"label": "Direcciones IP Permitidas",
"placeholder": "Direcciones IP Permitidas por Defecto"
},
"mtu": {
"label": "MTU",
"placeholder": "La MTU del cliente (0 = mantener por defecto)"
},
"keep-alive": {
"label": "Intervalo de Keep Alive",
"placeholder": "Keepalive Persistente (0 = por defecto)"
}
},
"button-apply-defaults": "Aplicar Valores Predeterminados de peers"
},
"peer-view": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"section-info": "Información del peer",
"section-status": "Estado Actual",
"section-config": "Configuración",
"identifier": "Identificador",
"ip": "Direcciones IP",
"user": "Usuario Asociado",
"notes": "Notas",
"expiry-status": "Expira en",
"disabled-status": "Deshabilitado en",
"traffic": "Tráfico",
"connection-status": "Estadísticas de Conexión",
"upload": "Bytes Subidos (del Servidor al peer)",
"download": "Bytes Descargados (del peer al Servidor)",
"pingable": "Es Alcanzable (Ping)",
"handshake": "Último Handshake",
"connected-since": "Conectado desde",
"endpoint": "Endpoint",
"button-download": "Descargar configuración",
"button-email": "Enviar configuración por Correo Electrónico",
"style-label": "Estilo de Configuración"
},
"peer-edit": {
"headline-edit-peer": "Editar peer:",
"headline-edit-endpoint": "Editar endpoint:",
"headline-new-peer": "Crear peer",
"headline-new-endpoint": "Crear endpoint",
"header-general": "General",
"header-network": "Red",
"header-crypto": "Criptografía",
"header-hooks": "Hooks (Ejecutados en el peer)",
"header-state": "Estado",
"display-name": {
"label": "Nombre para Mostrar",
"placeholder": "El nombre descriptivo para el peer"
},
"linked-user": {
"label": "Usuario Vinculado",
"placeholder": "La cuenta de usuario que posee este peer"
},
"private-key": {
"label": "Clave Privada",
"placeholder": "Clave privada",
"help": "La clave privada se almacena de forma segura en el servidor. Si el usuario ya posee una copia, puedes omitir este campo. El servidor sigue funcionando exclusivamente con la clave pública del peer."
},
"public-key": {
"label": "Cave Pública",
"placeholder": "La Clave pública"
},
"preshared-key": {
"label": "Clave pre-compartida",
"placeholder": "Clave pre-compartida opcional"
},
"endpoint": {
"label": "Dirección del endpoint",
"placeholder": "La dirección del endpoint remoto"
},
"ip": {
"label": "Direcciones IP",
"placeholder": "Direcciones IP (formato CIDR)"
},
"allowed-ip": {
"label": "Direcciones IP permitidas",
"placeholder": "Direcciones IP permitidas (formato CIDR)"
},
"extra-allowed-ip": {
"label": "Direcciones IP permitidas extra",
"placeholder": "IPs extra permitidas (lado del servidor)",
"description": "Esas IPs serán agregadas en la interfaz remota de WireGuard como direcciones IP permitidas."
},
"dns": {
"label": "Servidor DNS",
"placeholder": "Los servidores DNS que deben usarse"
},
"dns-search": {
"label": "Dominios de búsqueda DNS",
"placeholder": "Prefijos de búsqueda DNS"
},
"keep-alive": {
"label": "Intervalo de Keep Alive",
"placeholder": "Keepalive Persistente (0 = por defecto)"
},
"mtu": {
"label": "MTU",
"placeholder": "La MTU del cliente (0 = mantener por defecto)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"disabled": {
"label": "Peer Deshabilitado"
},
"ignore-global": {
"label": "Ignorar configuración global"
},
"expires-at": {
"label": "Fecha de expiración"
}
},
"peer-multi-create": {
"headline-peer": "Crear múltiples peers",
"headline-endpoint": "Crear múltiples endpoints",
"identifiers": {
"label": "Identificadores de Usuario",
"placeholder": "Identificadores de Usuario",
"description": "Un identificador de usuario (el nombre de usuario) para el cual debe crearse un peer."
},
"prefix": {
"headline-peer": "peer:",
"headline-endpoint": "Endpoint:",
"label": "Prefijo del Nombre peera Mostrar",
"placeholder": "El prefijo",
"description": "Un prefijo que se agregará al nombre mostrado de los peers."
}
}
}
}

View File

@@ -217,14 +217,14 @@ onMounted(async () => {
<td>{{ $t('interfaces.interface.ip') }}:</td> <td>{{ $t('interfaces.interface.ip') }}:</td>
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.Addresses" :key="addr">{{addr}}</span></td> <td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.Addresses" :key="addr">{{addr}}</span></td>
</tr> </tr>
<tr>
<td>{{ $t('interfaces.interface.dns') }}:</td>
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.Dns" :key="addr">{{addr}}</span></td>
</tr>
<tr> <tr>
<td>{{ $t('interfaces.interface.mtu') }}:</td> <td>{{ $t('interfaces.interface.mtu') }}:</td>
<td>{{interfaces.GetSelected.Mtu}}</td> <td>{{interfaces.GetSelected.Mtu}}</td>
</tr> </tr>
<tr>
<td>{{ $t('interfaces.interface.default-dns') }}:</td>
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.PeerDefDns" :key="addr">{{addr}}</span></td>
</tr>
<tr> <tr>
<td>{{ $t('interfaces.interface.default-keep-alive') }}:</td> <td>{{ $t('interfaces.interface.default-keep-alive') }}:</td>
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td> <td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
@@ -400,7 +400,7 @@ onMounted(async () => {
<span v-if="!peer.Disabled && peer.ExpiresAt" class="text-warning" :title="$t('interfaces.peer-expiring') + ' ' + peer.ExpiresAt"><i class="fas fa-hourglass-end expiring-peer"></i></span> <span v-if="!peer.Disabled && peer.ExpiresAt" class="text-warning" :title="$t('interfaces.peer-expiring') + ' ' + peer.ExpiresAt"><i class="fas fa-hourglass-end expiring-peer"></i></span>
</td> </td>
<td><span v-if="peer.DisplayName" :title="peer.Identifier">{{peer.DisplayName}}</span><span v-else :title="peer.Identifier">{{ $filters.truncate(peer.Identifier, 10)}}</span></td> <td><span v-if="peer.DisplayName" :title="peer.Identifier">{{peer.DisplayName}}</span><span v-else :title="peer.Identifier">{{ $filters.truncate(peer.Identifier, 10)}}</span></td>
<td>{{peer.UserIdentifier}}</td> <td><span :title="peer.UserDisplayName">{{peer.UserIdentifier}}</span></td>
<td> <td>
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span> <span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
</td> </td>

58
go.mod
View File

@@ -5,12 +5,12 @@ go 1.24.0
require ( require (
github.com/a8m/envsubst v1.4.3 github.com/a8m/envsubst v1.4.3
github.com/alexedwards/scs/v2 v2.9.0 github.com/alexedwards/scs/v2 v2.9.0
github.com/coreos/go-oidc/v3 v3.15.0 github.com/coreos/go-oidc/v3 v3.16.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-pkgz/routegroup v1.5.3 github.com/go-pkgz/routegroup v1.5.3
github.com/go-playground/validator/v10 v10.27.0 github.com/go-playground/validator/v10 v10.28.0
github.com/go-webauthn/webauthn v0.13.4 github.com/go-webauthn/webauthn v0.14.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/prometheus-community/pro-bing v0.7.0 github.com/prometheus-community/pro-bing v0.7.0
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
@@ -29,7 +29,7 @@ require (
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlserver v1.6.1 gorm.io/driver/sqlserver v1.6.1
gorm.io/gorm v1.30.5 gorm.io/gorm v1.31.0
) )
require ( require (
@@ -44,41 +44,34 @@ require (
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/jsonpointer v0.22.0 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-openapi/swag v0.24.1 // indirect github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/swag/conv v0.24.0 // indirect github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
github.com/go-openapi/swag/fileutils v0.24.0 // indirect github.com/go-openapi/swag/loading v0.25.1 // indirect
github.com/go-openapi/swag/jsonname v0.24.0 // indirect github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/jsonutils v0.24.0 // indirect github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/loading v0.24.0 // indirect github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-openapi/swag/mangling v0.24.0 // indirect
github.com/go-openapi/swag/netutils v0.24.0 // indirect
github.com/go-openapi/swag/stringutils v0.24.0 // indirect
github.com/go-openapi/swag/typeutils v0.24.0 // indirect
github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-test/deep v1.1.1 // indirect github.com/go-test/deep v1.1.1 // indirect
github.com/go-webauthn/x v0.1.24 // indirect github.com/go-webauthn/x v0.1.25 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-tpm v0.9.5 // indirect github.com/google/go-tpm v0.9.6 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect github.com/mdlayher/netlink v1.8.0 // indirect
@@ -96,18 +89,19 @@ require (
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/mod v0.28.0 // indirect golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.43.0 // indirect golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect golang.org/x/tools v0.37.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.66.8 // indirect modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.38.2 // indirect modernc.org/sqlite v1.39.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect
) )

135
go.sum
View File

@@ -30,16 +30,16 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg= github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -58,40 +58,33 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY=
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg=
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
github.com/go-pkgz/routegroup v1.5.3 h1:IvH1KLcQkMap9jucQGBlef3IBloxSAe8USUFvxShFqs= github.com/go-pkgz/routegroup v1.5.3 h1:IvH1KLcQkMap9jucQGBlef3IBloxSAe8USUFvxShFqs=
github.com/go-pkgz/routegroup v1.5.3/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg= github.com/go-pkgz/routegroup v1.5.3/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -100,16 +93,16 @@ 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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 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/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ= github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI= github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
github.com/go-webauthn/x v0.1.24 h1:6LaWf2zzWqbyKT8IyQkhje1/1KCGhlEkMz4V1tDnt/A= github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
github.com/go-webauthn/x v0.1.24/go.mod h1:2o5XKJ+X1AKqYKGgHdKflGnoQFQZ6flJ2IFCBKSbSOw= github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
@@ -122,8 +115,8 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -158,8 +151,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -173,8 +164,6 @@ 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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
@@ -255,10 +244,12 @@ github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
@@ -273,8 +264,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -300,8 +291,8 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -371,15 +362,15 @@ 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= 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= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -397,20 +388,20 @@ gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXD
gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY= gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY=
gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U= gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s= gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.26.4 h1:jPhG8oNjtTYuP2FA4YefTJ/wioNUGALmGuEWt7SUR6s= modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.28 h1:Vp156KUA2nPu9F1NEv036x9UGOjg2qsi5QlWTjZmtMk= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.28/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.8 h1:/awsvTnyN/sNjvJm6S3lb7KZw5WV4ly/sBEG7ZUzmIE= modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.8/go.mod h1:aVdcY7udcawRqauu0HukYYxtBSizV+R80n/6aQe9D5k= modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -419,8 +410,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -14,7 +14,6 @@ import (
probing "github.com/prometheus-community/pro-bing" probing "github.com/prometheus-community/pro-bing"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -84,8 +83,8 @@ func NewLocalController(cfg *config.Config) (*LocalController, error) {
wg: wg, wg: wg,
nl: nl, nl: nl,
shellCmd: "bash", // we only support bash at the moment shellCmd: "bash", // we only support bash at the moment
resolvConfIfacePrefix: "tun.", // WireGuard interfaces have a tun. prefix in resolvconf resolvConfIfacePrefix: cfg.Backend.LocalResolvconfPrefix, // WireGuard interfaces have a tun. prefix in resolvconf
} }
return repo, nil return repo, nil
@@ -546,7 +545,11 @@ func (c LocalController) deletePeer(deviceId domain.InterfaceIdentifier, id doma
// region wg-quick-related // region wg-quick-related
func (c LocalController) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error { func (c LocalController) ExecuteInterfaceHook(
_ context.Context,
id domain.InterfaceIdentifier,
hookCmd string,
) error {
if hookCmd == "" { if hookCmd == "" {
return nil return nil
} }
@@ -560,7 +563,7 @@ func (c LocalController) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hoo
return nil return nil
} }
func (c LocalController) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error { func (c LocalController) SetDNS(_ context.Context, id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
if dnsStr == "" && dnsSearchStr == "" { if dnsStr == "" && dnsSearchStr == "" {
return nil return nil
} }
@@ -589,7 +592,7 @@ func (c LocalController) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearch
return nil return nil
} }
func (c LocalController) UnsetDNS(id domain.InterfaceIdentifier) error { func (c LocalController) UnsetDNS(_ context.Context, id domain.InterfaceIdentifier, _, _ string) error {
dnsCommand := "resolvconf -d %resPref%i -f" dnsCommand := "resolvconf -d %resPref%i -f"
err := c.exec(dnsCommand, id) err := c.exec(dnsCommand, id)
@@ -611,7 +614,7 @@ func (c LocalController) exec(command string, interfaceId domain.InterfaceIdenti
if len(stdin) > 0 { if len(stdin) > 0 {
b := &bytes.Buffer{} b := &bytes.Buffer{}
for _, ln := range stdin { for _, ln := range stdin {
if _, err := fmt.Fprint(b, ln); err != nil { if _, err := fmt.Fprint(b, ln+"\n"); err != nil {
return err return err
} }
} }
@@ -619,6 +622,8 @@ func (c LocalController) exec(command string, interfaceId domain.InterfaceIdenti
} }
out, err := cmd.CombinedOutput() // execute and wait for output out, err := cmd.CombinedOutput() // execute and wait for output
if err != nil { if err != nil {
slog.Warn("failed to executed shell command",
"command", commandWithInterfaceName, "stdin", stdin, "output", string(out), "error", err)
return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err) return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
} }
slog.Debug("executed shell command", slog.Debug("executed shell command",
@@ -631,205 +636,28 @@ func (c LocalController) exec(command string, interfaceId domain.InterfaceIdenti
// region routing-related // region routing-related
func (c LocalController) SyncRouteRules(_ context.Context, rules []domain.RouteRule) error { // SetRoutes sets the routes for the given interface. If no routes are provided, the function is a no-op.
// update fwmark rules func (c LocalController) SetRoutes(
if err := c.setFwMarkRules(rules); err != nil { ctx context.Context,
return err interfaceId domain.InterfaceIdentifier,
} table int,
fwMark uint32,
// update main rule cidrs []domain.Cidr,
if err := c.setMainRule(rules); err != nil { ) error {
return err
}
// cleanup old main rules
if err := c.cleanupMainRule(rules); err != nil {
return err
}
return nil return nil
} }
func (c LocalController) setFwMarkRules(rules []domain.RouteRule) error { // RemoveRoutes removes the routes for the given interface. If no routes are provided, the function is a no-op.
for _, rule := range rules { func (c LocalController) RemoveRoutes(
existingRules, err := c.nl.RuleList(int(rule.IpFamily)) ctx context.Context,
if err != nil { interfaceId domain.InterfaceIdentifier,
return fmt.Errorf("failed to get existing rules for family %s: %w", rule.IpFamily, err) table int,
} fwMark uint32,
oldCidrs []domain.Cidr,
ruleExists := false ) error {
for _, existingRule := range existingRules {
if rule.FwMark == existingRule.Mark && rule.Table == existingRule.Table {
ruleExists = true
break
}
}
if ruleExists {
continue // rule already exists, no need to recreate it
}
// create a missing rule
if err := c.nl.RuleAdd(&netlink.Rule{
Family: int(rule.IpFamily),
Table: rule.Table,
Mark: rule.FwMark,
Invert: true,
SuppressIfgroup: -1,
SuppressPrefixlen: -1,
Priority: c.getRulePriority(existingRules),
Mask: nil,
Goto: -1,
Flow: -1,
}); err != nil {
return fmt.Errorf("failed to setup %s rule for fwmark %d and table %d: %w",
rule.IpFamily, rule.FwMark, rule.Table, err)
}
}
return nil return nil
} }
func (c LocalController) getRulePriority(existingRules []netlink.Rule) int {
prio := 32700 // linux main rule has a priority of 32766
for {
isFresh := true
for _, existingRule := range existingRules {
if existingRule.Priority == prio {
isFresh = false
break
}
}
if isFresh {
break
} else {
prio--
}
}
return prio
}
func (c LocalController) setMainRule(rules []domain.RouteRule) error {
var family domain.IpFamily
shouldHaveMainRule := false
for _, rule := range rules {
family = rule.IpFamily
if rule.HasDefault == true {
shouldHaveMainRule = true
break
}
}
if !shouldHaveMainRule {
return nil
}
existingRules, err := c.nl.RuleList(int(family))
if err != nil {
return fmt.Errorf("failed to get existing rules for family %s: %w", family, err)
}
ruleExists := false
for _, existingRule := range existingRules {
if existingRule.Table == unix.RT_TABLE_MAIN && existingRule.SuppressPrefixlen == 0 {
ruleExists = true
break
}
}
if ruleExists {
return nil // rule already exists, skip re-creation
}
if err := c.nl.RuleAdd(&netlink.Rule{
Family: int(family),
Table: unix.RT_TABLE_MAIN,
SuppressIfgroup: -1,
SuppressPrefixlen: 0,
Priority: c.getMainRulePriority(existingRules),
Mark: 0,
Mask: nil,
Goto: -1,
Flow: -1,
}); err != nil {
return fmt.Errorf("failed to setup rule for main table: %w", err)
}
return nil
}
func (c LocalController) getMainRulePriority(existingRules []netlink.Rule) int {
priority := c.cfg.Advanced.RulePrioOffset
for {
isFresh := true
for _, existingRule := range existingRules {
if existingRule.Priority == priority {
isFresh = false
break
}
}
if isFresh {
break
} else {
priority++
}
}
return priority
}
func (c LocalController) cleanupMainRule(rules []domain.RouteRule) error {
var family domain.IpFamily
for _, rule := range rules {
family = rule.IpFamily
break
}
existingRules, err := c.nl.RuleList(int(family))
if err != nil {
return fmt.Errorf("failed to get existing rules for family %s: %w", family, err)
}
shouldHaveMainRule := false
for _, rule := range rules {
if rule.HasDefault == true {
shouldHaveMainRule = true
break
}
}
mainRules := 0
for _, existingRule := range existingRules {
if existingRule.Table == unix.RT_TABLE_MAIN && existingRule.SuppressPrefixlen == 0 {
mainRules++
}
}
removalCount := 0
if mainRules > 1 {
removalCount = mainRules - 1 // we only want one single rule
}
if !shouldHaveMainRule {
removalCount = mainRules
}
for _, existingRule := range existingRules {
if existingRule.Table == unix.RT_TABLE_MAIN && existingRule.SuppressPrefixlen == 0 {
if removalCount > 0 {
existingRule.Family = int(family) // set family, somehow the RuleList method does not populate the family field
if err := c.nl.RuleDel(&existingRule); err != nil {
return fmt.Errorf("failed to delete main rule: %w", err)
}
removalCount--
}
}
}
return nil
}
func (c LocalController) DeleteRouteRules(_ context.Context, rules []domain.RouteRule) error {
// TODO implement me
panic("implement me")
}
// endregion routing-related // endregion routing-related
// region statistics-related // region statistics-related

View File

@@ -3,14 +3,13 @@ package wgcontroller
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"log/slog"
"github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
"github.com/h44z/wg-portal/internal/lowlevel" "github.com/h44z/wg-portal/internal/lowlevel"
@@ -23,8 +22,9 @@ type MikrotikController struct {
client *lowlevel.MikrotikApiClient client *lowlevel.MikrotikApiClient
// Add mutexes to prevent race conditions // Add mutexes to prevent race conditions
interfaceMutexes sync.Map // map[domain.InterfaceIdentifier]*sync.Mutex interfaceMutexes sync.Map // map[domain.InterfaceIdentifier]*sync.Mutex
peerMutexes sync.Map // map[domain.PeerIdentifier]*sync.Mutex peerMutexes sync.Map // map[domain.PeerIdentifier]*sync.Mutex
coreMutex sync.Mutex // for updating the core configuration such as routing table or DNS settings
} }
func NewMikrotikController(coreCfg *config.Config, cfg *config.BackendMikrotik) (*MikrotikController, error) { func NewMikrotikController(coreCfg *config.Config, cfg *config.BackendMikrotik) (*MikrotikController, error) {
@@ -41,6 +41,7 @@ func NewMikrotikController(coreCfg *config.Config, cfg *config.BackendMikrotik)
interfaceMutexes: sync.Map{}, interfaceMutexes: sync.Map{},
peerMutexes: sync.Map{}, peerMutexes: sync.Map{},
coreMutex: sync.Mutex{},
}, nil }, nil
} }
@@ -678,11 +679,15 @@ func (c *MikrotikController) updatePeer(
extras := pp.GetExtras().(domain.MikrotikPeerExtras) extras := pp.GetExtras().(domain.MikrotikPeerExtras)
peerId := extras.Id peerId := extras.Id
endpoint := pp.Endpoint endpoint := "" // by default, we have no endpoint (the peer does not initiate a connection)
endpointPort := "51820" // default port if not set endpointPort := "0" // by default, we have no endpoint port (the peer does not initiate a connection)
if s := strings.Split(endpoint, ":"); len(s) == 2 { if !extras.IsResponder { // if the peer is not only a responder, it needs the endpoint to initiate a connection
endpoint = s[0] endpoint = pp.Endpoint
endpointPort = s[1] endpointPort = "51820" // default port if not set
if s := strings.Split(endpoint, ":"); len(s) == 2 {
endpoint = s[0]
endpointPort = s[1]
}
} }
allowedAddressStr := domain.CidrsToString(pp.AllowedIPs) allowedAddressStr := domain.CidrsToString(pp.AllowedIPs)
@@ -760,33 +765,126 @@ func (c *MikrotikController) DeletePeer(
// region wg-quick-related // region wg-quick-related
func (c *MikrotikController) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error { func (c *MikrotikController) ExecuteInterfaceHook(
_ context.Context,
_ domain.InterfaceIdentifier,
_ string,
) error {
// TODO implement me // TODO implement me
panic("implement me") slog.Error("interface hooks are not yet supported for Mikrotik backends, please open an issue on GitHub")
return nil
} }
func (c *MikrotikController) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error { func (c *MikrotikController) SetDNS(
// TODO implement me ctx context.Context,
panic("implement me") _ domain.InterfaceIdentifier,
dnsStr, _ string,
) error {
// Lock the interface to prevent concurrent modifications
c.coreMutex.Lock()
defer c.coreMutex.Unlock()
// check if the server is already configured
wgReply := c.client.Get(ctx, "/ip/dns", &lowlevel.MikrotikRequestOptions{
PropList: []string{"servers"},
})
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("unable to find WireGuard dns settings: %v", wgReply.Error)
}
var existingServers []string
existingServers = append(existingServers, strings.Split(wgReply.Data.GetString("servers"), ",")...)
newServers := strings.Split(dnsStr, ",")
mergedServers := slices.Clone(existingServers)
for _, s := range newServers {
if s == "" {
continue
}
if !slices.Contains(mergedServers, s) {
mergedServers = append(mergedServers, s)
}
}
mergedServersStr := strings.Join(mergedServers, ",")
reply := c.client.ExecList(ctx, "/ip/dns/set", lowlevel.GenericJsonObject{
"servers": mergedServersStr,
})
if reply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("failed to set DNS servers: %s: %v", mergedServersStr, reply.Error)
}
return nil
} }
func (c *MikrotikController) UnsetDNS(id domain.InterfaceIdentifier) error { func (c *MikrotikController) UnsetDNS(
// TODO implement me ctx context.Context,
panic("implement me") _ domain.InterfaceIdentifier,
dnsStr, _ string,
) error {
// Lock the interface to prevent concurrent modifications
c.coreMutex.Lock()
defer c.coreMutex.Unlock()
// retrieve current DNS settings
wgReply := c.client.Get(ctx, "/ip/dns", &lowlevel.MikrotikRequestOptions{
PropList: []string{"servers"},
})
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("unable to find WireGuard dns settings: %v", wgReply.Error)
}
var existingServers []string
existingServers = append(existingServers, strings.Split(wgReply.Data.GetString("servers"), ",")...)
oldServers := strings.Split(dnsStr, ",")
mergedServers := make([]string, 0, len(existingServers))
for _, s := range existingServers {
if s == "" {
continue
}
if !slices.Contains(oldServers, s) {
mergedServers = append(mergedServers, s) // only keep the servers that are not in the old list
}
}
mergedServersStr := strings.Join(mergedServers, ",")
reply := c.client.ExecList(ctx, "/ip/dns/set", lowlevel.GenericJsonObject{
"servers": mergedServersStr,
})
if reply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("failed to set DNS servers: %s: %v", mergedServersStr, reply.Error)
}
return nil
} }
// endregion wg-quick-related // endregion wg-quick-related
// region routing-related // region routing-related
func (c *MikrotikController) SyncRouteRules(_ context.Context, rules []domain.RouteRule) error { // SetRoutes sets the routes for the given interface. If no routes are provided, the function is a no-op.
// TODO implement me func (c *MikrotikController) SetRoutes(
panic("implement me") ctx context.Context,
interfaceId domain.InterfaceIdentifier,
table int,
fwMark uint32,
cidrs []domain.Cidr,
) error {
return nil
} }
func (c *MikrotikController) DeleteRouteRules(_ context.Context, rules []domain.RouteRule) error { // RemoveRoutes removes the routes for the given interface. If no routes are provided, the function is a no-op.
// TODO implement me func (c *MikrotikController) RemoveRoutes(
panic("implement me") ctx context.Context,
interfaceId domain.InterfaceIdentifier,
table int,
fwMark uint32,
oldCidrs []domain.Cidr,
) error {
return nil
} }
// endregion routing-related // endregion routing-related

View File

@@ -1,113 +0,0 @@
package adapters
import (
"bytes"
"fmt"
"log/slog"
"os/exec"
"strings"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain"
)
// WgQuickRepo implements higher level wg-quick like interactions like setting DNS, routing tables or interface hooks.
type WgQuickRepo struct {
shellCmd string
resolvConfIfacePrefix string
}
// NewWgQuickRepo creates a new WgQuickRepo instance.
func NewWgQuickRepo() *WgQuickRepo {
return &WgQuickRepo{
shellCmd: "bash",
resolvConfIfacePrefix: "tun.",
}
}
// ExecuteInterfaceHook executes the given hook command.
// The hook command can contain the following placeholders:
//
// %i: the interface identifier.
func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
if hookCmd == "" {
return nil
}
slog.Debug("executing interface hook", "interface", id, "hook", hookCmd)
err := r.exec(hookCmd, id)
if err != nil {
return fmt.Errorf("failed to exec hook: %w", err)
}
return nil
}
// SetDNS sets the DNS settings for the given interface. It uses resolvconf to set the DNS settings.
func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
if dnsStr == "" && dnsSearchStr == "" {
return nil
}
dnsServers := internal.SliceString(dnsStr)
dnsSearchDomains := internal.SliceString(dnsSearchStr)
dnsCommand := "resolvconf -a %resPref%i -m 0 -x"
dnsCommandInput := make([]string, 0, len(dnsServers)+len(dnsSearchDomains))
for _, dnsServer := range dnsServers {
dnsCommandInput = append(dnsCommandInput, fmt.Sprintf("nameserver %s", dnsServer))
}
for _, searchDomain := range dnsSearchDomains {
dnsCommandInput = append(dnsCommandInput, fmt.Sprintf("search %s", searchDomain))
}
err := r.exec(dnsCommand, id, dnsCommandInput...)
if err != nil {
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
}
// UnsetDNS unsets the DNS settings for the given interface. It uses resolvconf to unset the DNS settings.
func (r *WgQuickRepo) UnsetDNS(id domain.InterfaceIdentifier) error {
dnsCommand := "resolvconf -d %resPref%i -f"
err := r.exec(dnsCommand, id)
if err != nil {
return fmt.Errorf("failed to unset dns settings: %w", err)
}
return nil
}
func (r *WgQuickRepo) replaceCommandPlaceHolders(command string, interfaceId domain.InterfaceIdentifier) string {
command = strings.ReplaceAll(command, "%resPref", r.resolvConfIfacePrefix)
return strings.ReplaceAll(command, "%i", string(interfaceId))
}
func (r *WgQuickRepo) exec(command string, interfaceId domain.InterfaceIdentifier, stdin ...string) error {
commandWithInterfaceName := r.replaceCommandPlaceHolders(command, interfaceId)
cmd := exec.Command(r.shellCmd, "-ce", commandWithInterfaceName)
if len(stdin) > 0 {
b := &bytes.Buffer{}
for _, ln := range stdin {
if _, err := fmt.Fprint(b, ln); err != nil {
return err
}
}
cmd.Stdin = b
}
out, err := cmd.CombinedOutput() // execute and wait for output
if err != nil {
return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
}
slog.Debug("executed shell command",
"command", commandWithInterfaceName,
"output", string(out))
return nil
}

View File

@@ -43,6 +43,7 @@ type Peer struct {
Identifier string `json:"Identifier" example:"super_nice_peer"` // peer unique identifier Identifier string `json:"Identifier" example:"super_nice_peer"` // peer unique identifier
DisplayName string `json:"DisplayName"` // a nice display name/ description for the peer DisplayName string `json:"DisplayName"` // a nice display name/ description for the peer
UserIdentifier string `json:"UserIdentifier"` // the owner UserIdentifier string `json:"UserIdentifier"` // the owner
UserDisplayName string `json:"UserDisplayName"` // the owner display name
InterfaceIdentifier string `json:"InterfaceIdentifier"` // the interface id InterfaceIdentifier string `json:"InterfaceIdentifier"` // the interface id
Disabled bool `json:"Disabled"` // flag that specifies if the peer is enabled (up) or not (down) Disabled bool `json:"Disabled"` // flag that specifies if the peer is enabled (up) or not (down)
DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled
@@ -80,7 +81,7 @@ type Peer struct {
} }
func NewPeer(src *domain.Peer) *Peer { func NewPeer(src *domain.Peer) *Peer {
return &Peer{ p := &Peer{
Identifier: string(src.Identifier), Identifier: string(src.Identifier),
DisplayName: src.DisplayName, DisplayName: src.DisplayName,
UserIdentifier: string(src.UserIdentifier), UserIdentifier: string(src.UserIdentifier),
@@ -111,6 +112,12 @@ func NewPeer(src *domain.Peer) *Peer {
PostDown: ConfigOptionFromDomain(src.Interface.PostDown), PostDown: ConfigOptionFromDomain(src.Interface.PostDown),
Filename: src.GetConfigFileName(), Filename: src.GetConfigFileName(),
} }
if src.User != nil {
p.UserDisplayName = src.User.DisplayName()
}
return p
} }
func NewPeers(src []domain.Peer) []Peer { func NewPeers(src []domain.Peer) []Peer {

View File

@@ -125,11 +125,27 @@ func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserM
// It sets up the external authentication providers (OIDC, OAuth, LDAP) and retries in case of errors. // It sets up the external authentication providers (OIDC, OAuth, LDAP) and retries in case of errors.
func (a *Authenticator) StartBackgroundJobs(ctx context.Context) { func (a *Authenticator) StartBackgroundJobs(ctx context.Context) {
go func() { go func() {
slog.Debug("setting up external auth providers...")
// Initialize local copies of authentication providers to allow retry in case of errors // Initialize local copies of authentication providers to allow retry in case of errors
oidcQueue := a.cfg.OpenIDConnect oidcQueue := a.cfg.OpenIDConnect
oauthQueue := a.cfg.OAuth oauthQueue := a.cfg.OAuth
ldapQueue := a.cfg.Ldap ldapQueue := a.cfg.Ldap
// Immediate attempt
failedOidc, failedOauth, failedLdap := a.setupExternalAuthProviders(oidcQueue, oauthQueue, ldapQueue)
if len(failedOidc) == 0 && len(failedOauth) == 0 && len(failedLdap) == 0 {
slog.Info("successfully setup all external auth providers")
return
}
// Prepare for retries with only the failed ones
oidcQueue = failedOidc
oauthQueue = failedOauth
ldapQueue = failedLdap
slog.Warn("failed to setup some external auth providers, retrying in 30 seconds",
"failedOidc", len(failedOidc), "failedOauth", len(failedOauth), "failedLdap", len(failedLdap))
ticker := time.NewTicker(30 * time.Second) // Ticker for delay between retries ticker := time.NewTicker(30 * time.Second) // Ticker for delay between retries
defer ticker.Stop() defer ticker.Stop()
@@ -358,12 +374,15 @@ func (a *Authenticator) passwordAuthentication(
rawUserInfo, err := ldapAuth.GetUserInfo(context.Background(), identifier) rawUserInfo, err := ldapAuth.GetUserInfo(context.Background(), identifier)
if err != nil { if err != nil {
if !errors.Is(err, domain.ErrNotFound) { if !errors.Is(err, domain.ErrNotFound) {
slog.Warn("failed to fetch ldap user info", "identifier", identifier, "error", err) slog.Warn("failed to fetch ldap user info",
"source", ldapAuth.GetName(), "identifier", identifier, "error", err)
} }
continue // user not found / other ldap error continue // user not found / other ldap error
} }
ldapUserInfo, err = ldapAuth.ParseUserInfo(rawUserInfo) ldapUserInfo, err = ldapAuth.ParseUserInfo(rawUserInfo)
if err != nil { if err != nil {
slog.Error("failed to parse ldap user info",
"source", ldapAuth.GetName(), "identifier", identifier, "error", err)
continue continue
} }
@@ -376,10 +395,14 @@ func (a *Authenticator) passwordAuthentication(
} }
if userSource == "" { if userSource == "" {
slog.Warn("no user source found for user",
"identifier", identifier, "ldapProviderCount", len(a.ldapAuthenticators), "inDb", userInDatabase)
return nil, errors.New("user not found") return nil, errors.New("user not found")
} }
if userSource == domain.UserSourceLdap && ldapProvider == nil { if userSource == domain.UserSourceLdap && ldapProvider == nil {
slog.Warn("no ldap provider found for user",
"identifier", identifier, "ldapProviderCount", len(a.ldapAuthenticators), "inDb", userInDatabase)
return nil, errors.New("ldap provider not found") return nil, errors.New("ldap provider not found")
} }

View File

@@ -113,10 +113,13 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
} }
if len(sr.Entries) == 0 { if len(sr.Entries) == 0 {
slog.Debug("LDAP user not found", "source", l.GetName(), "userId", userId, "filter", loginFilter)
return nil, domain.ErrNotFound return nil, domain.ErrNotFound
} }
if len(sr.Entries) > 1 { if len(sr.Entries) > 1 {
slog.Debug("LDAP user not unique",
"source", l.GetName(), "userId", userId, "filter", loginFilter, "entries", len(sr.Entries))
return nil, domain.ErrNotUnique return nil, domain.ErrNotUnique
} }

View File

@@ -5,24 +5,21 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/h44z/wg-portal/internal/app" "github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
"github.com/h44z/wg-portal/internal/lowlevel"
) )
// region dependencies // region dependencies
type ControllerManager interface {
// GetController returns the controller for the given interface.
GetController(iface domain.Interface) domain.InterfaceController
}
type InterfaceAndPeerDatabaseRepo interface { type InterfaceAndPeerDatabaseRepo interface {
// GetAllInterfaces returns all interfaces // GetInterface returns the interface with the given identifier.
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error)
// GetInterfacePeers returns all peers for a given interface
GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error)
} }
type EventBus interface { type EventBus interface {
@@ -30,6 +27,25 @@ type EventBus interface {
Subscribe(topic string, fn interface{}) error Subscribe(topic string, fn interface{}) error
} }
type RoutesController interface {
// SetRoutes sets the routes for the given interface. If no routes are provided, the function is a no-op.
SetRoutes(
ctx context.Context,
interfaceId domain.InterfaceIdentifier,
table int,
fwMark uint32,
cidrs []domain.Cidr,
) error
// RemoveRoutes removes the routes for the given interface. If no routes are provided, the function is a no-op.
RemoveRoutes(
ctx context.Context,
interfaceId domain.InterfaceIdentifier,
table int,
fwMark uint32,
oldCidrs []domain.Cidr,
) error
}
// endregion dependencies // endregion dependencies
type routeRuleInfo struct { type routeRuleInfo struct {
@@ -45,28 +61,24 @@ type routeRuleInfo struct {
type Manager struct { type Manager struct {
cfg *config.Config cfg *config.Config
bus EventBus bus EventBus
wg lowlevel.WireGuardClient db InterfaceAndPeerDatabaseRepo
nl lowlevel.NetlinkClient wgController ControllerManager
db InterfaceAndPeerDatabaseRepo
} }
// NewRouteManager creates a new route manager instance. // NewRouteManager creates a new route manager instance.
func NewRouteManager(cfg *config.Config, bus EventBus, db InterfaceAndPeerDatabaseRepo) (*Manager, error) { func NewRouteManager(
wg, err := wgctrl.New() cfg *config.Config,
if err != nil { bus EventBus,
panic("failed to init wgctrl: " + err.Error()) db InterfaceAndPeerDatabaseRepo,
} wgController ControllerManager,
) (*Manager, error) {
nl := &lowlevel.NetlinkManager{}
m := &Manager{ m := &Manager{
cfg: cfg, cfg: cfg,
bus: bus, bus: bus,
db: db, db: db,
wg: wg, wgController: wgController,
nl: nl,
} }
m.connectToMessageBus() m.connectToMessageBus()
@@ -85,17 +97,21 @@ func (m Manager) StartBackgroundJobs(_ context.Context) {
// this is a no-op for now // this is a no-op for now
} }
func (m Manager) handleRouteUpdateEvent(srcDescription string) { func (m Manager) handleRouteUpdateEvent(info domain.RoutingTableInfo) {
slog.Debug("handling route update event", "source", srcDescription) slog.Debug("handling route update event", "info", info.String())
err := m.syncRoutes(context.Background()) if !info.ManagementEnabled() {
if err != nil { return // route management disabled
slog.Error("failed to synchronize routes",
"source", srcDescription,
"error", err)
} }
slog.Debug("routes synchronized", "source", srcDescription) err := m.syncRoutes(context.Background(), info)
if err != nil {
slog.Error("failed to synchronize routes",
"info", info.String(), "error", err)
return
}
slog.Debug("routes synchronized", "info", info.String())
} }
func (m Manager) handleRouteRemoveEvent(info domain.RoutingTableInfo) { func (m Manager) handleRouteRemoveEvent(info domain.RoutingTableInfo) {
@@ -105,399 +121,40 @@ func (m Manager) handleRouteRemoveEvent(info domain.RoutingTableInfo) {
return // route management disabled return // route management disabled
} }
if err := m.removeFwMarkRules(info.FwMark, info.GetRoutingTable(), netlink.FAMILY_V4); err != nil { err := m.removeRoutes(context.Background(), info)
slog.Error("failed to remove v4 fwmark rules", "error", err)
}
if err := m.removeFwMarkRules(info.FwMark, info.GetRoutingTable(), netlink.FAMILY_V6); err != nil {
slog.Error("failed to remove v6 fwmark rules", "error", err)
}
slog.Debug("routes removed", "table", info.String())
}
func (m Manager) syncRoutes(ctx context.Context) error {
interfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to find all interfaces: %w", err) slog.Error("failed to synchronize routes",
"info", info.String(), "error", err)
return
} }
rules := map[int][]routeRuleInfo{ slog.Debug("routes removed", "info", info.String())
netlink.FAMILY_V4: nil,
netlink.FAMILY_V6: nil,
}
for _, iface := range interfaces {
if iface.IsDisabled() {
continue // disabled interface does not need route entries
}
if !iface.ManageRoutingTable() {
continue
}
peers, err := m.db.GetInterfacePeers(ctx, iface.Identifier)
if err != nil {
return fmt.Errorf("failed to find peers for %s: %w", iface.Identifier, err)
}
allowedIPs := iface.GetAllowedIPs(peers)
defRouteV4, defRouteV6 := m.containsDefaultRoute(allowedIPs)
link, err := m.nl.LinkByName(string(iface.Identifier))
if err != nil {
return fmt.Errorf("failed to find physical link for %s: %w", iface.Identifier, err)
}
table, fwmark, err := m.getRoutingTableAndFwMark(&iface, link)
if err != nil {
return fmt.Errorf("failed to get table and fwmark for %s: %w", iface.Identifier, err)
}
if err := m.setInterfaceRoutes(link, table, allowedIPs); err != nil {
return fmt.Errorf("failed to set routes for %s: %w", iface.Identifier, err)
}
if err := m.removeDeprecatedRoutes(link, netlink.FAMILY_V4, allowedIPs); err != nil {
return fmt.Errorf("failed to remove deprecated v4 routes for %s: %w", iface.Identifier, err)
}
if err := m.removeDeprecatedRoutes(link, netlink.FAMILY_V6, allowedIPs); err != nil {
return fmt.Errorf("failed to remove deprecated v6 routes for %s: %w", iface.Identifier, err)
}
if table != 0 {
rules[netlink.FAMILY_V4] = append(rules[netlink.FAMILY_V4], routeRuleInfo{
ifaceId: iface.Identifier,
fwMark: fwmark,
table: table,
family: netlink.FAMILY_V4,
hasDefault: defRouteV4,
})
}
if table != 0 {
rules[netlink.FAMILY_V6] = append(rules[netlink.FAMILY_V6], routeRuleInfo{
ifaceId: iface.Identifier,
fwMark: fwmark,
table: table,
family: netlink.FAMILY_V6,
hasDefault: defRouteV6,
})
}
}
return m.syncRouteRules(rules)
} }
func (m Manager) syncRouteRules(allRules map[int][]routeRuleInfo) error { func (m Manager) syncRoutes(ctx context.Context, info domain.RoutingTableInfo) error {
for family, rules := range allRules { rc, ok := m.wgController.GetController(info.Interface).(RoutesController)
// update fwmark rules if !ok {
if err := m.setFwMarkRules(rules, family); err != nil { slog.Warn("no capable routes-controller found for interface", "interface", info.Interface.Identifier)
return err
}
// update main rule
if err := m.setMainRule(rules, family); err != nil {
return err
}
// cleanup old main rules
if err := m.cleanupMainRule(rules, family); err != nil {
return err
}
}
return nil
}
func (m Manager) setFwMarkRules(rules []routeRuleInfo, family int) error {
for _, rule := range rules {
existingRules, err := m.nl.RuleList(family)
if err != nil {
return fmt.Errorf("failed to get existing rules for family %d: %w", family, err)
}
ruleExists := false
for _, existingRule := range existingRules {
if rule.fwMark == existingRule.Mark && rule.table == existingRule.Table {
ruleExists = true
break
}
}
if ruleExists {
continue // rule already exists, no need to recreate it
}
// create missing rule
if err := m.nl.RuleAdd(&netlink.Rule{
Family: family,
Table: rule.table,
Mark: rule.fwMark,
Invert: true,
SuppressIfgroup: -1,
SuppressPrefixlen: -1,
Priority: m.getRulePriority(existingRules),
Mask: nil,
Goto: -1,
Flow: -1,
}); err != nil {
return fmt.Errorf("failed to setup rule for fwmark %d and table %d: %w", rule.fwMark, rule.table, err)
}
}
return nil
}
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)
}
for _, existingRule := range existingRules {
if fwmark == existingRule.Mark && table == existingRule.Table {
existingRule.Family = family // set family, somehow the RuleList method does not populate the family field
if err := m.nl.RuleDel(&existingRule); err != nil {
return fmt.Errorf("failed to delete fwmark rule: %w", err)
}
}
}
return nil
}
func (m Manager) setMainRule(rules []routeRuleInfo, family int) error {
shouldHaveMainRule := false
for _, rule := range rules {
if rule.hasDefault == true {
shouldHaveMainRule = true
break
}
}
if !shouldHaveMainRule {
return nil return nil
} }
existingRules, err := m.nl.RuleList(family) err := rc.SetRoutes(ctx, info.Interface.Identifier, info.Table, info.FwMark, info.AllowedIps)
if err != nil { if err != nil {
return fmt.Errorf("failed to get existing rules for family %d: %w", family, err) return fmt.Errorf("failed to set routes for interface %s: %w", info.Interface.Identifier, err)
} }
ruleExists := false
for _, existingRule := range existingRules {
if existingRule.Table == unix.RT_TABLE_MAIN && existingRule.SuppressPrefixlen == 0 {
ruleExists = true
break
}
}
if ruleExists {
return nil // rule already exists, skip re-creation
}
if err := m.nl.RuleAdd(&netlink.Rule{
Family: family,
Table: unix.RT_TABLE_MAIN,
SuppressIfgroup: -1,
SuppressPrefixlen: 0,
Priority: m.getMainRulePriority(existingRules),
Mark: 0,
Mask: nil,
Goto: -1,
Flow: -1,
}); err != nil {
return fmt.Errorf("failed to setup rule for main table: %w", err)
}
return nil return nil
} }
func (m Manager) cleanupMainRule(rules []routeRuleInfo, family int) error { func (m Manager) removeRoutes(ctx context.Context, info domain.RoutingTableInfo) error {
existingRules, err := m.nl.RuleList(family) rc, ok := m.wgController.GetController(info.Interface).(RoutesController)
if !ok {
slog.Warn("no capable routes-controller found for interface", "interface", info.Interface.Identifier)
return nil
}
err := rc.RemoveRoutes(ctx, info.Interface.Identifier, info.Table, info.FwMark, info.AllowedIps)
if err != nil { if err != nil {
return fmt.Errorf("failed to get existing rules for family %d: %w", family, err) return fmt.Errorf("failed to remove routes for interface %s: %w", info.Interface.Identifier, err)
}
shouldHaveMainRule := false
for _, rule := range rules {
if rule.hasDefault == true {
shouldHaveMainRule = true
break
}
}
mainRules := 0
for _, existingRule := range existingRules {
if existingRule.Table == unix.RT_TABLE_MAIN && existingRule.SuppressPrefixlen == 0 {
mainRules++
}
}
removalCount := 0
if mainRules > 1 {
removalCount = mainRules - 1 // we only want one single rule
}
if !shouldHaveMainRule {
removalCount = mainRules
}
for _, existingRule := range existingRules {
if existingRule.Table == unix.RT_TABLE_MAIN && existingRule.SuppressPrefixlen == 0 {
if removalCount > 0 {
existingRule.Family = family // set family, somehow the RuleList method does not populate the family field
if err := m.nl.RuleDel(&existingRule); err != nil {
return fmt.Errorf("failed to delete main rule: %w", err)
}
removalCount--
}
}
}
return nil
}
func (m Manager) getMainRulePriority(existingRules []netlink.Rule) int {
prio := m.cfg.Advanced.RulePrioOffset
for {
isFresh := true
for _, existingRule := range existingRules {
if existingRule.Priority == prio {
isFresh = false
break
}
}
if isFresh {
break
} else {
prio++
}
}
return prio
}
func (m Manager) getRulePriority(existingRules []netlink.Rule) int {
prio := 32700 // linux main rule has a prio of 32766
for {
isFresh := true
for _, existingRule := range existingRules {
if existingRule.Priority == prio {
isFresh = false
break
}
}
if isFresh {
break
} else {
prio--
}
}
return prio
}
func (m Manager) setInterfaceRoutes(link netlink.Link, table int, allowedIPs []domain.Cidr) error {
for _, allowedIP := range allowedIPs {
err := m.nl.RouteReplace(&netlink.Route{
LinkIndex: link.Attrs().Index,
Dst: allowedIP.IpNet(),
Table: table,
Scope: unix.RT_SCOPE_LINK,
Type: unix.RTN_UNICAST,
})
if err != nil {
return fmt.Errorf("failed to add/update route %s: %w", allowedIP.String(), err)
}
}
return nil
}
func (m Manager) removeDeprecatedRoutes(link netlink.Link, family int, allowedIPs []domain.Cidr) error {
rawRoutes, err := m.nl.RouteListFiltered(family, &netlink.Route{
LinkIndex: link.Attrs().Index,
Table: unix.RT_TABLE_UNSPEC, // all tables
Scope: unix.RT_SCOPE_LINK,
Type: unix.RTN_UNICAST,
}, netlink.RT_FILTER_TABLE|netlink.RT_FILTER_TYPE|netlink.RT_FILTER_OIF)
if err != nil {
return fmt.Errorf("failed to fetch raw routes: %w", err)
}
for _, rawRoute := range rawRoutes {
if rawRoute.Dst == nil { // handle default route
var netlinkAddr domain.Cidr
if family == netlink.FAMILY_V4 {
netlinkAddr, _ = domain.CidrFromString("0.0.0.0/0")
} else {
netlinkAddr, _ = domain.CidrFromString("::/0")
}
rawRoute.Dst = netlinkAddr.IpNet()
}
netlinkAddr := domain.CidrFromIpNet(*rawRoute.Dst)
remove := true
for _, allowedIP := range allowedIPs {
if netlinkAddr == allowedIP {
remove = false
break
}
}
if !remove {
continue
}
err := m.nl.RouteDel(&rawRoute)
if err != nil {
return fmt.Errorf("failed to remove deprecated route %s: %w", netlinkAddr.String(), err)
}
} }
return nil return nil
} }
func (m Manager) getRoutingTableAndFwMark(iface *domain.Interface, link netlink.Link) (
table int,
fwmark uint32,
err error,
) {
table = iface.GetRoutingTable()
fwmark = iface.FirewallMark
if fwmark == 0 {
// generate a new (temporary) firewall mark based on the interface index
fwmark = uint32(m.cfg.Advanced.RouteTableOffset + link.Attrs().Index)
slog.Debug("using fwmark to handle routes",
"interface", iface.Identifier,
"fwmark", fwmark)
// apply the temporary fwmark to the wireguard interface
err = m.setFwMark(iface.Identifier, int(fwmark))
}
if table == 0 {
table = int(fwmark) // generate a new routing table base on interface index
slog.Debug("using routing table to handle default routes",
"interface", iface.Identifier,
"table", table)
}
return
}
func (m Manager) setFwMark(id domain.InterfaceIdentifier, fwmark int) error {
err := m.wg.ConfigureDevice(string(id), wgtypes.Config{
FirewallMark: &fwmark,
})
if err != nil {
return fmt.Errorf("failed to update fwmark to: %d: %w", fwmark, err)
}
return nil
}
func (m Manager) containsDefaultRoute(allowedIPs []domain.Cidr) (ipV4, ipV6 bool) {
for _, allowedIP := range allowedIPs {
if ipV4 && ipV6 {
break // speed up
}
if allowedIP.Prefix().Bits() == 0 {
if allowedIP.IsV4() {
ipV4 = true
} else {
ipV6 = true
}
}
}
return
}

View File

@@ -1,7 +1,6 @@
package wireguard package wireguard
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"maps" "maps"
@@ -12,33 +11,9 @@ import (
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
type InterfaceController interface {
GetId() domain.InterfaceBackend
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]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
DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error
PingAddresses(
ctx context.Context,
addr string,
) (*domain.PingerResult, error)
}
type backendInstance struct { type backendInstance struct {
Config config.BackendBase // Config is the configuration for the backend instance. Config config.BackendBase // Config is the configuration for the backend instance.
Implementation InterfaceController Implementation domain.InterfaceController
} }
type ControllerManager struct { type ControllerManager struct {
@@ -82,8 +57,9 @@ func (c *ControllerManager) registerLocalController() error {
c.controllers[config.LocalBackendName] = backendInstance{ c.controllers[config.LocalBackendName] = backendInstance{
Config: config.BackendBase{ Config: config.BackendBase{
Id: config.LocalBackendName, Id: config.LocalBackendName,
DisplayName: "Local WireGuard Controller", DisplayName: "Local WireGuard Controller",
IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces,
}, },
Implementation: localController, Implementation: localController,
} }
@@ -117,18 +93,18 @@ func (c *ControllerManager) logRegisteredControllers() {
} }
} }
func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) InterfaceController { func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) domain.InterfaceController {
return c.getController(backend, "") return c.getController(backend, "").Implementation
} }
func (c *ControllerManager) GetController(iface domain.Interface) InterfaceController { func (c *ControllerManager) GetController(iface domain.Interface) domain.InterfaceController {
return c.getController(iface.Backend, iface.Identifier) return c.getController(iface.Backend, iface.Identifier).Implementation
} }
func (c *ControllerManager) getController( func (c *ControllerManager) getController(
backend domain.InterfaceBackend, backend domain.InterfaceBackend,
ifaceId domain.InterfaceIdentifier, ifaceId domain.InterfaceIdentifier,
) InterfaceController { ) backendInstance {
if backend == "" { if backend == "" {
// If no backend is specified, use the local controller. // If no backend is specified, use the local controller.
// This might be the case for interfaces created in previous WireGuard Portal versions. // This might be the case for interfaces created in previous WireGuard Portal versions.
@@ -145,13 +121,13 @@ func (c *ControllerManager) getController(
slog.Warn("controller for backend not found, using local controller", slog.Warn("controller for backend not found, using local controller",
"backend", backend, "interface", ifaceId) "backend", backend, "interface", ifaceId)
} }
return controller.Implementation return controller
} }
func (c *ControllerManager) GetAllControllers() []InterfaceController { func (c *ControllerManager) GetAllControllers() []backendInstance {
var backendInstances = make([]InterfaceController, 0, len(c.controllers)) var backendInstances = make([]backendInstance, 0, len(c.controllers))
for instance := range maps.Values(c.controllers) { for instance := range maps.Values(c.controllers) {
backendInstances = append(backendInstances, instance.Implementation) backendInstances = append(backendInstances, instance)
} }
return backendInstances return backendInstances
} }

View File

@@ -38,9 +38,9 @@ type InterfaceAndPeerDatabaseRepo interface {
} }
type WgQuickController interface { type WgQuickController interface {
ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error ExecuteInterfaceHook(ctx context.Context, id domain.InterfaceIdentifier, hookCmd string) error
SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error SetDNS(ctx context.Context, id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error
UnsetDNS(id domain.InterfaceIdentifier) error UnsetDNS(ctx context.Context, id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error
} }
type EventBus interface { type EventBus interface {
@@ -53,11 +53,10 @@ type EventBus interface {
// endregion dependencies // endregion dependencies
type Manager struct { type Manager struct {
cfg *config.Config cfg *config.Config
bus EventBus bus EventBus
db InterfaceAndPeerDatabaseRepo db InterfaceAndPeerDatabaseRepo
wg *ControllerManager wg *ControllerManager
quick WgQuickController
userLockMap *sync.Map userLockMap *sync.Map
} }
@@ -66,7 +65,6 @@ func NewWireGuardManager(
cfg *config.Config, cfg *config.Config,
bus EventBus, bus EventBus,
wg *ControllerManager, wg *ControllerManager,
quick WgQuickController,
db InterfaceAndPeerDatabaseRepo, db InterfaceAndPeerDatabaseRepo,
) (*Manager, error) { ) (*Manager, error) {
m := &Manager{ m := &Manager{
@@ -74,7 +72,6 @@ func NewWireGuardManager(
bus: bus, bus: bus,
wg: wg, wg: wg,
db: db, db: db,
quick: quick,
userLockMap: &sync.Map{}, userLockMap: &sync.Map{},
} }

View File

@@ -15,26 +15,6 @@ import (
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
// GetImportableInterfaces returns all physical interfaces that are available on the system.
// This function also returns interfaces that are already available in the database.
func (m Manager) GetImportableInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
var allPhysicalInterfaces []domain.PhysicalInterface
for _, wgBackend := range m.wg.GetAllControllers() {
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
if err != nil {
return nil, err
}
allPhysicalInterfaces = append(allPhysicalInterfaces, physicalInterfaces...)
}
return allPhysicalInterfaces, nil
}
// GetInterfaceAndPeers returns the interface and all peers for the given interface identifier. // GetInterfaceAndPeers returns the interface and all peers for the given interface identifier.
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) ( func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.Interface, *domain.Interface,
@@ -110,52 +90,62 @@ func (m Manager) GetUserInterfaces(ctx context.Context, _ domain.UserIdentifier)
} }
// ImportNewInterfaces imports all new physical interfaces that are available on the system. // ImportNewInterfaces imports all new physical interfaces that are available on the system.
// If a filter is set, only interfaces that match the filter will be imported.
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) { func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil { if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return 0, err return 0, err
} }
var existingInterfaceIds []domain.InterfaceIdentifier
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil {
return 0, err
}
for _, existingInterface := range existingInterfaces {
existingInterfaceIds = append(existingInterfaceIds, existingInterface.Identifier)
}
imported := 0 imported := 0
for _, wgBackend := range m.wg.GetAllControllers() { for _, wgBackend := range m.wg.GetAllControllers() {
physicalInterfaces, err := wgBackend.GetInterfaces(ctx) physicalInterfaces, err := wgBackend.Implementation.GetInterfaces(ctx)
if err != nil { if err != nil {
return 0, err return 0, err
} }
// if no filter is given, exclude already existing interfaces
var excludedInterfaces []domain.InterfaceIdentifier
if len(filter) == 0 {
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil {
return 0, err
}
for _, existingInterface := range existingInterfaces {
excludedInterfaces = append(excludedInterfaces, existingInterface.Identifier)
}
}
for _, physicalInterface := range physicalInterfaces { for _, physicalInterface := range physicalInterfaces {
if slices.Contains(excludedInterfaces, physicalInterface.Identifier) { if slices.Contains(wgBackend.Config.IgnoredInterfaces, string(physicalInterface.Identifier)) {
slog.Info("ignoring interface due to backend filter restrictions",
"interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces,
"backend", wgBackend.Config.Id)
continue // skip ignored interfaces
}
if slices.Contains(existingInterfaceIds, physicalInterface.Identifier) {
continue // skip interfaces that already exist
}
if len(filter) > 0 && !slices.Contains(filter, physicalInterface.Identifier) {
slog.Info("ignoring interface due to filter restrictions",
"interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces,
"backend", wgBackend.Config.Id)
continue continue
} }
if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) { slog.Info("importing new interface",
continue "interface", physicalInterface.Identifier, "backend", wgBackend.Config.Id)
}
slog.Info("importing new interface", "interface", physicalInterface.Identifier) physicalPeers, err := wgBackend.Implementation.GetPeers(ctx, physicalInterface.Identifier)
physicalPeers, err := wgBackend.GetPeers(ctx, physicalInterface.Identifier)
if err != nil { if err != nil {
return 0, err return 0, err
} }
err = m.importInterface(ctx, wgBackend, &physicalInterface, physicalPeers) err = m.importInterface(ctx, wgBackend.Implementation, &physicalInterface, physicalPeers)
if err != nil { if err != nil {
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err) return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
} }
slog.Info("imported new interface", "interface", physicalInterface.Identifier, "peers", len(physicalPeers)) slog.Info("imported new interface",
"interface", physicalInterface.Identifier, "peers", len(physicalPeers), "backend", wgBackend.Config.Id)
imported++ imported++
} }
} }
@@ -221,9 +211,11 @@ func (m Manager) RestoreInterfaceState(
return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err) return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err)
} }
_, err = m.wg.GetController(iface).GetInterface(ctx, iface.Identifier) controller := m.wg.GetController(iface)
_, err = controller.GetInterface(ctx, iface.Identifier)
if err != nil && !iface.IsDisabled() { if err != nil && !iface.IsDisabled() {
slog.Debug("creating missing interface", "interface", iface.Identifier) slog.Debug("creating missing interface", "interface", iface.Identifier, "backend", controller.GetId())
// temporarily disable interface in database so that the current state is reflected correctly // temporarily disable interface in database so that the current state is reflected correctly
_ = m.db.SaveInterface(ctx, iface.Identifier, _ = m.db.SaveInterface(ctx, iface.Identifier,
@@ -250,7 +242,8 @@ func (m Manager) RestoreInterfaceState(
return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err) return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err)
} }
} else { } else {
slog.Debug("restoring interface state", "interface", iface.Identifier, "disabled", iface.IsDisabled()) slog.Debug("restoring interface state",
"interface", iface.Identifier, "disabled", iface.IsDisabled(), "backend", controller.GetId())
// try to move interface to stored state // try to move interface to stored state
_, err = m.saveInterface(ctx, &iface) _, err = m.saveInterface(ctx, &iface)
@@ -278,13 +271,13 @@ func (m Manager) RestoreInterfaceState(
for _, peer := range peers { for _, peer := range peers {
switch { switch {
case iface.IsDisabled() && iface.Backend == config.LocalBackendName: // if interface is disabled, delete all peers case iface.IsDisabled() && iface.Backend == config.LocalBackendName: // if interface is disabled, delete all peers
if err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier, if err := controller.DeletePeer(ctx, iface.Identifier,
peer.Identifier); err != nil { peer.Identifier); err != nil {
return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w", return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w",
peer.Identifier, iface.Identifier, err) peer.Identifier, iface.Identifier, err)
} }
default: // update peer default: // update peer
err := m.wg.GetController(iface).SavePeer(ctx, iface.Identifier, peer.Identifier, err := controller.SavePeer(ctx, iface.Identifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) { func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, &peer) domain.MergeToPhysicalPeer(pp, &peer)
return pp, nil return pp, nil
@@ -297,7 +290,7 @@ func (m Manager) RestoreInterfaceState(
} }
// remove non-wgportal peers // remove non-wgportal peers
physicalPeers, _ := m.wg.GetController(iface).GetPeers(ctx, iface.Identifier) physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier)
for _, physicalPeer := range physicalPeers { for _, physicalPeer := range physicalPeers {
isWgPortalPeer := false isWgPortalPeer := false
for _, peer := range peers { for _, peer := range peers {
@@ -307,7 +300,7 @@ func (m Manager) RestoreInterfaceState(
} }
} }
if !isWgPortalPeer { if !isWgPortalPeer {
err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier, err := controller.DeletePeer(ctx, iface.Identifier,
domain.PeerIdentifier(physicalPeer.PublicKey)) domain.PeerIdentifier(physicalPeer.PublicKey))
if err != nil { if err != nil {
return fmt.Errorf("failed to remove non-wgportal peer %s from interface %s: %w", return fmt.Errorf("failed to remove non-wgportal peer %s from interface %s: %w",
@@ -460,7 +453,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
return err return err
} }
existingInterface, err := m.db.GetInterface(ctx, id) existingInterface, existingPeers, err := m.db.GetInterfaceAndPeers(ctx, id)
if err != nil { if err != nil {
return fmt.Errorf("unable to find interface %s: %w", id, err) return fmt.Errorf("unable to find interface %s: %w", id, err)
} }
@@ -475,15 +468,16 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
physicalInterface, _ := m.wg.GetController(*existingInterface).GetInterface(ctx, id) physicalInterface, _ := m.wg.GetController(*existingInterface).GetInterface(ctx, id)
if err := m.handleInterfacePreSaveHooks(existingInterface, !existingInterface.IsDisabled(), false); err != nil { if err := m.handleInterfacePreSaveHooks(ctx, existingInterface, !existingInterface.IsDisabled(),
false); err != nil {
return fmt.Errorf("pre-delete hooks failed: %w", err) return fmt.Errorf("pre-delete hooks failed: %w", err)
} }
if err := m.handleInterfacePreSaveActions(existingInterface); err != nil { if err := m.handleInterfacePreSaveActions(ctx, existingInterface); err != nil {
return fmt.Errorf("pre-delete actions failed: %w", err) return fmt.Errorf("pre-delete actions failed: %w", err)
} }
if err := m.deleteInterfacePeers(ctx, id); err != nil { if err := m.deleteInterfacePeers(ctx, existingInterface, existingPeers); err != nil {
return fmt.Errorf("peer deletion failure: %w", err) return fmt.Errorf("peer deletion failure: %w", err)
} }
@@ -500,11 +494,18 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
fwMark = physicalInterface.FirewallMark fwMark = physicalInterface.FirewallMark
} }
m.bus.Publish(app.TopicRouteRemove, domain.RoutingTableInfo{ m.bus.Publish(app.TopicRouteRemove, domain.RoutingTableInfo{
FwMark: fwMark, Interface: *existingInterface,
Table: existingInterface.GetRoutingTable(), AllowedIps: existingInterface.GetAllowedIPs(existingPeers),
FwMark: fwMark,
Table: existingInterface.GetRoutingTable(),
}) })
if err := m.handleInterfacePostSaveHooks(existingInterface, !existingInterface.IsDisabled(), false); err != nil { if err := m.handleInterfacePostSaveHooks(
ctx,
existingInterface,
!existingInterface.IsDisabled(),
false,
); err != nil {
return fmt.Errorf("post-delete hooks failed: %w", err) return fmt.Errorf("post-delete hooks failed: %w", err)
} }
@@ -525,11 +526,11 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
oldEnabled, newEnabled := m.getInterfaceStateHistory(ctx, iface) oldEnabled, newEnabled := m.getInterfaceStateHistory(ctx, iface)
if err := m.handleInterfacePreSaveHooks(iface, oldEnabled, newEnabled); err != nil { if err := m.handleInterfacePreSaveHooks(ctx, iface, oldEnabled, newEnabled); err != nil {
return nil, fmt.Errorf("pre-save hooks failed: %w", err) return nil, fmt.Errorf("pre-save hooks failed: %w", err)
} }
if err := m.handleInterfacePreSaveActions(iface); err != nil { if err := m.handleInterfacePreSaveActions(ctx, iface); err != nil {
return nil, fmt.Errorf("pre-save actions failed: %w", err) return nil, fmt.Errorf("pre-save actions failed: %w", err)
} }
@@ -551,6 +552,30 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
return nil, fmt.Errorf("failed to save interface: %w", err) return nil, fmt.Errorf("failed to save interface: %w", err)
} }
// update the interface type of peers in db
peers, err := m.db.GetInterfacePeers(ctx, iface.Identifier)
if err != nil {
return nil, fmt.Errorf("failed to load peers for interface %s: %w", iface.Identifier, err)
}
for _, peer := range peers {
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
switch iface.Type {
case domain.InterfaceTypeAny:
peer.Interface.Type = domain.InterfaceTypeAny
case domain.InterfaceTypeClient:
peer.Interface.Type = domain.InterfaceTypeServer
case domain.InterfaceTypeServer:
peer.Interface.Type = domain.InterfaceTypeClient
}
return &peer, nil
})
if err != nil {
return nil, fmt.Errorf("failed to update peer %s for interface %s: %w", peer.Identifier,
iface.Identifier, err)
}
}
if iface.IsDisabled() { if iface.IsDisabled() {
physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier) physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier)
fwMark := iface.FirewallMark fwMark := iface.FirewallMark
@@ -558,14 +583,21 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
fwMark = physicalInterface.FirewallMark fwMark = physicalInterface.FirewallMark
} }
m.bus.Publish(app.TopicRouteRemove, domain.RoutingTableInfo{ m.bus.Publish(app.TopicRouteRemove, domain.RoutingTableInfo{
FwMark: fwMark, Interface: *iface,
Table: iface.GetRoutingTable(), AllowedIps: iface.GetAllowedIPs(peers),
FwMark: fwMark,
Table: iface.GetRoutingTable(),
}) })
} else { } else {
m.bus.Publish(app.TopicRouteUpdate, "interface updated: "+string(iface.Identifier)) m.bus.Publish(app.TopicRouteUpdate, domain.RoutingTableInfo{
Interface: *iface,
AllowedIps: iface.GetAllowedIPs(peers),
FwMark: iface.FirewallMark,
Table: iface.GetRoutingTable(),
})
} }
if err := m.handleInterfacePostSaveHooks(iface, oldEnabled, newEnabled); err != nil { if err := m.handleInterfacePostSaveHooks(ctx, iface, oldEnabled, newEnabled); err != nil {
return nil, fmt.Errorf("post-save hooks failed: %w", err) return nil, fmt.Errorf("post-save hooks failed: %w", err)
} }
@@ -610,51 +642,83 @@ func (m Manager) getInterfaceStateHistory(ctx context.Context, iface *domain.Int
return !oldInterface.IsDisabled(), !iface.IsDisabled() return !oldInterface.IsDisabled(), !iface.IsDisabled()
} }
func (m Manager) handleInterfacePreSaveActions(iface *domain.Interface) error { func (m Manager) handleInterfacePreSaveActions(ctx context.Context, iface *domain.Interface) error {
if !iface.IsDisabled() { wgQuickController, ok := m.wg.GetController(*iface).(WgQuickController)
if err := m.quick.SetDNS(iface.Identifier, iface.DnsStr, iface.DnsSearchStr); err != nil { if !ok {
return fmt.Errorf("failed to update dns settings: %w", err) slog.Warn("failed to perform pre-save actions", "interface", iface.Identifier,
} "error", "no capable controller found")
} else { return nil
if err := m.quick.UnsetDNS(iface.Identifier); err != nil { }
return fmt.Errorf("failed to clear dns settings: %w", err)
// update DNS settings only for client interfaces
if iface.Type == domain.InterfaceTypeClient || iface.Type == domain.InterfaceTypeAny {
if !iface.IsDisabled() {
if err := wgQuickController.SetDNS(ctx, iface.Identifier, iface.DnsStr, iface.DnsSearchStr); err != nil {
return fmt.Errorf("failed to update dns settings: %w", err)
}
} else {
if err := wgQuickController.UnsetDNS(ctx, iface.Identifier, iface.DnsStr, iface.DnsSearchStr); err != nil {
return fmt.Errorf("failed to clear dns settings: %w", err)
}
} }
} }
return nil return nil
} }
func (m Manager) handleInterfacePreSaveHooks(iface *domain.Interface, oldEnabled, newEnabled bool) error { func (m Manager) handleInterfacePreSaveHooks(
ctx context.Context,
iface *domain.Interface,
oldEnabled, newEnabled bool,
) error {
if oldEnabled == newEnabled { if oldEnabled == newEnabled {
return nil // do nothing if state did not change return nil // do nothing if state did not change
} }
slog.Debug("executing pre-save hooks", "interface", iface.Identifier, "up", newEnabled) slog.Debug("executing pre-save hooks", "interface", iface.Identifier, "up", newEnabled)
wgQuickController, ok := m.wg.GetController(*iface).(WgQuickController)
if !ok {
slog.Warn("failed to execute pre-save hooks", "interface", iface.Identifier, "up", newEnabled,
"error", "no capable controller found")
return nil
}
if newEnabled { if newEnabled {
if err := m.quick.ExecuteInterfaceHook(iface.Identifier, iface.PreUp); err != nil { if err := wgQuickController.ExecuteInterfaceHook(ctx, iface.Identifier, iface.PreUp); err != nil {
return fmt.Errorf("failed to execute pre-up hook: %w", err) return fmt.Errorf("failed to execute pre-up hook: %w", err)
} }
} else { } else {
if err := m.quick.ExecuteInterfaceHook(iface.Identifier, iface.PreDown); err != nil { if err := wgQuickController.ExecuteInterfaceHook(ctx, iface.Identifier, iface.PreDown); err != nil {
return fmt.Errorf("failed to execute pre-down hook: %w", err) return fmt.Errorf("failed to execute pre-down hook: %w", err)
} }
} }
return nil return nil
} }
func (m Manager) handleInterfacePostSaveHooks(iface *domain.Interface, oldEnabled, newEnabled bool) error { func (m Manager) handleInterfacePostSaveHooks(
ctx context.Context,
iface *domain.Interface,
oldEnabled, newEnabled bool,
) error {
if oldEnabled == newEnabled { if oldEnabled == newEnabled {
return nil // do nothing if state did not change return nil // do nothing if state did not change
} }
slog.Debug("executing post-save hooks", "interface", iface.Identifier, "up", newEnabled) slog.Debug("executing post-save hooks", "interface", iface.Identifier, "up", newEnabled)
wgQuickController, ok := m.wg.GetController(*iface).(WgQuickController)
if !ok {
slog.Warn("failed to execute post-save hooks", "interface", iface.Identifier, "up", newEnabled,
"error", "no capable controller found")
return nil
}
if newEnabled { if newEnabled {
if err := m.quick.ExecuteInterfaceHook(iface.Identifier, iface.PostUp); err != nil { if err := wgQuickController.ExecuteInterfaceHook(ctx, iface.Identifier, iface.PostUp); err != nil {
return fmt.Errorf("failed to execute post-up hook: %w", err) return fmt.Errorf("failed to execute post-up hook: %w", err)
} }
} else { } else {
if err := m.quick.ExecuteInterfaceHook(iface.Identifier, iface.PostDown); err != nil { if err := wgQuickController.ExecuteInterfaceHook(ctx, iface.Identifier, iface.PostDown); err != nil {
return fmt.Errorf("failed to execute post-down hook: %w", err) return fmt.Errorf("failed to execute post-down hook: %w", err)
} }
} }
@@ -782,7 +846,7 @@ func (m Manager) getFreshListenPort(ctx context.Context) (port int, err error) {
func (m Manager) importInterface( func (m Manager) importInterface(
ctx context.Context, ctx context.Context,
backend InterfaceController, backend domain.InterfaceController,
in *domain.PhysicalInterface, in *domain.PhysicalInterface,
peers []domain.PhysicalPeer, peers []domain.PhysicalPeer,
) error { ) error {
@@ -884,13 +948,9 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
return nil return nil
} }
func (m Manager) deleteInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) error { func (m Manager) deleteInterfacePeers(ctx context.Context, iface *domain.Interface, allPeers []domain.Peer) error {
iface, allPeers, err := m.db.GetInterfaceAndPeers(ctx, id)
if err != nil {
return err
}
for _, peer := range allPeers { for _, peer := range allPeers {
err = m.wg.GetController(*iface).DeletePeer(ctx, id, peer.Identifier) err := m.wg.GetController(*iface).DeletePeer(ctx, iface.Identifier, peer.Identifier)
if err != nil && !errors.Is(err, os.ErrNotExist) { if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("wireguard peer deletion failure for %s: %w", peer.Identifier, err) return fmt.Errorf("wireguard peer deletion failure for %s: %w", peer.Identifier, err)
} }

View File

@@ -188,6 +188,8 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
sessionUser := domain.GetUserInfo(ctx) sessionUser := domain.GetUserInfo(ctx)
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // ensure that identifier corresponds to the public key
// Enforce peer limit for non-admin users if LimitAdditionalUserPeers is set // Enforce peer limit for non-admin users if LimitAdditionalUserPeers is set
if m.cfg.Core.SelfProvisioningAllowed && !sessionUser.IsAdmin && m.cfg.Advanced.LimitAdditionalUserPeers > 0 { if m.cfg.Core.SelfProvisioningAllowed && !sessionUser.IsAdmin && m.cfg.Advanced.LimitAdditionalUserPeers > 0 {
peers, err := m.db.GetUserPeers(ctx, peer.UserIdentifier) peers, err := m.db.GetUserPeers(ctx, peer.UserIdentifier)
@@ -386,9 +388,19 @@ func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
return fmt.Errorf("failed to delete peer %s: %w", id, err) return fmt.Errorf("failed to delete peer %s: %w", id, err)
} }
peers, err := m.db.GetInterfacePeers(ctx, iface.Identifier)
if err != nil {
return fmt.Errorf("failed to load peers for interface %s: %w", iface.Identifier, err)
}
m.bus.Publish(app.TopicPeerDeleted, *peer) m.bus.Publish(app.TopicPeerDeleted, *peer)
// Update routes after peers have changed // Update routes after peers have changed
m.bus.Publish(app.TopicRouteUpdate, "peers updated") m.bus.Publish(app.TopicRouteUpdate, domain.RoutingTableInfo{
Interface: *iface,
AllowedIps: iface.GetAllowedIPs(peers),
FwMark: iface.FirewallMark,
Table: iface.GetRoutingTable(),
})
// Update interface after peers have changed // Update interface after peers have changed
m.bus.Publish(app.TopicPeerInterfaceUpdated, peer.InterfaceIdentifier) m.bus.Publish(app.TopicPeerInterfaceUpdated, peer.InterfaceIdentifier)
@@ -436,20 +448,28 @@ func (m Manager) GetUserPeerStats(ctx context.Context, id domain.UserIdentifier)
// region helper-functions // region helper-functions
func (m Manager) savePeers(ctx context.Context, peers ...*domain.Peer) error { func (m Manager) savePeers(ctx context.Context, peers ...*domain.Peer) error {
interfaces := make(map[domain.InterfaceIdentifier]struct{}) interfaces := make(map[domain.InterfaceIdentifier]domain.Interface)
interfacePeers := make(map[domain.InterfaceIdentifier][]domain.Peer)
for _, peer := range peers { for _, peer := range peers {
iface, err := m.db.GetInterface(ctx, peer.InterfaceIdentifier) // get interface from db if it is not yet in the map
if err != nil { if _, ok := interfaces[peer.InterfaceIdentifier]; !ok {
return fmt.Errorf("unable to find interface %s: %w", peer.InterfaceIdentifier, err) iface, err := m.db.GetInterface(ctx, peer.InterfaceIdentifier)
if err != nil {
return fmt.Errorf("unable to find interface %s: %w", peer.InterfaceIdentifier, err)
}
interfaces[peer.InterfaceIdentifier] = *iface
} }
iface := interfaces[peer.InterfaceIdentifier]
interfacePeers[iface.Identifier] = append(interfacePeers[iface.Identifier], *peer)
// Always save the peer to the backend, regardless of disabled/expired state // Always save the peer to the backend, regardless of disabled/expired state
// The backend will handle the disabled state appropriately // The backend will handle the disabled state appropriately
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) { err := m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
peer.CopyCalculatedAttributes(p) peer.CopyCalculatedAttributes(p)
err := m.wg.GetController(*iface).SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier, err := m.wg.GetController(iface).SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) { func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, peer) domain.MergeToPhysicalPeer(pp, peer)
return pp, nil return pp, nil
@@ -473,13 +493,16 @@ func (m Manager) savePeers(ctx context.Context, peers ...*domain.Peer) error {
Peer: *peer, Peer: *peer,
}, },
}) })
interfaces[peer.InterfaceIdentifier] = struct{}{}
} }
// Update routes after peers have changed // Update routes after peers have changed
if len(interfaces) != 0 { for id, iface := range interfaces {
m.bus.Publish(app.TopicRouteUpdate, "peers updated") m.bus.Publish(app.TopicRouteUpdate, domain.RoutingTableInfo{
Interface: iface,
AllowedIps: iface.GetAllowedIPs(interfacePeers[id]),
FwMark: iface.FirewallMark,
Table: iface.GetRoutingTable(),
})
} }
for iface := range interfaces { for iface := range interfaces {

View File

@@ -0,0 +1,194 @@
package wireguard
import (
"context"
"testing"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
// --- Test mocks ---
type mockBus struct{}
func (f *mockBus) Publish(topic string, args ...any) {}
func (f *mockBus) Subscribe(topic string, fn interface{}) error { return nil }
type mockController struct{}
func (f *mockController) GetId() domain.InterfaceBackend { return "local" }
func (f *mockController) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
return nil, nil
}
func (f *mockController) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (
*domain.PhysicalInterface,
error,
) {
return &domain.PhysicalInterface{Identifier: id}, nil
}
func (f *mockController) GetPeers(_ context.Context, _ domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
return nil, nil
}
func (f *mockController) SaveInterface(
_ context.Context,
_ domain.InterfaceIdentifier,
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
) error {
_, _ = updateFunc(&domain.PhysicalInterface{})
return nil
}
func (f *mockController) DeleteInterface(_ context.Context, _ domain.InterfaceIdentifier) error {
return nil
}
func (f *mockController) SavePeer(
_ context.Context,
_ domain.InterfaceIdentifier,
_ domain.PeerIdentifier,
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
) error {
_, _ = updateFunc(&domain.PhysicalPeer{})
return nil
}
func (f *mockController) DeletePeer(_ context.Context, _ domain.InterfaceIdentifier, _ domain.PeerIdentifier) error {
return nil
}
func (f *mockController) PingAddresses(_ context.Context, _ string) (*domain.PingerResult, error) {
return nil, nil
}
type mockDB struct {
savedPeers map[domain.PeerIdentifier]*domain.Peer
iface *domain.Interface
}
func (f *mockDB) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
if f.iface != nil && f.iface.Identifier == id {
return f.iface, nil
}
return &domain.Interface{Identifier: id}, nil
}
func (f *mockDB) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.Interface,
[]domain.Peer,
error,
) {
return f.iface, nil, nil
}
func (f *mockDB) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
return nil, nil
}
func (f *mockDB) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) { return nil, nil }
func (f *mockDB) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
return nil, nil
}
func (f *mockDB) SaveInterface(
ctx context.Context,
id domain.InterfaceIdentifier,
updateFunc func(in *domain.Interface) (*domain.Interface, error),
) error {
if f.iface == nil {
f.iface = &domain.Interface{Identifier: id}
}
var err error
f.iface, err = updateFunc(f.iface)
return err
}
func (f *mockDB) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
return nil
}
func (f *mockDB) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
return nil, nil
}
func (f *mockDB) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
return nil, nil
}
func (f *mockDB) SavePeer(
ctx context.Context,
id domain.PeerIdentifier,
updateFunc func(in *domain.Peer) (*domain.Peer, error),
) error {
if f.savedPeers == nil {
f.savedPeers = make(map[domain.PeerIdentifier]*domain.Peer)
}
existing := f.savedPeers[id]
if existing == nil {
existing = &domain.Peer{Identifier: id}
}
updated, err := updateFunc(existing)
if err != nil {
return err
}
f.savedPeers[updated.Identifier] = updated
return nil
}
func (f *mockDB) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error { return nil }
func (f *mockDB) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
return nil, domain.ErrNotFound
}
func (f *mockDB) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (
map[domain.Cidr][]domain.Cidr,
error,
) {
return map[domain.Cidr][]domain.Cidr{}, nil
}
// --- Test ---
func TestCreatePeer_SetsIdentifier_FromPublicKey(t *testing.T) {
// Arrange
cfg := &config.Config{}
cfg.Core.SelfProvisioningAllowed = true
cfg.Core.EditableKeys = true
cfg.Advanced.LimitAdditionalUserPeers = 0
bus := &mockBus{}
// Prepare a controller manager with our mock controller
ctrlMgr := &ControllerManager{
controllers: map[domain.InterfaceBackend]backendInstance{
config.LocalBackendName: {Implementation: &mockController{}},
},
}
db := &mockDB{iface: &domain.Interface{Identifier: "wg0", Type: domain.InterfaceTypeServer}}
m := Manager{
cfg: cfg,
bus: bus,
db: db,
wg: ctrlMgr,
}
userId := domain.UserIdentifier("user@example.com")
ctx := domain.SetUserInfo(context.Background(), &domain.ContextUserInfo{Id: userId, IsAdmin: false})
pubKey := "TEST_PUBLIC_KEY_ABC123"
input := &domain.Peer{
Identifier: "should_be_overwritten",
UserIdentifier: userId,
InterfaceIdentifier: domain.InterfaceIdentifier("wg0"),
Interface: domain.PeerInterfaceConfig{
KeyPair: domain.KeyPair{PublicKey: pubKey},
},
}
// Act
out, err := m.CreatePeer(ctx, input)
// Assert
if err != nil {
t.Fatalf("CreatePeer returned error: %v", err)
}
expectedId := domain.PeerIdentifier(pubKey)
if out.Identifier != expectedId {
t.Fatalf("expected Identifier to be set from public key %q, got %q", expectedId, out.Identifier)
}
// Ensure the saved peer in DB also has the expected identifier
if db.savedPeers[expectedId] == nil {
t.Fatalf("expected peer with identifier %q to be saved in DB", expectedId)
}
}

View File

@@ -10,6 +10,13 @@ const LocalBackendName = "local"
type Backend struct { type Backend struct {
Default string `yaml:"default"` // The default backend to use (defaults to the internal backend) Default string `yaml:"default"` // The default backend to use (defaults to the internal backend)
// Local Backend-specific configuration
IgnoredLocalInterfaces []string `yaml:"ignored_local_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
LocalResolvconfPrefix string `yaml:"local_resolvconf_prefix"` // The prefix to use for interface names when passing them to resolvconf.
// External Backend-specific configuration
Mikrotik []BackendMikrotik `yaml:"mikrotik"` Mikrotik []BackendMikrotik `yaml:"mikrotik"`
} }
@@ -42,6 +49,8 @@ func (b *Backend) Validate() error {
type BackendBase struct { type BackendBase struct {
Id string `yaml:"id"` // A unique id for the backend Id string `yaml:"id"` // A unique id for the backend
DisplayName string `yaml:"display_name"` // A display name for the backend DisplayName string `yaml:"display_name"` // A display name for the backend
IgnoredInterfaces []string `yaml:"ignored_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
} }
// GetDisplayName returns the display name of the backend. // GetDisplayName returns the display name of the backend.

View File

@@ -134,6 +134,9 @@ func defaultConfig() *Config {
cfg.Backend = Backend{ cfg.Backend = Backend{
Default: LocalBackendName, // local backend is the default (using wgcrtl) Default: LocalBackendName, // local backend is the default (using wgcrtl)
// Most resolconf implementations use "tun." as a prefix for interface names.
// But systemd's implementation uses no prefix, for example.
LocalResolvconfPrefix: "tun.",
} }
cfg.Web = WebConfig{ cfg.Web = WebConfig{

View File

@@ -308,12 +308,14 @@ func MergeToPhysicalInterface(pi *PhysicalInterface, i *Interface) {
} }
type RoutingTableInfo struct { type RoutingTableInfo struct {
FwMark uint32 Interface Interface
Table int AllowedIps []Cidr
FwMark uint32
Table int
} }
func (r RoutingTableInfo) String() string { func (r RoutingTableInfo) String() string {
return fmt.Sprintf("%d -> %d", r.FwMark, r.Table) return fmt.Sprintf("%s: %d -> %d", r.Interface.Identifier, r.FwMark, r.Table)
} }
func (r RoutingTableInfo) ManagementEnabled() bool { func (r RoutingTableInfo) ManagementEnabled() bool {

View File

@@ -0,0 +1,27 @@
package domain
import "context"
type InterfaceController interface {
GetId() InterfaceBackend
GetInterfaces(_ context.Context) ([]PhysicalInterface, error)
GetInterface(_ context.Context, id InterfaceIdentifier) (*PhysicalInterface, error)
GetPeers(_ context.Context, deviceId InterfaceIdentifier) ([]PhysicalPeer, error)
SaveInterface(
_ context.Context,
id InterfaceIdentifier,
updateFunc func(pi *PhysicalInterface) (*PhysicalInterface, error),
) error
DeleteInterface(_ context.Context, id InterfaceIdentifier) error
SavePeer(
_ context.Context,
deviceId InterfaceIdentifier,
id PeerIdentifier,
updateFunc func(pp *PhysicalPeer) (*PhysicalPeer, error),
) error
DeletePeer(_ context.Context, deviceId InterfaceIdentifier, id PeerIdentifier) error
PingAddresses(
ctx context.Context,
addr string,
) (*PingerResult, error)
}

View File

@@ -199,3 +199,22 @@ func (c Cidr) Contains(other Cidr) bool {
return subnet.Contains(otherIP) return subnet.Contains(otherIP)
} }
// ContainsDefaultRoute returns true if the given CIDRs contain a default route.
func ContainsDefaultRoute(cidrs []Cidr) (ipV4, ipV6 bool) {
for _, allowedIP := range cidrs {
if ipV4 && ipV6 {
break // speed up
}
if allowedIP.Prefix().Bits() == 0 {
if allowedIP.IsV4() {
ipV4 = true
} else {
ipV6 = true
}
}
}
return
}

View File

@@ -1,12 +1,14 @@
package domain package domain
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time" "time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/gorm"
"github.com/h44z/wg-portal/internal" "github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/config"
@@ -44,6 +46,7 @@ type Peer struct {
DisplayName string // a nice display name/ description for the peer DisplayName string // a nice display name/ description for the peer
Identifier PeerIdentifier `gorm:"primaryKey;column:identifier"` // peer unique identifier Identifier PeerIdentifier `gorm:"primaryKey;column:identifier"` // peer unique identifier
UserIdentifier UserIdentifier `gorm:"index;column:user_identifier"` // the owner UserIdentifier UserIdentifier `gorm:"index;column:user_identifier"` // the owner
User *User `gorm:"-"` // the owner user object; loaded automatically after fetch
InterfaceIdentifier InterfaceIdentifier `gorm:"index;column:interface_identifier"` // the interface id InterfaceIdentifier InterfaceIdentifier `gorm:"index;column:interface_identifier"` // the interface id
Disabled *time.Time `gorm:"column:disabled"` // if this field is set, the peer is disabled Disabled *time.Time `gorm:"column:disabled"` // if this field is set, the peer is disabled
DisabledReason string // the reason why the peer has been disabled DisabledReason string // the reason why the peer has been disabled
@@ -305,22 +308,33 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) { func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
pp.Identifier = p.Identifier pp.Identifier = p.Identifier
pp.Endpoint = p.Endpoint.GetValue() pp.PresharedKey = p.PresharedKey
if p.Interface.Type == InterfaceTypeServer { pp.PublicKey = p.Interface.PublicKey
allowedIPs, _ := CidrsFromString(p.AllowedIPsStr.GetValue())
extraAllowedIPs, _ := CidrsFromString(p.ExtraAllowedIPsStr) switch p.Interface.Type {
pp.AllowedIPs = append(allowedIPs, extraAllowedIPs...) case InterfaceTypeClient: // this means that the corresponding interface in wgportal is a server interface
} else {
allowedIPs := make([]Cidr, len(p.Interface.Addresses)) allowedIPs := make([]Cidr, len(p.Interface.Addresses))
for i, ip := range p.Interface.Addresses { for i, ip := range p.Interface.Addresses {
allowedIPs[i] = ip.HostAddr() allowedIPs[i] = ip.HostAddr() // add the peer's host address to the allowed IPs
} }
extraAllowedIPs, _ := CidrsFromString(p.ExtraAllowedIPsStr) extraAllowedIPs, _ := CidrsFromString(p.ExtraAllowedIPsStr)
pp.AllowedIPs = append(allowedIPs, extraAllowedIPs...) pp.AllowedIPs = append(allowedIPs, extraAllowedIPs...)
case InterfaceTypeServer: // this means that the corresponding interface in wgportal is a client interface
allowedIPs, _ := CidrsFromString(p.AllowedIPsStr.GetValue())
extraAllowedIPs, _ := CidrsFromString(p.ExtraAllowedIPsStr)
pp.AllowedIPs = append(allowedIPs, extraAllowedIPs...)
pp.Endpoint = p.Endpoint.GetValue()
pp.PersistentKeepalive = p.PersistentKeepalive.GetValue()
case InterfaceTypeAny: // this means that the corresponding interface in wgportal has no specific type
allowedIPs := make([]Cidr, len(p.Interface.Addresses))
for i, ip := range p.Interface.Addresses {
allowedIPs[i] = ip.HostAddr() // add the peer's host address to the allowed IPs
}
extraAllowedIPs, _ := CidrsFromString(p.ExtraAllowedIPsStr)
pp.AllowedIPs = append(allowedIPs, extraAllowedIPs...)
pp.Endpoint = p.Endpoint.GetValue()
pp.PersistentKeepalive = p.PersistentKeepalive.GetValue()
} }
pp.PresharedKey = p.PresharedKey
pp.PublicKey = p.Interface.PublicKey
pp.PersistentKeepalive = p.PersistentKeepalive.GetValue()
switch pp.ImportSource { switch pp.ImportSource {
case ControllerTypeMikrotik: case ControllerTypeMikrotik:
@@ -328,7 +342,7 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
Id: "", Id: "",
Name: p.DisplayName, Name: p.DisplayName,
Comment: p.Notes, Comment: p.Notes,
IsResponder: false, IsResponder: p.Interface.Type == InterfaceTypeClient,
Disabled: p.IsDisabled(), Disabled: p.IsDisabled(),
ClientEndpoint: p.Endpoint.GetValue(), ClientEndpoint: p.Endpoint.GetValue(),
ClientAddress: CidrsToString(p.Interface.Addresses), ClientAddress: CidrsToString(p.Interface.Addresses),
@@ -348,3 +362,26 @@ type PeerCreationRequest struct {
UserIdentifiers []string UserIdentifiers []string
Prefix string Prefix string
} }
// AfterFind is a GORM hook that automatically loads the associated User object
// based on the UserIdentifier field. If the identifier is empty or no user is
// found, the User field is set to nil.
func (p *Peer) AfterFind(tx *gorm.DB) error {
if p == nil {
return nil
}
if p.UserIdentifier == "" {
p.User = nil
return nil
}
var u User
if err := tx.Where("identifier = ?", p.UserIdentifier).First(&u).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
p.User = nil
return nil
}
return err
}
p.User = &u
return nil
}

View File

@@ -185,6 +185,27 @@ func (u *User) CopyCalculatedAttributes(src *User) {
u.LinkedPeerCount = src.LinkedPeerCount u.LinkedPeerCount = src.LinkedPeerCount
} }
// DisplayName returns the display name of the user.
// The display name is the first and last name, or the email address of the user.
// If none of these fields are set, the user identifier is returned.
func (u *User) DisplayName() string {
var displayName string
switch {
case u.Firstname != "" && u.Lastname != "":
displayName = fmt.Sprintf("%s %s", u.Firstname, u.Lastname)
case u.Firstname != "":
displayName = u.Firstname
case u.Lastname != "":
displayName = u.Lastname
case u.Email != "":
displayName = u.Email
default:
displayName = string(u.Identifier)
}
return displayName
}
// region webauthn // region webauthn
func (u *User) WebAuthnID() []byte { func (u *User) WebAuthnID() []byte {
@@ -209,19 +230,7 @@ func (u *User) WebAuthnName() string {
} }
func (u *User) WebAuthnDisplayName() string { func (u *User) WebAuthnDisplayName() string {
var userName string return u.DisplayName()
switch {
case u.Firstname != "" && u.Lastname != "":
userName = fmt.Sprintf("%s %s", u.Firstname, u.Lastname)
case u.Firstname != "":
userName = u.Firstname
case u.Lastname != "":
userName = u.Lastname
default:
userName = string(u.Identifier)
}
return userName
} }
func (u *User) WebAuthnCredentials() []webauthn.Credential { func (u *User) WebAuthnCredentials() []webauthn.Credential {

View File

@@ -267,6 +267,7 @@ func parseHttpResponse[T any](resp *http.Response, err error) MikrotikApiRespons
} }
defer func(Body io.ReadCloser) { defer func(Body io.ReadCloser) {
_, _ = io.Copy(io.Discard, Body) // ensure to empty the body
err := Body.Close() err := Body.Close()
if err != nil { if err != nil {
slog.Error("failed to close response body", "error", err) slog.Error("failed to close response body", "error", err)

View File

@@ -6,8 +6,12 @@ repo_name: h44z/wg-portal
repo_url: https://github.com/h44z/wg-portal repo_url: https://github.com/h44z/wg-portal
copyright: Copyright &copy; 2023-2025 WireGuard Portal Project copyright: Copyright &copy; 2023-2025 WireGuard Portal Project
extra_javascript:
- javascript/img-comparison-slider.js
extra_css: extra_css:
- stylesheets/extra.css - stylesheets/extra.css
- stylesheets/img-comparison-slider.css
theme: theme:
name: material name: material