mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 16:06:17 +00:00
Compare commits
29 Commits
mail_templ
...
v2.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
d311313cb4 | ||
|
0cbca61c15 | ||
|
c79a6c83a8 | ||
|
098a9fe23e | ||
|
41cab5f7ea | ||
|
708c558211 | ||
|
99df4ca3cd | ||
|
9884d8c002 | ||
|
b099e8abfa | ||
|
112f6bfb77 | ||
|
a86f83a219 | ||
|
131413b470 | ||
|
2246829151 | ||
|
c20f17cddf | ||
|
3f76aa416f | ||
|
6a8b28df88 | ||
|
ffef1f7b12 | ||
|
dc002b156b | ||
|
1794b8653a | ||
|
a6d985d3ce | ||
|
a7bd3b3f95 | ||
|
f286840964 | ||
|
edb88b5768 | ||
|
588bbca141 | ||
|
f08740991b | ||
|
dd28a8dddf | ||
|
f994700caf | ||
|
be29abd29a | ||
|
94785c10ec |
4
.github/workflows/chart.yml
vendored
4
.github/workflows/chart.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
|
8
.github/workflows/docker-publish.yml
vendored
8
.github/workflows/docker-publish.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -66,10 +66,6 @@ jobs:
|
|||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
type=semver,pattern=v{{major}}.{{minor}}
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
type=semver,pattern=v{{major}}
|
type=semver,pattern=v{{major}}
|
||||||
# add v{{major}} tag, even for beta or release-canidate releases
|
|
||||||
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
|
||||||
# add {{major}} tag, even for beta releases or release-canidate releases
|
|
||||||
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -114,7 +110,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download binaries
|
- name: Download binaries
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: binaries
|
name: binaries
|
||||||
|
|
||||||
|
2
.github/workflows/pages.yml
vendored
2
.github/workflows/pages.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
@@ -20,7 +20,7 @@ RUN npm run build
|
|||||||
######
|
######
|
||||||
# Build backend
|
# Build backend
|
||||||
######
|
######
|
||||||
FROM --platform=${BUILDPLATFORM} golang:1.24-alpine AS builder
|
FROM --platform=${BUILDPLATFORM} golang:1.25-alpine AS builder
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
# Download dependencies
|
# Download dependencies
|
||||||
@@ -50,7 +50,7 @@ COPY --from=builder /build/dist/wg-portal /
|
|||||||
######
|
######
|
||||||
# Final image
|
# Final image
|
||||||
######
|
######
|
||||||
FROM alpine:3.19
|
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
|
||||||
# Setup timezone
|
# Setup timezone
|
||||||
|
@@ -32,6 +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])
|
||||||
* 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
|
||||||
|
@@ -50,7 +50,8 @@ func main() {
|
|||||||
database, err := adapters.NewSqlRepository(rawDb)
|
database, err := adapters.NewSqlRepository(rawDb)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
wireGuard := adapters.NewWireGuardRepository()
|
wireGuard, err := wireguard.NewControllerManager(cfg)
|
||||||
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
wgQuick := adapters.NewWgQuickRepo()
|
wgQuick := adapters.NewWgQuickRepo()
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ func main() {
|
|||||||
|
|
||||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
authenticator.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, eventBus, userManager)
|
webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, eventBus, userManager)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
@@ -133,7 +135,7 @@ func main() {
|
|||||||
apiV0EndpointUsers := handlersV0.NewUserEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendUsers)
|
apiV0EndpointUsers := handlersV0.NewUserEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendUsers)
|
||||||
apiV0EndpointInterfaces := handlersV0.NewInterfaceEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendInterfaces)
|
apiV0EndpointInterfaces := handlersV0.NewInterfaceEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendInterfaces)
|
||||||
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
||||||
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth)
|
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard)
|
||||||
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
||||||
|
|
||||||
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
||||||
|
@@ -25,6 +25,9 @@ core:
|
|||||||
import_existing: true
|
import_existing: true
|
||||||
restore_state: true
|
restore_state: true
|
||||||
|
|
||||||
|
backend:
|
||||||
|
default: local
|
||||||
|
|
||||||
advanced:
|
advanced:
|
||||||
log_level: info
|
log_level: info
|
||||||
log_pretty: false
|
log_pretty: false
|
||||||
@@ -76,6 +79,7 @@ auth:
|
|||||||
webauthn:
|
webauthn:
|
||||||
enabled: true
|
enabled: true
|
||||||
min_password_length: 16
|
min_password_length: 16
|
||||||
|
hide_login_form: false
|
||||||
|
|
||||||
web:
|
web:
|
||||||
listening_address: :8888
|
listening_address: :8888
|
||||||
@@ -101,6 +105,7 @@ webhook:
|
|||||||
|
|
||||||
Below you will find sections like
|
Below you will find sections like
|
||||||
[`core`](#core),
|
[`core`](#core),
|
||||||
|
[`backend`](#backend),
|
||||||
[`advanced`](#advanced),
|
[`advanced`](#advanced),
|
||||||
[`database`](#database),
|
[`database`](#database),
|
||||||
[`statistics`](#statistics),
|
[`statistics`](#statistics),
|
||||||
@@ -164,6 +169,65 @@ More advanced options are found in the subsequent `Advanced` section.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
Configuration options for the WireGuard backend, which manages the WireGuard interfaces and peers.
|
||||||
|
The current MikroTik backend is in **BETA** and may not support all features.
|
||||||
|
|
||||||
|
### `default`
|
||||||
|
- **Default:** `local`
|
||||||
|
- **Description:** The default backend to use for managing WireGuard interfaces.
|
||||||
|
Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
Below are the properties for each entry inside `backend.mikrotik`:
|
||||||
|
|
||||||
|
#### `id`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A unique identifier for this backend.
|
||||||
|
This value can be referenced by `backend.default` to use this backend as default.
|
||||||
|
The identifier must be unique across all backends and must not use the reserved keyword `local`.
|
||||||
|
|
||||||
|
#### `display_name`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A human-friendly display name for this backend. If omitted, the `id` will be used as the display name.
|
||||||
|
|
||||||
|
#### `api_url`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Base URL of the MikroTik REST API, including scheme and path, e.g., `https://10.10.10.10:8729/rest`.
|
||||||
|
|
||||||
|
#### `api_user`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Username for authenticating against the MikroTik API.
|
||||||
|
Ensure that the user has sufficient permissions to manage WireGuard interfaces and peers.
|
||||||
|
|
||||||
|
#### `api_password`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Password for the specified API user.
|
||||||
|
|
||||||
|
#### `api_verify_tls`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** Whether to verify the TLS certificate of the MikroTik API endpoint. Set to `false` to allow self-signed certificates (not recommended for production).
|
||||||
|
|
||||||
|
#### `api_timeout`
|
||||||
|
- **Default:** `30s`
|
||||||
|
- **Description:** Timeout for API requests to the MikroTik device. Uses Go duration format (e.g., `10s`, `1m`). If omitted, a default of 30 seconds is used.
|
||||||
|
|
||||||
|
#### `concurrency`
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
#### `debug`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** Enable verbose debug logging for the MikroTik backend.
|
||||||
|
|
||||||
|
For more details on configuring the MikroTik backend, see the [Backends](../usage/backends.md) documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Advanced
|
## Advanced
|
||||||
|
|
||||||
Additional or more specialized configuration options for logging and interface creation details.
|
Additional or more specialized configuration options for logging and interface creation details.
|
||||||
@@ -354,6 +418,12 @@ Some core authentication options are shared across all providers, while others a
|
|||||||
The default admin password strength is also enforced by this setting.
|
The default admin password strength is also enforced by this setting.
|
||||||
- **Important:** The password should be strong and secure. It is recommended to use a password with at least 16 characters, including uppercase and lowercase letters, numbers, and special characters.
|
- **Important:** The password should be strong and secure. It is recommended to use a password with at least 16 characters, including uppercase and lowercase letters, numbers, and special characters.
|
||||||
|
|
||||||
|
### `hide_login_form`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `true`, the login form is hidden and only the OIDC, OAuth, LDAP, or WebAuthn providers are shown. This is useful if you want to enforce a specific authentication method.
|
||||||
|
If no social login providers are configured, the login form is always shown, regardless of this setting.
|
||||||
|
- **Important:** You can still access the login form by adding the `?all` query parameter to the login URL (e.g. https://wg.portal/#/login?all).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### OIDC
|
### OIDC
|
||||||
@@ -666,18 +736,7 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
|
|||||||
## Webhook
|
## Webhook
|
||||||
|
|
||||||
The webhook section allows you to configure a webhook that is called on certain events in WireGuard Portal.
|
The webhook section allows you to configure a webhook that is called on certain events in WireGuard Portal.
|
||||||
A JSON object is sent in a POST request to the webhook URL with the following structure:
|
Further details can be found in the [usage documentation](../usage/webhooks.md).
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "peer_created",
|
|
||||||
"entity": "peer",
|
|
||||||
"identifier": "the-peer-identifier",
|
|
||||||
"payload": {
|
|
||||||
// The payload of the event, e.g. peer data.
|
|
||||||
// Check the API documentation for the exact structure.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `url`
|
### `url`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
|
@@ -403,6 +403,12 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
models.ProvisioningRequest:
|
models.ProvisioningRequest:
|
||||||
properties:
|
properties:
|
||||||
|
DisplayName:
|
||||||
|
description: |-
|
||||||
|
DisplayName is an optional name for the new peer.
|
||||||
|
If unset, a default template value (e.g., "API Peer ...") will be assigned.
|
||||||
|
example: API Peer xyz
|
||||||
|
type: string
|
||||||
InterfaceIdentifier:
|
InterfaceIdentifier:
|
||||||
description: InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.
|
description: InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.
|
||||||
example: wg0
|
example: wg0
|
||||||
|
57
docs/documentation/usage/backends.md
Normal file
57
docs/documentation/usage/backends.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Backends
|
||||||
|
|
||||||
|
WireGuard Portal can manage WireGuard interfaces and peers on different backends.
|
||||||
|
Each backend represents a system where interfaces actually live.
|
||||||
|
You can register multiple backends and choose which one to use per interface.
|
||||||
|
A global default backend determines where newly created interfaces go (unless you explicitly choose another in the UI).
|
||||||
|
|
||||||
|
**Supported backends:**
|
||||||
|
- **Local** (default): Manages interfaces on the host running WireGuard Portal (Linux WireGuard via wgctrl). Use this when the portal should directly configure wg devices on the same server.
|
||||||
|
- **MikroTik** RouterOS (_beta_): Manages interfaces and peers on MikroTik devices via the RouterOS REST API. Use this to control WG interfaces on RouterOS v7+.
|
||||||
|
|
||||||
|
How backend selection works:
|
||||||
|
- The default backend is configured at `backend.default` (_local_ or the id of a defined MikroTik backend).
|
||||||
|
New interfaces created in the UI will use this backend by default.
|
||||||
|
- Each interface stores its backend. You can select a different backend when creating a new interface.
|
||||||
|
|
||||||
|
## Configuring MikroTik backends (RouterOS v7+)
|
||||||
|
|
||||||
|
> :warning: The MikroTik backend is currently marked beta. While basic functionality is implemented, some advanced features are not yet implemented or contain bugs. Please test carefully before using in production.
|
||||||
|
|
||||||
|
The MikroTik backend uses the [REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API) under a base URL ending with /rest.
|
||||||
|
You can register one or more MikroTik devices as backends for a single WireGuard Portal instance.
|
||||||
|
|
||||||
|
### Prerequisites on MikroTik:
|
||||||
|
- RouterOS v7 with WireGuard support.
|
||||||
|
- REST API enabled and reachable over HTTP(S). A typical base URL is https://<router-address>:8729/rest or https://<router-address>/rest depending on your service setup.
|
||||||
|
- A dedicated RouterOS user with the following group permissions:
|
||||||
|
- **api** (for logging in via REST API)
|
||||||
|
- **rest-api** (for logging in via REST API)
|
||||||
|
- **read** (to read interface and peer data)
|
||||||
|
- **write** (to create/update interfaces and peers)
|
||||||
|
- **test** (to perform ping checks)
|
||||||
|
- **sensitive** (to read private keys)
|
||||||
|
- TLS certificate on the device is recommended. If you use a self-signed certificate during testing, set `api_verify_tls`: _false_ in wg-portal (not recommended for production).
|
||||||
|
|
||||||
|
Example WireGuard Portal configuration (config/config.yaml):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backend:
|
||||||
|
# default backend decides where new interfaces are created
|
||||||
|
default: mikrotik-prod
|
||||||
|
|
||||||
|
mikrotik:
|
||||||
|
- id: mikrotik-prod # 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: true # 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Known limitations:
|
||||||
|
- The MikroTik backend is still in beta. Some features may not work as expected.
|
||||||
|
- Not all WireGuard Portal features are supported yet (e.g., no support for interface hooks)
|
285
docs/documentation/usage/webhooks.md
Normal file
285
docs/documentation/usage/webhooks.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
|
||||||
|
Webhooks allow WireGuard Portal to notify external services about events such as user creation, device changes, or configuration updates. This enables integration with other systems and automation workflows.
|
||||||
|
|
||||||
|
When webhooks are configured and a specified event occurs, WireGuard Portal sends an HTTP **POST** request to the configured webhook URL.
|
||||||
|
The payload contains event-specific data in JSON format.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All available configuration options for webhooks can be found in the [configuration overview](../configuration/overview.md#webhook).
|
||||||
|
|
||||||
|
A basic webhook configuration looks like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webhook:
|
||||||
|
url: https://your-service.example.com/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
Webhooks can be secured by using a shared secret. This secret is included in the `Authorization` header of the webhook request, allowing your service to verify the authenticity of the request.
|
||||||
|
You can set the shared secret in the webhook configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webhook:
|
||||||
|
url: https://your-service.example.com/webhook
|
||||||
|
secret: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
|
||||||
|
```
|
||||||
|
|
||||||
|
You should also make sure that your webhook endpoint is secured with HTTPS to prevent eavesdropping and tampering.
|
||||||
|
|
||||||
|
## Available Events
|
||||||
|
|
||||||
|
WireGuard Portal supports various events that can trigger webhooks. The following events are available:
|
||||||
|
|
||||||
|
- `create`: Triggered when a new entity is created.
|
||||||
|
- `update`: Triggered when an existing entity is updated.
|
||||||
|
- `delete`: Triggered when an entity is deleted.
|
||||||
|
- `connect`: Triggered when a user connects to the VPN.
|
||||||
|
- `disconnect`: Triggered when a user disconnects from the VPN.
|
||||||
|
|
||||||
|
The following entity models are supported for webhook events:
|
||||||
|
|
||||||
|
- `user`: WireGuard Portal users support creation, update, or deletion events.
|
||||||
|
- `peer`: Peers support creation, update, or deletion events. Via the `peer_metric` entity, you can also receive connection status updates.
|
||||||
|
- `peer_metric`: Peer metrics support connection status updates, such as when a peer connects or disconnects.
|
||||||
|
- `interface`: WireGuard interfaces support creation, update, or deletion events.
|
||||||
|
|
||||||
|
## Payload Structure
|
||||||
|
|
||||||
|
All webhook events send a JSON payload containing relevant data. The structure of the payload depends on the event type and entity involved.
|
||||||
|
A common shell structure for webhook payloads is as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "create", // The event type, e.g. "create", "update", "delete", "connect", "disconnect"
|
||||||
|
"entity": "user", // The entity type, e.g. "user", "peer", "peer_metric", "interface"
|
||||||
|
"identifier": "the-user-identifier", // Unique identifier of the entity, e.g. user ID or peer ID
|
||||||
|
"payload": {
|
||||||
|
// The payload of the event, e.g. a Peer model.
|
||||||
|
// Detailed model descriptions are provided below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Payload Models
|
||||||
|
|
||||||
|
All payload models are encoded as JSON objects. Fields with empty values might be omitted in the payload.
|
||||||
|
|
||||||
|
#### User Payload (entity: `user`)
|
||||||
|
|
||||||
|
| JSON Field | Type | Description |
|
||||||
|
|----------------|-------------|-----------------------------------|
|
||||||
|
| CreatedBy | string | Creator identifier |
|
||||||
|
| UpdatedBy | string | Last updater identifier |
|
||||||
|
| CreatedAt | time.Time | Time of creation |
|
||||||
|
| UpdatedAt | time.Time | Time of last update |
|
||||||
|
| Identifier | string | Unique user identifier |
|
||||||
|
| Email | string | User email |
|
||||||
|
| Source | string | Authentication source |
|
||||||
|
| ProviderName | string | Name of auth provider |
|
||||||
|
| IsAdmin | bool | Whether user has admin privileges |
|
||||||
|
| Firstname | string | User's first name (optional) |
|
||||||
|
| Lastname | string | User's last name (optional) |
|
||||||
|
| Phone | string | Contact phone number (optional) |
|
||||||
|
| Department | string | User's department (optional) |
|
||||||
|
| Notes | string | Additional notes (optional) |
|
||||||
|
| Disabled | *time.Time | When user was disabled |
|
||||||
|
| DisabledReason | string | Reason for deactivation |
|
||||||
|
| Locked | *time.Time | When user account was locked |
|
||||||
|
| LockedReason | string | Reason for being locked |
|
||||||
|
|
||||||
|
|
||||||
|
#### Peer Payload (entity: `peer`)
|
||||||
|
|
||||||
|
| JSON Field | Type | Description |
|
||||||
|
|----------------------|------------|----------------------------------------|
|
||||||
|
| CreatedBy | string | Creator identifier |
|
||||||
|
| UpdatedBy | string | Last updater identifier |
|
||||||
|
| CreatedAt | time.Time | Creation timestamp |
|
||||||
|
| UpdatedAt | time.Time | Last update timestamp |
|
||||||
|
| Endpoint | string | Peer endpoint address |
|
||||||
|
| EndpointPublicKey | string | Public key of peer endpoint |
|
||||||
|
| AllowedIPsStr | string | Allowed IPs |
|
||||||
|
| ExtraAllowedIPsStr | string | Extra allowed IPs |
|
||||||
|
| PresharedKey | string | Pre-shared key for encryption |
|
||||||
|
| PersistentKeepalive | int | Keepalive interval in seconds |
|
||||||
|
| DisplayName | string | Display name of the peer |
|
||||||
|
| Identifier | string | Unique identifier |
|
||||||
|
| UserIdentifier | string | Associated user ID (optional) |
|
||||||
|
| InterfaceIdentifier | string | Interface this peer is attached to |
|
||||||
|
| Disabled | *time.Time | When the peer was disabled |
|
||||||
|
| DisabledReason | string | Reason for being disabled |
|
||||||
|
| ExpiresAt | *time.Time | Expiration date |
|
||||||
|
| Notes | string | Notes for this peer |
|
||||||
|
| AutomaticallyCreated | bool | Whether peer was auto-generated |
|
||||||
|
| PrivateKey | string | Peer private key |
|
||||||
|
| PublicKey | string | Peer public key |
|
||||||
|
| InterfaceType | string | Type of the peer interface |
|
||||||
|
| Addresses | []string | IP addresses |
|
||||||
|
| CheckAliveAddress | string | Address used for alive checks |
|
||||||
|
| DnsStr | string | DNS servers |
|
||||||
|
| DnsSearchStr | string | DNS search domains |
|
||||||
|
| Mtu | int | MTU (Maximum Transmission Unit) |
|
||||||
|
| FirewallMark | uint32 | Firewall mark (optional) |
|
||||||
|
| RoutingTable | string | Custom routing table (optional) |
|
||||||
|
| PreUp | string | Command before bringing up interface |
|
||||||
|
| PostUp | string | Command after bringing up interface |
|
||||||
|
| PreDown | string | Command before bringing down interface |
|
||||||
|
| PostDown | string | Command after bringing down interface |
|
||||||
|
|
||||||
|
|
||||||
|
#### Interface Payload (entity: `interface`)
|
||||||
|
|
||||||
|
| JSON Field | Type | Description |
|
||||||
|
|----------------------------|------------|----------------------------------------|
|
||||||
|
| CreatedBy | string | Creator identifier |
|
||||||
|
| UpdatedBy | string | Last updater identifier |
|
||||||
|
| CreatedAt | time.Time | Creation timestamp |
|
||||||
|
| UpdatedAt | time.Time | Last update timestamp |
|
||||||
|
| Identifier | string | Unique identifier |
|
||||||
|
| PrivateKey | string | Private key for the interface |
|
||||||
|
| PublicKey | string | Public key for the interface |
|
||||||
|
| ListenPort | int | Listening port |
|
||||||
|
| Addresses | []string | IP addresses |
|
||||||
|
| DnsStr | string | DNS servers |
|
||||||
|
| DnsSearchStr | string | DNS search domains |
|
||||||
|
| Mtu | int | MTU (Maximum Transmission Unit) |
|
||||||
|
| FirewallMark | uint32 | Firewall mark |
|
||||||
|
| RoutingTable | string | Custom routing table |
|
||||||
|
| PreUp | string | Command before bringing up interface |
|
||||||
|
| PostUp | string | Command after bringing up interface |
|
||||||
|
| PreDown | string | Command before bringing down interface |
|
||||||
|
| PostDown | string | Command after bringing down interface |
|
||||||
|
| SaveConfig | bool | Whether to save config to file |
|
||||||
|
| DisplayName | string | Human-readable name |
|
||||||
|
| Type | string | Type of interface |
|
||||||
|
| DriverType | string | Driver used |
|
||||||
|
| Disabled | *time.Time | When the interface was disabled |
|
||||||
|
| DisabledReason | string | Reason for being disabled |
|
||||||
|
| PeerDefNetworkStr | string | Default peer network configuration |
|
||||||
|
| PeerDefDnsStr | string | Default peer DNS servers |
|
||||||
|
| PeerDefDnsSearchStr | string | Default peer DNS search domains |
|
||||||
|
| PeerDefEndpoint | string | Default peer endpoint |
|
||||||
|
| PeerDefAllowedIPsStr | string | Default peer allowed IPs |
|
||||||
|
| PeerDefMtu | int | Default peer MTU |
|
||||||
|
| PeerDefPersistentKeepalive | int | Default keepalive value |
|
||||||
|
| PeerDefFirewallMark | uint32 | Default firewall mark for peers |
|
||||||
|
| PeerDefRoutingTable | string | Default routing table for peers |
|
||||||
|
| PeerDefPreUp | string | Default peer pre-up command |
|
||||||
|
| PeerDefPostUp | string | Default peer post-up command |
|
||||||
|
| PeerDefPreDown | string | Default peer pre-down command |
|
||||||
|
| PeerDefPostDown | string | Default peer post-down command |
|
||||||
|
|
||||||
|
|
||||||
|
#### Peer Metrics Payload (entity: `peer_metric`)
|
||||||
|
|
||||||
|
| JSON Field | Type | Description |
|
||||||
|
|------------|------------|----------------------------|
|
||||||
|
| Status | PeerStatus | Current status of the peer |
|
||||||
|
| Peer | Peer | Peer data |
|
||||||
|
|
||||||
|
`PeerStatus` sub-structure:
|
||||||
|
|
||||||
|
| JSON Field | Type | Description |
|
||||||
|
|------------------|------------|------------------------------|
|
||||||
|
| UpdatedAt | time.Time | Time of last status update |
|
||||||
|
| IsConnected | bool | Is peer currently connected |
|
||||||
|
| IsPingable | bool | Can peer be pinged |
|
||||||
|
| LastPing | *time.Time | Time of last successful ping |
|
||||||
|
| BytesReceived | uint64 | Bytes received from peer |
|
||||||
|
| BytesTransmitted | uint64 | Bytes sent to peer |
|
||||||
|
| Endpoint | string | Last known endpoint |
|
||||||
|
| LastHandshake | *time.Time | Last successful handshake |
|
||||||
|
| LastSessionStart | *time.Time | Time the last session began |
|
||||||
|
|
||||||
|
|
||||||
|
### Example Payloads
|
||||||
|
|
||||||
|
The following payload is an example of a webhook event when a peer connects to the VPN:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "connect",
|
||||||
|
"entity": "peer_metric",
|
||||||
|
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"payload": {
|
||||||
|
"Status": {
|
||||||
|
"UpdatedAt": "2025-06-27T22:20:08.734900034+02:00",
|
||||||
|
"IsConnected": true,
|
||||||
|
"IsPingable": false,
|
||||||
|
"BytesReceived": 212,
|
||||||
|
"BytesTransmitted": 2884,
|
||||||
|
"Endpoint": "10.55.66.77:58756",
|
||||||
|
"LastHandshake": "2025-06-27T22:19:46.580842776+02:00",
|
||||||
|
"LastSessionStart": "2025-06-27T22:19:46.580842776+02:00"
|
||||||
|
},
|
||||||
|
"Peer": {
|
||||||
|
"CreatedBy": "admin@wgportal.local",
|
||||||
|
"UpdatedBy": "admin@wgportal.local",
|
||||||
|
"CreatedAt": "2025-06-26T21:43:49.251839574+02:00",
|
||||||
|
"UpdatedAt": "2025-06-27T22:18:39.67763985+02:00",
|
||||||
|
"Endpoint": "10.55.66.1:51820",
|
||||||
|
"EndpointPublicKey": "eiVibpi3C2PUPcx2kwA5s09OgHx7AEaKMd33k0LQ5mM=",
|
||||||
|
"AllowedIPsStr": "10.11.12.0/24,fdfd:d3ad:c0de:1234::/64",
|
||||||
|
"ExtraAllowedIPsStr": "",
|
||||||
|
"PresharedKey": "p9DDeLUSLOdQcjS8ZsBAiqUzwDIUvTyzavRZFuzhvyE=",
|
||||||
|
"PersistentKeepalive": 16,
|
||||||
|
"DisplayName": "Peer Fb5TaziA",
|
||||||
|
"Identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"UserIdentifier": "admin@wgportal.local",
|
||||||
|
"InterfaceIdentifier": "wgTesting",
|
||||||
|
"AutomaticallyCreated": false,
|
||||||
|
"PrivateKey": "QBFNBe+7J49ergH0ze2TGUJMFrL/2bOL50Z2cgluYW8=",
|
||||||
|
"PublicKey": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"InterfaceType": "client",
|
||||||
|
"Addresses": [
|
||||||
|
"10.11.12.10/32",
|
||||||
|
"fdfd:d3ad:c0de:1234::a/128"
|
||||||
|
],
|
||||||
|
"CheckAliveAddress": "",
|
||||||
|
"DnsStr": "",
|
||||||
|
"DnsSearchStr": "",
|
||||||
|
"Mtu": 1420
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is another example of a webhook event when a peer is updated:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "update",
|
||||||
|
"entity": "peer",
|
||||||
|
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"payload": {
|
||||||
|
"CreatedBy": "admin@wgportal.local",
|
||||||
|
"UpdatedBy": "admin@wgportal.local",
|
||||||
|
"CreatedAt": "2025-06-26T21:43:49.251839574+02:00",
|
||||||
|
"UpdatedAt": "2025-06-27T22:18:39.67763985+02:00",
|
||||||
|
"Endpoint": "10.55.66.1:51820",
|
||||||
|
"EndpointPublicKey": "eiVibpi3C2PUPcx2kwA5s09OgHx7AEaKMd33k0LQ5mM=",
|
||||||
|
"AllowedIPsStr": "10.11.12.0/24,fdfd:d3ad:c0de:1234::/64",
|
||||||
|
"ExtraAllowedIPsStr": "",
|
||||||
|
"PresharedKey": "p9DDeLUSLOdQcjS8ZsBAiqUzwDIUvTyzavRZFuzhvyE=",
|
||||||
|
"PersistentKeepalive": 16,
|
||||||
|
"DisplayName": "Peer Fb5TaziA",
|
||||||
|
"Identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"UserIdentifier": "admin@wgportal.local",
|
||||||
|
"InterfaceIdentifier": "wgTesting",
|
||||||
|
"AutomaticallyCreated": false,
|
||||||
|
"PrivateKey": "QBFNBe+7J49ergH0ze2TGUJMFrL/2bOL50Z2cgluYW8=",
|
||||||
|
"PublicKey": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"InterfaceType": "client",
|
||||||
|
"Addresses": [
|
||||||
|
"10.11.12.10/32",
|
||||||
|
"fdfd:d3ad:c0de:1234::a/128"
|
||||||
|
],
|
||||||
|
"CheckAliveAddress": "",
|
||||||
|
"DnsStr": "",
|
||||||
|
"DnsSearchStr": "",
|
||||||
|
"Mtu": 1420
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-bs-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link href="/favicon.ico" rel="icon" />
|
<link href="/favicon.ico" rel="icon" />
|
||||||
|
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@@ -14,8 +14,8 @@
|
|||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||||
"bootstrap": "^5.3.5",
|
"bootstrap": "^5.3.7",
|
||||||
"bootswatch": "^5.3.5",
|
"bootswatch": "^5.3.7",
|
||||||
"flag-icons": "^7.3.2",
|
"flag-icons": "^7.3.2",
|
||||||
"ip-address": "^10.0.1",
|
"ip-address": "^10.0.1",
|
||||||
"is-cidr": "^5.1.1",
|
"is-cidr": "^5.1.1",
|
||||||
@@ -1048,9 +1048,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bootstrap": {
|
"node_modules/bootstrap": {
|
||||||
"version": "5.3.5",
|
"version": "5.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
|
||||||
"integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==",
|
"integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1067,9 +1067,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bootswatch": {
|
"node_modules/bootswatch": {
|
||||||
"version": "5.3.5",
|
"version": "5.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.3.7.tgz",
|
||||||
"integrity": "sha512-1z8LNoUL5NHmv/hNROALQ6qtjw9OJIjMgP8ovBlIft+oI15b/mvnzxGL896iO9LtoDZH0Vdm+D2YW+j03GduSg==",
|
"integrity": "sha512-n0X99+Jmpmd4vgkli5KwMOuAkgdyUPhq7cIAwoGXbM6WhE/mmkWACfxpr7WZeG9Pdx509Ndi+2K1HlzXXOr8/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/buffer-builder": {
|
"node_modules/buffer-builder": {
|
||||||
|
@@ -14,8 +14,8 @@
|
|||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||||
"bootstrap": "^5.3.5",
|
"bootstrap": "^5.3.7",
|
||||||
"bootswatch": "^5.3.5",
|
"bootswatch": "^5.3.7",
|
||||||
"flag-icons": "^7.3.2",
|
"flag-icons": "^7.3.2",
|
||||||
"ip-address": "^10.0.1",
|
"ip-address": "^10.0.1",
|
||||||
"is-cidr": "^5.1.1",
|
"is-cidr": "^5.1.1",
|
||||||
|
@@ -14,6 +14,10 @@ const settings = settingsStore()
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log("Starting WireGuard Portal frontend...");
|
console.log("Starting WireGuard Portal frontend...");
|
||||||
|
|
||||||
|
// restore theme from localStorage
|
||||||
|
const theme = localStorage.getItem('wgTheme') || 'light';
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||||
|
|
||||||
await sec.LoadSecurityProperties();
|
await sec.LoadSecurityProperties();
|
||||||
await auth.LoadProviders();
|
await auth.LoadProviders();
|
||||||
|
|
||||||
@@ -40,6 +44,13 @@ const switchLanguage = function (lang) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const switchTheme = function (theme) {
|
||||||
|
if (document.documentElement.getAttribute('data-bs-theme') !== theme) {
|
||||||
|
localStorage.setItem('wgTheme', theme);
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const languageFlag = computed(() => {
|
const languageFlag = computed(() => {
|
||||||
// `this` points to the component instance
|
// `this` points to the component instance
|
||||||
let lang = appGlobal.$i18n.locale.toLowerCase();
|
let lang = appGlobal.$i18n.locale.toLowerCase();
|
||||||
@@ -125,6 +136,24 @@ const userDisplayName = computed(() => {
|
|||||||
<div v-if="!auth.IsAuthenticated" class="nav-item">
|
<div v-if="!auth.IsAuthenticated" class="nav-item">
|
||||||
<RouterLink :to="{ name: 'login' }" class="nav-link"><i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }}</RouterLink>
|
<RouterLink :to="{ name: 'login' }" class="nav-link"><i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }}</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item dropdown" data-bs-theme="light">
|
||||||
|
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="theme-menu" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" aria-label="Toggle theme">
|
||||||
|
<i class="fa-solid fa-circle-half-stroke"></i>
|
||||||
|
<span class="d-lg-none ms-2">Toggle theme</span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center" @click.prevent="switchTheme('light')" aria-pressed="false">
|
||||||
|
<i class="fa-solid fa-sun"></i><span class="ms-2">Light</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center" @click.prevent="switchTheme('dark')" aria-pressed="true">
|
||||||
|
<i class="fa-solid fa-moon"></i><span class="ms-2">Dark</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +170,7 @@ const userDisplayName = computed(() => {
|
|||||||
<div class="col-6 text-end">
|
<div class="col-6 text-end">
|
||||||
<div :aria-label="$t('menu.lang')" class="btn-group" role="group">
|
<div :aria-label="$t('menu.lang')" class="btn-group" role="group">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0"
|
<button aria-expanded="false" aria-haspopup="true" class="btn flag-button pe-0"
|
||||||
data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
|
data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
|
||||||
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
|
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
||||||
@@ -163,4 +192,31 @@ const userDisplayName = computed(() => {
|
|||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style></style>
|
<style>
|
||||||
|
.flag-button:active,.flag-button:hover,.flag-button:focus,.flag-button:checked,.flag-button:disabled,.flag-button:not(:disabled) {
|
||||||
|
border: 1px solid transparent!important;
|
||||||
|
}
|
||||||
|
[data-bs-theme=dark] .form-select {
|
||||||
|
color: #0c0c0c!important;
|
||||||
|
background-color: #c1c1c1!important;
|
||||||
|
--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")!important;
|
||||||
|
}
|
||||||
|
[data-bs-theme=dark] .form-control {
|
||||||
|
color: #0c0c0c!important;
|
||||||
|
background-color: #c1c1c1!important;
|
||||||
|
}
|
||||||
|
[data-bs-theme=dark] .form-control:focus {
|
||||||
|
color: #0c0c0c!important;
|
||||||
|
background-color: #c1c1c1!important;
|
||||||
|
}
|
||||||
|
[data-bs-theme=dark] .badge.bg-light {
|
||||||
|
--bs-bg-opacity: 1;
|
||||||
|
background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
|
||||||
|
color: var(--bs-badge-color)!important;
|
||||||
|
}
|
||||||
|
[data-bs-theme=dark] span.input-group-text {
|
||||||
|
--bs-bg-opacity: 1;
|
||||||
|
background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
|
||||||
|
color: var(--bs-badge-color)!important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -65,6 +65,14 @@ a.disabled {
|
|||||||
color: var(--bs-body-color);
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-bs-theme=dark] .vue-tags-input .ti-tag {
|
||||||
|
position: relative;
|
||||||
|
background: #3c3c3c;
|
||||||
|
border: 2px solid var(--bs-body-color);
|
||||||
|
margin: 6px;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* the styles if a tag is invalid */
|
/* the styles if a tag is invalid */
|
||||||
.vue-tags-input .ti-tag.ti-invalid {
|
.vue-tags-input .ti-tag.ti-invalid {
|
||||||
background-color: #e88a74;
|
background-color: #e88a74;
|
||||||
|
@@ -10,11 +10,13 @@ import isCidr from "is-cidr";
|
|||||||
import {isIP} from 'is-ip';
|
import {isIP} from 'is-ip';
|
||||||
import { freshInterface } from '@/helpers/models';
|
import { freshInterface } from '@/helpers/models';
|
||||||
import {peerStore} from "@/stores/peers";
|
import {peerStore} from "@/stores/peers";
|
||||||
|
import {settingsStore} from "@/stores/settings";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const interfaces = interfaceStore()
|
const interfaces = interfaceStore()
|
||||||
const peers = peerStore()
|
const peers = peerStore()
|
||||||
|
const settings = settingsStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
interfaceId: String,
|
interfaceId: String,
|
||||||
@@ -48,6 +50,26 @@ const currentTags = ref({
|
|||||||
PeerDefDnsSearch: ""
|
PeerDefDnsSearch: ""
|
||||||
})
|
})
|
||||||
const formData = ref(freshInterface())
|
const formData = ref(freshInterface())
|
||||||
|
const isSaving = ref(false)
|
||||||
|
const isDeleting = ref(false)
|
||||||
|
const isApplyingDefaults = ref(false)
|
||||||
|
|
||||||
|
const isBackendValid = computed(() => {
|
||||||
|
if (!props.visible || !selectedInterface.value) {
|
||||||
|
return true // if modal is not visible or no interface is selected, we don't care about backend validity
|
||||||
|
}
|
||||||
|
|
||||||
|
let backendId = selectedInterface.value.Backend
|
||||||
|
|
||||||
|
let valid = false
|
||||||
|
let availableBackends = settings.Setting('AvailableBackends') || []
|
||||||
|
availableBackends.forEach(backend => {
|
||||||
|
if (backend.Id === backendId) {
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return valid
|
||||||
|
})
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
|
|
||||||
@@ -61,6 +83,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
|||||||
formData.value.Identifier = interfaces.Prepared.Identifier
|
formData.value.Identifier = interfaces.Prepared.Identifier
|
||||||
formData.value.DisplayName = interfaces.Prepared.DisplayName
|
formData.value.DisplayName = interfaces.Prepared.DisplayName
|
||||||
formData.value.Mode = interfaces.Prepared.Mode
|
formData.value.Mode = interfaces.Prepared.Mode
|
||||||
|
formData.value.Backend = interfaces.Prepared.Backend
|
||||||
|
|
||||||
formData.value.PublicKey = interfaces.Prepared.PublicKey
|
formData.value.PublicKey = interfaces.Prepared.PublicKey
|
||||||
formData.value.PrivateKey = interfaces.Prepared.PrivateKey
|
formData.value.PrivateKey = interfaces.Prepared.PrivateKey
|
||||||
@@ -99,6 +122,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
|||||||
formData.value.Identifier = selectedInterface.value.Identifier
|
formData.value.Identifier = selectedInterface.value.Identifier
|
||||||
formData.value.DisplayName = selectedInterface.value.DisplayName
|
formData.value.DisplayName = selectedInterface.value.DisplayName
|
||||||
formData.value.Mode = selectedInterface.value.Mode
|
formData.value.Mode = selectedInterface.value.Mode
|
||||||
|
formData.value.Backend = selectedInterface.value.Backend
|
||||||
|
|
||||||
formData.value.PublicKey = selectedInterface.value.PublicKey
|
formData.value.PublicKey = selectedInterface.value.PublicKey
|
||||||
formData.value.PrivateKey = selectedInterface.value.PrivateKey
|
formData.value.PrivateKey = selectedInterface.value.PrivateKey
|
||||||
@@ -237,6 +261,8 @@ function handleChangePeerDefDnsSearch(tags) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (isSaving.value) return
|
||||||
|
isSaving.value = true
|
||||||
try {
|
try {
|
||||||
if (props.interfaceId!=='#NEW#') {
|
if (props.interfaceId!=='#NEW#') {
|
||||||
await interfaces.UpdateInterface(selectedInterface.value.Identifier, formData.value)
|
await interfaces.UpdateInterface(selectedInterface.value.Identifier, formData.value)
|
||||||
@@ -251,6 +277,8 @@ async function save() {
|
|||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +287,8 @@ async function applyPeerDefaults() {
|
|||||||
return; // do nothing for new interfaces
|
return; // do nothing for new interfaces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isApplyingDefaults.value) return
|
||||||
|
isApplyingDefaults.value = true
|
||||||
try {
|
try {
|
||||||
await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value)
|
await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value)
|
||||||
|
|
||||||
@@ -276,10 +306,14 @@ async function applyPeerDefaults() {
|
|||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isApplyingDefaults.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del() {
|
async function del() {
|
||||||
|
if (isDeleting.value) return
|
||||||
|
isDeleting.value = true
|
||||||
try {
|
try {
|
||||||
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
|
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
|
||||||
close()
|
close()
|
||||||
@@ -290,6 +324,8 @@ async function del() {
|
|||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isDeleting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,13 +350,22 @@ async function del() {
|
|||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label>
|
||||||
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text">
|
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="row">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
|
<div class="form-group col-md-6">
|
||||||
<select v-model="formData.Mode" class="form-select">
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
|
||||||
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
|
<select v-model="formData.Mode" class="form-select">
|
||||||
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
|
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
|
||||||
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
|
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
|
||||||
</select>
|
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label class="form-label mt-4" for="ifaceBackendSelector">{{ $t('modals.interface-edit.backend.label') }}</label>
|
||||||
|
<select id="ifaceBackendSelector" v-model="formData.Backend" class="form-select" aria-describedby="backendHelp">
|
||||||
|
<option v-for="backend in settings.Setting('AvailableBackends')" :value="backend.Id">{{ backend.Id === 'local' ? $t(backend.Name) : backend.Name }}</option>
|
||||||
|
</select>
|
||||||
|
<small v-if="!isBackendValid" id="backendHelp" class="form-text text-warning">{{ $t('modals.interface-edit.backend.invalid-label') }}</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label>
|
||||||
@@ -385,12 +430,14 @@ async function del() {
|
|||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label>
|
||||||
<input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number">
|
<input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6" v-if="formData.Backend==='local'">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
|
||||||
<input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
|
<input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group col-md-6" v-else>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row" v-if="formData.Backend==='local'">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
|
||||||
<input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text">
|
<input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text">
|
||||||
@@ -530,16 +577,25 @@ async function del() {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset v-if="props.interfaceId!=='#NEW#'" class="text-end">
|
<fieldset v-if="props.interfaceId!=='#NEW#'" class="text-end">
|
||||||
<hr class="mt-4">
|
<hr class="mt-4">
|
||||||
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults">{{ $t('modals.interface-edit.button-apply-defaults') }}</button>
|
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults" :disabled="isApplyingDefaults">
|
||||||
|
<span v-if="isApplyingDefaults" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('modals.interface-edit.button-apply-defaults') }}
|
||||||
|
</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex-fill text-start">
|
<div class="flex-fill text-start">
|
||||||
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
|
||||||
|
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.delete') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
|
||||||
|
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.save') }}
|
||||||
|
</button>
|
||||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -73,6 +73,8 @@ const currentTags = ref({
|
|||||||
DnsSearch: ""
|
DnsSearch: ""
|
||||||
})
|
})
|
||||||
const formData = ref(freshPeer())
|
const formData = ref(freshPeer())
|
||||||
|
const isSaving = ref(false)
|
||||||
|
const isDeleting = ref(false)
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
|
|
||||||
@@ -270,6 +272,8 @@ function handleChangeDnsSearch(tags) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (isSaving.value) return
|
||||||
|
isSaving.value = true
|
||||||
try {
|
try {
|
||||||
if (props.peerId !== '#NEW#') {
|
if (props.peerId !== '#NEW#') {
|
||||||
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
||||||
@@ -278,26 +282,30 @@ async function save() {
|
|||||||
}
|
}
|
||||||
close()
|
close()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log(e)
|
|
||||||
notify({
|
notify({
|
||||||
title: "Failed to save peer!",
|
title: "Failed to save peer!",
|
||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del() {
|
async function del() {
|
||||||
|
if (isDeleting.value) return
|
||||||
|
isDeleting.value = true
|
||||||
try {
|
try {
|
||||||
await peers.DeletePeer(selectedPeer.value.Identifier)
|
await peers.DeletePeer(selectedPeer.value.Identifier)
|
||||||
close()
|
close()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log(e)
|
|
||||||
notify({
|
notify({
|
||||||
title: "Failed to delete peer!",
|
title: "Failed to delete peer!",
|
||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isDeleting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,10 +478,15 @@ async function del() {
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex-fill text-start">
|
<div class="flex-fill text-start">
|
||||||
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{
|
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
|
||||||
$t('general.delete') }}</button>
|
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.delete') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
|
||||||
|
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.save') }}
|
||||||
|
</button>
|
||||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -32,12 +32,13 @@ const selectedInterface = computed(() => {
|
|||||||
function freshForm() {
|
function freshForm() {
|
||||||
return {
|
return {
|
||||||
Identifiers: [],
|
Identifiers: [],
|
||||||
Suffix: "",
|
Prefix: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTag = ref("")
|
const currentTag = ref("")
|
||||||
const formData = ref(freshForm())
|
const formData = ref(freshForm())
|
||||||
|
const isSaving = ref(false)
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
if (!props.visible) {
|
if (!props.visible) {
|
||||||
@@ -60,12 +61,15 @@ function handleChangeUserIdentifiers(tags) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (isSaving.value) return
|
||||||
|
isSaving.value = true
|
||||||
if (formData.value.Identifiers.length === 0) {
|
if (formData.value.Identifiers.length === 0) {
|
||||||
notify({
|
notify({
|
||||||
title: "Missing Identifiers",
|
title: "Missing Identifiers",
|
||||||
text: "At least one identifier is required to create a new peer.",
|
text: "At least one identifier is required to create a new peer.",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
isSaving.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +83,8 @@ async function save() {
|
|||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +108,16 @@ async function save() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
|
||||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Suffix">
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Prefix">
|
||||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
|
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
|
||||||
|
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.save') }}
|
||||||
|
</button>
|
||||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -50,7 +50,7 @@ const selectedStats = computed(() => {
|
|||||||
|
|
||||||
if (!s) {
|
if (!s) {
|
||||||
if (!!props.peerId || props.peerId.length) {
|
if (!!props.peerId || props.peerId.length) {
|
||||||
p = profile.Statistics(props.peerId)
|
s = profile.Statistics(props.peerId)
|
||||||
} else {
|
} else {
|
||||||
s = freshStats() // dummy stats to avoid 'undefined' exceptions
|
s = freshStats() // dummy stats to avoid 'undefined' exceptions
|
||||||
}
|
}
|
||||||
@@ -79,13 +79,19 @@ const title = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const configStyle = ref("wgquick")
|
||||||
|
|
||||||
watch(() => props.visible, async (newValue, oldValue) => {
|
watch(() => props.visible, async (newValue, oldValue) => {
|
||||||
if (oldValue === false && newValue === true) { // if modal is shown
|
if (oldValue === false && newValue === true) { // if modal is shown
|
||||||
await peers.LoadPeerConfig(selectedPeer.value.Identifier)
|
await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value)
|
||||||
configString.value = peers.configuration
|
configString.value = peers.configuration
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
watch(() => configStyle.value, async () => {
|
||||||
|
await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value)
|
||||||
|
configString.value = peers.configuration
|
||||||
|
})
|
||||||
|
|
||||||
function download() {
|
function download() {
|
||||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||||
@@ -103,7 +109,7 @@ function download() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function email() {
|
function email() {
|
||||||
peers.MailPeerConfig(settings.Setting("MailLinkOnly"), [selectedPeer.value.Identifier]).catch(e => {
|
peers.MailPeerConfig(settings.Setting("MailLinkOnly"), configStyle.value, [selectedPeer.value.Identifier]).catch(e => {
|
||||||
notify({
|
notify({
|
||||||
title: "Failed to send mail with peer configuration!",
|
title: "Failed to send mail with peer configuration!",
|
||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
@@ -114,7 +120,7 @@ function email() {
|
|||||||
|
|
||||||
function ConfigQrUrl() {
|
function ConfigQrUrl() {
|
||||||
if (props.peerId.length) {
|
if (props.peerId.length) {
|
||||||
return apiWrapper.url(`/peer/config-qr/${base64_url_encode(props.peerId)}`)
|
return apiWrapper.url(`/peer/config-qr/${base64_url_encode(props.peerId)}?style=${configStyle.value}`)
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -124,6 +130,15 @@ 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">
|
||||||
|
<span class="me-2">{{ $t('modals.peer-view.style-label') }}: </span>
|
||||||
|
<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">
|
||||||
|
<label class="btn btn-outline-dark btn-sm" for="raw">Raw</label>
|
||||||
|
<input type="radio" class="btn-check" name="configstyle" id="wgquick" value="wgquick" autocomplete="off" checked="" v-model="configStyle">
|
||||||
|
<label class="btn btn-outline-dark btn-sm" for="wgquick">WG-Quick</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="accordion" id="peerInformation">
|
<div class="accordion" id="peerInformation">
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header">
|
<h2 class="accordion-header">
|
||||||
@@ -213,6 +228,14 @@ function ConfigQrUrl() {
|
|||||||
</template>
|
</template>
|
||||||
</Modal></template>
|
</Modal></template>
|
||||||
|
|
||||||
<style>.config-qr-img {
|
<style>
|
||||||
|
.config-qr-img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}</style>
|
}
|
||||||
|
|
||||||
|
.btn-switch-group .btn {
|
||||||
|
border-width: 1px;
|
||||||
|
padding: 5px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -34,6 +34,8 @@ const title = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = ref(freshUser())
|
const formData = ref(freshUser())
|
||||||
|
const isSaving = ref(false)
|
||||||
|
const isDeleting = ref(false)
|
||||||
|
|
||||||
const passwordWeak = computed(() => {
|
const passwordWeak = computed(() => {
|
||||||
return formData.value.Password && formData.value.Password.length > 0 && formData.value.Password.length < settings.Setting('MinPasswordLength')
|
return formData.value.Password && formData.value.Password.length > 0 && formData.value.Password.length < settings.Setting('MinPasswordLength')
|
||||||
@@ -89,6 +91,8 @@ function close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (isSaving.value) return
|
||||||
|
isSaving.value = true
|
||||||
try {
|
try {
|
||||||
if (props.userId!=='#NEW#') {
|
if (props.userId!=='#NEW#') {
|
||||||
await users.UpdateUser(selectedUser.value.Identifier, formData.value)
|
await users.UpdateUser(selectedUser.value.Identifier, formData.value)
|
||||||
@@ -102,10 +106,14 @@ async function save() {
|
|||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del() {
|
async function del() {
|
||||||
|
if (isDeleting.value) return
|
||||||
|
isDeleting.value = true
|
||||||
try {
|
try {
|
||||||
await users.DeleteUser(selectedUser.value.Identifier)
|
await users.DeleteUser(selectedUser.value.Identifier)
|
||||||
close()
|
close()
|
||||||
@@ -115,6 +123,8 @@ async function del() {
|
|||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isDeleting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,9 +203,15 @@ async function del() {
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex-fill text-start">
|
<div class="flex-fill text-start">
|
||||||
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
|
||||||
|
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.delete') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="!formValid">{{ $t('general.save') }}</button>
|
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="!formValid || isSaving">
|
||||||
|
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.save') }}
|
||||||
|
</button>
|
||||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -55,6 +55,8 @@ const title = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = ref(freshPeer())
|
const formData = ref(freshPeer())
|
||||||
|
const isSaving = ref(false)
|
||||||
|
const isDeleting = ref(false)
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
|
|
||||||
@@ -163,6 +165,8 @@ function close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (isSaving.value) return
|
||||||
|
isSaving.value = true
|
||||||
try {
|
try {
|
||||||
if (props.peerId !== '#NEW#') {
|
if (props.peerId !== '#NEW#') {
|
||||||
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
||||||
@@ -171,26 +175,30 @@ async function save() {
|
|||||||
}
|
}
|
||||||
close()
|
close()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log(e)
|
|
||||||
notify({
|
notify({
|
||||||
title: "Failed to save peer!",
|
title: "Failed to save peer!",
|
||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del() {
|
async function del() {
|
||||||
|
if (isDeleting.value) return
|
||||||
|
isDeleting.value = true
|
||||||
try {
|
try {
|
||||||
await peers.DeletePeer(selectedPeer.value.Identifier)
|
await peers.DeletePeer(selectedPeer.value.Identifier)
|
||||||
close()
|
close()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log(e)
|
|
||||||
notify({
|
notify({
|
||||||
title: "Failed to delete peer!",
|
title: "Failed to delete peer!",
|
||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
isDeleting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,10 +291,15 @@ async function del() {
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex-fill text-start">
|
<div class="flex-fill text-start">
|
||||||
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{
|
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
|
||||||
$t('general.delete') }}</button>
|
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.delete') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
|
||||||
|
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||||
|
{{ $t('general.save') }}
|
||||||
|
</button>
|
||||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -5,6 +5,7 @@ export function freshInterface() {
|
|||||||
DisplayName: "",
|
DisplayName: "",
|
||||||
Identifier: "",
|
Identifier: "",
|
||||||
Mode: "server",
|
Mode: "server",
|
||||||
|
Backend: "local",
|
||||||
|
|
||||||
PublicKey: "",
|
PublicKey: "",
|
||||||
PrivateKey: "",
|
PrivateKey: "",
|
||||||
|
@@ -102,7 +102,9 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "Schnittstellenstatus für",
|
"headline": "Schnittstellenstatus für",
|
||||||
"mode": "Modus",
|
"backend": "Backend",
|
||||||
|
"unknown-backend": "Unbekannt",
|
||||||
|
"wrong-backend": "Ungültiges Backend, das lokale WireGuard Backend wird stattdessen verwendet!",
|
||||||
"key": "Öffentlicher Schlüssel",
|
"key": "Öffentlicher Schlüssel",
|
||||||
"endpoint": "Öffentlicher Endpunkt",
|
"endpoint": "Öffentlicher Endpunkt",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -357,6 +359,11 @@
|
|||||||
"client": "Client-Modus",
|
"client": "Client-Modus",
|
||||||
"any": "Unbekannter Modus"
|
"any": "Unbekannter Modus"
|
||||||
},
|
},
|
||||||
|
"backend": {
|
||||||
|
"label": "Schnittstellenbackend",
|
||||||
|
"invalid-label": "Ursprüngliches Backend ist ungültig, das lokale WireGuard Backend wird stattdessen verwendet!",
|
||||||
|
"local": "Lokales WireGuard Backend"
|
||||||
|
},
|
||||||
"display-name": {
|
"display-name": {
|
||||||
"label": "Anzeigename",
|
"label": "Anzeigename",
|
||||||
"placeholder": "Der beschreibende Name für die Schnittstelle"
|
"placeholder": "Der beschreibende Name für die Schnittstelle"
|
||||||
@@ -467,7 +474,8 @@
|
|||||||
"connected-since": "Verbunden seit",
|
"connected-since": "Verbunden seit",
|
||||||
"endpoint": "Endpunkt",
|
"endpoint": "Endpunkt",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"peer-edit": {
|
"peer-edit": {
|
||||||
"headline-edit-peer": "Peer bearbeiten:",
|
"headline-edit-peer": "Peer bearbeiten:",
|
||||||
|
@@ -102,7 +102,9 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "Interface status for",
|
"headline": "Interface status for",
|
||||||
"mode": "mode",
|
"backend": "Backend",
|
||||||
|
"unknown-backend": "Unknown",
|
||||||
|
"wrong-backend": "Invalid backend, using local WireGuard backend instead!",
|
||||||
"key": "Public Key",
|
"key": "Public Key",
|
||||||
"endpoint": "Public Endpoint",
|
"endpoint": "Public Endpoint",
|
||||||
"port": "Listening Port",
|
"port": "Listening Port",
|
||||||
@@ -357,6 +359,11 @@
|
|||||||
"client": "Client Mode",
|
"client": "Client Mode",
|
||||||
"any": "Unknown Mode"
|
"any": "Unknown Mode"
|
||||||
},
|
},
|
||||||
|
"backend": {
|
||||||
|
"label": "Interface Backend",
|
||||||
|
"invalid-label": "Original backend is no longer available, using local WireGuard backend instead!",
|
||||||
|
"local": "Local WireGuard Backend"
|
||||||
|
},
|
||||||
"display-name": {
|
"display-name": {
|
||||||
"label": "Display Name",
|
"label": "Display Name",
|
||||||
"placeholder": "The descriptive name for the interface"
|
"placeholder": "The descriptive name for the interface"
|
||||||
@@ -468,7 +475,8 @@
|
|||||||
"connected-since": "Connected since",
|
"connected-since": "Connected since",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"peer-edit": {
|
"peer-edit": {
|
||||||
"headline-edit-peer": "Edit peer:",
|
"headline-edit-peer": "Edit peer:",
|
||||||
|
@@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "État de l'interface pour",
|
"headline": "État de l'interface pour",
|
||||||
"mode": "mode",
|
"backend": "backend",
|
||||||
"key": "Clé publique",
|
"key": "Clé publique",
|
||||||
"endpoint": "Point de terminaison public",
|
"endpoint": "Point de terminaison public",
|
||||||
"port": "Port d'écoute",
|
"port": "Port d'écoute",
|
||||||
|
@@ -100,7 +100,7 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "인터페이스 상태:",
|
"headline": "인터페이스 상태:",
|
||||||
"mode": "모드",
|
"backend": "백엔드",
|
||||||
"key": "공개 키",
|
"key": "공개 키",
|
||||||
"endpoint": "공개 엔드포인트",
|
"endpoint": "공개 엔드포인트",
|
||||||
"port": "수신 포트",
|
"port": "수신 포트",
|
||||||
|
@@ -101,7 +101,7 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "Status da interface para",
|
"headline": "Status da interface para",
|
||||||
"mode": "modo",
|
"mode": "backend",
|
||||||
"key": "Chave Pública",
|
"key": "Chave Pública",
|
||||||
"endpoint": "Endpoint Público",
|
"endpoint": "Endpoint Público",
|
||||||
"port": "Porta de Escuta",
|
"port": "Porta de Escuta",
|
||||||
|
@@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "Статус интерфейса для",
|
"headline": "Статус интерфейса для",
|
||||||
"mode": "режим",
|
"backend": "бэкэнд",
|
||||||
"key": "Публичный ключ",
|
"key": "Публичный ключ",
|
||||||
"endpoint": "Публичная конечная точка",
|
"endpoint": "Публичная конечная точка",
|
||||||
"port": "Порт прослушивания",
|
"port": "Порт прослушивания",
|
||||||
|
@@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "Статус інтерфейсу для",
|
"headline": "Статус інтерфейсу для",
|
||||||
"mode": "режим",
|
"backend": "бекенд",
|
||||||
"key": "Публічний ключ",
|
"key": "Публічний ключ",
|
||||||
"endpoint": "Публічна кінцева точка",
|
"endpoint": "Публічна кінцева точка",
|
||||||
"port": "Порт прослуховування",
|
"port": "Порт прослуховування",
|
||||||
|
@@ -98,7 +98,7 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "Trạng thái giao diện cho",
|
"headline": "Trạng thái giao diện cho",
|
||||||
"mode": "chế độ",
|
"backend": "phần sau",
|
||||||
"key": "Khóa Công khai",
|
"key": "Khóa Công khai",
|
||||||
"endpoint": "Điểm cuối Công khai",
|
"endpoint": "Điểm cuối Công khai",
|
||||||
"port": "Cổng Nghe",
|
"port": "Cổng Nghe",
|
||||||
|
@@ -98,7 +98,7 @@
|
|||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "接口状态",
|
"headline": "接口状态",
|
||||||
"mode": "模式",
|
"backend": "后端",
|
||||||
"key": "公钥",
|
"key": "公钥",
|
||||||
"endpoint": "公开节点",
|
"endpoint": "公开节点",
|
||||||
"port": "监听端口",
|
"port": "监听端口",
|
||||||
|
@@ -142,8 +142,8 @@ export const peerStore = defineStore('peers', {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async MailPeerConfig(linkOnly, ids) {
|
async MailPeerConfig(linkOnly, style, ids) {
|
||||||
return apiWrapper.post(`${baseUrl}/config-mail`, {
|
return apiWrapper.post(`${baseUrl}/config-mail?style=${style}`, {
|
||||||
Identifiers: ids,
|
Identifiers: ids,
|
||||||
LinkOnly: linkOnly
|
LinkOnly: linkOnly
|
||||||
})
|
})
|
||||||
@@ -158,8 +158,8 @@ export const peerStore = defineStore('peers', {
|
|||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async LoadPeerConfig(id) {
|
async LoadPeerConfig(id, style) {
|
||||||
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
|
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}?style=${style}`)
|
||||||
.then(this.setPeerConfig)
|
.then(this.setPeerConfig)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.configuration = ""
|
this.configuration = ""
|
||||||
|
@@ -26,7 +26,7 @@ onMounted(async () => {
|
|||||||
<div class="form-group d-inline">
|
<div class="form-group d-inline">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input v-model="audit.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="audit.afterPageSizeChange">
|
<input v-model="audit.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="audit.afterPageSizeChange">
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
<button class="btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -13,21 +13,21 @@ const auth = authStore()
|
|||||||
<p class="lead">{{ $t('home.abstract') }}</p>
|
<p class="lead">{{ $t('home.abstract') }}</p>
|
||||||
|
|
||||||
|
|
||||||
<div class="bg-light p-5" v-if="auth.IsAuthenticated">
|
<div class="card border-secondary p-5" v-if="auth.IsAuthenticated">
|
||||||
<h2 class="display-5">{{ $t('home.profiles.headline') }}</h2>
|
<h2 class="display-5">{{ $t('home.profiles.headline') }}</h2>
|
||||||
<p class="lead">{{ $t('home.profiles.abstract') }}</p>
|
<p class="lead">{{ $t('home.profiles.abstract') }}</p>
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
<p>{{ $t('home.profiles.content') }}</p>
|
<p class="card-text">{{ $t('home.profiles.content') }}</p>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
<RouterLink :to="{ name: 'profile' }" class="btn btn-primary btn-lg">{{ $t('home.profiles.button') }}</RouterLink>
|
<RouterLink :to="{ name: 'profile' }" class="btn btn-primary btn-lg">{{ $t('home.profiles.button') }}</RouterLink>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-light p-5 mt-4" v-if="auth.IsAuthenticated && auth.IsAdmin">
|
<div class="card border-secondary p-5 mt-4" v-if="auth.IsAuthenticated && auth.IsAdmin">
|
||||||
<h2 class="display-5">{{ $t('home.admin.headline') }}</h2>
|
<h2 class="display-5">{{ $t('home.admin.headline') }}</h2>
|
||||||
<p class="lead">{{ $t('home.admin.abstract') }}</p>
|
<p class="lead">{{ $t('home.admin.abstract') }}</p>
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
<p>{{ $t('home.admin.content') }}</p>
|
<p class="card-text">{{ $t('home.admin.content') }}</p>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
<RouterLink :to="{ name: 'interfaces' }" class="btn btn-primary btn-lg me-2">{{ $t('home.admin.button-admin') }}
|
<RouterLink :to="{ name: 'interfaces' }" class="btn btn-primary btn-lg me-2">{{ $t('home.admin.button-admin') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
@@ -5,17 +5,20 @@ import PeerMultiCreateModal from "../components/PeerMultiCreateModal.vue";
|
|||||||
import InterfaceEditModal from "../components/InterfaceEditModal.vue";
|
import InterfaceEditModal from "../components/InterfaceEditModal.vue";
|
||||||
import InterfaceViewModal from "../components/InterfaceViewModal.vue";
|
import InterfaceViewModal from "../components/InterfaceViewModal.vue";
|
||||||
|
|
||||||
import {onMounted, ref} from "vue";
|
import {computed, onMounted, ref} from "vue";
|
||||||
import {peerStore} from "@/stores/peers";
|
import {peerStore} from "@/stores/peers";
|
||||||
import {interfaceStore} from "@/stores/interfaces";
|
import {interfaceStore} from "@/stores/interfaces";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
import {settingsStore} from "@/stores/settings";
|
import {settingsStore} from "@/stores/settings";
|
||||||
import {humanFileSize} from '@/helpers/utils';
|
import {humanFileSize} from '@/helpers/utils';
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
const settings = settingsStore()
|
const settings = settingsStore()
|
||||||
const interfaces = interfaceStore()
|
const interfaces = interfaceStore()
|
||||||
const peers = peerStore()
|
const peers = peerStore()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const viewedPeerId = ref("")
|
const viewedPeerId = ref("")
|
||||||
const editPeerId = ref("")
|
const editPeerId = ref("")
|
||||||
const multiCreatePeerId = ref("")
|
const multiCreatePeerId = ref("")
|
||||||
@@ -45,6 +48,33 @@ function calculateInterfaceName(id, name) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calculateBackendName = computed(() => {
|
||||||
|
let backendId = interfaces.GetSelected.Backend
|
||||||
|
|
||||||
|
let backendName = t('interfaces.interface.unknown-backend')
|
||||||
|
let availableBackends = settings.Setting('AvailableBackends') || []
|
||||||
|
availableBackends.forEach(backend => {
|
||||||
|
if (backend.Id === backendId) {
|
||||||
|
backendName = backend.Id === 'local' ? t(backend.Name) : backend.Name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return backendName
|
||||||
|
})
|
||||||
|
|
||||||
|
const isBackendValid = computed(() => {
|
||||||
|
let backendId = interfaces.GetSelected.Backend
|
||||||
|
|
||||||
|
let valid = false
|
||||||
|
let availableBackends = settings.Setting('AvailableBackends') || []
|
||||||
|
availableBackends.forEach(backend => {
|
||||||
|
if (backend.Id === backendId) {
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return valid
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
async function download() {
|
async function download() {
|
||||||
await interfaces.LoadInterfaceConfig(interfaces.GetSelected.Identifier)
|
await interfaces.LoadInterfaceConfig(interfaces.GetSelected.Identifier)
|
||||||
|
|
||||||
@@ -112,7 +142,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-interface')" @click.prevent="editInterfaceId='#NEW#'">
|
<button class="btn btn-primary" :title="$t('interfaces.button-add-interface')" @click.prevent="editInterfaceId='#NEW#'">
|
||||||
<i class="fa-solid fa-plus-circle"></i>
|
<i class="fa-solid fa-plus-circle"></i>
|
||||||
</button>
|
</button>
|
||||||
<select v-model="interfaces.selected" :disabled="interfaces.Count===0" class="form-select" @change="() => { peers.LoadPeers(); peers.LoadStats() }">
|
<select v-model="interfaces.selected" :disabled="interfaces.Count===0" class="form-select" @change="() => { peers.LoadPeers(); peers.LoadStats() }">
|
||||||
@@ -141,7 +171,7 @@ onMounted(async () => {
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-8">
|
<div class="col-12 col-lg-8">
|
||||||
{{ $t('interfaces.interface.headline') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{interfaces.GetSelected.Mode}} {{ $t('interfaces.interface.mode') }})
|
{{ $t('interfaces.interface.headline') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{ $t('modals.interface-edit.mode.' + interfaces.GetSelected.Mode )}} | {{ $t('interfaces.interface.backend') + ": " + calculateBackendName }}<span v-if="!isBackendValid" :title="t('interfaces.interface.wrong-backend')" class="ms-1 me-1"><i class="fa-solid fa-triangle-exclamation"></i></span>)
|
||||||
<span v-if="interfaces.GetSelected.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="interfaces.GetSelected.DisabledReason"></i></span>
|
<span v-if="interfaces.GetSelected.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="interfaces.GetSelected.DisabledReason"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4 text-lg-end">
|
<div class="col-12 col-lg-4 text-lg-end">
|
||||||
@@ -314,7 +344,7 @@ onMounted(async () => {
|
|||||||
<div class="form-group d-inline">
|
<div class="form-group d-inline">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input v-model="peers.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="peers.afterPageSizeChange">
|
<input v-model="peers.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="peers.afterPageSizeChange">
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
<button class="btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -429,3 +459,5 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
@@ -16,7 +16,10 @@ const password = ref("")
|
|||||||
const usernameInvalid = computed(() => username.value === "")
|
const usernameInvalid = computed(() => username.value === "")
|
||||||
const passwordInvalid = computed(() => password.value === "")
|
const passwordInvalid = computed(() => password.value === "")
|
||||||
const disableLoginBtn = computed(() => username.value === "" || password.value === "" || loggingIn.value)
|
const disableLoginBtn = computed(() => username.value === "" || password.value === "" || loggingIn.value)
|
||||||
|
const showLoginForm = computed(() => {
|
||||||
|
console.log(router.currentRoute.value.query)
|
||||||
|
return settings.Setting('LoginFormVisible') || router.currentRoute.value.query.hasOwnProperty('all');
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await settings.LoadSettings()
|
await settings.LoadSettings()
|
||||||
@@ -98,7 +101,7 @@ const externalLogin = function (provider) {
|
|||||||
</div></div>
|
</div></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<fieldset>
|
<fieldset v-if="showLoginForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="inputUsername">{{ $t('login.username.label') }}</label>
|
<label class="form-label" for="inputUsername">{{ $t('login.username.label') }}</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
@@ -118,19 +121,40 @@ const externalLogin = function (provider) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-5 mb-2">
|
<div class="row mt-5 mb-2">
|
||||||
<div class="col-lg-4">
|
<div class="col-sm-4 col-xs-12">
|
||||||
<button :disabled="disableLoginBtn" class="btn btn-primary" type="submit" @click.prevent="login">
|
<button :disabled="disableLoginBtn" class="btn btn-primary mb-2" type="submit" @click.prevent="login">
|
||||||
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-8 mb-2 text-end">
|
<div class="col-sm-8 col-xs-12 text-sm-end">
|
||||||
<button v-if="settings.Setting('WebAuthnEnabled')" class="btn btn-primary" type="submit" @click.prevent="loginWebAuthn">
|
<button v-if="settings.Setting('WebAuthnEnabled')" class="btn btn-primary" type="submit" @click.prevent="loginWebAuthn">
|
||||||
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-5 d-flex">
|
<div class="row mt-4 d-flex">
|
||||||
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
|
<!-- OpenIdConnect / OAUTH providers -->
|
||||||
|
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||||
|
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
||||||
|
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset v-else>
|
||||||
|
<div class="row mt-1 mb-2" v-if="settings.Setting('WebAuthnEnabled')">
|
||||||
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
|
<button class="btn btn-outline-primary flex-fill" type="submit" @click.prevent="loginWebAuthn">
|
||||||
|
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-1 d-flex">
|
||||||
<div class="col-lg-12 d-flex mb-2">
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
<!-- OpenIdConnect / OAUTH providers -->
|
<!-- OpenIdConnect / OAUTH providers -->
|
||||||
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||||
@@ -144,7 +168,6 @@ const externalLogin = function (provider) {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -65,7 +65,7 @@ onMounted(async () => {
|
|||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input v-model="profile.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text"
|
<input v-model="profile.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text"
|
||||||
@keyup="profile.afterPageSizeChange">
|
@keyup="profile.afterPageSizeChange">
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i
|
<button class="btn btn-primary" :title="$t('general.search.button')"><i
|
||||||
class="fa-solid fa-search"></i></button>
|
class="fa-solid fa-search"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +73,7 @@ onMounted(async () => {
|
|||||||
<div class="col-12 col-lg-3 text-lg-end">
|
<div class="col-12 col-lg-3 text-lg-end">
|
||||||
<div class="form-group" v-if="settings.Setting('SelfProvisioning')">
|
<div class="form-group" v-if="settings.Setting('SelfProvisioning')">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'">
|
<button class="btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'">
|
||||||
<i class="fa fa-plus me-1"></i><i class="fa fa-user"></i>
|
<i class="fa fa-plus me-1"></i><i class="fa fa-user"></i>
|
||||||
</button>
|
</button>
|
||||||
<select v-model="profile.selectedInterfaceId" :disabled="profile.CountInterfaces===0" class="form-select">
|
<select v-model="profile.selectedInterfaceId" :disabled="profile.CountInterfaces===0" class="form-select">
|
||||||
|
@@ -44,7 +44,7 @@ async function saveRename(credential) {
|
|||||||
<p class="lead">{{ $t('settings.abstract') }}</p>
|
<p class="lead">{{ $t('settings.abstract') }}</p>
|
||||||
|
|
||||||
<div v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
|
<div v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
|
||||||
<div class="bg-light p-5" v-if="profile.user.ApiToken">
|
<div class="card border-secondary p-5" v-if="profile.user.ApiToken">
|
||||||
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||||
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
@@ -72,7 +72,7 @@ async function saveRename(credential) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
|
<button class="btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
|
||||||
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
|
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,18 +81,18 @@ async function saveRename(credential) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-light p-5" v-else>
|
<div class="card border-secondary p-5" v-else>
|
||||||
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||||
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
<p>{{ $t('settings.api.inactive-description') }}</p>
|
<p>{{ $t('settings.api.inactive-description') }}</p>
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
|
<button class="btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
|
||||||
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
|
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-light p-5 mt-5" v-if="settings.Setting('WebAuthnEnabled')">
|
<div class="card border-secondary p-5 mt-5" v-if="settings.Setting('WebAuthnEnabled')">
|
||||||
<h2 class="display-7">{{ $t('settings.webauthn.headline') }}</h2>
|
<h2 class="display-7">{{ $t('settings.webauthn.headline') }}</h2>
|
||||||
<p class="lead">{{ $t('settings.webauthn.abstract') }}</p>
|
<p class="lead">{{ $t('settings.webauthn.abstract') }}</p>
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
@@ -101,7 +101,7 @@ async function saveRename(credential) {
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('settings.webauthn.button-register-text')" @click.prevent="auth.RegisterWebAuthn" :disabled="auth.isFetching">
|
<button class="btn btn-primary" :title="$t('settings.webauthn.button-register-text')" @click.prevent="auth.RegisterWebAuthn" :disabled="auth.isFetching">
|
||||||
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.webauthn.button-register-title') }}
|
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.webauthn.button-register-title') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -35,7 +35,7 @@ onMounted(() => {
|
|||||||
<div class="form-group d-inline">
|
<div class="form-group d-inline">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input v-model="users.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="users.afterPageSizeChange">
|
<input v-model="users.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="users.afterPageSizeChange">
|
||||||
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
<button class="btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
59
go.mod
59
go.mod
@@ -4,32 +4,32 @@ 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.8.0
|
github.com/alexedwards/scs/v2 v2.9.0
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1
|
github.com/coreos/go-oidc/v3 v3.15.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.11
|
||||||
github.com/go-pkgz/routegroup v1.4.1
|
github.com/go-pkgz/routegroup v1.5.3
|
||||||
github.com/go-playground/validator/v10 v10.26.0
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/go-webauthn/webauthn v0.13.0
|
github.com/go-webauthn/webauthn v0.13.4
|
||||||
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.22.0
|
github.com/prometheus/client_golang v1.23.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/vardius/message-bus v1.1.5
|
github.com/vardius/message-bus v1.1.5
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
||||||
golang.org/x/crypto v0.39.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/oauth2 v0.30.0
|
golang.org/x/oauth2 v0.30.0
|
||||||
golang.org/x/sys v0.33.0
|
golang.org/x/sys v0.35.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
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.0
|
gorm.io/driver/sqlserver v1.6.1
|
||||||
gorm.io/gorm v1.30.0
|
gorm.io/gorm v1.30.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -40,28 +40,28 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // 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.0 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
github.com/go-openapi/jsonpointer v0.21.2 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.1 // indirect
|
github.com/go-openapi/swag v0.23.1 // 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.2 // 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.21 // indirect
|
github.com/go-webauthn/x v0.1.23 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.3 // 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.5 // 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.4 // indirect
|
github.com/jackc/pgx/v5 v5.7.5 // 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
|
||||||
@@ -73,29 +73,30 @@ require (
|
|||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/microsoft/go-mssqldb v1.8.0 // indirect
|
github.com/microsoft/go-mssqldb v1.9.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.63.0 // indirect
|
github.com/prometheus/common v0.65.0 // indirect
|
||||||
github.com/prometheus/procfs v0.16.0 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
||||||
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
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect
|
||||||
golang.org/x/net v0.40.0 // indirect
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/net v0.43.0 // indirect
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/tools v0.33.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
modernc.org/libc v1.63.0 // indirect
|
modernc.org/libc v1.66.6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.10.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.37.0 // indirect
|
modernc.org/sqlite v1.38.2 // indirect
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
246
go.sum
246
go.sum
@@ -1,38 +1,46 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
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-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
|
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
|
||||||
github.com/alexedwards/scs/v2 v2.8.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.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||||
|
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=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -40,8 +48,8 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/
|
|||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
@@ -54,38 +62,37 @@ github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0
|
|||||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
|
||||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||||
github.com/go-pkgz/routegroup v1.4.1 h1:iw1yW3lXuurZZOv/DF9fY8Mkpvy6J9UjBiP1oDIQE/s=
|
github.com/go-pkgz/routegroup v1.5.3 h1:IvH1KLcQkMap9jucQGBlef3IBloxSAe8USUFvxShFqs=
|
||||||
github.com/go-pkgz/routegroup v1.4.1/go.mod h1:kDDPDRLRiRY1vnENrZJw1jQAzQX7fvsbsHGRQFNQfKc=
|
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=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.2/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.0 h1:cJIL1/1l+22UekVhipziAaSgESJxokYkowUqAIsWs0Y=
|
github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ=
|
||||||
github.com/go-webauthn/webauthn v0.13.0/go.mod h1:Oy9o2o79dbLKRPZWWgRIOdtBGAhKnDIaBp2PFkICRHs=
|
github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI=
|
||||||
github.com/go-webauthn/x v0.1.21 h1:nFbckQxudvHEJn2uy1VEi713MeSpApoAv9eRqsb9AdQ=
|
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
|
||||||
github.com/go-webauthn/x v0.1.21/go.mod h1:sEYohtg1zL4An1TXIUIQ5csdmoO+WO0R4R2pGKaHYKA=
|
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
|
||||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
|
||||||
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=
|
||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
@@ -98,7 +105,6 @@ github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
|||||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm v0.9.5/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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
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=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -111,20 +117,18 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
|
||||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
@@ -139,8 +143,11 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL
|
|||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
@@ -157,48 +164,56 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
|||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||||
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
|
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
|
||||||
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
|
github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs=
|
||||||
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
|
github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
|
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
|
||||||
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
|
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
|
||||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA=
|
||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
@@ -219,41 +234,51 @@ github.com/yeqown/go-qrcode/writer/compressed v1.0.1/go.mod h1:BJScsGUIKM+eg0CCL
|
|||||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
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.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90=
|
||||||
|
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
|
||||||
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
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=
|
||||||
@@ -263,61 +288,71 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||||
|
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
|
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.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.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
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-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
@@ -325,43 +360,44 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/
|
|||||||
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=
|
||||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY=
|
||||||
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
|
||||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
|
||||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs=
|
||||||
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||||
modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY=
|
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
|
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||||
|
modernc.org/fileutil v1.3.8/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/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.66.6 h1:RyQpwAhM/19nXD8y3iejM/AjmKwY2TjxZTlUWTsWw2U=
|
||||||
|
modernc.org/libc v1.66.6/go.mod h1:j8z0EYAuumoMQ3+cWXtmw6m+LYn3qm8dcZDFtFTSq+M=
|
||||||
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.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
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.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
modernc.org/sqlite v1.38.2/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=
|
||||||
|
@@ -133,5 +133,5 @@ func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerS
|
|||||||
}
|
}
|
||||||
m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
|
m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
|
||||||
m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
|
m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
|
||||||
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected()))
|
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected))
|
||||||
}
|
}
|
||||||
|
864
internal/adapters/wgcontroller/local.go
Normal file
864
internal/adapters/wgcontroller/local.go
Normal file
@@ -0,0 +1,864 @@
|
|||||||
|
package wgcontroller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
probing "github.com/prometheus-community/pro-bing"
|
||||||
|
"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"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||||
|
)
|
||||||
|
|
||||||
|
// region dependencies
|
||||||
|
|
||||||
|
// WgCtrlRepo is used to control local WireGuard devices via the wgctrl-go library.
|
||||||
|
type WgCtrlRepo interface {
|
||||||
|
io.Closer
|
||||||
|
Devices() ([]*wgtypes.Device, error)
|
||||||
|
Device(name string) (*wgtypes.Device, error)
|
||||||
|
ConfigureDevice(name string, cfg wgtypes.Config) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NetlinkClient is a type which can control a netlink device.
|
||||||
|
type NetlinkClient interface {
|
||||||
|
LinkAdd(link netlink.Link) error
|
||||||
|
LinkDel(link netlink.Link) error
|
||||||
|
LinkByName(name string) (netlink.Link, error)
|
||||||
|
LinkSetUp(link netlink.Link) error
|
||||||
|
LinkSetDown(link netlink.Link) error
|
||||||
|
LinkSetMTU(link netlink.Link, mtu int) error
|
||||||
|
AddrReplace(link netlink.Link, addr *netlink.Addr) error
|
||||||
|
AddrAdd(link netlink.Link, addr *netlink.Addr) error
|
||||||
|
AddrList(link netlink.Link) ([]netlink.Addr, error)
|
||||||
|
AddrDel(link netlink.Link, addr *netlink.Addr) error
|
||||||
|
RouteAdd(route *netlink.Route) error
|
||||||
|
RouteDel(route *netlink.Route) error
|
||||||
|
RouteReplace(route *netlink.Route) error
|
||||||
|
RouteList(link netlink.Link, family int) ([]netlink.Route, error)
|
||||||
|
RouteListFiltered(family int, filter *netlink.Route, filterMask uint64) ([]netlink.Route, error)
|
||||||
|
RuleAdd(rule *netlink.Rule) error
|
||||||
|
RuleDel(rule *netlink.Rule) error
|
||||||
|
RuleList(family int) ([]netlink.Rule, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion dependencies
|
||||||
|
|
||||||
|
type LocalController struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
wg WgCtrlRepo
|
||||||
|
nl NetlinkClient
|
||||||
|
|
||||||
|
shellCmd string
|
||||||
|
resolvConfIfacePrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocalController creates a new local controller instance.
|
||||||
|
// This repository is used to interact with the WireGuard kernel or userspace module.
|
||||||
|
func NewLocalController(cfg *config.Config) (*LocalController, error) {
|
||||||
|
wg, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create wgctrl client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nl := &lowlevel.NetlinkManager{}
|
||||||
|
|
||||||
|
repo := &LocalController{
|
||||||
|
cfg: cfg,
|
||||||
|
|
||||||
|
wg: wg,
|
||||||
|
nl: nl,
|
||||||
|
|
||||||
|
shellCmd: "bash", // we only support bash at the moment
|
||||||
|
resolvConfIfacePrefix: "tun.", // WireGuard interfaces have a tun. prefix in resolvconf
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) GetId() domain.InterfaceBackend {
|
||||||
|
return config.LocalBackendName
|
||||||
|
}
|
||||||
|
|
||||||
|
// region wireguard-related
|
||||||
|
|
||||||
|
func (c LocalController) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||||
|
devices, err := c.wg.Devices()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("device list error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces := make([]domain.PhysicalInterface, 0, len(devices))
|
||||||
|
for _, device := range devices {
|
||||||
|
interfaceModel, err := c.convertWireGuardInterface(device)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("interface convert failed for %s: %w", device.Name, err)
|
||||||
|
}
|
||||||
|
interfaces = append(interfaces, interfaceModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.PhysicalInterface,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
return c.getInterface(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) convertWireGuardInterface(device *wgtypes.Device) (domain.PhysicalInterface, error) {
|
||||||
|
// read data from wgctrl interface
|
||||||
|
|
||||||
|
iface := domain.PhysicalInterface{
|
||||||
|
Identifier: domain.InterfaceIdentifier(device.Name),
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: device.PrivateKey.String(),
|
||||||
|
PublicKey: device.PublicKey.String(),
|
||||||
|
},
|
||||||
|
ListenPort: device.ListenPort,
|
||||||
|
Addresses: nil,
|
||||||
|
Mtu: 0,
|
||||||
|
FirewallMark: uint32(device.FirewallMark),
|
||||||
|
DeviceUp: false,
|
||||||
|
ImportSource: domain.ControllerTypeLocal,
|
||||||
|
DeviceType: device.Type.String(),
|
||||||
|
BytesUpload: 0,
|
||||||
|
BytesDownload: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// read data from netlink interface
|
||||||
|
|
||||||
|
lowLevelInterface, err := c.nl.LinkByName(device.Name)
|
||||||
|
if err != nil {
|
||||||
|
return domain.PhysicalInterface{}, fmt.Errorf("netlink error for %s: %w", device.Name, err)
|
||||||
|
}
|
||||||
|
ipAddresses, err := c.nl.AddrList(lowLevelInterface)
|
||||||
|
if err != nil {
|
||||||
|
return domain.PhysicalInterface{}, fmt.Errorf("ip read error for %s: %w", device.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range ipAddresses {
|
||||||
|
iface.Addresses = append(iface.Addresses, domain.CidrFromNetlinkAddr(addr))
|
||||||
|
}
|
||||||
|
iface.Mtu = lowLevelInterface.Attrs().MTU
|
||||||
|
iface.DeviceUp = lowLevelInterface.Attrs().OperState == netlink.OperUnknown // wg only supports unknown
|
||||||
|
if stats := lowLevelInterface.Attrs().Statistics; stats != nil {
|
||||||
|
iface.BytesUpload = stats.TxBytes
|
||||||
|
iface.BytesDownload = stats.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
return iface, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) (
|
||||||
|
[]domain.PhysicalPeer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
device, err := c.wg.Device(string(deviceId))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("device error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peers := make([]domain.PhysicalPeer, 0, len(device.Peers))
|
||||||
|
for _, peer := range device.Peers {
|
||||||
|
peerModel, err := c.convertWireGuardPeer(&peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("peer convert failed for %v: %w", peer.PublicKey, err)
|
||||||
|
}
|
||||||
|
peers = append(peers, peerModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) convertWireGuardPeer(peer *wgtypes.Peer) (domain.PhysicalPeer, error) {
|
||||||
|
peerModel := domain.PhysicalPeer{
|
||||||
|
Identifier: domain.PeerIdentifier(peer.PublicKey.String()),
|
||||||
|
Endpoint: "",
|
||||||
|
AllowedIPs: nil,
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PublicKey: peer.PublicKey.String(),
|
||||||
|
},
|
||||||
|
PresharedKey: "",
|
||||||
|
PersistentKeepalive: int(peer.PersistentKeepaliveInterval.Seconds()),
|
||||||
|
LastHandshake: peer.LastHandshakeTime,
|
||||||
|
ProtocolVersion: peer.ProtocolVersion,
|
||||||
|
BytesUpload: uint64(peer.ReceiveBytes),
|
||||||
|
BytesDownload: uint64(peer.TransmitBytes),
|
||||||
|
ImportSource: domain.ControllerTypeLocal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set local extras - local peers are never disabled in the kernel
|
||||||
|
peerModel.SetExtras(domain.LocalPeerExtras{
|
||||||
|
Disabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, addr := range peer.AllowedIPs {
|
||||||
|
peerModel.AllowedIPs = append(peerModel.AllowedIPs, domain.CidrFromIpNet(addr))
|
||||||
|
}
|
||||||
|
if peer.Endpoint != nil {
|
||||||
|
peerModel.Endpoint = peer.Endpoint.String()
|
||||||
|
}
|
||||||
|
if peer.PresharedKey != (wgtypes.Key{}) {
|
||||||
|
peerModel.PresharedKey = domain.PreSharedKey(peer.PresharedKey.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerModel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) SaveInterface(
|
||||||
|
_ context.Context,
|
||||||
|
id domain.InterfaceIdentifier,
|
||||||
|
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
||||||
|
) error {
|
||||||
|
physicalInterface, err := c.getOrCreateInterface(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateFunc != nil {
|
||||||
|
physicalInterface, err = updateFunc(physicalInterface)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.updateLowLevelInterface(physicalInterface); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.updateWireGuardInterface(physicalInterface); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||||
|
device, err := c.getInterface(id)
|
||||||
|
if err == nil {
|
||||||
|
return device, nil // interface exists
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, fmt.Errorf("device error: %w", err) // unknown error
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new device
|
||||||
|
if err := c.createLowLevelInterface(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err = c.getInterface(id)
|
||||||
|
return device, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) getInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||||
|
device, err := c.wg.Device(string(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pi, err := c.convertWireGuardInterface(device)
|
||||||
|
return &pi, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) createLowLevelInterface(id domain.InterfaceIdentifier) error {
|
||||||
|
link := &netlink.GenericLink{
|
||||||
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
|
Name: string(id),
|
||||||
|
},
|
||||||
|
LinkType: "wireguard",
|
||||||
|
}
|
||||||
|
err := c.nl.LinkAdd(link)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("link add failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) updateLowLevelInterface(pi *domain.PhysicalInterface) error {
|
||||||
|
link, err := c.nl.LinkByName(string(pi.Identifier))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pi.Mtu != 0 {
|
||||||
|
if err := c.nl.LinkSetMTU(link, pi.Mtu); err != nil {
|
||||||
|
return fmt.Errorf("mtu error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range pi.Addresses {
|
||||||
|
err := c.nl.AddrReplace(link, addr.NetlinkAddr())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set ip %s: %w", addr.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove unwanted IP addresses
|
||||||
|
rawAddresses, err := c.nl.AddrList(link)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch interface ips: %w", err)
|
||||||
|
}
|
||||||
|
for _, rawAddr := range rawAddresses {
|
||||||
|
netlinkAddr := domain.CidrFromNetlinkAddr(rawAddr)
|
||||||
|
remove := true
|
||||||
|
for _, addr := range pi.Addresses {
|
||||||
|
if addr == netlinkAddr {
|
||||||
|
remove = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !remove {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.nl.AddrDel(link, &rawAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove deprecated ip %s: %w", netlinkAddr.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update link state
|
||||||
|
if pi.DeviceUp {
|
||||||
|
if err := c.nl.LinkSetUp(link); err != nil {
|
||||||
|
return fmt.Errorf("failed to bring up device: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := c.nl.LinkSetDown(link); err != nil {
|
||||||
|
return fmt.Errorf("failed to bring down device: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
|
||||||
|
pKey, err := wgtypes.NewKey(pi.KeyPair.GetPrivateKeyBytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fwMark *int
|
||||||
|
if pi.FirewallMark != 0 {
|
||||||
|
intFwMark := int(pi.FirewallMark)
|
||||||
|
fwMark = &intFwMark
|
||||||
|
}
|
||||||
|
err = c.wg.ConfigureDevice(string(pi.Identifier), wgtypes.Config{
|
||||||
|
PrivateKey: &pKey,
|
||||||
|
ListenPort: &pi.ListenPort,
|
||||||
|
FirewallMark: fwMark,
|
||||||
|
ReplacePeers: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
||||||
|
if err := c.deleteLowLevelInterface(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) deleteLowLevelInterface(id domain.InterfaceIdentifier) error {
|
||||||
|
link, err := c.nl.LinkByName(string(id))
|
||||||
|
if err != nil {
|
||||||
|
var linkNotFoundError netlink.LinkNotFoundError
|
||||||
|
if errors.As(err, &linkNotFoundError) {
|
||||||
|
return nil // ignore not found error
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to find low level interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.nl.LinkDel(link)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete low level interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) SavePeer(
|
||||||
|
_ context.Context,
|
||||||
|
deviceId domain.InterfaceIdentifier,
|
||||||
|
id domain.PeerIdentifier,
|
||||||
|
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
||||||
|
) error {
|
||||||
|
physicalPeer, err := c.getOrCreatePeer(deviceId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalPeer, err = updateFunc(physicalPeer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the peer is disabled by looking at the backend extras
|
||||||
|
// For local controller, disabled peers should be deleted
|
||||||
|
if physicalPeer.GetExtras() != nil {
|
||||||
|
switch extras := physicalPeer.GetExtras().(type) {
|
||||||
|
case domain.LocalPeerExtras:
|
||||||
|
if extras.Disabled {
|
||||||
|
// Delete the peer instead of updating it
|
||||||
|
return c.deletePeer(deviceId, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.updatePeer(deviceId, physicalPeer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (
|
||||||
|
*domain.PhysicalPeer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
peer, err := c.getPeer(deviceId, id)
|
||||||
|
if err == nil {
|
||||||
|
return peer, nil // peer exists
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, fmt.Errorf("peer error: %w", err) // unknown error
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new peer
|
||||||
|
err = c.wg.ConfigureDevice(string(deviceId), wgtypes.Config{
|
||||||
|
Peers: []wgtypes.PeerConfig{
|
||||||
|
{
|
||||||
|
PublicKey: id.ToPublicKey(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("peer create error for %s: %w", id.ToPublicKey(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err = c.getPeer(deviceId, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("peer error after create: %w", err)
|
||||||
|
}
|
||||||
|
return peer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) getPeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (
|
||||||
|
*domain.PhysicalPeer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if !id.IsPublicKey() {
|
||||||
|
return nil, errors.New("invalid public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err := c.wg.Device(string(deviceId))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey := id.ToPublicKey()
|
||||||
|
for _, peer := range device.Peers {
|
||||||
|
if peer.PublicKey != publicKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
peerModel, err := c.convertWireGuardPeer(&peer)
|
||||||
|
return &peerModel, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.PhysicalPeer) error {
|
||||||
|
cfg := wgtypes.PeerConfig{
|
||||||
|
PublicKey: pp.GetPublicKey(),
|
||||||
|
Remove: false,
|
||||||
|
UpdateOnly: true,
|
||||||
|
PresharedKey: pp.GetPresharedKey(),
|
||||||
|
Endpoint: pp.GetEndpointAddress(),
|
||||||
|
PersistentKeepaliveInterval: pp.GetPersistentKeepaliveTime(),
|
||||||
|
ReplaceAllowedIPs: true,
|
||||||
|
AllowedIPs: pp.GetAllowedIPs(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) DeletePeer(
|
||||||
|
_ context.Context,
|
||||||
|
deviceId domain.InterfaceIdentifier,
|
||||||
|
id domain.PeerIdentifier,
|
||||||
|
) error {
|
||||||
|
if !id.IsPublicKey() {
|
||||||
|
return errors.New("invalid public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.deletePeer(deviceId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) deletePeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
||||||
|
cfg := wgtypes.PeerConfig{
|
||||||
|
PublicKey: id.ToPublicKey(),
|
||||||
|
Remove: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion wireguard-related
|
||||||
|
|
||||||
|
// region wg-quick-related
|
||||||
|
|
||||||
|
func (c LocalController) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
|
||||||
|
if hookCmd == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("executing interface hook", "interface", id, "hook", hookCmd)
|
||||||
|
err := c.exec(hookCmd, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to exec hook: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) 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 := c.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) UnsetDNS(id domain.InterfaceIdentifier) error {
|
||||||
|
dnsCommand := "resolvconf -d %resPref%i -f"
|
||||||
|
|
||||||
|
err := c.exec(dnsCommand, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unset dns settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) replaceCommandPlaceHolders(command string, interfaceId domain.InterfaceIdentifier) string {
|
||||||
|
command = strings.ReplaceAll(command, "%resPref", c.resolvConfIfacePrefix)
|
||||||
|
return strings.ReplaceAll(command, "%i", string(interfaceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) exec(command string, interfaceId domain.InterfaceIdentifier, stdin ...string) error {
|
||||||
|
commandWithInterfaceName := c.replaceCommandPlaceHolders(command, interfaceId)
|
||||||
|
cmd := exec.Command(c.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion wg-quick-related
|
||||||
|
|
||||||
|
// region routing-related
|
||||||
|
|
||||||
|
func (c LocalController) SyncRouteRules(_ context.Context, rules []domain.RouteRule) error {
|
||||||
|
// update fwmark rules
|
||||||
|
if err := c.setFwMarkRules(rules); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update main rule
|
||||||
|
if err := c.setMainRule(rules); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup old main rules
|
||||||
|
if err := c.cleanupMainRule(rules); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c LocalController) setFwMarkRules(rules []domain.RouteRule) error {
|
||||||
|
for _, rule := range rules {
|
||||||
|
existingRules, err := c.nl.RuleList(int(rule.IpFamily))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get existing rules for family %s: %w", rule.IpFamily, 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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// region statistics-related
|
||||||
|
|
||||||
|
func (c LocalController) PingAddresses(
|
||||||
|
ctx context.Context,
|
||||||
|
addr string,
|
||||||
|
) (*domain.PingerResult, error) {
|
||||||
|
pinger, err := probing.NewPinger(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to instantiate pinger for %s: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCount := 1
|
||||||
|
pinger.SetPrivileged(!c.cfg.Statistics.PingUnprivileged)
|
||||||
|
pinger.Count = checkCount
|
||||||
|
pinger.Timeout = 2 * time.Second
|
||||||
|
err = pinger.RunWithContext(ctx) // Blocks until finished.
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to ping %s: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := pinger.Statistics()
|
||||||
|
|
||||||
|
return &domain.PingerResult{
|
||||||
|
PacketsRecv: stats.PacketsRecv,
|
||||||
|
PacketsSent: stats.PacketsSent,
|
||||||
|
Rtts: stats.Rtts,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion statistics-related
|
829
internal/adapters/wgcontroller/mikrotik.go
Normal file
829
internal/adapters/wgcontroller/mikrotik.go
Normal file
@@ -0,0 +1,829 @@
|
|||||||
|
package wgcontroller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MikrotikController struct {
|
||||||
|
coreCfg *config.Config
|
||||||
|
cfg *config.BackendMikrotik
|
||||||
|
|
||||||
|
client *lowlevel.MikrotikApiClient
|
||||||
|
|
||||||
|
// Add mutexes to prevent race conditions
|
||||||
|
interfaceMutexes sync.Map // map[domain.InterfaceIdentifier]*sync.Mutex
|
||||||
|
peerMutexes sync.Map // map[domain.PeerIdentifier]*sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMikrotikController(coreCfg *config.Config, cfg *config.BackendMikrotik) (*MikrotikController, error) {
|
||||||
|
client, err := lowlevel.NewMikrotikApiClient(coreCfg, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create Mikrotik API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MikrotikController{
|
||||||
|
coreCfg: coreCfg,
|
||||||
|
cfg: cfg,
|
||||||
|
|
||||||
|
client: client,
|
||||||
|
|
||||||
|
interfaceMutexes: sync.Map{},
|
||||||
|
peerMutexes: sync.Map{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) GetId() domain.InterfaceBackend {
|
||||||
|
return domain.InterfaceBackend(c.cfg.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInterfaceMutex returns a mutex for the given interface to prevent concurrent modifications
|
||||||
|
func (c *MikrotikController) getInterfaceMutex(id domain.InterfaceIdentifier) *sync.Mutex {
|
||||||
|
mutex, _ := c.interfaceMutexes.LoadOrStore(id, &sync.Mutex{})
|
||||||
|
return mutex.(*sync.Mutex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPeerMutex returns a mutex for the given peer to prevent concurrent modifications
|
||||||
|
func (c *MikrotikController) getPeerMutex(id domain.PeerIdentifier) *sync.Mutex {
|
||||||
|
mutex, _ := c.peerMutexes.LoadOrStore(id, &sync.Mutex{})
|
||||||
|
return mutex.(*sync.Mutex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// region wireguard-related
|
||||||
|
|
||||||
|
func (c *MikrotikController) GetInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
|
||||||
|
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running", "comment",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query interfaces: %v", wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parallelize loading of interface details to speed up overall latency.
|
||||||
|
// Use a bounded semaphore to avoid overloading the MikroTik device.
|
||||||
|
maxConcurrent := c.cfg.GetConcurrency()
|
||||||
|
sem := make(chan struct{}, maxConcurrent)
|
||||||
|
|
||||||
|
interfaces := make([]domain.PhysicalInterface, 0, len(wgReply.Data))
|
||||||
|
var mu sync.Mutex
|
||||||
|
var wgWait sync.WaitGroup
|
||||||
|
var firstErr error
|
||||||
|
ctx2, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
for _, wgObj := range wgReply.Data {
|
||||||
|
wgWait.Add(1)
|
||||||
|
sem <- struct{}{} // block if more than maxConcurrent requests are processing
|
||||||
|
go func(wg lowlevel.GenericJsonObject) {
|
||||||
|
defer wgWait.Done()
|
||||||
|
defer func() { <-sem }() // read from the semaphore and make space for the next entry
|
||||||
|
if firstErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pi, err := c.loadInterfaceData(ctx2, wg)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
interfaces = append(interfaces, *pi)
|
||||||
|
mu.Unlock()
|
||||||
|
}(wgObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
wgWait.Wait()
|
||||||
|
if firstErr != nil {
|
||||||
|
return nil, firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.PhysicalInterface,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"name": string(id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query interface %s: %v", id, wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(wgReply.Data) == 0 {
|
||||||
|
return nil, fmt.Errorf("interface %s not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.loadInterfaceData(ctx, wgReply.Data[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) loadInterfaceData(
|
||||||
|
ctx context.Context,
|
||||||
|
wireGuardObj lowlevel.GenericJsonObject,
|
||||||
|
) (*domain.PhysicalInterface, error) {
|
||||||
|
deviceId := wireGuardObj.GetString(".id")
|
||||||
|
deviceName := wireGuardObj.GetString("name")
|
||||||
|
ifaceReply := c.client.Get(ctx, "/interface/"+deviceId, &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
"name", "rx-byte", "tx-byte",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if ifaceReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query interface %s: %v", deviceId, ifaceReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv4, ipv6, err := c.loadIpAddresses(ctx, deviceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query IP addresses for interface %s: %v", deviceId, err)
|
||||||
|
}
|
||||||
|
addresses := c.convertIpAddresses(ipv4, ipv6)
|
||||||
|
|
||||||
|
interfaceModel, err := c.convertWireGuardInterface(wireGuardObj, ifaceReply.Data, addresses)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("interface convert failed for %s: %w", deviceName, err)
|
||||||
|
}
|
||||||
|
return &interfaceModel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) loadIpAddresses(
|
||||||
|
ctx context.Context,
|
||||||
|
deviceName string,
|
||||||
|
) (ipv4 []lowlevel.GenericJsonObject, ipv6 []lowlevel.GenericJsonObject, err error) {
|
||||||
|
// Query IPv4 and IPv6 addresses in parallel to reduce latency.
|
||||||
|
var (
|
||||||
|
v4 []lowlevel.GenericJsonObject
|
||||||
|
v6 []lowlevel.GenericJsonObject
|
||||||
|
v4Err error
|
||||||
|
v6Err error
|
||||||
|
wg sync.WaitGroup
|
||||||
|
)
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
addrV4Reply := c.client.Query(ctx, "/ip/address", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
".id", "address", "network",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"interface": deviceName,
|
||||||
|
"dynamic": "false", // we only want static addresses
|
||||||
|
"disabled": "false", // we only want addresses that are not disabled
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if addrV4Reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
v4Err = fmt.Errorf("failed to query IPv4 addresses for interface %s: %v", deviceName, addrV4Reply.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v4 = addrV4Reply.Data
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
addrV6Reply := c.client.Query(ctx, "/ipv6/address", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
".id", "address", "network",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"interface": deviceName,
|
||||||
|
"dynamic": "false", // we only want static addresses
|
||||||
|
"disabled": "false", // we only want addresses that are not disabled
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if addrV6Reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
v6Err = fmt.Errorf("failed to query IPv6 addresses for interface %s: %v", deviceName, addrV6Reply.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v6 = addrV6Reply.Data
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
if v4Err != nil {
|
||||||
|
return nil, nil, v4Err
|
||||||
|
}
|
||||||
|
if v6Err != nil {
|
||||||
|
return nil, nil, v6Err
|
||||||
|
}
|
||||||
|
|
||||||
|
return v4, v6, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) convertIpAddresses(
|
||||||
|
ipv4, ipv6 []lowlevel.GenericJsonObject,
|
||||||
|
) []domain.Cidr {
|
||||||
|
addresses := make([]domain.Cidr, 0, len(ipv4)+len(ipv6))
|
||||||
|
for _, addr := range append(ipv4, ipv6...) {
|
||||||
|
addrStr := addr.GetString("address")
|
||||||
|
if addrStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cidr, err := domain.CidrFromString(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses = append(addresses, cidr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) convertWireGuardInterface(
|
||||||
|
wg, iface lowlevel.GenericJsonObject,
|
||||||
|
addresses []domain.Cidr,
|
||||||
|
) (
|
||||||
|
domain.PhysicalInterface,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
pi := domain.PhysicalInterface{
|
||||||
|
Identifier: domain.InterfaceIdentifier(wg.GetString("name")),
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: wg.GetString("private-key"),
|
||||||
|
PublicKey: wg.GetString("public-key"),
|
||||||
|
},
|
||||||
|
ListenPort: wg.GetInt("listen-port"),
|
||||||
|
Addresses: addresses,
|
||||||
|
Mtu: wg.GetInt("mtu"),
|
||||||
|
FirewallMark: 0,
|
||||||
|
DeviceUp: wg.GetBool("running"),
|
||||||
|
ImportSource: domain.ControllerTypeMikrotik,
|
||||||
|
DeviceType: domain.ControllerTypeMikrotik,
|
||||||
|
BytesUpload: uint64(iface.GetInt("tx-byte")),
|
||||||
|
BytesDownload: uint64(iface.GetInt("rx-byte")),
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.SetExtras(domain.MikrotikInterfaceExtras{
|
||||||
|
Id: wg.GetString(".id"),
|
||||||
|
Comment: wg.GetString("comment"),
|
||||||
|
Disabled: wg.GetBool("disabled"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return pi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) GetPeers(ctx context.Context, deviceId domain.InterfaceIdentifier) (
|
||||||
|
[]domain.PhysicalPeer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment",
|
||||||
|
"current-endpoint-address", "current-endpoint-port", "last-handshake", "persistent-keepalive",
|
||||||
|
"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder", "client-dns",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"interface": string(deviceId),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query peers for %s: %v", deviceId, wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(wgReply.Data) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
peers := make([]domain.PhysicalPeer, 0, len(wgReply.Data))
|
||||||
|
for _, peer := range wgReply.Data {
|
||||||
|
peerModel, err := c.convertWireGuardPeer(peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("peer convert failed for %v: %w", peer.GetString("name"), err)
|
||||||
|
}
|
||||||
|
peers = append(peers, peerModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject) (
|
||||||
|
domain.PhysicalPeer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
keepAliveSeconds := 0
|
||||||
|
duration, err := time.ParseDuration(peer.GetString("persistent-keepalive"))
|
||||||
|
if err == nil {
|
||||||
|
keepAliveSeconds = int(duration.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEndpoint := ""
|
||||||
|
if peer.GetString("current-endpoint-address") != "" && peer.GetString("current-endpoint-port") != "" {
|
||||||
|
currentEndpoint = peer.GetString("current-endpoint-address") + ":" + peer.GetString("current-endpoint-port")
|
||||||
|
}
|
||||||
|
|
||||||
|
lastHandshakeTime := time.Time{}
|
||||||
|
if peer.GetString("last-handshake") != "" {
|
||||||
|
relDuration, err := time.ParseDuration(peer.GetString("last-handshake"))
|
||||||
|
if err == nil {
|
||||||
|
lastHandshakeTime = time.Now().Add(-relDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedAddresses, _ := domain.CidrsFromString(peer.GetString("allowed-address"))
|
||||||
|
|
||||||
|
clientKeepAliveSeconds := 0
|
||||||
|
duration, err = time.ParseDuration(peer.GetString("client-keepalive"))
|
||||||
|
if err == nil {
|
||||||
|
clientKeepAliveSeconds = int(duration.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
peerModel := domain.PhysicalPeer{
|
||||||
|
Identifier: domain.PeerIdentifier(peer.GetString("public-key")),
|
||||||
|
Endpoint: currentEndpoint,
|
||||||
|
AllowedIPs: allowedAddresses,
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PublicKey: peer.GetString("public-key"),
|
||||||
|
PrivateKey: peer.GetString("private-key"),
|
||||||
|
},
|
||||||
|
PresharedKey: domain.PreSharedKey(peer.GetString("preshared-key")),
|
||||||
|
PersistentKeepalive: keepAliveSeconds,
|
||||||
|
LastHandshake: lastHandshakeTime,
|
||||||
|
ProtocolVersion: 0, // Mikrotik does not support protocol versioning, so we set it to 0
|
||||||
|
BytesUpload: uint64(peer.GetInt("rx")),
|
||||||
|
BytesDownload: uint64(peer.GetInt("tx")),
|
||||||
|
ImportSource: domain.ControllerTypeMikrotik,
|
||||||
|
}
|
||||||
|
|
||||||
|
peerModel.SetExtras(domain.MikrotikPeerExtras{
|
||||||
|
Id: peer.GetString(".id"),
|
||||||
|
Name: peer.GetString("name"),
|
||||||
|
Comment: peer.GetString("comment"),
|
||||||
|
IsResponder: peer.GetBool("responder"),
|
||||||
|
Disabled: peer.GetBool("disabled"),
|
||||||
|
ClientEndpoint: peer.GetString("client-endpoint"),
|
||||||
|
ClientAddress: peer.GetString("client-address"),
|
||||||
|
ClientDns: peer.GetString("client-dns"),
|
||||||
|
ClientKeepalive: clientKeepAliveSeconds,
|
||||||
|
})
|
||||||
|
|
||||||
|
return peerModel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) SaveInterface(
|
||||||
|
ctx context.Context,
|
||||||
|
id domain.InterfaceIdentifier,
|
||||||
|
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
||||||
|
) error {
|
||||||
|
// Lock the interface to prevent concurrent modifications
|
||||||
|
mutex := c.getInterfaceMutex(id)
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
physicalInterface, err := c.getOrCreateInterface(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceId := physicalInterface.GetExtras().(domain.MikrotikInterfaceExtras).Id
|
||||||
|
if updateFunc != nil {
|
||||||
|
physicalInterface, err = updateFunc(physicalInterface)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newExtras := physicalInterface.GetExtras().(domain.MikrotikInterfaceExtras)
|
||||||
|
newExtras.Id = deviceId // ensure the ID is not changed
|
||||||
|
physicalInterface.SetExtras(newExtras)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.updateInterface(ctx, physicalInterface); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) getOrCreateInterface(
|
||||||
|
ctx context.Context,
|
||||||
|
id domain.InterfaceIdentifier,
|
||||||
|
) (*domain.PhysicalInterface, error) {
|
||||||
|
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"name": string(id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status == lowlevel.MikrotikApiStatusOk && len(wgReply.Data) > 0 {
|
||||||
|
return c.loadInterfaceData(ctx, wgReply.Data[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new interface if it does not exist
|
||||||
|
createReply := c.client.Create(ctx, "/interface/wireguard", lowlevel.GenericJsonObject{
|
||||||
|
"name": string(id),
|
||||||
|
})
|
||||||
|
if wgReply.Status == lowlevel.MikrotikApiStatusOk {
|
||||||
|
return c.loadInterfaceData(ctx, createReply.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to create interface %s: %v", id, createReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) updateInterface(ctx context.Context, pi *domain.PhysicalInterface) error {
|
||||||
|
extras := pi.GetExtras().(domain.MikrotikInterfaceExtras)
|
||||||
|
interfaceId := extras.Id
|
||||||
|
wgReply := c.client.Update(ctx, "/interface/wireguard/"+interfaceId, lowlevel.GenericJsonObject{
|
||||||
|
"name": pi.Identifier,
|
||||||
|
"comment": extras.Comment,
|
||||||
|
"mtu": strconv.Itoa(pi.Mtu),
|
||||||
|
"listen-port": strconv.Itoa(pi.ListenPort),
|
||||||
|
"private-key": pi.KeyPair.PrivateKey,
|
||||||
|
"disabled": strconv.FormatBool(!pi.DeviceUp),
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to update interface %s: %v", pi.Identifier, wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the interface's addresses
|
||||||
|
currentV4, currentV6, err := c.loadIpAddresses(ctx, string(pi.Identifier))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load current addresses for interface %s: %v", pi.Identifier, err)
|
||||||
|
}
|
||||||
|
currentAddresses := c.convertIpAddresses(currentV4, currentV6)
|
||||||
|
|
||||||
|
// get all addresses that are currently not in the interface, only in pi
|
||||||
|
newAddresses := make([]domain.Cidr, 0, len(pi.Addresses))
|
||||||
|
for _, addr := range pi.Addresses {
|
||||||
|
if slices.Contains(currentAddresses, addr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newAddresses = append(newAddresses, addr)
|
||||||
|
}
|
||||||
|
// get obsolete addresses that are in the interface, but not in pi
|
||||||
|
obsoleteAddresses := make([]domain.Cidr, 0, len(currentAddresses))
|
||||||
|
for _, addr := range currentAddresses {
|
||||||
|
if slices.Contains(pi.Addresses, addr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
obsoleteAddresses = append(obsoleteAddresses, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the IP addresses for the interface
|
||||||
|
if err := c.updateIpAddresses(ctx, string(pi.Identifier), currentV4, currentV6,
|
||||||
|
newAddresses, obsoleteAddresses); err != nil {
|
||||||
|
return fmt.Errorf("failed to update IP addresses for interface %s: %v", pi.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) updateIpAddresses(
|
||||||
|
ctx context.Context,
|
||||||
|
deviceName string,
|
||||||
|
currentV4, currentV6 []lowlevel.GenericJsonObject,
|
||||||
|
new, obsolete []domain.Cidr,
|
||||||
|
) error {
|
||||||
|
// first, delete all obsolete addresses
|
||||||
|
for _, addr := range obsolete {
|
||||||
|
// find ID of the address to delete
|
||||||
|
if addr.IsV4() {
|
||||||
|
for _, a := range currentV4 {
|
||||||
|
if a.GetString("address") == addr.String() {
|
||||||
|
// delete the address
|
||||||
|
reply := c.client.Delete(ctx, "/ip/address/"+a.GetString(".id"))
|
||||||
|
if reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to delete obsolete IPv4 address %s: %v", addr, reply.Error)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, a := range currentV6 {
|
||||||
|
if a.GetString("address") == addr.String() {
|
||||||
|
// delete the address
|
||||||
|
reply := c.client.Delete(ctx, "/ipv6/address/"+a.GetString(".id"))
|
||||||
|
if reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to delete obsolete IPv6 address %s: %v", addr, reply.Error)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// then, add all new addresses
|
||||||
|
for _, addr := range new {
|
||||||
|
var createPath string
|
||||||
|
if addr.IsV4() {
|
||||||
|
createPath = "/ip/address"
|
||||||
|
} else {
|
||||||
|
createPath = "/ipv6/address"
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the address
|
||||||
|
reply := c.client.Create(ctx, createPath, lowlevel.GenericJsonObject{
|
||||||
|
"address": addr.String(),
|
||||||
|
"interface": deviceName,
|
||||||
|
})
|
||||||
|
if reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to create new address %s: %v", addr, reply.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||||
|
// Lock the interface to prevent concurrent modifications
|
||||||
|
mutex := c.getInterfaceMutex(id)
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
// delete the interface's addresses
|
||||||
|
currentV4, currentV6, err := c.loadIpAddresses(ctx, string(id))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load current addresses for interface %s: %v", id, err)
|
||||||
|
}
|
||||||
|
for _, a := range currentV4 {
|
||||||
|
// delete the address
|
||||||
|
reply := c.client.Delete(ctx, "/ip/address/"+a.GetString(".id"))
|
||||||
|
if reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to delete IPv4 address %s: %v", a.GetString("address"), reply.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, a := range currentV6 {
|
||||||
|
// delete the address
|
||||||
|
reply := c.client.Delete(ctx, "/ipv6/address/"+a.GetString(".id"))
|
||||||
|
if reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to delete IPv6 address %s: %v", a.GetString("address"), reply.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the WireGuard interface
|
||||||
|
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{".id"},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"name": string(id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("unable to find WireGuard interface %s: %v", id, wgReply.Error)
|
||||||
|
}
|
||||||
|
if len(wgReply.Data) == 0 {
|
||||||
|
return nil // interface does not exist, nothing to delete
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceId := wgReply.Data[0].GetString(".id")
|
||||||
|
deleteReply := c.client.Delete(ctx, "/interface/wireguard/"+interfaceId)
|
||||||
|
if deleteReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to delete WireGuard interface %s: %v", id, deleteReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) SavePeer(
|
||||||
|
ctx context.Context,
|
||||||
|
deviceId domain.InterfaceIdentifier,
|
||||||
|
id domain.PeerIdentifier,
|
||||||
|
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
||||||
|
) error {
|
||||||
|
// Lock the peer to prevent concurrent modifications
|
||||||
|
mutex := c.getPeerMutex(id)
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
physicalPeer, err := c.getOrCreatePeer(ctx, deviceId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerId := physicalPeer.GetExtras().(domain.MikrotikPeerExtras).Id
|
||||||
|
physicalPeer, err = updateFunc(physicalPeer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newExtras := physicalPeer.GetExtras().(domain.MikrotikPeerExtras)
|
||||||
|
newExtras.Id = peerId // ensure the ID is not changed
|
||||||
|
physicalPeer.SetExtras(newExtras)
|
||||||
|
|
||||||
|
if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) getOrCreatePeer(
|
||||||
|
ctx context.Context,
|
||||||
|
deviceId domain.InterfaceIdentifier,
|
||||||
|
id domain.PeerIdentifier,
|
||||||
|
) (*domain.PhysicalPeer, error) {
|
||||||
|
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
".id", "name", "public-key", "private-key", "preshared-key", "persistent-keepalive", "client-address",
|
||||||
|
"client-endpoint", "client-keepalive", "allowed-address", "client-dns", "comment", "disabled", "responder",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"public-key": string(id),
|
||||||
|
"interface": string(deviceId),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status == lowlevel.MikrotikApiStatusOk && len(wgReply.Data) > 0 {
|
||||||
|
slog.Debug("found existing Mikrotik peer", "peer", id, "interface", deviceId)
|
||||||
|
existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &existingPeer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new peer if it does not exist
|
||||||
|
slog.Debug("creating new Mikrotik peer", "peer", id, "interface", deviceId)
|
||||||
|
createReply := c.client.Create(ctx, "/interface/wireguard/peers", lowlevel.GenericJsonObject{
|
||||||
|
"name": fmt.Sprintf("tmp-wg-%s", id[0:8]),
|
||||||
|
"interface": string(deviceId),
|
||||||
|
"public-key": string(id),
|
||||||
|
"allowed-address": "0.0.0.0/0", // Use 0.0.0.0/0 as default, will be updated by updatePeer
|
||||||
|
})
|
||||||
|
if createReply.Status == lowlevel.MikrotikApiStatusOk {
|
||||||
|
newPeer, err := c.convertWireGuardPeer(createReply.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
slog.Debug("successfully created Mikrotik peer", "peer", id, "interface", deviceId)
|
||||||
|
return &newPeer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to create peer %s for interface %s: %v", id, deviceId, createReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) updatePeer(
|
||||||
|
ctx context.Context,
|
||||||
|
deviceId domain.InterfaceIdentifier,
|
||||||
|
pp *domain.PhysicalPeer,
|
||||||
|
) error {
|
||||||
|
extras := pp.GetExtras().(domain.MikrotikPeerExtras)
|
||||||
|
peerId := extras.Id
|
||||||
|
|
||||||
|
endpoint := pp.Endpoint
|
||||||
|
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)
|
||||||
|
slog.Debug("updating Mikrotik peer",
|
||||||
|
"peer", pp.Identifier,
|
||||||
|
"interface", deviceId,
|
||||||
|
"allowed-address", allowedAddressStr,
|
||||||
|
"allowed-ips-count", len(pp.AllowedIPs),
|
||||||
|
"disabled", extras.Disabled)
|
||||||
|
|
||||||
|
wgReply := c.client.Update(ctx, "/interface/wireguard/peers/"+peerId, lowlevel.GenericJsonObject{
|
||||||
|
"name": extras.Name,
|
||||||
|
"comment": extras.Comment,
|
||||||
|
"preshared-key": pp.PresharedKey,
|
||||||
|
"public-key": pp.KeyPair.PublicKey,
|
||||||
|
"private-key": pp.KeyPair.PrivateKey,
|
||||||
|
"persistent-keepalive": (time.Duration(pp.PersistentKeepalive) * time.Second).String(),
|
||||||
|
"disabled": strconv.FormatBool(extras.Disabled),
|
||||||
|
"responder": strconv.FormatBool(extras.IsResponder),
|
||||||
|
"client-endpoint": extras.ClientEndpoint,
|
||||||
|
"client-address": extras.ClientAddress,
|
||||||
|
"client-keepalive": (time.Duration(extras.ClientKeepalive) * time.Second).String(),
|
||||||
|
"client-dns": extras.ClientDns,
|
||||||
|
"endpoint-address": endpoint,
|
||||||
|
"endpoint-port": endpointPort,
|
||||||
|
"allowed-address": allowedAddressStr, // Add the missing allowed-address field
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to update peer %s on interface %s: %v", pp.Identifier, deviceId, wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if extras.Disabled {
|
||||||
|
slog.Debug("successfully disabled Mikrotik peer", "peer", pp.Identifier, "interface", deviceId)
|
||||||
|
} else {
|
||||||
|
slog.Debug("successfully updated Mikrotik peer", "peer", pp.Identifier, "interface", deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) DeletePeer(
|
||||||
|
ctx context.Context,
|
||||||
|
deviceId domain.InterfaceIdentifier,
|
||||||
|
id domain.PeerIdentifier,
|
||||||
|
) error {
|
||||||
|
// Lock the peer to prevent concurrent modifications
|
||||||
|
mutex := c.getPeerMutex(id)
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{".id"},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"public-key": string(id),
|
||||||
|
"interface": string(deviceId),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("unable to find WireGuard peer %s for interface %s: %v", id, deviceId, wgReply.Error)
|
||||||
|
}
|
||||||
|
if len(wgReply.Data) == 0 {
|
||||||
|
return nil // peer does not exist, nothing to delete
|
||||||
|
}
|
||||||
|
|
||||||
|
peerId := wgReply.Data[0].GetString(".id")
|
||||||
|
deleteReply := c.client.Delete(ctx, "/interface/wireguard/peers/"+peerId)
|
||||||
|
if deleteReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to delete WireGuard peer %s for interface %s: %v", id, deviceId, deleteReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion wireguard-related
|
||||||
|
|
||||||
|
// region wg-quick-related
|
||||||
|
|
||||||
|
func (c *MikrotikController) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
|
||||||
|
// TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
|
||||||
|
// TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) UnsetDNS(id domain.InterfaceIdentifier) error {
|
||||||
|
// TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion wg-quick-related
|
||||||
|
|
||||||
|
// region routing-related
|
||||||
|
|
||||||
|
func (c *MikrotikController) SyncRouteRules(_ context.Context, rules []domain.RouteRule) error {
|
||||||
|
// TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MikrotikController) DeleteRouteRules(_ context.Context, rules []domain.RouteRule) error {
|
||||||
|
// TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion routing-related
|
||||||
|
|
||||||
|
// region statistics-related
|
||||||
|
|
||||||
|
func (c *MikrotikController) PingAddresses(
|
||||||
|
ctx context.Context,
|
||||||
|
addr string,
|
||||||
|
) (*domain.PingerResult, error) {
|
||||||
|
wgReply := c.client.ExecList(ctx, "/tool/ping",
|
||||||
|
// limit to 1 packet with a max running time of 2 seconds
|
||||||
|
lowlevel.GenericJsonObject{"address": addr, "count": 1, "interval": "00:00:02"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to ping %s: %v", addr, wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result domain.PingerResult
|
||||||
|
for _, item := range wgReply.Data {
|
||||||
|
result.PacketsRecv += item.GetInt("received")
|
||||||
|
result.PacketsSent += item.GetInt("sent")
|
||||||
|
|
||||||
|
rttStr := item.GetString("avg-rtt")
|
||||||
|
if rttStr != "" {
|
||||||
|
rtt, err := time.ParseDuration(rttStr)
|
||||||
|
if err == nil {
|
||||||
|
result.Rtts = append(result.Rtts, rtt)
|
||||||
|
} else {
|
||||||
|
// use a high value to indicate failure or timeout
|
||||||
|
result.Rtts = append(result.Rtts, 999999*time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion statistics-related
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
@@ -16,8 +17,9 @@ import (
|
|||||||
|
|
||||||
// WgRepo implements all low-level WireGuard interactions.
|
// WgRepo implements all low-level WireGuard interactions.
|
||||||
type WgRepo struct {
|
type WgRepo struct {
|
||||||
wg lowlevel.WireGuardClient
|
wg lowlevel.WireGuardClient
|
||||||
nl lowlevel.NetlinkClient
|
nl lowlevel.NetlinkClient
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWireGuardRepository creates a new WgRepo instance.
|
// NewWireGuardRepository creates a new WgRepo instance.
|
||||||
@@ -31,8 +33,9 @@ func NewWireGuardRepository() *WgRepo {
|
|||||||
nl := &lowlevel.NetlinkManager{}
|
nl := &lowlevel.NetlinkManager{}
|
||||||
|
|
||||||
repo := &WgRepo{
|
repo := &WgRepo{
|
||||||
wg: wg,
|
wg: wg,
|
||||||
nl: nl,
|
nl: nl,
|
||||||
|
log: slog.Default().With(slog.String("adapter", "wireguard")),
|
||||||
}
|
}
|
||||||
|
|
||||||
return repo
|
return repo
|
||||||
@@ -40,8 +43,10 @@ func NewWireGuardRepository() *WgRepo {
|
|||||||
|
|
||||||
// GetInterfaces returns all existing WireGuard interfaces.
|
// GetInterfaces returns all existing WireGuard interfaces.
|
||||||
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||||
|
r.log.Debug("getting all interfaces")
|
||||||
devices, err := r.wg.Devices()
|
devices, err := r.wg.Devices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("failed to get devices", "error", err)
|
||||||
return nil, fmt.Errorf("device list error: %w", err)
|
return nil, fmt.Errorf("device list error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +65,17 @@ func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, e
|
|||||||
// GetInterface returns the interface with the given id.
|
// GetInterface returns the interface with the given id.
|
||||||
// If no interface is found, an error os.ErrNotExist is returned.
|
// If no interface is found, an error os.ErrNotExist is returned.
|
||||||
func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||||
|
r.log.Debug("getting interface", "id", id)
|
||||||
return r.getInterface(id)
|
return r.getInterface(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeers returns all peers associated with the given interface id.
|
// GetPeers returns all peers associated with the given interface id.
|
||||||
// If the requested interface is found, an error os.ErrNotExist is returned.
|
// If the requested interface is found, an error os.ErrNotExist is returned.
|
||||||
func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
|
func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
|
||||||
|
r.log.Debug("getting peers for interface", "deviceId", deviceId)
|
||||||
device, err := r.wg.Device(string(deviceId))
|
device, err := r.wg.Device(string(deviceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("failed to get device", "deviceId", deviceId, "error", err)
|
||||||
return nil, fmt.Errorf("device error: %w", err)
|
return nil, fmt.Errorf("device error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +98,7 @@ func (r *WgRepo) GetPeer(
|
|||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
) (*domain.PhysicalPeer, error) {
|
) (*domain.PhysicalPeer, error) {
|
||||||
|
r.log.Debug("getting peer", "deviceId", deviceId, "peerId", id)
|
||||||
return r.getPeer(deviceId, id)
|
return r.getPeer(deviceId, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,25 +183,31 @@ func (r *WgRepo) SaveInterface(
|
|||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
||||||
) error {
|
) error {
|
||||||
|
r.log.Debug("saving interface", "id", id)
|
||||||
physicalInterface, err := r.getOrCreateInterface(id)
|
physicalInterface, err := r.getOrCreateInterface(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("failed to get or create interface", "id", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateFunc != nil {
|
if updateFunc != nil {
|
||||||
physicalInterface, err = updateFunc(physicalInterface)
|
physicalInterface, err = updateFunc(physicalInterface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("interface update function failed", "id", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.updateLowLevelInterface(physicalInterface); err != nil {
|
if err := r.updateLowLevelInterface(physicalInterface); err != nil {
|
||||||
|
r.log.Error("failed to update low level interface", "id", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := r.updateWireGuardInterface(physicalInterface); err != nil {
|
if err := r.updateWireGuardInterface(physicalInterface); err != nil {
|
||||||
|
r.log.Error("failed to update wireguard interface", "id", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.log.Debug("successfully saved interface", "id", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,10 +338,13 @@ func (r *WgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
|
|||||||
// DeleteInterface deletes the interface with the given id.
|
// DeleteInterface deletes the interface with the given id.
|
||||||
// If the requested interface is found, no error is returned.
|
// If the requested interface is found, no error is returned.
|
||||||
func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
||||||
|
r.log.Debug("deleting interface", "id", id)
|
||||||
if err := r.deleteLowLevelInterface(id); err != nil {
|
if err := r.deleteLowLevelInterface(id); err != nil {
|
||||||
|
r.log.Error("failed to delete low level interface", "id", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.log.Debug("successfully deleted interface", "id", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,20 +374,25 @@ func (r *WgRepo) SavePeer(
|
|||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
||||||
) error {
|
) error {
|
||||||
|
r.log.Debug("saving peer", "deviceId", deviceId, "peerId", id)
|
||||||
physicalPeer, err := r.getOrCreatePeer(deviceId, id)
|
physicalPeer, err := r.getOrCreatePeer(deviceId, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("failed to get or create peer", "deviceId", deviceId, "peerId", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
physicalPeer, err = updateFunc(physicalPeer)
|
physicalPeer, err = updateFunc(physicalPeer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("peer update function failed", "deviceId", deviceId, "peerId", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.updatePeer(deviceId, physicalPeer); err != nil {
|
if err := r.updatePeer(deviceId, physicalPeer); err != nil {
|
||||||
|
r.log.Error("failed to update peer", "deviceId", deviceId, "peerId", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.log.Debug("successfully saved peer", "deviceId", deviceId, "peerId", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +464,7 @@ func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.Phys
|
|||||||
|
|
||||||
err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
|
err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("failed to configure device for peer update", "deviceId", deviceId, "peerId", pp.Identifier, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,15 +474,20 @@ func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.Phys
|
|||||||
// DeletePeer deletes the peer with the given id.
|
// DeletePeer deletes the peer with the given id.
|
||||||
// If the requested interface or peer is found, no error is returned.
|
// If the requested interface or peer is found, no error is returned.
|
||||||
func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
||||||
|
r.log.Debug("deleting peer", "deviceId", deviceId, "peerId", id)
|
||||||
if !id.IsPublicKey() {
|
if !id.IsPublicKey() {
|
||||||
return errors.New("invalid public key")
|
err := errors.New("invalid public key")
|
||||||
|
r.log.Error("invalid peer id", "peerId", id, "error", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.deletePeer(deviceId, id)
|
err := r.deletePeer(deviceId, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("failed to delete peer", "deviceId", deviceId, "peerId", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.log.Debug("successfully deleted peer", "deviceId", deviceId, "peerId", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,6 +499,7 @@ func (r *WgRepo) deletePeer(deviceId domain.InterfaceIdentifier, id domain.PeerI
|
|||||||
|
|
||||||
err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
|
err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.log.Error("failed to configure device for peer deletion", "deviceId", deviceId, "peerId", id, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -819,6 +819,12 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/model.PeerMailRequest"
|
"$ref": "#/definitions/model.PeerMailRequest"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The configuration style",
|
||||||
|
"name": "style",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -858,6 +864,12 @@
|
|||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The configuration style",
|
||||||
|
"name": "style",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -899,6 +911,12 @@
|
|||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The configuration style",
|
||||||
|
"name": "style",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -1763,6 +1781,11 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Backend": {
|
||||||
|
"description": "the backend used for this interface e.g., local, mikrotik, ...",
|
||||||
|
"type": "string",
|
||||||
|
"example": "local"
|
||||||
|
},
|
||||||
"Disabled": {
|
"Disabled": {
|
||||||
"description": "flag that specifies if the interface is enabled (up) or not (down)",
|
"description": "flag that specifies if the interface is enabled (up) or not (down)",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -1951,7 +1974,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Suffix": {
|
"Prefix": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2231,6 +2254,15 @@
|
|||||||
"ApiAdminOnly": {
|
"ApiAdminOnly": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"AvailableBackends": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.SettingsBackendNames"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LoginFormVisible": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"MailLinkOnly": {
|
"MailLinkOnly": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -2248,6 +2280,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"model.SettingsBackendNames": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.User": {
|
"model.User": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@@ -65,6 +65,10 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
Backend:
|
||||||
|
description: the backend used for this interface e.g., local, mikrotik, ...
|
||||||
|
example: local
|
||||||
|
type: string
|
||||||
Disabled:
|
Disabled:
|
||||||
description: flag that specifies if the interface is enabled (up) or not (down)
|
description: flag that specifies if the interface is enabled (up) or not (down)
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -206,7 +210,7 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
Suffix:
|
Prefix:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
model.Peer:
|
model.Peer:
|
||||||
@@ -381,6 +385,12 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
ApiAdminOnly:
|
ApiAdminOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
AvailableBackends:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.SettingsBackendNames'
|
||||||
|
type: array
|
||||||
|
LoginFormVisible:
|
||||||
|
type: boolean
|
||||||
MailLinkOnly:
|
MailLinkOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
MinPasswordLength:
|
MinPasswordLength:
|
||||||
@@ -392,6 +402,13 @@ definitions:
|
|||||||
WebAuthnEnabled:
|
WebAuthnEnabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
|
model.SettingsBackendNames:
|
||||||
|
properties:
|
||||||
|
Id:
|
||||||
|
type: string
|
||||||
|
Name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
model.User:
|
model.User:
|
||||||
properties:
|
properties:
|
||||||
ApiEnabled:
|
ApiEnabled:
|
||||||
@@ -1070,6 +1087,10 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/model.PeerMailRequest'
|
$ref: '#/definitions/model.PeerMailRequest'
|
||||||
|
- description: The configuration style
|
||||||
|
in: query
|
||||||
|
name: style
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -1095,6 +1116,10 @@ paths:
|
|||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- description: The configuration style
|
||||||
|
in: query
|
||||||
|
name: style
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- image/png
|
- image/png
|
||||||
- application/json
|
- application/json
|
||||||
@@ -1123,6 +1148,10 @@ paths:
|
|||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- description: The configuration style
|
||||||
|
in: query
|
||||||
|
name: style
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@@ -2086,6 +2086,11 @@
|
|||||||
"InterfaceIdentifier"
|
"InterfaceIdentifier"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"DisplayName": {
|
||||||
|
"description": "DisplayName is an optional name for the new peer.\nIf unset, a default template value (e.g., \"API Peer ...\") will be assigned.",
|
||||||
|
"type": "string",
|
||||||
|
"example": "API Peer xyz"
|
||||||
|
},
|
||||||
"InterfaceIdentifier": {
|
"InterfaceIdentifier": {
|
||||||
"description": "InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.",
|
"description": "InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@@ -445,6 +445,12 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
models.ProvisioningRequest:
|
models.ProvisioningRequest:
|
||||||
properties:
|
properties:
|
||||||
|
DisplayName:
|
||||||
|
description: |-
|
||||||
|
DisplayName is an optional name for the new peer.
|
||||||
|
If unset, a default template value (e.g., "API Peer ...") will be assigned.
|
||||||
|
example: API Peer xyz
|
||||||
|
type: string
|
||||||
InterfaceIdentifier:
|
InterfaceIdentifier:
|
||||||
description: InterfaceIdentifier is the identifier of the WireGuard interface
|
description: InterfaceIdentifier is the identifier of the WireGuard interface
|
||||||
the peer should be linked to.
|
the peer should be linked to.
|
||||||
|
@@ -100,6 +100,7 @@ func (s *Server) Run(ctx context.Context, listenAddress string) {
|
|||||||
srvContext, cancelFn := context.WithCancel(ctx)
|
srvContext, cancelFn := context.WithCancel(ctx)
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
var err error
|
||||||
|
slog.Debug("starting server", "certFile", s.cfg.Web.CertFile, "keyFile", s.cfg.Web.KeyFile)
|
||||||
if s.cfg.Web.CertFile != "" && s.cfg.Web.KeyFile != "" {
|
if s.cfg.Web.CertFile != "" && s.cfg.Web.KeyFile != "" {
|
||||||
err = srv.ListenAndServeTLS(s.cfg.Web.CertFile, s.cfg.Web.KeyFile)
|
err = srv.ListenAndServeTLS(s.cfg.Web.CertFile, s.cfg.Web.KeyFile)
|
||||||
} else {
|
} else {
|
||||||
@@ -138,7 +139,8 @@ func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
|||||||
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
|
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
|
||||||
s.versions[version].HandleFunc("GET /doc.html", s.rapiDocHandler(version))
|
s.versions[version].HandleFunc("GET /doc.html", s.rapiDocHandler(version))
|
||||||
|
|
||||||
groupSetupFn(s.versions[version])
|
versionGroup := s.versions[version].Group()
|
||||||
|
groupSetupFn(versionGroup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,12 +27,12 @@ type PeerServicePeerManager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PeerServiceConfigFileManager interface {
|
type PeerServiceConfigFileManager interface {
|
||||||
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||||
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerServiceMailManager interface {
|
type PeerServiceMailManager interface {
|
||||||
SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
|
SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion dependencies
|
// endregion dependencies
|
||||||
@@ -95,16 +95,24 @@ func (p PeerService) DeletePeer(ctx context.Context, id domain.PeerIdentifier) e
|
|||||||
return p.peers.DeletePeer(ctx, id)
|
return p.peers.DeletePeer(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PeerService) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
|
func (p PeerService) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) {
|
||||||
return p.configFile.GetPeerConfig(ctx, id)
|
return p.configFile.GetPeerConfig(ctx, id, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PeerService) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
|
func (p PeerService) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (
|
||||||
return p.configFile.GetPeerConfigQrCode(ctx, id)
|
io.Reader,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
return p.configFile.GetPeerConfigQrCode(ctx, id, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PeerService) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error {
|
func (p PeerService) SendPeerEmail(
|
||||||
return p.mailer.SendPeerEmail(ctx, linkOnly, peers...)
|
ctx context.Context,
|
||||||
|
linkOnly bool,
|
||||||
|
style string,
|
||||||
|
peers ...domain.PeerIdentifier,
|
||||||
|
) error {
|
||||||
|
return p.mailer.SendPeerEmail(ctx, linkOnly, style, peers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PeerService) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) {
|
func (p PeerService) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) {
|
||||||
|
@@ -21,17 +21,23 @@ import (
|
|||||||
//go:embed frontend_config.js.gotpl
|
//go:embed frontend_config.js.gotpl
|
||||||
var frontendJs embed.FS
|
var frontendJs embed.FS
|
||||||
|
|
||||||
|
type ControllerManager interface {
|
||||||
|
GetControllerNames() []config.BackendBase
|
||||||
|
}
|
||||||
|
|
||||||
type ConfigEndpoint struct {
|
type ConfigEndpoint struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
authenticator Authenticator
|
authenticator Authenticator
|
||||||
|
controllerMgr ControllerManager
|
||||||
|
|
||||||
tpl *respond.TemplateRenderer
|
tpl *respond.TemplateRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigEndpoint(cfg *config.Config, authenticator Authenticator) ConfigEndpoint {
|
func NewConfigEndpoint(cfg *config.Config, authenticator Authenticator, ctrlMgr ControllerManager) ConfigEndpoint {
|
||||||
ep := ConfigEndpoint{
|
ep := ConfigEndpoint{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
authenticator: authenticator,
|
authenticator: authenticator,
|
||||||
|
controllerMgr: ctrlMgr,
|
||||||
tpl: respond.NewTemplateRenderer(template.Must(template.ParseFS(frontendJs,
|
tpl: respond.NewTemplateRenderer(template.Must(template.ParseFS(frontendJs,
|
||||||
"frontend_config.js.gotpl"))),
|
"frontend_config.js.gotpl"))),
|
||||||
}
|
}
|
||||||
@@ -96,10 +102,36 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
sessionUser := domain.GetUserInfo(r.Context())
|
sessionUser := domain.GetUserInfo(r.Context())
|
||||||
|
|
||||||
|
controllerFn := func() []model.SettingsBackendNames {
|
||||||
|
controllers := e.controllerMgr.GetControllerNames()
|
||||||
|
names := make([]model.SettingsBackendNames, 0, len(controllers))
|
||||||
|
|
||||||
|
for _, controller := range controllers {
|
||||||
|
displayName := controller.GetDisplayName()
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = controller.Id // fallback to ID if no display name is set
|
||||||
|
}
|
||||||
|
if controller.Id == config.LocalBackendName {
|
||||||
|
displayName = "modals.interface-edit.backend.local" // use a localized string for the local backend
|
||||||
|
}
|
||||||
|
names = append(names, model.SettingsBackendNames{
|
||||||
|
Id: controller.Id,
|
||||||
|
Name: displayName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSocialLogin := len(e.cfg.Auth.OAuth) > 0 || len(e.cfg.Auth.OpenIDConnect) > 0 || e.cfg.Auth.WebAuthn.Enabled
|
||||||
|
|
||||||
// For anonymous users, we return the settings object with minimal information
|
// For anonymous users, we return the settings object with minimal information
|
||||||
if sessionUser.Id == domain.CtxUnknownUserId || sessionUser.Id == "" {
|
if sessionUser.Id == domain.CtxUnknownUserId || sessionUser.Id == "" {
|
||||||
respond.JSON(w, http.StatusOK, model.Settings{
|
respond.JSON(w, http.StatusOK, model.Settings{
|
||||||
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
||||||
|
AvailableBackends: []model.SettingsBackendNames{}, // return an empty list instead of null
|
||||||
|
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
respond.JSON(w, http.StatusOK, model.Settings{
|
respond.JSON(w, http.StatusOK, model.Settings{
|
||||||
@@ -109,6 +141,8 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
|||||||
ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly,
|
ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly,
|
||||||
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
||||||
MinPasswordLength: e.cfg.Auth.MinPasswordLength,
|
MinPasswordLength: e.cfg.Auth.MinPasswordLength,
|
||||||
|
AvailableBackends: controllerFn(),
|
||||||
|
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,11 +34,11 @@ type PeerService interface {
|
|||||||
// DeletePeer deletes the peer with the given id.
|
// DeletePeer deletes the peer with the given id.
|
||||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
// GetPeerConfig returns the peer configuration for the given id.
|
// GetPeerConfig returns the peer configuration for the given id.
|
||||||
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||||
// GetPeerConfigQrCode returns the peer configuration as qr code for the given id.
|
// GetPeerConfigQrCode returns the peer configuration as qr code for the given id.
|
||||||
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||||
// SendPeerEmail sends the peer configuration via email.
|
// SendPeerEmail sends the peer configuration via email.
|
||||||
SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
|
SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error
|
||||||
// GetPeerStats returns the peer stats for the given interface.
|
// GetPeerStats returns the peer stats for the given interface.
|
||||||
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
|
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
|
||||||
}
|
}
|
||||||
@@ -355,6 +355,7 @@ func (e PeerEndpoint) handleDelete() http.HandlerFunc {
|
|||||||
// @Summary Get peer configuration as string.
|
// @Summary Get peer configuration as string.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "The peer identifier"
|
// @Param id path string true "The peer identifier"
|
||||||
|
// @Param style query string false "The configuration style"
|
||||||
// @Success 200 {object} string
|
// @Success 200 {object} string
|
||||||
// @Failure 400 {object} model.Error
|
// @Failure 400 {object} model.Error
|
||||||
// @Failure 500 {object} model.Error
|
// @Failure 500 {object} model.Error
|
||||||
@@ -369,7 +370,9 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configTxt, err := e.peerService.GetPeerConfig(r.Context(), domain.PeerIdentifier(id))
|
configStyle := e.getConfigStyle(r)
|
||||||
|
|
||||||
|
configTxt, err := e.peerService.GetPeerConfig(r.Context(), domain.PeerIdentifier(id), configStyle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respond.JSON(w, http.StatusInternalServerError, model.Error{
|
respond.JSON(w, http.StatusInternalServerError, model.Error{
|
||||||
Code: http.StatusInternalServerError, Message: err.Error(),
|
Code: http.StatusInternalServerError, Message: err.Error(),
|
||||||
@@ -397,6 +400,7 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc {
|
|||||||
// @Produce png
|
// @Produce png
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "The peer identifier"
|
// @Param id path string true "The peer identifier"
|
||||||
|
// @Param style query string false "The configuration style"
|
||||||
// @Success 200 {file} binary
|
// @Success 200 {file} binary
|
||||||
// @Failure 400 {object} model.Error
|
// @Failure 400 {object} model.Error
|
||||||
// @Failure 500 {object} model.Error
|
// @Failure 500 {object} model.Error
|
||||||
@@ -411,7 +415,9 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configQr, err := e.peerService.GetPeerConfigQrCode(r.Context(), domain.PeerIdentifier(id))
|
configStyle := e.getConfigStyle(r)
|
||||||
|
|
||||||
|
configQr, err := e.peerService.GetPeerConfigQrCode(r.Context(), domain.PeerIdentifier(id), configStyle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respond.JSON(w, http.StatusInternalServerError, model.Error{
|
respond.JSON(w, http.StatusInternalServerError, model.Error{
|
||||||
Code: http.StatusInternalServerError, Message: err.Error(),
|
Code: http.StatusInternalServerError, Message: err.Error(),
|
||||||
@@ -438,6 +444,7 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc {
|
|||||||
// @Summary Send peer configuration via email.
|
// @Summary Send peer configuration via email.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param request body model.PeerMailRequest true "The peer mail request data"
|
// @Param request body model.PeerMailRequest true "The peer mail request data"
|
||||||
|
// @Param style query string false "The configuration style"
|
||||||
// @Success 204 "No content if mail sending was successful"
|
// @Success 204 "No content if mail sending was successful"
|
||||||
// @Failure 400 {object} model.Error
|
// @Failure 400 {object} model.Error
|
||||||
// @Failure 500 {object} model.Error
|
// @Failure 500 {object} model.Error
|
||||||
@@ -460,11 +467,13 @@ func (e PeerEndpoint) handleEmailPost() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configStyle := e.getConfigStyle(r)
|
||||||
|
|
||||||
peerIds := make([]domain.PeerIdentifier, len(req.Identifiers))
|
peerIds := make([]domain.PeerIdentifier, len(req.Identifiers))
|
||||||
for i := range req.Identifiers {
|
for i := range req.Identifiers {
|
||||||
peerIds[i] = domain.PeerIdentifier(req.Identifiers[i])
|
peerIds[i] = domain.PeerIdentifier(req.Identifiers[i])
|
||||||
}
|
}
|
||||||
if err := e.peerService.SendPeerEmail(r.Context(), req.LinkOnly, peerIds...); err != nil {
|
if err := e.peerService.SendPeerEmail(r.Context(), req.LinkOnly, configStyle, peerIds...); err != nil {
|
||||||
respond.JSON(w, http.StatusInternalServerError,
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
@@ -504,3 +513,11 @@ func (e PeerEndpoint) handleStatsGet() http.HandlerFunc {
|
|||||||
respond.JSON(w, http.StatusOK, model.NewPeerStats(e.cfg.Statistics.CollectPeerData, stats))
|
respond.JSON(w, http.StatusOK, model.NewPeerStats(e.cfg.Statistics.CollectPeerData, stats))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e PeerEndpoint) getConfigStyle(r *http.Request) string {
|
||||||
|
configStyle := request.QueryDefault(r, "style", domain.ConfigStyleWgQuick)
|
||||||
|
if configStyle != domain.ConfigStyleWgQuick && configStyle != domain.ConfigStyleRaw {
|
||||||
|
configStyle = domain.ConfigStyleWgQuick // default to wg-quick style
|
||||||
|
}
|
||||||
|
return configStyle
|
||||||
|
}
|
||||||
|
@@ -6,10 +6,17 @@ type Error struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
MailLinkOnly bool `json:"MailLinkOnly"`
|
MailLinkOnly bool `json:"MailLinkOnly"`
|
||||||
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
||||||
SelfProvisioning bool `json:"SelfProvisioning"`
|
SelfProvisioning bool `json:"SelfProvisioning"`
|
||||||
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
||||||
WebAuthnEnabled bool `json:"WebAuthnEnabled"`
|
WebAuthnEnabled bool `json:"WebAuthnEnabled"`
|
||||||
MinPasswordLength int `json:"MinPasswordLength"`
|
MinPasswordLength int `json:"MinPasswordLength"`
|
||||||
|
AvailableBackends []SettingsBackendNames `json:"AvailableBackends"`
|
||||||
|
LoginFormVisible bool `json:"LoginFormVisible"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsBackendNames struct {
|
||||||
|
Id string `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ type Interface struct {
|
|||||||
Identifier string `json:"Identifier" example:"wg0"` // device name, for example: wg0
|
Identifier string `json:"Identifier" example:"wg0"` // device name, for example: wg0
|
||||||
DisplayName string `json:"DisplayName"` // a nice display name/ description for the interface
|
DisplayName string `json:"DisplayName"` // a nice display name/ description for the interface
|
||||||
Mode string `json:"Mode" example:"server"` // the interface type, either 'server', 'client' or 'any'
|
Mode string `json:"Mode" example:"server"` // the interface type, either 'server', 'client' or 'any'
|
||||||
|
Backend string `json:"Backend" example:"local"` // the backend used for this interface e.g., local, mikrotik, ...
|
||||||
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server interface
|
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server interface
|
||||||
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
|
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
|
||||||
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
|
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||||
@@ -57,6 +59,7 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
|||||||
Identifier: string(src.Identifier),
|
Identifier: string(src.Identifier),
|
||||||
DisplayName: src.DisplayName,
|
DisplayName: src.DisplayName,
|
||||||
Mode: string(src.Type),
|
Mode: string(src.Type),
|
||||||
|
Backend: string(src.Backend),
|
||||||
PrivateKey: src.PrivateKey,
|
PrivateKey: src.PrivateKey,
|
||||||
PublicKey: src.PublicKey,
|
PublicKey: src.PublicKey,
|
||||||
Disabled: src.IsDisabled(),
|
Disabled: src.IsDisabled(),
|
||||||
@@ -92,6 +95,10 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
|||||||
Filename: src.GetConfigFileName(),
|
Filename: src.GetConfigFileName(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if iface.Backend == "" {
|
||||||
|
iface.Backend = config.LocalBackendName // default to local backend
|
||||||
|
}
|
||||||
|
|
||||||
if len(peers) > 0 {
|
if len(peers) > 0 {
|
||||||
iface.TotalPeers = len(peers)
|
iface.TotalPeers = len(peers)
|
||||||
|
|
||||||
@@ -146,6 +153,7 @@ func NewDomainInterface(src *Interface) *domain.Interface {
|
|||||||
SaveConfig: src.SaveConfig,
|
SaveConfig: src.SaveConfig,
|
||||||
DisplayName: src.DisplayName,
|
DisplayName: src.DisplayName,
|
||||||
Type: domain.InterfaceType(src.Mode),
|
Type: domain.InterfaceType(src.Mode),
|
||||||
|
Backend: domain.InterfaceBackend(src.Backend),
|
||||||
DriverType: "", // currently unused
|
DriverType: "", // currently unused
|
||||||
Disabled: nil, // set below
|
Disabled: nil, // set below
|
||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
|
@@ -172,13 +172,13 @@ func NewDomainPeer(src *Peer) *domain.Peer {
|
|||||||
|
|
||||||
type MultiPeerRequest struct {
|
type MultiPeerRequest struct {
|
||||||
Identifiers []string `json:"Identifiers"`
|
Identifiers []string `json:"Identifiers"`
|
||||||
Suffix string `json:"Suffix"`
|
Prefix string `json:"Prefix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDomainPeerCreationRequest(src *MultiPeerRequest) *domain.PeerCreationRequest {
|
func NewDomainPeerCreationRequest(src *MultiPeerRequest) *domain.PeerCreationRequest {
|
||||||
return &domain.PeerCreationRequest{
|
return &domain.PeerCreationRequest{
|
||||||
UserIdentifiers: src.Identifiers,
|
UserIdentifiers: src.Identifiers,
|
||||||
Suffix: src.Suffix,
|
Prefix: src.Prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ func NewPeerStats(enabled bool, src []domain.PeerStatus) *PeerStats {
|
|||||||
|
|
||||||
for _, srcStat := range src {
|
for _, srcStat := range src {
|
||||||
stats[string(srcStat.PeerId)] = PeerStatData{
|
stats[string(srcStat.PeerId)] = PeerStatData{
|
||||||
IsConnected: srcStat.IsConnected(),
|
IsConnected: srcStat.IsConnected,
|
||||||
IsPingable: srcStat.IsPingable,
|
IsPingable: srcStat.IsPingable,
|
||||||
LastPing: srcStat.LastPing,
|
LastPing: srcStat.LastPing,
|
||||||
BytesReceived: srcStat.BytesReceived,
|
BytesReceived: srcStat.BytesReceived,
|
||||||
|
@@ -23,8 +23,8 @@ type ProvisioningServicePeerManagerRepo interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProvisioningServiceConfigFileManagerRepo interface {
|
type ProvisioningServiceConfigFileManagerRepo interface {
|
||||||
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||||
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProvisioningService struct {
|
type ProvisioningService struct {
|
||||||
@@ -96,7 +96,7 @@ func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.Pe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
|
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier, domain.ConfigStyleWgQuick)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.Pee
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
|
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier, domain.ConfigStyleWgQuick)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,11 @@ func (p ProvisioningService) NewPeer(ctx context.Context, req models.Provisionin
|
|||||||
if req.PresharedKey != "" {
|
if req.PresharedKey != "" {
|
||||||
peer.PresharedKey = domain.PreSharedKey(req.PresharedKey)
|
peer.PresharedKey = domain.PreSharedKey(req.PresharedKey)
|
||||||
}
|
}
|
||||||
peer.GenerateDisplayName("API")
|
if req.DisplayName == "" {
|
||||||
|
peer.GenerateDisplayName("API")
|
||||||
|
} else {
|
||||||
|
peer.DisplayName = req.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
// save new peer
|
// save new peer
|
||||||
peer, err = p.peers.CreatePeer(ctx, peer)
|
peer, err = p.peers.CreatePeer(ctx, peer)
|
||||||
|
@@ -68,6 +68,10 @@ type ProvisioningRequest struct {
|
|||||||
// If no user identifier is set, the authenticated user is used.
|
// If no user identifier is set, the authenticated user is used.
|
||||||
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
|
||||||
|
// DisplayName is an optional name for the new peer.
|
||||||
|
// If unset, a default template value (e.g., "API Peer ...") will be assigned.
|
||||||
|
DisplayName string `json:"DisplayName" example:"API Peer xyz" binding:"omitempty"`
|
||||||
|
|
||||||
// PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.
|
// PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.
|
||||||
PublicKey string `json:"PublicKey" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"omitempty,len=44"`
|
PublicKey string `json:"PublicKey" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"omitempty,len=44"`
|
||||||
// PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.
|
// PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.
|
||||||
|
@@ -46,7 +46,7 @@ func Initialize(
|
|||||||
users: users,
|
users: users,
|
||||||
}
|
}
|
||||||
|
|
||||||
startupContext, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
startupContext, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Switch to admin user context
|
// Switch to admin user context
|
||||||
|
@@ -93,6 +93,8 @@ type Authenticator struct {
|
|||||||
// URL prefix for the callback endpoints, this is a combination of the external URL and the API prefix
|
// URL prefix for the callback endpoints, this is a combination of the external URL and the API prefix
|
||||||
callbackUrlPrefix string
|
callbackUrlPrefix string
|
||||||
|
|
||||||
|
callbackUrl *url.URL
|
||||||
|
|
||||||
users UserManager
|
users UserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,82 +104,136 @@ func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserM
|
|||||||
error,
|
error,
|
||||||
) {
|
) {
|
||||||
a := &Authenticator{
|
a := &Authenticator{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
users: users,
|
users: users,
|
||||||
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
|
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
|
||||||
|
oauthAuthenticators: make(map[string]AuthenticatorOauth, len(cfg.OpenIDConnect)+len(cfg.OAuth)),
|
||||||
|
ldapAuthenticators: make(map[string]AuthenticatorLdap, len(cfg.Ldap)),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
parsedExtUrl, err := url.Parse(a.callbackUrlPrefix)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := a.setupExternalAuthProviders(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to parse external URL: %w", err)
|
||||||
}
|
}
|
||||||
|
a.callbackUrl = parsedExtUrl
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) setupExternalAuthProviders(ctx context.Context) error {
|
// StartBackgroundJobs starts the background jobs for the authenticator.
|
||||||
extUrl, err := url.Parse(a.callbackUrlPrefix)
|
// It sets up the external authentication providers (OIDC, OAuth, LDAP) and retries in case of errors.
|
||||||
if err != nil {
|
func (a *Authenticator) StartBackgroundJobs(ctx context.Context) {
|
||||||
return fmt.Errorf("failed to parse external url: %w", err)
|
go func() {
|
||||||
}
|
// Initialize local copies of authentication providers to allow retry in case of errors
|
||||||
|
oidcQueue := a.cfg.OpenIDConnect
|
||||||
|
oauthQueue := a.cfg.OAuth
|
||||||
|
ldapQueue := a.cfg.Ldap
|
||||||
|
|
||||||
a.oauthAuthenticators = make(map[string]AuthenticatorOauth, len(a.cfg.OpenIDConnect)+len(a.cfg.OAuth))
|
ticker := time.NewTicker(30 * time.Second) // Ticker for delay between retries
|
||||||
a.ldapAuthenticators = make(map[string]AuthenticatorLdap, len(a.cfg.Ldap))
|
defer ticker.Stop()
|
||||||
|
|
||||||
for i := range a.cfg.OpenIDConnect { // OIDC
|
for {
|
||||||
providerCfg := &a.cfg.OpenIDConnect[i]
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
failedOidc, failedOauth, failedLdap := a.setupExternalAuthProviders(oidcQueue, oauthQueue, ldapQueue)
|
||||||
|
if len(failedOidc) > 0 || len(failedOauth) > 0 || len(failedLdap) > 0 {
|
||||||
|
slog.Warn("failed to setup some external auth providers, retrying in 30 seconds",
|
||||||
|
"failedOidc", len(failedOidc), "failedOauth", len(failedOauth), "failedLdap", len(failedLdap))
|
||||||
|
// Retry failed providers
|
||||||
|
oidcQueue = failedOidc
|
||||||
|
oauthQueue = failedOauth
|
||||||
|
ldapQueue = failedLdap
|
||||||
|
} else {
|
||||||
|
slog.Info("successfully setup all external auth providers")
|
||||||
|
return // Exit goroutine if all providers are set up successfully
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Info("context cancelled, stopping setup of external auth providers")
|
||||||
|
return // Exit goroutine if context is cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) setupExternalAuthProviders(
|
||||||
|
oidc []config.OpenIDConnectProvider,
|
||||||
|
oauth []config.OAuthProvider,
|
||||||
|
ldap []config.LdapProvider,
|
||||||
|
) (
|
||||||
|
[]config.OpenIDConnectProvider,
|
||||||
|
[]config.OAuthProvider,
|
||||||
|
[]config.LdapProvider,
|
||||||
|
) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var failedOidc []config.OpenIDConnectProvider
|
||||||
|
var failedOauth []config.OAuthProvider
|
||||||
|
var failedLdap []config.LdapProvider
|
||||||
|
|
||||||
|
for i := range oidc { // OIDC
|
||||||
|
providerCfg := &oidc[i]
|
||||||
providerId := strings.ToLower(providerCfg.ProviderName)
|
providerId := strings.ToLower(providerCfg.ProviderName)
|
||||||
|
|
||||||
if _, exists := a.oauthAuthenticators[providerId]; exists {
|
if _, exists := a.oauthAuthenticators[providerId]; exists {
|
||||||
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
|
// this is an unrecoverable error, we cannot register the same provider twice
|
||||||
|
slog.Error("OIDC auth provider is already registered", "name", providerId)
|
||||||
|
continue // skip this provider
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectUrl := *extUrl
|
redirectUrl := *a.callbackUrl
|
||||||
redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback")
|
redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback")
|
||||||
|
|
||||||
provider, err := newOidcAuthenticator(ctx, redirectUrl.String(), providerCfg)
|
provider, err := newOidcAuthenticator(ctx, redirectUrl.String(), providerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to setup oidc authentication provider %s: %w", providerCfg.ProviderName, err)
|
failedOidc = append(failedOidc, oidc[i])
|
||||||
|
slog.Error("failed to setup oidc authentication provider", "name", providerId, "error", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
a.oauthAuthenticators[providerId] = provider
|
a.oauthAuthenticators[providerId] = provider
|
||||||
}
|
}
|
||||||
for i := range a.cfg.OAuth { // PLAIN OAUTH
|
for i := range oauth { // PLAIN OAUTH
|
||||||
providerCfg := &a.cfg.OAuth[i]
|
providerCfg := &oauth[i]
|
||||||
providerId := strings.ToLower(providerCfg.ProviderName)
|
providerId := strings.ToLower(providerCfg.ProviderName)
|
||||||
|
|
||||||
if _, exists := a.oauthAuthenticators[providerId]; exists {
|
if _, exists := a.oauthAuthenticators[providerId]; exists {
|
||||||
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
|
// this is an unrecoverable error, we cannot register the same provider twice
|
||||||
|
slog.Error("OAUTH auth provider is already registered", "name", providerId)
|
||||||
|
continue // skip this provider
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectUrl := *extUrl
|
redirectUrl := *a.callbackUrl
|
||||||
redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback")
|
redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback")
|
||||||
|
|
||||||
provider, err := newPlainOauthAuthenticator(ctx, redirectUrl.String(), providerCfg)
|
provider, err := newPlainOauthAuthenticator(ctx, redirectUrl.String(), providerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to setup oauth authentication provider %s: %w", providerId, err)
|
failedOauth = append(failedOauth, oauth[i])
|
||||||
|
slog.Error("failed to setup oauth authentication provider", "name", providerId, "error", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
a.oauthAuthenticators[providerId] = provider
|
a.oauthAuthenticators[providerId] = provider
|
||||||
}
|
}
|
||||||
for i := range a.cfg.Ldap { // LDAP
|
for i := range ldap { // LDAP
|
||||||
providerCfg := &a.cfg.Ldap[i]
|
providerCfg := &ldap[i]
|
||||||
providerId := strings.ToLower(providerCfg.URL)
|
providerId := strings.ToLower(providerCfg.URL)
|
||||||
|
|
||||||
if _, exists := a.ldapAuthenticators[providerId]; exists {
|
if _, exists := a.ldapAuthenticators[providerId]; exists {
|
||||||
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
|
// this is an unrecoverable error, we cannot register the same provider twice
|
||||||
|
slog.Error("LDAP auth provider is already registered", "name", providerId)
|
||||||
|
continue // skip this provider
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := newLdapAuthenticator(ctx, providerCfg)
|
provider, err := newLdapAuthenticator(ctx, providerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to setup ldap authentication provider %s: %w", providerId, err)
|
failedLdap = append(failedLdap, ldap[i])
|
||||||
|
slog.Error("failed to setup ldap authentication provider", "name", providerId, "error", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
a.ldapAuthenticators[providerId] = provider
|
a.ldapAuthenticators[providerId] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return failedOidc, failedOauth, failedLdap
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExternalLoginProviders returns a list of all available external login providers.
|
// GetExternalLoginProviders returns a list of all available external login providers.
|
||||||
@@ -434,6 +490,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
|
|||||||
return nil, fmt.Errorf("unable to parse user information: %w", err)
|
return nil, fmt.Errorf("unable to parse user information: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) {
|
||||||
|
return nil, fmt.Errorf("user %s is not in allowed domains", userInfo.Email)
|
||||||
|
}
|
||||||
|
|
||||||
ctx = domain.SetUserInfo(ctx,
|
ctx = domain.SetUserInfo(ctx,
|
||||||
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
|
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
|
||||||
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(),
|
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(),
|
||||||
@@ -450,10 +510,6 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
|
|||||||
return nil, fmt.Errorf("unable to process user information: %w", err)
|
return nil, fmt.Errorf("unable to process user information: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) {
|
|
||||||
return nil, fmt.Errorf("user is not in allowed domains: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.IsLocked() || user.IsDisabled() {
|
if user.IsLocked() || user.IsDisabled() {
|
||||||
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
|
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
|
@@ -54,7 +54,7 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
|
|||||||
|
|
||||||
attrs := []string{"dn"}
|
attrs := []string{"dn"}
|
||||||
|
|
||||||
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1)
|
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", ldap.EscapeFilter(string(userId)), -1)
|
||||||
searchRequest := ldap.NewSearchRequest(
|
searchRequest := ldap.NewSearchRequest(
|
||||||
l.cfg.BaseDN,
|
l.cfg.BaseDN,
|
||||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
|
||||||
@@ -100,7 +100,7 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
|
|||||||
|
|
||||||
attrs := internal.LdapSearchAttributes(&l.cfg.FieldMap)
|
attrs := internal.LdapSearchAttributes(&l.cfg.FieldMap)
|
||||||
|
|
||||||
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1)
|
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", ldap.EscapeFilter(string(userId)), -1)
|
||||||
searchRequest := ldap.NewSearchRequest(
|
searchRequest := ldap.NewSearchRequest(
|
||||||
l.cfg.BaseDN,
|
l.cfg.BaseDN,
|
||||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
|
||||||
|
@@ -46,7 +46,7 @@ type TemplateRenderer interface {
|
|||||||
// GetInterfaceConfig returns the configuration file for the given interface.
|
// GetInterfaceConfig returns the configuration file for the given interface.
|
||||||
GetInterfaceConfig(iface *domain.Interface, peers []domain.Peer) (io.Reader, error)
|
GetInterfaceConfig(iface *domain.Interface, peers []domain.Peer) (io.Reader, error)
|
||||||
// GetPeerConfig returns the configuration file for the given peer.
|
// GetPeerConfig returns the configuration file for the given peer.
|
||||||
GetPeerConfig(peer *domain.Peer) (io.Reader, error)
|
GetPeerConfig(peer *domain.Peer, style string) (io.Reader, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventBus interface {
|
type EventBus interface {
|
||||||
@@ -186,7 +186,7 @@ func (m Manager) GetInterfaceConfig(ctx context.Context, id domain.InterfaceIden
|
|||||||
|
|
||||||
// GetPeerConfig returns the configuration file for the given peer.
|
// GetPeerConfig returns the configuration file for the given peer.
|
||||||
// The file is structured in wg-quick format.
|
// The file is structured in wg-quick format.
|
||||||
func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
|
func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) {
|
||||||
peer, err := m.wg.GetPeer(ctx, id)
|
peer, err := m.wg.GetPeer(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err)
|
return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err)
|
||||||
@@ -196,11 +196,11 @@ func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (i
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.tplHandler.GetPeerConfig(peer)
|
return m.tplHandler.GetPeerConfig(peer, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeerConfigQrCode returns a QR code image containing the configuration for the given peer.
|
// GetPeerConfigQrCode returns a QR code image containing the configuration for the given peer.
|
||||||
func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
|
func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) {
|
||||||
peer, err := m.wg.GetPeer(ctx, id)
|
peer, err := m.wg.GetPeer(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err)
|
return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err)
|
||||||
@@ -210,7 +210,7 @@ func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgData, err := m.tplHandler.GetPeerConfig(peer)
|
cfgData, err := m.tplHandler.GetPeerConfig(peer, style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err)
|
return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
@@ -55,11 +55,12 @@ func (c TemplateHandler) GetInterfaceConfig(cfg *domain.Interface, peers []domai
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetPeerConfig returns the rendered configuration file for a WireGuard peer.
|
// GetPeerConfig returns the rendered configuration file for a WireGuard peer.
|
||||||
func (c TemplateHandler) GetPeerConfig(peer *domain.Peer) (io.Reader, error) {
|
func (c TemplateHandler) GetPeerConfig(peer *domain.Peer, style string) (io.Reader, error) {
|
||||||
var tplBuff bytes.Buffer
|
var tplBuff bytes.Buffer
|
||||||
|
|
||||||
err := c.templates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]any{
|
err := c.templates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]any{
|
||||||
"Peer": peer,
|
"Style": style,
|
||||||
|
"Peer": peer,
|
||||||
"Portal": map[string]any{
|
"Portal": map[string]any{
|
||||||
"Version": "unknown",
|
"Version": "unknown",
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
# AUTOGENERATED FILE - DO NOT EDIT
|
# AUTOGENERATED FILE - DO NOT EDIT
|
||||||
# This file uses wg-quick format.
|
# This file uses {{ .Style }} format.
|
||||||
|
{{- if eq .Style "wgquick"}}
|
||||||
# See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
|
# See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
|
||||||
|
{{- end}}
|
||||||
# Lines starting with the -WGP- tag are used by
|
# Lines starting with the -WGP- tag are used by
|
||||||
# the WireGuard Portal configuration parser.
|
# the WireGuard Portal configuration parser.
|
||||||
|
|
||||||
@@ -21,22 +23,27 @@
|
|||||||
|
|
||||||
# Core settings
|
# Core settings
|
||||||
PrivateKey = {{ .Peer.Interface.KeyPair.PrivateKey }}
|
PrivateKey = {{ .Peer.Interface.KeyPair.PrivateKey }}
|
||||||
|
{{- if eq .Style "wgquick"}}
|
||||||
Address = {{ CidrsToString .Peer.Interface.Addresses }}
|
Address = {{ CidrsToString .Peer.Interface.Addresses }}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
# Misc. settings (optional)
|
# Misc. settings (optional)
|
||||||
|
{{- if eq .Style "wgquick"}}
|
||||||
{{- if .Peer.Interface.DnsStr.GetValue}}
|
{{- if .Peer.Interface.DnsStr.GetValue}}
|
||||||
DNS = {{ .Peer.Interface.DnsStr.GetValue }} {{- if .Peer.Interface.DnsSearchStr.GetValue}}, {{ .Peer.Interface.DnsSearchStr.GetValue }} {{- end}}
|
DNS = {{ .Peer.Interface.DnsStr.GetValue }} {{- if .Peer.Interface.DnsSearchStr.GetValue}}, {{ .Peer.Interface.DnsSearchStr.GetValue }} {{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- if ne .Peer.Interface.Mtu.GetValue 0}}
|
{{- if ne .Peer.Interface.Mtu.GetValue 0}}
|
||||||
MTU = {{ .Peer.Interface.Mtu.GetValue }}
|
MTU = {{ .Peer.Interface.Mtu.GetValue }}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- if ne .Peer.Interface.FirewallMark.GetValue 0}}
|
|
||||||
FwMark = {{ .Peer.Interface.FirewallMark.GetValue }}
|
|
||||||
{{- end}}
|
|
||||||
{{- if ne .Peer.Interface.RoutingTable.GetValue ""}}
|
{{- if ne .Peer.Interface.RoutingTable.GetValue ""}}
|
||||||
Table = {{ .Peer.Interface.RoutingTable.GetValue }}
|
Table = {{ .Peer.Interface.RoutingTable.GetValue }}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
{{- if ne .Peer.Interface.FirewallMark.GetValue 0}}
|
||||||
|
FwMark = {{ .Peer.Interface.FirewallMark.GetValue }}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{- if eq .Style "wgquick"}}
|
||||||
# Interface hooks (optional)
|
# Interface hooks (optional)
|
||||||
{{- if .Peer.Interface.PreUp.GetValue}}
|
{{- if .Peer.Interface.PreUp.GetValue}}
|
||||||
PreUp = {{ .Peer.Interface.PreUp.GetValue }}
|
PreUp = {{ .Peer.Interface.PreUp.GetValue }}
|
||||||
@@ -50,6 +57,7 @@ PreDown = {{ .Peer.Interface.PreDown.GetValue }}
|
|||||||
{{- if .Peer.Interface.PostDown.GetValue}}
|
{{- if .Peer.Interface.PostDown.GetValue}}
|
||||||
PostDown = {{ .Peer.Interface.PostDown.GetValue }}
|
PostDown = {{ .Peer.Interface.PostDown.GetValue }}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
PublicKey = {{ .Peer.EndpointPublicKey.GetValue }}
|
PublicKey = {{ .Peer.EndpointPublicKey.GetValue }}
|
||||||
|
@@ -36,6 +36,7 @@ const TopicPeerDeleted = "peer:deleted"
|
|||||||
const TopicPeerUpdated = "peer:updated"
|
const TopicPeerUpdated = "peer:updated"
|
||||||
const TopicPeerInterfaceUpdated = "peer:interface:updated"
|
const TopicPeerInterfaceUpdated = "peer:interface:updated"
|
||||||
const TopicPeerIdentifierUpdated = "peer:identifier:updated"
|
const TopicPeerIdentifierUpdated = "peer:identifier:updated"
|
||||||
|
const TopicPeerStateChanged = "peer:state:changed"
|
||||||
|
|
||||||
// endregion peer-events
|
// endregion peer-events
|
||||||
|
|
||||||
|
@@ -21,9 +21,9 @@ type ConfigFileManager interface {
|
|||||||
// GetInterfaceConfig returns the configuration for the given interface.
|
// GetInterfaceConfig returns the configuration for the given interface.
|
||||||
GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error)
|
GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error)
|
||||||
// GetPeerConfig returns the configuration for the given peer.
|
// GetPeerConfig returns the configuration for the given peer.
|
||||||
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||||
// GetPeerConfigQrCode returns the QR code for the given peer.
|
// GetPeerConfigQrCode returns the QR code for the given peer.
|
||||||
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserDatabaseRepo interface {
|
type UserDatabaseRepo interface {
|
||||||
@@ -71,7 +71,7 @@ func NewMailManager(
|
|||||||
users UserDatabaseRepo,
|
users UserDatabaseRepo,
|
||||||
wg WireguardDatabaseRepo,
|
wg WireguardDatabaseRepo,
|
||||||
) (*Manager, error) {
|
) (*Manager, error) {
|
||||||
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl)
|
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
|
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ func NewMailManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendPeerEmail sends an email to the user linked to the given peers.
|
// SendPeerEmail sends an email to the user linked to the given peers.
|
||||||
func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error {
|
func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error {
|
||||||
for _, peerId := range peers {
|
for _, peerId := range peers {
|
||||||
peer, err := m.wg.GetPeer(ctx, peerId)
|
peer, err := m.wg.GetPeer(ctx, peerId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -123,7 +123,7 @@ func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...doma
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.sendPeerEmail(ctx, linkOnly, user, peer)
|
err = m.sendPeerEmail(ctx, linkOnly, style, user, peer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to send peer email for %s: %w", peerId, err)
|
return fmt.Errorf("failed to send peer email for %s: %w", peerId, err)
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,13 @@ func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...doma
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) sendPeerEmail(ctx context.Context, linkOnly bool, user *domain.User, peer *domain.Peer) error {
|
func (m Manager) sendPeerEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
linkOnly bool,
|
||||||
|
style string,
|
||||||
|
user *domain.User,
|
||||||
|
peer *domain.Peer,
|
||||||
|
) error {
|
||||||
qrName := "WireGuardQRCode.png"
|
qrName := "WireGuardQRCode.png"
|
||||||
configName := peer.GetConfigFileName()
|
configName := peer.GetConfigFileName()
|
||||||
|
|
||||||
@@ -148,12 +154,12 @@ func (m Manager) sendPeerEmail(ctx context.Context, linkOnly bool, user *domain.
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier)
|
peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier, style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch peer config for %s: %w", peer.Identifier, err)
|
return fmt.Errorf("failed to fetch peer config for %s: %w", peer.Identifier, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConfigQr, err := m.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
|
peerConfigQr, err := m.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier, style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch peer config QR code for %s: %w", peer.Identifier, err)
|
return fmt.Errorf("failed to fetch peer config QR code for %s: %w", peer.Identifier, err)
|
||||||
}
|
}
|
||||||
|
@@ -17,11 +17,12 @@ var TemplateFiles embed.FS
|
|||||||
// TemplateHandler is a struct that holds the html and text templates.
|
// TemplateHandler is a struct that holds the html and text templates.
|
||||||
type TemplateHandler struct {
|
type TemplateHandler struct {
|
||||||
portalUrl string
|
portalUrl string
|
||||||
|
portalName string
|
||||||
htmlTemplates *htmlTemplate.Template
|
htmlTemplates *htmlTemplate.Template
|
||||||
textTemplates *template.Template
|
textTemplates *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTemplateHandler(portalUrl string) (*TemplateHandler, error) {
|
func newTemplateHandler(portalUrl, portalName string) (*TemplateHandler, error) {
|
||||||
htmlTemplateCache, err := htmlTemplate.New("Html").ParseFS(TemplateFiles, "tpl_files/*.gohtml")
|
htmlTemplateCache, err := htmlTemplate.New("Html").ParseFS(TemplateFiles, "tpl_files/*.gohtml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse html template files: %w", err)
|
return nil, fmt.Errorf("failed to parse html template files: %w", err)
|
||||||
@@ -34,6 +35,7 @@ func newTemplateHandler(portalUrl string) (*TemplateHandler, error) {
|
|||||||
|
|
||||||
handler := &TemplateHandler{
|
handler := &TemplateHandler{
|
||||||
portalUrl: portalUrl,
|
portalUrl: portalUrl,
|
||||||
|
portalName: portalName,
|
||||||
htmlTemplates: htmlTemplateCache,
|
htmlTemplates: htmlTemplateCache,
|
||||||
textTemplates: txtTemplateCache,
|
textTemplates: txtTemplateCache,
|
||||||
}
|
}
|
||||||
@@ -81,6 +83,7 @@ func (c TemplateHandler) GetConfigMailWithAttachment(user *domain.User, cfgName,
|
|||||||
"ConfigFileName": cfgName,
|
"ConfigFileName": cfgName,
|
||||||
"QrcodePngName": qrName,
|
"QrcodePngName": qrName,
|
||||||
"PortalUrl": c.portalUrl,
|
"PortalUrl": c.portalUrl,
|
||||||
|
"PortalName": c.portalName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gotpl: %w", err)
|
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gotpl: %w", err)
|
||||||
@@ -91,6 +94,7 @@ func (c TemplateHandler) GetConfigMailWithAttachment(user *domain.User, cfgName,
|
|||||||
"ConfigFileName": cfgName,
|
"ConfigFileName": cfgName,
|
||||||
"QrcodePngName": qrName,
|
"QrcodePngName": qrName,
|
||||||
"PortalUrl": c.portalUrl,
|
"PortalUrl": c.portalUrl,
|
||||||
|
"PortalName": c.portalName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gohtml: %w", err)
|
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gohtml: %w", err)
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
<!--[if !mso]><!-->
|
<!--[if !mso]><!-->
|
||||||
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
<title>Email Template</title>
|
<title>{{$.PortalName}}</title>
|
||||||
<!--[if gte mso 9]>
|
<!--[if gte mso 9]>
|
||||||
<style type="text/css" media="all">
|
<style type="text/css" media="all">
|
||||||
sup { font-size: 100% !important; }
|
sup { font-size: 100% !important; }
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
<td align="left">
|
<td align="left">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="blue-button text-button" style="background:#000000; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
<td class="blue-button text-button" style="background:#000000; color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
@@ -167,10 +167,10 @@
|
|||||||
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td>
|
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated by {{$.PortalName}}.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td>
|
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit {{$.PortalName}}</span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -20,5 +20,5 @@ You can download and install the WireGuard VPN client from:
|
|||||||
https://www.wireguard.com/install/
|
https://www.wireguard.com/install/
|
||||||
|
|
||||||
|
|
||||||
This mail was generated using WireGuard Portal.
|
This mail was generated by {{$.PortalName}}.
|
||||||
{{$.PortalUrl}}
|
{{$.PortalUrl}}
|
@@ -19,7 +19,7 @@
|
|||||||
<!--[if !mso]><!-->
|
<!--[if !mso]><!-->
|
||||||
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
<title>Email Template</title>
|
<title>{{$.PortalName}}</title>
|
||||||
<!--[if gte mso 9]>
|
<!--[if gte mso 9]>
|
||||||
<style type="text/css" media="all">
|
<style type="text/css" media="all">
|
||||||
sup { font-size: 100% !important; }
|
sup { font-size: 100% !important; }
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
<td align="left">
|
<td align="left">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="blue-button text-button" style="background:#000000; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
<td class="blue-button text-button" style="background:#000000; color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
@@ -167,10 +167,10 @@
|
|||||||
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td>
|
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated by {{$.PortalName}}.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td>
|
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit {{$.PortalName}}</span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -20,5 +20,5 @@ You can download and install the WireGuard VPN client from:
|
|||||||
https://www.wireguard.com/install/
|
https://www.wireguard.com/install/
|
||||||
|
|
||||||
|
|
||||||
This mail was generated using WireGuard Portal.
|
This mail was generated by {{$.PortalName}}.
|
||||||
{{$.PortalUrl}}
|
{{$.PortalUrl}}
|
@@ -47,11 +47,18 @@ func migrateFromV1(db *gorm.DB, source, typ string) error {
|
|||||||
}
|
}
|
||||||
latestVersion := "1.0.9"
|
latestVersion := "1.0.9"
|
||||||
if lastVersion.Version != latestVersion {
|
if lastVersion.Version != latestVersion {
|
||||||
return fmt.Errorf("unsupported old version, update to database version %s first: %w", latestVersion, err)
|
return fmt.Errorf("unsupported old version, update to database version %s first", latestVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("found valid V1 database", "version", lastVersion.Version)
|
slog.Info("found valid V1 database", "version", lastVersion.Version)
|
||||||
|
|
||||||
|
// validate target database
|
||||||
|
if err := validateTargetDatabase(db); err != nil {
|
||||||
|
return fmt.Errorf("target database validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("found valid target database, starting migration...")
|
||||||
|
|
||||||
if err := migrateV1Users(oldDb, db); err != nil {
|
if err := migrateV1Users(oldDb, db); err != nil {
|
||||||
return fmt.Errorf("user migration failed: %w", err)
|
return fmt.Errorf("user migration failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -70,6 +77,36 @@ func migrateFromV1(db *gorm.DB, source, typ string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateTargetDatabase checks if the target database is empty and ready for migration.
|
||||||
|
func validateTargetDatabase(db *gorm.DB) error {
|
||||||
|
var count int64
|
||||||
|
err := db.Model(&domain.User{}).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check user table: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("target database contains %d users, please use an empty database for migration", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(&domain.Interface{}).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check interface table: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("target database contains %d interfaces, please use an empty database for migration", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(&domain.Peer{}).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check peer table: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("target database contains %d peers, please use an empty database for migration", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
||||||
type User struct {
|
type User struct {
|
||||||
Email string `gorm:"primaryKey"`
|
Email string `gorm:"primaryKey"`
|
||||||
@@ -123,7 +160,7 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
|||||||
LinkedPeerCount: 0,
|
LinkedPeerCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := newDb.Save(&newUser).Error; err != nil {
|
if err := newDb.Create(&newUser).Error; err != nil {
|
||||||
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
|
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +254,8 @@ func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
|
|||||||
PeerDefPostDown: "",
|
PeerDefPostDown: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := newDb.Save(&newInterface).Error; err != nil {
|
// Create new interface with associations
|
||||||
|
if err := newDb.Create(&newInterface).Error; err != nil {
|
||||||
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
|
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +400,7 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := newDb.Save(&newPeer).Error; err != nil {
|
if err := newDb.Create(&newPeer).Error; err != nil {
|
||||||
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
|
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/webhooks/models"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -64,6 +65,7 @@ func (m Manager) connectToMessageBus() {
|
|||||||
_ = m.bus.Subscribe(app.TopicPeerCreated, m.handlePeerCreateEvent)
|
_ = m.bus.Subscribe(app.TopicPeerCreated, m.handlePeerCreateEvent)
|
||||||
_ = m.bus.Subscribe(app.TopicPeerUpdated, m.handlePeerUpdateEvent)
|
_ = m.bus.Subscribe(app.TopicPeerUpdated, m.handlePeerUpdateEvent)
|
||||||
_ = m.bus.Subscribe(app.TopicPeerDeleted, m.handlePeerDeleteEvent)
|
_ = m.bus.Subscribe(app.TopicPeerDeleted, m.handlePeerDeleteEvent)
|
||||||
|
_ = m.bus.Subscribe(app.TopicPeerStateChanged, m.handlePeerStateChangeEvent)
|
||||||
|
|
||||||
_ = m.bus.Subscribe(app.TopicInterfaceCreated, m.handleInterfaceCreateEvent)
|
_ = m.bus.Subscribe(app.TopicInterfaceCreated, m.handleInterfaceCreateEvent)
|
||||||
_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceUpdateEvent)
|
_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceUpdateEvent)
|
||||||
@@ -100,39 +102,47 @@ func (m Manager) sendWebhook(ctx context.Context, data io.Reader) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleUserCreateEvent(user domain.User) {
|
func (m Manager) handleUserCreateEvent(user domain.User) {
|
||||||
m.handleGenericEvent(WebhookEventCreate, user)
|
m.handleGenericEvent(WebhookEventCreate, models.NewUser(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleUserUpdateEvent(user domain.User) {
|
func (m Manager) handleUserUpdateEvent(user domain.User) {
|
||||||
m.handleGenericEvent(WebhookEventUpdate, user)
|
m.handleGenericEvent(WebhookEventUpdate, models.NewUser(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleUserDeleteEvent(user domain.User) {
|
func (m Manager) handleUserDeleteEvent(user domain.User) {
|
||||||
m.handleGenericEvent(WebhookEventDelete, user)
|
m.handleGenericEvent(WebhookEventDelete, models.NewUser(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handlePeerCreateEvent(peer domain.Peer) {
|
func (m Manager) handlePeerCreateEvent(peer domain.Peer) {
|
||||||
m.handleGenericEvent(WebhookEventCreate, peer)
|
m.handleGenericEvent(WebhookEventCreate, models.NewPeer(peer))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handlePeerUpdateEvent(peer domain.Peer) {
|
func (m Manager) handlePeerUpdateEvent(peer domain.Peer) {
|
||||||
m.handleGenericEvent(WebhookEventUpdate, peer)
|
m.handleGenericEvent(WebhookEventUpdate, models.NewPeer(peer))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handlePeerDeleteEvent(peer domain.Peer) {
|
func (m Manager) handlePeerDeleteEvent(peer domain.Peer) {
|
||||||
m.handleGenericEvent(WebhookEventDelete, peer)
|
m.handleGenericEvent(WebhookEventDelete, models.NewPeer(peer))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleInterfaceCreateEvent(iface domain.Interface) {
|
func (m Manager) handleInterfaceCreateEvent(iface domain.Interface) {
|
||||||
m.handleGenericEvent(WebhookEventCreate, iface)
|
m.handleGenericEvent(WebhookEventCreate, models.NewInterface(iface))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleInterfaceUpdateEvent(iface domain.Interface) {
|
func (m Manager) handleInterfaceUpdateEvent(iface domain.Interface) {
|
||||||
m.handleGenericEvent(WebhookEventUpdate, iface)
|
m.handleGenericEvent(WebhookEventUpdate, models.NewInterface(iface))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleInterfaceDeleteEvent(iface domain.Interface) {
|
func (m Manager) handleInterfaceDeleteEvent(iface domain.Interface) {
|
||||||
m.handleGenericEvent(WebhookEventDelete, iface)
|
m.handleGenericEvent(WebhookEventDelete, models.NewInterface(iface))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Manager) handlePeerStateChangeEvent(peerStatus domain.PeerStatus, peer domain.Peer) {
|
||||||
|
if peerStatus.IsConnected {
|
||||||
|
m.handleGenericEvent(WebhookEventConnect, models.NewPeerMetrics(peerStatus, peer))
|
||||||
|
} else {
|
||||||
|
m.handleGenericEvent(WebhookEventDisconnect, models.NewPeerMetrics(peerStatus, peer))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleGenericEvent(action WebhookEvent, payload any) {
|
func (m Manager) handleGenericEvent(action WebhookEvent, payload any) {
|
||||||
@@ -168,15 +178,18 @@ func (m Manager) createWebhookData(action WebhookEvent, payload any) (*WebhookDa
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch v := payload.(type) {
|
switch v := payload.(type) {
|
||||||
case domain.User:
|
case models.User:
|
||||||
d.Entity = WebhookEntityUser
|
d.Entity = WebhookEntityUser
|
||||||
d.Identifier = string(v.Identifier)
|
d.Identifier = v.Identifier
|
||||||
case domain.Peer:
|
case models.Peer:
|
||||||
d.Entity = WebhookEntityPeer
|
d.Entity = WebhookEntityPeer
|
||||||
d.Identifier = string(v.Identifier)
|
d.Identifier = v.Identifier
|
||||||
case domain.Interface:
|
case models.Interface:
|
||||||
d.Entity = WebhookEntityInterface
|
d.Entity = WebhookEntityInterface
|
||||||
d.Identifier = string(v.Identifier)
|
d.Identifier = v.Identifier
|
||||||
|
case models.PeerMetrics:
|
||||||
|
d.Entity = WebhookEntityPeerMetric
|
||||||
|
d.Identifier = v.Peer.Identifier
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported payload type: %T", v)
|
return nil, fmt.Errorf("unsupported payload type: %T", v)
|
||||||
}
|
}
|
||||||
|
@@ -34,15 +34,18 @@ func (d *WebhookData) Serialize() (io.Reader, error) {
|
|||||||
type WebhookEntity = string
|
type WebhookEntity = string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WebhookEntityUser WebhookEntity = "user"
|
WebhookEntityUser WebhookEntity = "user"
|
||||||
WebhookEntityPeer WebhookEntity = "peer"
|
WebhookEntityPeer WebhookEntity = "peer"
|
||||||
WebhookEntityInterface WebhookEntity = "interface"
|
WebhookEntityPeerMetric WebhookEntity = "peer_metric"
|
||||||
|
WebhookEntityInterface WebhookEntity = "interface"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebhookEvent = string
|
type WebhookEvent = string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WebhookEventCreate WebhookEvent = "create"
|
WebhookEventCreate WebhookEvent = "create"
|
||||||
WebhookEventUpdate WebhookEvent = "update"
|
WebhookEventUpdate WebhookEvent = "update"
|
||||||
WebhookEventDelete WebhookEvent = "delete"
|
WebhookEventDelete WebhookEvent = "delete"
|
||||||
|
WebhookEventConnect WebhookEvent = "connect"
|
||||||
|
WebhookEventDisconnect WebhookEvent = "disconnect"
|
||||||
)
|
)
|
||||||
|
99
internal/app/webhooks/models/interface.go
Normal file
99
internal/app/webhooks/models/interface.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface represents an interface model for webhooks. For details about the fields, see the domain.Interface struct.
|
||||||
|
type Interface struct {
|
||||||
|
CreatedBy string `json:"CreatedBy"`
|
||||||
|
UpdatedBy string `json:"UpdatedBy"`
|
||||||
|
CreatedAt time.Time `json:"CreatedAt"`
|
||||||
|
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||||
|
|
||||||
|
Identifier string `json:"Identifier"`
|
||||||
|
PrivateKey string `json:"PrivateKey"`
|
||||||
|
PublicKey string `json:"PublicKey"`
|
||||||
|
ListenPort int `json:"ListenPort"`
|
||||||
|
|
||||||
|
Addresses []string `json:"Addresses"`
|
||||||
|
DnsStr string `json:"DnsStr"`
|
||||||
|
DnsSearchStr string `json:"DnsSearchStr"`
|
||||||
|
|
||||||
|
Mtu int `json:"Mtu"`
|
||||||
|
FirewallMark uint32 `json:"FirewallMark"`
|
||||||
|
RoutingTable string `json:"RoutingTable"`
|
||||||
|
|
||||||
|
PreUp string `json:"PreUp"`
|
||||||
|
PostUp string `json:"PostUp"`
|
||||||
|
PreDown string `json:"PreDown"`
|
||||||
|
PostDown string `json:"PostDown"`
|
||||||
|
|
||||||
|
SaveConfig bool `json:"SaveConfig"`
|
||||||
|
|
||||||
|
DisplayName string `json:"DisplayName"`
|
||||||
|
Type string `json:"Type"`
|
||||||
|
DriverType string `json:"DriverType"`
|
||||||
|
Disabled *time.Time `json:"Disabled,omitempty"`
|
||||||
|
DisabledReason string `json:"DisabledReason,omitempty"`
|
||||||
|
|
||||||
|
PeerDefNetworkStr string `json:"PeerDefNetworkStr,omitempty"`
|
||||||
|
PeerDefDnsStr string `json:"PeerDefDnsStr,omitempty"`
|
||||||
|
PeerDefDnsSearchStr string `json:"PeerDefDnsSearchStr,omitempty"`
|
||||||
|
PeerDefEndpoint string `json:"PeerDefEndpoint,omitempty"`
|
||||||
|
PeerDefAllowedIPsStr string `json:"PeerDefAllowedIPsStr,omitempty"`
|
||||||
|
PeerDefMtu int `json:"PeerDefMtu,omitempty"`
|
||||||
|
PeerDefPersistentKeepalive int `json:"PeerDefPersistentKeepalive,omitempty"`
|
||||||
|
PeerDefFirewallMark uint32 `json:"PeerDefFirewallMark,omitempty"`
|
||||||
|
PeerDefRoutingTable string `json:"PeerDefRoutingTable,omitempty"`
|
||||||
|
|
||||||
|
PeerDefPreUp string `json:"PeerDefPreUp,omitempty"`
|
||||||
|
PeerDefPostUp string `json:"PeerDefPostUp,omitempty"`
|
||||||
|
PeerDefPreDown string `json:"PeerDefPreDown,omitempty"`
|
||||||
|
PeerDefPostDown string `json:"PeerDefPostDown,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInterface creates a new Interface model from a domain.Interface.
|
||||||
|
func NewInterface(src domain.Interface) Interface {
|
||||||
|
return Interface{
|
||||||
|
CreatedBy: src.CreatedBy,
|
||||||
|
UpdatedBy: src.UpdatedBy,
|
||||||
|
CreatedAt: src.CreatedAt,
|
||||||
|
UpdatedAt: src.UpdatedAt,
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
PrivateKey: src.KeyPair.PrivateKey,
|
||||||
|
PublicKey: src.KeyPair.PublicKey,
|
||||||
|
ListenPort: src.ListenPort,
|
||||||
|
Addresses: domain.CidrsToStringSlice(src.Addresses),
|
||||||
|
DnsStr: src.DnsStr,
|
||||||
|
DnsSearchStr: src.DnsSearchStr,
|
||||||
|
Mtu: src.Mtu,
|
||||||
|
FirewallMark: src.FirewallMark,
|
||||||
|
RoutingTable: src.RoutingTable,
|
||||||
|
PreUp: src.PreUp,
|
||||||
|
PostUp: src.PostUp,
|
||||||
|
PreDown: src.PreDown,
|
||||||
|
PostDown: src.PostDown,
|
||||||
|
SaveConfig: src.SaveConfig,
|
||||||
|
DisplayName: string(src.Identifier),
|
||||||
|
Type: string(src.Type),
|
||||||
|
DriverType: src.DriverType,
|
||||||
|
Disabled: src.Disabled,
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
PeerDefNetworkStr: src.PeerDefNetworkStr,
|
||||||
|
PeerDefDnsStr: src.PeerDefDnsStr,
|
||||||
|
PeerDefDnsSearchStr: src.PeerDefDnsSearchStr,
|
||||||
|
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||||
|
PeerDefAllowedIPsStr: src.PeerDefAllowedIPsStr,
|
||||||
|
PeerDefMtu: src.PeerDefMtu,
|
||||||
|
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||||
|
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||||
|
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||||
|
PeerDefPreUp: src.PeerDefPreUp,
|
||||||
|
PeerDefPostUp: src.PeerDefPostUp,
|
||||||
|
PeerDefPreDown: src.PeerDefPreDown,
|
||||||
|
PeerDefPostDown: src.PeerDefPostDown,
|
||||||
|
}
|
||||||
|
}
|
89
internal/app/webhooks/models/peer.go
Normal file
89
internal/app/webhooks/models/peer.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Peer represents a peer model for webhooks. For details about the fields, see the domain.Peer struct.
|
||||||
|
type Peer struct {
|
||||||
|
CreatedBy string `json:"CreatedBy"`
|
||||||
|
UpdatedBy string `json:"UpdatedBy"`
|
||||||
|
CreatedAt time.Time `json:"CreatedAt"`
|
||||||
|
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||||
|
|
||||||
|
Endpoint string `json:"Endpoint"`
|
||||||
|
EndpointPublicKey string `json:"EndpointPublicKey"`
|
||||||
|
AllowedIPsStr string `json:"AllowedIPsStr"`
|
||||||
|
ExtraAllowedIPsStr string `json:"ExtraAllowedIPsStr"`
|
||||||
|
PresharedKey string `json:"PresharedKey"`
|
||||||
|
PersistentKeepalive int `json:"PersistentKeepalive"`
|
||||||
|
|
||||||
|
DisplayName string `json:"DisplayName"`
|
||||||
|
Identifier string `json:"Identifier"`
|
||||||
|
UserIdentifier string `json:"UserIdentifier"`
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier"`
|
||||||
|
Disabled *time.Time `json:"Disabled,omitempty"`
|
||||||
|
DisabledReason string `json:"DisabledReason,omitempty"`
|
||||||
|
ExpiresAt *time.Time `json:"ExpiresAt,omitempty"`
|
||||||
|
Notes string `json:"Notes,omitempty"`
|
||||||
|
AutomaticallyCreated bool `json:"AutomaticallyCreated"`
|
||||||
|
|
||||||
|
PrivateKey string `json:"PrivateKey"`
|
||||||
|
PublicKey string `json:"PublicKey"`
|
||||||
|
|
||||||
|
InterfaceType string `json:"InterfaceType"`
|
||||||
|
|
||||||
|
Addresses []string `json:"Addresses"`
|
||||||
|
CheckAliveAddress string `json:"CheckAliveAddress"`
|
||||||
|
DnsStr string `json:"DnsStr"`
|
||||||
|
DnsSearchStr string `json:"DnsSearchStr"`
|
||||||
|
Mtu int `json:"Mtu"`
|
||||||
|
FirewallMark uint32 `json:"FirewallMark,omitempty"`
|
||||||
|
RoutingTable string `json:"RoutingTable,omitempty"`
|
||||||
|
|
||||||
|
PreUp string `json:"PreUp,omitempty"`
|
||||||
|
PostUp string `json:"PostUp,omitempty"`
|
||||||
|
PreDown string `json:"PreDown,omitempty"`
|
||||||
|
PostDown string `json:"PostDown,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPeer creates a new Peer model from a domain.Peer.
|
||||||
|
func NewPeer(src domain.Peer) Peer {
|
||||||
|
return Peer{
|
||||||
|
CreatedBy: src.CreatedBy,
|
||||||
|
UpdatedBy: src.UpdatedBy,
|
||||||
|
CreatedAt: src.CreatedAt,
|
||||||
|
UpdatedAt: src.UpdatedAt,
|
||||||
|
Endpoint: src.Endpoint.GetValue(),
|
||||||
|
EndpointPublicKey: src.EndpointPublicKey.GetValue(),
|
||||||
|
AllowedIPsStr: src.AllowedIPsStr.GetValue(),
|
||||||
|
ExtraAllowedIPsStr: src.ExtraAllowedIPsStr,
|
||||||
|
PresharedKey: string(src.PresharedKey),
|
||||||
|
PersistentKeepalive: src.PersistentKeepalive.GetValue(),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
UserIdentifier: string(src.UserIdentifier),
|
||||||
|
InterfaceIdentifier: string(src.InterfaceIdentifier),
|
||||||
|
Disabled: src.Disabled,
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
ExpiresAt: src.ExpiresAt,
|
||||||
|
Notes: src.Notes,
|
||||||
|
AutomaticallyCreated: src.AutomaticallyCreated,
|
||||||
|
PrivateKey: src.Interface.KeyPair.PrivateKey,
|
||||||
|
PublicKey: src.Interface.KeyPair.PublicKey,
|
||||||
|
InterfaceType: string(src.Interface.Type),
|
||||||
|
Addresses: domain.CidrsToStringSlice(src.Interface.Addresses),
|
||||||
|
CheckAliveAddress: src.Interface.CheckAliveAddress,
|
||||||
|
DnsStr: src.Interface.DnsStr.GetValue(),
|
||||||
|
DnsSearchStr: src.Interface.DnsSearchStr.GetValue(),
|
||||||
|
Mtu: src.Interface.Mtu.GetValue(),
|
||||||
|
FirewallMark: src.Interface.FirewallMark.GetValue(),
|
||||||
|
RoutingTable: src.Interface.RoutingTable.GetValue(),
|
||||||
|
PreUp: src.Interface.PreUp.GetValue(),
|
||||||
|
PostUp: src.Interface.PostUp.GetValue(),
|
||||||
|
PreDown: src.Interface.PreDown.GetValue(),
|
||||||
|
PostDown: src.Interface.PostDown.GetValue(),
|
||||||
|
}
|
||||||
|
}
|
50
internal/app/webhooks/models/peer_metrics.go
Normal file
50
internal/app/webhooks/models/peer_metrics.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PeerMetrics represents a peer metrics model for webhooks.
|
||||||
|
// For details about the fields, see the domain.PeerStatus and domain.Peer structs.
|
||||||
|
type PeerMetrics struct {
|
||||||
|
Status PeerStatus `json:"Status"`
|
||||||
|
Peer Peer `json:"Peer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerStatus represents the status of a peer for webhooks.
|
||||||
|
// For details about the fields, see the domain.PeerStatus struct.
|
||||||
|
type PeerStatus struct {
|
||||||
|
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||||
|
|
||||||
|
IsConnected bool `json:"IsConnected"`
|
||||||
|
|
||||||
|
IsPingable bool `json:"IsPingable"`
|
||||||
|
LastPing *time.Time `json:"LastPing,omitempty"`
|
||||||
|
|
||||||
|
BytesReceived uint64 `json:"BytesReceived"`
|
||||||
|
BytesTransmitted uint64 `json:"BytesTransmitted"`
|
||||||
|
|
||||||
|
Endpoint string `json:"Endpoint"`
|
||||||
|
LastHandshake *time.Time `json:"LastHandshake,omitempty"`
|
||||||
|
LastSessionStart *time.Time `json:"LastSessionStart,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPeerMetrics creates a new PeerMetrics model from the domain.PeerStatus and domain.Peer models.
|
||||||
|
func NewPeerMetrics(status domain.PeerStatus, peer domain.Peer) PeerMetrics {
|
||||||
|
return PeerMetrics{
|
||||||
|
Status: PeerStatus{
|
||||||
|
UpdatedAt: status.UpdatedAt,
|
||||||
|
IsConnected: status.IsConnected,
|
||||||
|
IsPingable: status.IsPingable,
|
||||||
|
LastPing: status.LastPing,
|
||||||
|
BytesReceived: status.BytesReceived,
|
||||||
|
BytesTransmitted: status.BytesTransmitted,
|
||||||
|
Endpoint: status.Endpoint,
|
||||||
|
LastHandshake: status.LastHandshake,
|
||||||
|
LastSessionStart: status.LastSessionStart,
|
||||||
|
},
|
||||||
|
Peer: NewPeer(peer),
|
||||||
|
}
|
||||||
|
}
|
56
internal/app/webhooks/models/user.go
Normal file
56
internal/app/webhooks/models/user.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a user model for webhooks. For details about the fields, see the domain.User struct.
|
||||||
|
type User struct {
|
||||||
|
CreatedBy string `json:"CreatedBy"`
|
||||||
|
UpdatedBy string `json:"UpdatedBy"`
|
||||||
|
CreatedAt time.Time `json:"CreatedAt"`
|
||||||
|
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||||
|
|
||||||
|
Identifier string `json:"Identifier"`
|
||||||
|
Email string `json:"Email"`
|
||||||
|
Source string `json:"Source"`
|
||||||
|
ProviderName string `json:"ProviderName"`
|
||||||
|
IsAdmin bool `json:"IsAdmin"`
|
||||||
|
|
||||||
|
Firstname string `json:"Firstname,omitempty"`
|
||||||
|
Lastname string `json:"Lastname,omitempty"`
|
||||||
|
Phone string `json:"Phone,omitempty"`
|
||||||
|
Department string `json:"Department,omitempty"`
|
||||||
|
Notes string `json:"Notes,omitempty"`
|
||||||
|
|
||||||
|
Disabled *time.Time `json:"Disabled,omitempty"`
|
||||||
|
DisabledReason string `json:"DisabledReason,omitempty"`
|
||||||
|
Locked *time.Time `json:"Locked,omitempty"`
|
||||||
|
LockedReason string `json:"LockedReason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUser creates a new User model from a domain.User
|
||||||
|
func NewUser(src domain.User) User {
|
||||||
|
return User{
|
||||||
|
CreatedBy: src.CreatedBy,
|
||||||
|
UpdatedBy: src.UpdatedBy,
|
||||||
|
CreatedAt: src.CreatedAt,
|
||||||
|
UpdatedAt: src.UpdatedAt,
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
Email: src.Email,
|
||||||
|
Source: string(src.Source),
|
||||||
|
ProviderName: src.ProviderName,
|
||||||
|
IsAdmin: src.IsAdmin,
|
||||||
|
Firstname: src.Firstname,
|
||||||
|
Lastname: src.Lastname,
|
||||||
|
Phone: src.Phone,
|
||||||
|
Department: src.Department,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Disabled: src.Disabled,
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
Locked: src.Locked,
|
||||||
|
LockedReason: src.LockedReason,
|
||||||
|
}
|
||||||
|
}
|
166
internal/app/wireguard/controller_manager.go
Normal file
166
internal/app/wireguard/controller_manager.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package wireguard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/adapters/wgcontroller"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"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 {
|
||||||
|
Config config.BackendBase // Config is the configuration for the backend instance.
|
||||||
|
Implementation InterfaceController
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControllerManager struct {
|
||||||
|
cfg *config.Config
|
||||||
|
controllers map[domain.InterfaceBackend]backendInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewControllerManager(cfg *config.Config) (*ControllerManager, error) {
|
||||||
|
c := &ControllerManager{
|
||||||
|
cfg: cfg,
|
||||||
|
controllers: make(map[domain.InterfaceBackend]backendInstance),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.init()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) init() error {
|
||||||
|
if err := c.registerLocalController(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.registerMikrotikControllers(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logRegisteredControllers()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) registerLocalController() error {
|
||||||
|
localController, err := wgcontroller.NewLocalController(c.cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create local WireGuard controller: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.controllers[config.LocalBackendName] = backendInstance{
|
||||||
|
Config: config.BackendBase{
|
||||||
|
Id: config.LocalBackendName,
|
||||||
|
DisplayName: "Local WireGuard Controller",
|
||||||
|
},
|
||||||
|
Implementation: localController,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) registerMikrotikControllers() error {
|
||||||
|
for _, backendConfig := range c.cfg.Backend.Mikrotik {
|
||||||
|
if backendConfig.Id == config.LocalBackendName {
|
||||||
|
slog.Warn("skipping registration of Mikrotik controller with reserved ID", "id", config.LocalBackendName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
controller, err := wgcontroller.NewMikrotikController(c.cfg, &backendConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create Mikrotik controller for backend %s: %w", backendConfig.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{
|
||||||
|
Config: backendConfig.BackendBase,
|
||||||
|
Implementation: controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) logRegisteredControllers() {
|
||||||
|
for backend, controller := range c.controllers {
|
||||||
|
slog.Debug("backend controller registered",
|
||||||
|
"backend", backend, "type", fmt.Sprintf("%T", controller.Implementation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) InterfaceController {
|
||||||
|
return c.getController(backend, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) GetController(iface domain.Interface) InterfaceController {
|
||||||
|
return c.getController(iface.Backend, iface.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) getController(
|
||||||
|
backend domain.InterfaceBackend,
|
||||||
|
ifaceId domain.InterfaceIdentifier,
|
||||||
|
) InterfaceController {
|
||||||
|
if backend == "" {
|
||||||
|
// If no backend is specified, use the local controller.
|
||||||
|
// This might be the case for interfaces created in previous WireGuard Portal versions.
|
||||||
|
backend = config.LocalBackendName
|
||||||
|
}
|
||||||
|
|
||||||
|
controller, exists := c.controllers[backend]
|
||||||
|
if !exists {
|
||||||
|
controller, exists = c.controllers[config.LocalBackendName] // Fallback to local controller
|
||||||
|
if !exists {
|
||||||
|
// If the local controller is also not found, panic
|
||||||
|
panic(fmt.Sprintf("%s interface controller for backend %s not found", ifaceId, backend))
|
||||||
|
}
|
||||||
|
slog.Warn("controller for backend not found, using local controller",
|
||||||
|
"backend", backend, "interface", ifaceId)
|
||||||
|
}
|
||||||
|
return controller.Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) GetAllControllers() []InterfaceController {
|
||||||
|
var backendInstances = make([]InterfaceController, 0, len(c.controllers))
|
||||||
|
for instance := range maps.Values(c.controllers) {
|
||||||
|
backendInstances = append(backendInstances, instance.Implementation)
|
||||||
|
}
|
||||||
|
return backendInstances
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControllerManager) GetControllerNames() []config.BackendBase {
|
||||||
|
var names []config.BackendBase
|
||||||
|
for _, id := range slices.Sorted(maps.Keys(c.controllers)) {
|
||||||
|
names = append(names, c.controllers[id].Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
@@ -6,8 +6,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
probing "github.com/prometheus-community/pro-bing"
|
|
||||||
|
|
||||||
"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"
|
||||||
@@ -30,11 +28,6 @@ type StatisticsDatabaseRepo interface {
|
|||||||
DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error
|
DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatisticsInterfaceController interface {
|
|
||||||
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
|
|
||||||
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatisticsMetricsServer interface {
|
type StatisticsMetricsServer interface {
|
||||||
UpdateInterfaceMetrics(status domain.InterfaceStatus)
|
UpdateInterfaceMetrics(status domain.InterfaceStatus)
|
||||||
UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus)
|
UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus)
|
||||||
@@ -43,6 +36,13 @@ type StatisticsMetricsServer interface {
|
|||||||
type StatisticsEventBus interface {
|
type StatisticsEventBus interface {
|
||||||
// Subscribe subscribes to a topic
|
// Subscribe subscribes to a topic
|
||||||
Subscribe(topic string, fn interface{}) error
|
Subscribe(topic string, fn interface{}) error
|
||||||
|
// Publish sends a message to the message bus.
|
||||||
|
Publish(topic string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pingJob struct {
|
||||||
|
Peer domain.Peer
|
||||||
|
Backend domain.InterfaceBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatisticsCollector struct {
|
type StatisticsCollector struct {
|
||||||
@@ -50,11 +50,13 @@ type StatisticsCollector struct {
|
|||||||
bus StatisticsEventBus
|
bus StatisticsEventBus
|
||||||
|
|
||||||
pingWaitGroup sync.WaitGroup
|
pingWaitGroup sync.WaitGroup
|
||||||
pingJobs chan domain.Peer
|
pingJobs chan pingJob
|
||||||
|
|
||||||
db StatisticsDatabaseRepo
|
db StatisticsDatabaseRepo
|
||||||
wg StatisticsInterfaceController
|
wg *ControllerManager
|
||||||
ms StatisticsMetricsServer
|
ms StatisticsMetricsServer
|
||||||
|
|
||||||
|
peerChangeEvent chan domain.PeerIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStatisticsCollector creates a new statistics collector.
|
// NewStatisticsCollector creates a new statistics collector.
|
||||||
@@ -62,7 +64,7 @@ func NewStatisticsCollector(
|
|||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
bus StatisticsEventBus,
|
bus StatisticsEventBus,
|
||||||
db StatisticsDatabaseRepo,
|
db StatisticsDatabaseRepo,
|
||||||
wg StatisticsInterfaceController,
|
wg *ControllerManager,
|
||||||
ms StatisticsMetricsServer,
|
ms StatisticsMetricsServer,
|
||||||
) (*StatisticsCollector, error) {
|
) (*StatisticsCollector, error) {
|
||||||
c := &StatisticsCollector{
|
c := &StatisticsCollector{
|
||||||
@@ -113,7 +115,7 @@ func (c *StatisticsCollector) collectInterfaceData(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, in := range interfaces {
|
for _, in := range interfaces {
|
||||||
physicalInterface, err := c.wg.GetInterface(ctx, in.Identifier)
|
physicalInterface, err := c.wg.GetController(in).GetInterface(ctx, in.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("failed to load physical interface for data collection", "interface", in.Identifier,
|
slog.Warn("failed to load physical interface for data collection", "interface", in.Identifier,
|
||||||
"error", err)
|
"error", err)
|
||||||
@@ -165,14 +167,18 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, in := range interfaces {
|
for _, in := range interfaces {
|
||||||
peers, err := c.wg.GetPeers(ctx, in.Identifier)
|
peers, err := c.wg.GetController(in).GetPeers(ctx, in.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("failed to fetch peers for data collection", "interface", in.Identifier, "error", err)
|
slog.Warn("failed to fetch peers for data collection", "interface", in.Identifier, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
|
var connectionStateChanged bool
|
||||||
|
var newPeerStatus domain.PeerStatus
|
||||||
err = c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
err = c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
||||||
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
||||||
|
wasConnected := p.IsConnected
|
||||||
|
|
||||||
var lastHandshake *time.Time
|
var lastHandshake *time.Time
|
||||||
if !peer.LastHandshake.IsZero() {
|
if !peer.LastHandshake.IsZero() {
|
||||||
lastHandshake = &peer.LastHandshake
|
lastHandshake = &peer.LastHandshake
|
||||||
@@ -186,6 +192,13 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
|
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
|
||||||
p.Endpoint = peer.Endpoint
|
p.Endpoint = peer.Endpoint
|
||||||
p.LastHandshake = lastHandshake
|
p.LastHandshake = lastHandshake
|
||||||
|
p.CalcConnected()
|
||||||
|
|
||||||
|
if wasConnected != p.IsConnected {
|
||||||
|
slog.Debug("peer connection state changed", "peer", peer.Identifier, "connected", p.IsConnected)
|
||||||
|
connectionStateChanged = true
|
||||||
|
newPeerStatus = *p // store new status for event publishing
|
||||||
|
}
|
||||||
|
|
||||||
// Update prometheus metrics
|
// Update prometheus metrics
|
||||||
go c.updatePeerMetrics(ctx, *p)
|
go c.updatePeerMetrics(ctx, *p)
|
||||||
@@ -197,6 +210,17 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
slog.Debug("updated peer status", "peer", peer.Identifier)
|
slog.Debug("updated peer status", "peer", peer.Identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if connectionStateChanged {
|
||||||
|
peerModel, err := c.db.GetPeer(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to fetch peer for data collection", "peer", peer.Identifier, "error",
|
||||||
|
err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// publish event if connection state changed
|
||||||
|
c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus, *peerModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +269,7 @@ func (c *StatisticsCollector) startPingWorkers(ctx context.Context) {
|
|||||||
|
|
||||||
c.pingWaitGroup = sync.WaitGroup{}
|
c.pingWaitGroup = sync.WaitGroup{}
|
||||||
c.pingWaitGroup.Add(c.cfg.Statistics.PingCheckWorkers)
|
c.pingWaitGroup.Add(c.cfg.Statistics.PingCheckWorkers)
|
||||||
c.pingJobs = make(chan domain.Peer, c.cfg.Statistics.PingCheckWorkers)
|
c.pingJobs = make(chan pingJob, c.cfg.Statistics.PingCheckWorkers)
|
||||||
|
|
||||||
// start workers
|
// start workers
|
||||||
for i := 0; i < c.cfg.Statistics.PingCheckWorkers; i++ {
|
for i := 0; i < c.cfg.Statistics.PingCheckWorkers; i++ {
|
||||||
@@ -288,7 +312,10 @@ func (c *StatisticsCollector) enqueuePingChecks(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
c.pingJobs <- peer
|
c.pingJobs <- pingJob{
|
||||||
|
Peer: peer,
|
||||||
|
Backend: in.Backend,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,13 +324,21 @@ func (c *StatisticsCollector) enqueuePingChecks(ctx context.Context) {
|
|||||||
|
|
||||||
func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
||||||
defer c.pingWaitGroup.Done()
|
defer c.pingWaitGroup.Done()
|
||||||
for peer := range c.pingJobs {
|
for job := range c.pingJobs {
|
||||||
peerPingable := c.isPeerPingable(ctx, peer)
|
peer := job.Peer
|
||||||
|
backend := job.Backend
|
||||||
|
|
||||||
|
var connectionStateChanged bool
|
||||||
|
var newPeerStatus domain.PeerStatus
|
||||||
|
|
||||||
|
peerPingable := c.isPeerPingable(ctx, backend, peer)
|
||||||
slog.Debug("peer ping check completed", "peer", peer.Identifier, "pingable", peerPingable)
|
slog.Debug("peer ping check completed", "peer", peer.Identifier, "pingable", peerPingable)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
err := c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
err := c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
||||||
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
||||||
|
wasConnected := p.IsConnected
|
||||||
|
|
||||||
if peerPingable {
|
if peerPingable {
|
||||||
p.IsPingable = true
|
p.IsPingable = true
|
||||||
p.LastPing = &now
|
p.LastPing = &now
|
||||||
@@ -311,6 +346,13 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
|||||||
p.IsPingable = false
|
p.IsPingable = false
|
||||||
p.LastPing = nil
|
p.LastPing = nil
|
||||||
}
|
}
|
||||||
|
p.UpdatedAt = time.Now()
|
||||||
|
p.CalcConnected()
|
||||||
|
|
||||||
|
if wasConnected != p.IsConnected {
|
||||||
|
connectionStateChanged = true
|
||||||
|
newPeerStatus = *p // store new status for event publishing
|
||||||
|
}
|
||||||
|
|
||||||
// Update prometheus metrics
|
// Update prometheus metrics
|
||||||
go c.updatePeerMetrics(ctx, *p)
|
go c.updatePeerMetrics(ctx, *p)
|
||||||
@@ -322,10 +364,19 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
slog.Debug("updated peer ping status", "peer", peer.Identifier)
|
slog.Debug("updated peer ping status", "peer", peer.Identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if connectionStateChanged {
|
||||||
|
// publish event if connection state changed
|
||||||
|
c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus, peer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *StatisticsCollector) isPeerPingable(ctx context.Context, peer domain.Peer) bool {
|
func (c *StatisticsCollector) isPeerPingable(
|
||||||
|
ctx context.Context,
|
||||||
|
backend domain.InterfaceBackend,
|
||||||
|
peer domain.Peer,
|
||||||
|
) bool {
|
||||||
if !c.cfg.Statistics.UsePingChecks {
|
if !c.cfg.Statistics.UsePingChecks {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -335,23 +386,13 @@ func (c *StatisticsCollector) isPeerPingable(ctx context.Context, peer domain.Pe
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
pinger, err := probing.NewPinger(checkAddr)
|
stats, err := c.wg.GetControllerByName(backend).PingAddresses(ctx, checkAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("failed to instantiate pinger", "peer", peer.Identifier, "address", checkAddr, "error", err)
|
slog.Debug("failed to ping peer", "peer", peer.Identifier, "error", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCount := 1
|
return stats.IsPingable()
|
||||||
pinger.SetPrivileged(!c.cfg.Statistics.PingUnprivileged)
|
|
||||||
pinger.Count = checkCount
|
|
||||||
pinger.Timeout = 2 * time.Second
|
|
||||||
err = pinger.RunWithContext(ctx) // Blocks until finished.
|
|
||||||
if err != nil {
|
|
||||||
slog.Debug("pinger for peer exited unexpectedly", "peer", peer.Identifier, "address", checkAddr, "error", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
stats := pinger.Statistics()
|
|
||||||
return stats.PacketsRecv == checkCount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *StatisticsCollector) updateInterfaceMetrics(status domain.InterfaceStatus) {
|
func (c *StatisticsCollector) updateInterfaceMetrics(status domain.InterfaceStatus) {
|
||||||
|
@@ -37,25 +37,6 @@ type InterfaceAndPeerDatabaseRepo interface {
|
|||||||
GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error)
|
GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceController interface {
|
|
||||||
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
|
|
||||||
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
|
|
||||||
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type WgQuickController interface {
|
type WgQuickController interface {
|
||||||
ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error
|
ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error
|
||||||
SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error
|
SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error
|
||||||
@@ -75,7 +56,7 @@ type Manager struct {
|
|||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
bus EventBus
|
bus EventBus
|
||||||
db InterfaceAndPeerDatabaseRepo
|
db InterfaceAndPeerDatabaseRepo
|
||||||
wg InterfaceController
|
wg *ControllerManager
|
||||||
quick WgQuickController
|
quick WgQuickController
|
||||||
|
|
||||||
userLockMap *sync.Map
|
userLockMap *sync.Map
|
||||||
@@ -84,7 +65,7 @@ type Manager struct {
|
|||||||
func NewWireGuardManager(
|
func NewWireGuardManager(
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
bus EventBus,
|
bus EventBus,
|
||||||
wg InterfaceController,
|
wg *ControllerManager,
|
||||||
quick WgQuickController,
|
quick WgQuickController,
|
||||||
db InterfaceAndPeerDatabaseRepo,
|
db InterfaceAndPeerDatabaseRepo,
|
||||||
) (*Manager, error) {
|
) (*Manager, error) {
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/audit"
|
"github.com/h44z/wg-portal/internal/app/audit"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,12 +22,17 @@ func (m Manager) GetImportableInterfaces(ctx context.Context) ([]domain.Physical
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
physicalInterfaces, err := m.wg.GetInterfaces(ctx)
|
var allPhysicalInterfaces []domain.PhysicalInterface
|
||||||
if err != nil {
|
for _, wgBackend := range m.wg.GetAllControllers() {
|
||||||
return nil, err
|
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allPhysicalInterfaces = append(allPhysicalInterfaces, physicalInterfaces...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return physicalInterfaces, nil
|
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.
|
||||||
@@ -109,47 +115,49 @@ func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.Inter
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
physicalInterfaces, err := m.wg.GetInterfaces(ctx)
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imported := 0
|
imported := 0
|
||||||
for _, physicalInterface := range physicalInterfaces {
|
for _, wgBackend := range m.wg.GetAllControllers() {
|
||||||
if slices.Contains(excludedInterfaces, physicalInterface.Identifier) {
|
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("importing new interface", "interface", physicalInterface.Identifier)
|
|
||||||
|
|
||||||
physicalPeers, err := m.wg.GetPeers(ctx, physicalInterface.Identifier)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.importInterface(ctx, &physicalInterface, physicalPeers)
|
// if no filter is given, exclude already existing interfaces
|
||||||
if err != nil {
|
var excludedInterfaces []domain.InterfaceIdentifier
|
||||||
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("imported new interface", "interface", physicalInterface.Identifier, "peers", len(physicalPeers))
|
for _, physicalInterface := range physicalInterfaces {
|
||||||
imported++
|
if slices.Contains(excludedInterfaces, physicalInterface.Identifier) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("importing new interface", "interface", physicalInterface.Identifier)
|
||||||
|
|
||||||
|
physicalPeers, err := wgBackend.GetPeers(ctx, physicalInterface.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.importInterface(ctx, wgBackend, &physicalInterface, physicalPeers)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("imported new interface", "interface", physicalInterface.Identifier, "peers", len(physicalPeers))
|
||||||
|
imported++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return imported, nil
|
return imported, nil
|
||||||
@@ -213,10 +221,19 @@ 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.GetInterface(ctx, iface.Identifier)
|
_, err = m.wg.GetController(iface).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)
|
||||||
|
|
||||||
|
// temporarily disable interface in database so that the current state is reflected correctly
|
||||||
|
_ = m.db.SaveInterface(ctx, iface.Identifier,
|
||||||
|
func(in *domain.Interface) (*domain.Interface, error) {
|
||||||
|
now := time.Now()
|
||||||
|
in.Disabled = &now // set
|
||||||
|
in.DisabledReason = domain.DisabledReasonInterfaceMissing
|
||||||
|
return in, nil
|
||||||
|
})
|
||||||
|
|
||||||
// try to create a new interface
|
// try to create a new interface
|
||||||
_, err = m.saveInterface(ctx, &iface)
|
_, err = m.saveInterface(ctx, &iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -260,18 +277,14 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
// restore peers
|
// restore peers
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
switch {
|
switch {
|
||||||
case iface.IsDisabled(): // 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.DeletePeer(ctx, iface.Identifier, peer.Identifier); err != nil {
|
if err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
case peer.IsDisabled(): // if peer is disabled, delete it
|
|
||||||
if err := m.wg.DeletePeer(ctx, iface.Identifier, peer.Identifier); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove disbaled peer %s from interface %s: %w",
|
|
||||||
peer.Identifier, iface.Identifier, err)
|
|
||||||
}
|
|
||||||
default: // update peer
|
default: // update peer
|
||||||
err := m.wg.SavePeer(ctx, iface.Identifier, peer.Identifier,
|
err := m.wg.GetController(iface).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
|
||||||
@@ -284,7 +297,7 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove non-wgportal peers
|
// remove non-wgportal peers
|
||||||
physicalPeers, _ := m.wg.GetPeers(ctx, iface.Identifier)
|
physicalPeers, _ := m.wg.GetController(iface).GetPeers(ctx, iface.Identifier)
|
||||||
for _, physicalPeer := range physicalPeers {
|
for _, physicalPeer := range physicalPeers {
|
||||||
isWgPortalPeer := false
|
isWgPortalPeer := false
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
@@ -294,7 +307,8 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !isWgPortalPeer {
|
if !isWgPortalPeer {
|
||||||
err := m.wg.DeletePeer(ctx, iface.Identifier, domain.PeerIdentifier(physicalPeer.PublicKey))
|
err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
|
||||||
|
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",
|
||||||
physicalPeer.PublicKey, iface.Identifier, err)
|
physicalPeer.PublicKey, iface.Identifier, err)
|
||||||
@@ -459,9 +473,9 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
|
|||||||
existingInterface.Disabled = &now // simulate a disabled interface
|
existingInterface.Disabled = &now // simulate a disabled interface
|
||||||
existingInterface.DisabledReason = domain.DisabledReasonDeleted
|
existingInterface.DisabledReason = domain.DisabledReasonDeleted
|
||||||
|
|
||||||
physicalInterface, _ := m.wg.GetInterface(ctx, id)
|
physicalInterface, _ := m.wg.GetController(*existingInterface).GetInterface(ctx, id)
|
||||||
|
|
||||||
if err := m.handleInterfacePreSaveHooks(true, existingInterface); err != nil {
|
if err := m.handleInterfacePreSaveHooks(existingInterface, !existingInterface.IsDisabled(), false); err != nil {
|
||||||
return fmt.Errorf("pre-delete hooks failed: %w", err)
|
return fmt.Errorf("pre-delete hooks failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +487,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
|
|||||||
return fmt.Errorf("peer deletion failure: %w", err)
|
return fmt.Errorf("peer deletion failure: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.wg.DeleteInterface(ctx, id); err != nil {
|
if err := m.wg.GetController(*existingInterface).DeleteInterface(ctx, id); err != nil {
|
||||||
return fmt.Errorf("wireguard deletion failure: %w", err)
|
return fmt.Errorf("wireguard deletion failure: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +504,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
|
|||||||
Table: existingInterface.GetRoutingTable(),
|
Table: existingInterface.GetRoutingTable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := m.handleInterfacePostSaveHooks(true, existingInterface); err != nil {
|
if err := m.handleInterfacePostSaveHooks(existingInterface, !existingInterface.IsDisabled(), false); err != nil {
|
||||||
return fmt.Errorf("post-delete hooks failed: %w", err)
|
return fmt.Errorf("post-delete hooks failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,9 +523,9 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
|||||||
return nil, fmt.Errorf("interface validation failed: %w", err)
|
return nil, fmt.Errorf("interface validation failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stateChanged := m.hasInterfaceStateChanged(ctx, iface)
|
oldEnabled, newEnabled := m.getInterfaceStateHistory(ctx, iface)
|
||||||
|
|
||||||
if err := m.handleInterfacePreSaveHooks(stateChanged, iface); err != nil {
|
if err := m.handleInterfacePreSaveHooks(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,7 +536,7 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
|||||||
err := m.db.SaveInterface(ctx, iface.Identifier, func(i *domain.Interface) (*domain.Interface, error) {
|
err := m.db.SaveInterface(ctx, iface.Identifier, func(i *domain.Interface) (*domain.Interface, error) {
|
||||||
iface.CopyCalculatedAttributes(i)
|
iface.CopyCalculatedAttributes(i)
|
||||||
|
|
||||||
err := m.wg.SaveInterface(ctx, iface.Identifier,
|
err := m.wg.GetController(*iface).SaveInterface(ctx, iface.Identifier,
|
||||||
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||||
domain.MergeToPhysicalInterface(pi, iface)
|
domain.MergeToPhysicalInterface(pi, iface)
|
||||||
return pi, nil
|
return pi, nil
|
||||||
@@ -538,7 +552,7 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if iface.IsDisabled() {
|
if iface.IsDisabled() {
|
||||||
physicalInterface, _ := m.wg.GetInterface(ctx, iface.Identifier)
|
physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier)
|
||||||
fwMark := iface.FirewallMark
|
fwMark := iface.FirewallMark
|
||||||
if physicalInterface != nil && fwMark == 0 {
|
if physicalInterface != nil && fwMark == 0 {
|
||||||
fwMark = physicalInterface.FirewallMark
|
fwMark = physicalInterface.FirewallMark
|
||||||
@@ -551,10 +565,31 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
|||||||
m.bus.Publish(app.TopicRouteUpdate, "interface updated: "+string(iface.Identifier))
|
m.bus.Publish(app.TopicRouteUpdate, "interface updated: "+string(iface.Identifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.handleInterfacePostSaveHooks(stateChanged, iface); err != nil {
|
if err := m.handleInterfacePostSaveHooks(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the interface has just been enabled, restore its peers on the physical controller
|
||||||
|
if !oldEnabled && newEnabled && iface.Backend == config.LocalBackendName {
|
||||||
|
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 {
|
||||||
|
saveErr := m.wg.GetController(*iface).SavePeer(ctx, iface.Identifier, peer.Identifier,
|
||||||
|
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
|
||||||
|
domain.MergeToPhysicalPeer(pp, &peer)
|
||||||
|
return pp, nil
|
||||||
|
})
|
||||||
|
if saveErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to restore peer %s for interface %s: %w", peer.Identifier,
|
||||||
|
iface.Identifier, saveErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// notify that peers for this interface have changed so config/routes can be updated
|
||||||
|
m.bus.Publish(app.TopicPeerInterfaceUpdated, iface.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
m.bus.Publish(app.TopicAuditInterfaceChanged, domain.AuditEventWrapper[audit.InterfaceEvent]{
|
m.bus.Publish(app.TopicAuditInterfaceChanged, domain.AuditEventWrapper[audit.InterfaceEvent]{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Event: audit.InterfaceEvent{
|
Event: audit.InterfaceEvent{
|
||||||
@@ -566,32 +601,13 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
|||||||
return iface, nil
|
return iface, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) hasInterfaceStateChanged(ctx context.Context, iface *domain.Interface) bool {
|
func (m Manager) getInterfaceStateHistory(ctx context.Context, iface *domain.Interface) (oldEnabled, newEnabled bool) {
|
||||||
oldInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
oldInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false, !iface.IsDisabled() // if the interface did not exist, we assume it was not enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldInterface.IsDisabled() != iface.IsDisabled() {
|
return !oldInterface.IsDisabled(), !iface.IsDisabled()
|
||||||
return true // interface in db has changed
|
|
||||||
}
|
|
||||||
|
|
||||||
wgInterface, err := m.wg.GetInterface(ctx, iface.Identifier)
|
|
||||||
if err != nil {
|
|
||||||
return true // interface might not exist - so we assume that there must be a change
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare physical interface settings
|
|
||||||
if len(wgInterface.Addresses) != len(iface.Addresses) ||
|
|
||||||
wgInterface.Mtu != iface.Mtu ||
|
|
||||||
wgInterface.FirewallMark != iface.FirewallMark ||
|
|
||||||
wgInterface.ListenPort != iface.ListenPort ||
|
|
||||||
wgInterface.PrivateKey != iface.PrivateKey ||
|
|
||||||
wgInterface.PublicKey != iface.PublicKey {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleInterfacePreSaveActions(iface *domain.Interface) error {
|
func (m Manager) handleInterfacePreSaveActions(iface *domain.Interface) error {
|
||||||
@@ -607,12 +623,14 @@ func (m Manager) handleInterfacePreSaveActions(iface *domain.Interface) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleInterfacePreSaveHooks(stateChanged bool, iface *domain.Interface) error {
|
func (m Manager) handleInterfacePreSaveHooks(iface *domain.Interface, oldEnabled, newEnabled bool) error {
|
||||||
if !stateChanged {
|
if oldEnabled == newEnabled {
|
||||||
return nil // do nothing if state did not change
|
return nil // do nothing if state did not change
|
||||||
}
|
}
|
||||||
|
|
||||||
if !iface.IsDisabled() {
|
slog.Debug("executing pre-save hooks", "interface", iface.Identifier, "up", newEnabled)
|
||||||
|
|
||||||
|
if newEnabled {
|
||||||
if err := m.quick.ExecuteInterfaceHook(iface.Identifier, iface.PreUp); err != nil {
|
if err := m.quick.ExecuteInterfaceHook(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)
|
||||||
}
|
}
|
||||||
@@ -624,12 +642,14 @@ func (m Manager) handleInterfacePreSaveHooks(stateChanged bool, iface *domain.In
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) handleInterfacePostSaveHooks(stateChanged bool, iface *domain.Interface) error {
|
func (m Manager) handleInterfacePostSaveHooks(iface *domain.Interface, oldEnabled, newEnabled bool) error {
|
||||||
if !stateChanged {
|
if oldEnabled == newEnabled {
|
||||||
return nil // do nothing if state did not change
|
return nil // do nothing if state did not change
|
||||||
}
|
}
|
||||||
|
|
||||||
if !iface.IsDisabled() {
|
slog.Debug("executing post-save hooks", "interface", iface.Identifier, "up", newEnabled)
|
||||||
|
|
||||||
|
if newEnabled {
|
||||||
if err := m.quick.ExecuteInterfaceHook(iface.Identifier, iface.PostUp); err != nil {
|
if err := m.quick.ExecuteInterfaceHook(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)
|
||||||
}
|
}
|
||||||
@@ -760,7 +780,12 @@ func (m Manager) getFreshListenPort(ctx context.Context) (port int, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) importInterface(ctx context.Context, in *domain.PhysicalInterface, peers []domain.PhysicalPeer) error {
|
func (m Manager) importInterface(
|
||||||
|
ctx context.Context,
|
||||||
|
backend InterfaceController,
|
||||||
|
in *domain.PhysicalInterface,
|
||||||
|
peers []domain.PhysicalPeer,
|
||||||
|
) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
iface := domain.ConvertPhysicalInterface(in)
|
iface := domain.ConvertPhysicalInterface(in)
|
||||||
iface.BaseModel = domain.BaseModel{
|
iface.BaseModel = domain.BaseModel{
|
||||||
@@ -769,8 +794,20 @@ func (m Manager) importInterface(ctx context.Context, in *domain.PhysicalInterfa
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
iface.Backend = backend.GetId()
|
||||||
iface.PeerDefAllowedIPsStr = iface.AddressStr()
|
iface.PeerDefAllowedIPsStr = iface.AddressStr()
|
||||||
|
|
||||||
|
// try to predict the interface type based on the number of peers
|
||||||
|
switch len(peers) {
|
||||||
|
case 0:
|
||||||
|
iface.Type = domain.InterfaceTypeAny // no peers means this is an unknown interface
|
||||||
|
case 1:
|
||||||
|
iface.Type = domain.InterfaceTypeClient // one peer means this is a client interface
|
||||||
|
default: // multiple peers means this is a server interface
|
||||||
|
|
||||||
|
iface.Type = domain.InterfaceTypeServer
|
||||||
|
}
|
||||||
|
|
||||||
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
||||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
return err
|
return err
|
||||||
@@ -821,16 +858,20 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
|
|||||||
peer.Interface.PreDown = domain.NewConfigOption(in.PeerDefPreDown, true)
|
peer.Interface.PreDown = domain.NewConfigOption(in.PeerDefPreDown, true)
|
||||||
peer.Interface.PostDown = domain.NewConfigOption(in.PeerDefPostDown, true)
|
peer.Interface.PostDown = domain.NewConfigOption(in.PeerDefPostDown, true)
|
||||||
|
|
||||||
|
var displayName string
|
||||||
switch in.Type {
|
switch in.Type {
|
||||||
case domain.InterfaceTypeAny:
|
case domain.InterfaceTypeAny:
|
||||||
peer.Interface.Type = domain.InterfaceTypeAny
|
peer.Interface.Type = domain.InterfaceTypeAny
|
||||||
peer.DisplayName = "Autodetected Peer (" + peer.Interface.PublicKey[0:8] + ")"
|
displayName = "Autodetected Peer (" + peer.Interface.PublicKey[0:8] + ")"
|
||||||
case domain.InterfaceTypeClient:
|
case domain.InterfaceTypeClient:
|
||||||
peer.Interface.Type = domain.InterfaceTypeServer
|
peer.Interface.Type = domain.InterfaceTypeServer
|
||||||
peer.DisplayName = "Autodetected Endpoint (" + peer.Interface.PublicKey[0:8] + ")"
|
displayName = "Autodetected Endpoint (" + peer.Interface.PublicKey[0:8] + ")"
|
||||||
case domain.InterfaceTypeServer:
|
case domain.InterfaceTypeServer:
|
||||||
peer.Interface.Type = domain.InterfaceTypeClient
|
peer.Interface.Type = domain.InterfaceTypeClient
|
||||||
peer.DisplayName = "Autodetected Client (" + peer.Interface.PublicKey[0:8] + ")"
|
displayName = "Autodetected Client (" + peer.Interface.PublicKey[0:8] + ")"
|
||||||
|
}
|
||||||
|
if peer.DisplayName == "" {
|
||||||
|
peer.DisplayName = displayName // use auto-generated display name if not set
|
||||||
}
|
}
|
||||||
|
|
||||||
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
|
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
|
||||||
@@ -844,12 +885,12 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) deleteInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) error {
|
func (m Manager) deleteInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||||
allPeers, err := m.db.GetInterfacePeers(ctx, id)
|
iface, allPeers, err := m.db.GetInterfaceAndPeers(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, peer := range allPeers {
|
for _, peer := range allPeers {
|
||||||
err = m.wg.DeletePeer(ctx, id, peer.Identifier)
|
err = m.wg.GetController(*iface).DeletePeer(ctx, id, 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)
|
||||||
}
|
}
|
||||||
|
@@ -188,29 +188,29 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
|||||||
|
|
||||||
sessionUser := domain.GetUserInfo(ctx)
|
sessionUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
// 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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch peers for user %s: %w", peer.UserIdentifier, err)
|
return nil, fmt.Errorf("failed to fetch peers for user %s: %w", peer.UserIdentifier, err)
|
||||||
}
|
}
|
||||||
// Count enabled peers (disabled IS NULL)
|
// Count enabled peers (disabled IS NULL)
|
||||||
peerCount := 0
|
peerCount := 0
|
||||||
for _, p := range peers {
|
for _, p := range peers {
|
||||||
if !p.IsDisabled() {
|
if !p.IsDisabled() {
|
||||||
peerCount++
|
peerCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalAllowedPeers := 1 + m.cfg.Advanced.LimitAdditionalUserPeers // 1 default peer + x additional peers
|
totalAllowedPeers := 1 + m.cfg.Advanced.LimitAdditionalUserPeers // 1 default peer + x additional peers
|
||||||
if peerCount >= totalAllowedPeers {
|
if peerCount >= totalAllowedPeers {
|
||||||
slog.WarnContext(ctx, "peer creation blocked due to limit",
|
slog.WarnContext(ctx, "peer creation blocked due to limit",
|
||||||
"user", peer.UserIdentifier,
|
"user", peer.UserIdentifier,
|
||||||
"current_count", peerCount,
|
"current_count", peerCount,
|
||||||
"allowed_count", totalAllowedPeers)
|
"allowed_count", totalAllowedPeers)
|
||||||
return nil, fmt.Errorf("peer limit reached (%d peers allowed): %w", totalAllowedPeers, domain.ErrNoPermission)
|
return nil, fmt.Errorf("peer limit reached (%d peers allowed): %w", totalAllowedPeers,
|
||||||
}
|
domain.ErrNoPermission)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
||||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
@@ -257,7 +257,7 @@ func (m Manager) CreateMultiplePeers(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPeers []*domain.Peer
|
createdPeers := make([]domain.Peer, 0, len(r.UserIdentifiers))
|
||||||
|
|
||||||
for _, id := range r.UserIdentifiers {
|
for _, id := range r.UserIdentifiers {
|
||||||
freshPeer, err := m.PreparePeer(ctx, interfaceId)
|
freshPeer, err := m.PreparePeer(ctx, interfaceId)
|
||||||
@@ -266,27 +266,22 @@ func (m Manager) CreateMultiplePeers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
freshPeer.UserIdentifier = domain.UserIdentifier(id) // use id as user identifier. peers are allowed to have invalid user identifiers
|
freshPeer.UserIdentifier = domain.UserIdentifier(id) // use id as user identifier. peers are allowed to have invalid user identifiers
|
||||||
if r.Suffix != "" {
|
if r.Prefix != "" {
|
||||||
freshPeer.DisplayName += " " + r.Suffix
|
freshPeer.DisplayName = r.Prefix + " " + freshPeer.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.validatePeerCreation(ctx, nil, freshPeer); err != nil {
|
if err := m.validatePeerCreation(ctx, nil, freshPeer); err != nil {
|
||||||
return nil, fmt.Errorf("creation not allowed: %w", err)
|
return nil, fmt.Errorf("creation not allowed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newPeers = append(newPeers, freshPeer)
|
// Save immediately to reserve the assigned IPs so the next prepared peer gets the next free IPs
|
||||||
}
|
if err := m.savePeers(ctx, freshPeer); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new peer %s: %w", freshPeer.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
err := m.savePeers(ctx, newPeers...)
|
createdPeers = append(createdPeers, *freshPeer)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create new peers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
createdPeers := make([]domain.Peer, len(newPeers))
|
m.bus.Publish(app.TopicPeerCreated, *freshPeer)
|
||||||
for i := range newPeers {
|
|
||||||
createdPeers[i] = *newPeers[i]
|
|
||||||
|
|
||||||
m.bus.Publish(app.TopicPeerCreated, *newPeers[i])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdPeers, nil
|
return createdPeers, nil
|
||||||
@@ -376,7 +371,12 @@ func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
|||||||
return fmt.Errorf("delete not allowed: %w", err)
|
return fmt.Errorf("delete not allowed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.wg.DeletePeer(ctx, peer.InterfaceIdentifier, id)
|
iface, err := m.db.GetInterface(ctx, peer.InterfaceIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to find interface %s: %w", peer.InterfaceIdentifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.wg.GetController(*iface).DeletePeer(ctx, peer.InterfaceIdentifier, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wireguard failed to delete peer %s: %w", id, err)
|
return fmt.Errorf("wireguard failed to delete peer %s: %w", id, err)
|
||||||
}
|
}
|
||||||
@@ -438,35 +438,28 @@ func (m Manager) GetUserPeerStats(ctx context.Context, id domain.UserIdentifier)
|
|||||||
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]struct{})
|
||||||
|
|
||||||
for i := range peers {
|
for _, peer := range peers {
|
||||||
peer := peers[i]
|
iface, err := m.db.GetInterface(ctx, peer.InterfaceIdentifier)
|
||||||
var err error
|
if err != nil {
|
||||||
if peer.IsDisabled() || peer.IsExpired() {
|
return fmt.Errorf("unable to find interface %s: %w", peer.InterfaceIdentifier, err)
|
||||||
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
|
|
||||||
peer.CopyCalculatedAttributes(p)
|
|
||||||
|
|
||||||
if err := m.wg.DeletePeer(ctx, peer.InterfaceIdentifier, peer.Identifier); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to delete wireguard peer %s: %w", peer.Identifier, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return peer, nil
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
|
|
||||||
peer.CopyCalculatedAttributes(p)
|
|
||||||
|
|
||||||
err := m.wg.SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
|
|
||||||
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
|
|
||||||
domain.MergeToPhysicalPeer(pp, peer)
|
|
||||||
return pp, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to save wireguard peer %s: %w", peer.Identifier, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return peer, nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always save the peer to the backend, regardless of disabled/expired state
|
||||||
|
// The backend will handle the disabled state appropriately
|
||||||
|
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
|
||||||
|
peer.CopyCalculatedAttributes(p)
|
||||||
|
|
||||||
|
err := m.wg.GetController(*iface).SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
|
||||||
|
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
|
||||||
|
domain.MergeToPhysicalPeer(pp, peer)
|
||||||
|
return pp, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save wireguard peer %s: %w", peer.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("save failure for peer %s: %w", peer.Identifier, err)
|
return fmt.Errorf("save failure for peer %s: %w", peer.Identifier, err)
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,9 @@ type Auth struct {
|
|||||||
// MinPasswordLength is the minimum password length for user accounts. This also applies to the admin user.
|
// MinPasswordLength is the minimum password length for user accounts. This also applies to the admin user.
|
||||||
// It is encouraged to set this value to at least 16 characters.
|
// It is encouraged to set this value to at least 16 characters.
|
||||||
MinPasswordLength int `yaml:"min_password_length"`
|
MinPasswordLength int `yaml:"min_password_length"`
|
||||||
|
// HideLoginForm specifies whether the login form should be hidden. If no social login providers are configured,
|
||||||
|
// the login form will be shown regardless of this setting.
|
||||||
|
HideLoginForm bool `yaml:"hide_login_form"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseFields contains the basic fields that are used to map user information from the authentication providers.
|
// BaseFields contains the basic fields that are used to map user information from the authentication providers.
|
||||||
|
94
internal/config/backend.go
Normal file
94
internal/config/backend.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const LocalBackendName = "local"
|
||||||
|
|
||||||
|
type Backend struct {
|
||||||
|
Default string `yaml:"default"` // The default backend to use (defaults to the internal backend)
|
||||||
|
|
||||||
|
Mikrotik []BackendMikrotik `yaml:"mikrotik"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks the backend configuration for errors.
|
||||||
|
func (b *Backend) Validate() error {
|
||||||
|
if b.Default == "" {
|
||||||
|
b.Default = LocalBackendName
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueMap := make(map[string]struct{})
|
||||||
|
for _, backend := range b.Mikrotik {
|
||||||
|
if backend.Id == LocalBackendName {
|
||||||
|
return fmt.Errorf("backend ID %q is a reserved keyword", LocalBackendName)
|
||||||
|
}
|
||||||
|
if _, exists := uniqueMap[backend.Id]; exists {
|
||||||
|
return fmt.Errorf("backend ID %q is not unique", backend.Id)
|
||||||
|
}
|
||||||
|
uniqueMap[backend.Id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Default != LocalBackendName {
|
||||||
|
if _, ok := uniqueMap[b.Default]; !ok {
|
||||||
|
return fmt.Errorf("default backend %q is not defined in the configuration", b.Default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackendBase struct {
|
||||||
|
Id string `yaml:"id"` // A unique id for the backend
|
||||||
|
DisplayName string `yaml:"display_name"` // A display name for the backend
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplayName returns the display name of the backend.
|
||||||
|
// If no display name is set, it falls back to the ID.
|
||||||
|
func (b BackendBase) GetDisplayName() string {
|
||||||
|
if b.DisplayName == "" {
|
||||||
|
return b.Id // Fallback to ID if no display name is set
|
||||||
|
}
|
||||||
|
return b.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackendMikrotik struct {
|
||||||
|
BackendBase `yaml:",inline"` // Embed the base fields
|
||||||
|
|
||||||
|
ApiUrl string `yaml:"api_url"` // The base URL of the Mikrotik API (e.g., "https://10.10.10.10:8729/rest")
|
||||||
|
ApiUser string `yaml:"api_user"`
|
||||||
|
ApiPassword string `yaml:"api_password"`
|
||||||
|
ApiVerifyTls bool `yaml:"api_verify_tls"` // Whether to verify the TLS certificate of the Mikrotik API
|
||||||
|
ApiTimeout time.Duration `yaml:"api_timeout"` // Timeout for API requests (default: 30 seconds)
|
||||||
|
|
||||||
|
// Concurrency controls the maximum number of concurrent API requests that this backend will issue
|
||||||
|
// when enumerating interfaces and their details. If 0 or negative, a default of 5 is used.
|
||||||
|
Concurrency int `yaml:"concurrency"`
|
||||||
|
|
||||||
|
Debug bool `yaml:"debug"` // Enable debug logging for the Mikrotik backend
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConcurrency returns the configured concurrency for this backend or a sane default (5)
|
||||||
|
// when the configured value is zero or negative.
|
||||||
|
func (b *BackendMikrotik) GetConcurrency() int {
|
||||||
|
if b == nil {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if b.Concurrency <= 0 {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
return b.Concurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApiTimeout returns the configured API timeout or a sane default (30 seconds)
|
||||||
|
// when the configured value is zero or negative.
|
||||||
|
func (b *BackendMikrotik) GetApiTimeout() time.Duration {
|
||||||
|
if b == nil {
|
||||||
|
return 30 * time.Second
|
||||||
|
}
|
||||||
|
if b.ApiTimeout <= 0 {
|
||||||
|
return 30 * time.Second
|
||||||
|
}
|
||||||
|
return b.ApiTimeout
|
||||||
|
}
|
@@ -44,6 +44,8 @@ type Config struct {
|
|||||||
LimitAdditionalUserPeers int `yaml:"limit_additional_user_peers"`
|
LimitAdditionalUserPeers int `yaml:"limit_additional_user_peers"`
|
||||||
} `yaml:"advanced"`
|
} `yaml:"advanced"`
|
||||||
|
|
||||||
|
Backend Backend `yaml:"backend"`
|
||||||
|
|
||||||
Statistics struct {
|
Statistics struct {
|
||||||
UsePingChecks bool `yaml:"use_ping_checks"`
|
UsePingChecks bool `yaml:"use_ping_checks"`
|
||||||
PingCheckWorkers int `yaml:"ping_check_workers"`
|
PingCheckWorkers int `yaml:"ping_check_workers"`
|
||||||
@@ -95,7 +97,16 @@ func (c *Config) LogStartupValues() {
|
|||||||
"oidcProviders", len(c.Auth.OpenIDConnect),
|
"oidcProviders", len(c.Auth.OpenIDConnect),
|
||||||
"oauthProviders", len(c.Auth.OAuth),
|
"oauthProviders", len(c.Auth.OAuth),
|
||||||
"ldapProviders", len(c.Auth.Ldap),
|
"ldapProviders", len(c.Auth.Ldap),
|
||||||
|
"webauthnEnabled", c.Auth.WebAuthn.Enabled,
|
||||||
|
"minPasswordLength", c.Auth.MinPasswordLength,
|
||||||
|
"hideLoginForm", c.Auth.HideLoginForm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
slog.Debug("Config Backend",
|
||||||
|
"defaultBackend", c.Backend.Default,
|
||||||
|
"extraBackends", len(c.Backend.Mikrotik),
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultConfig returns the default configuration
|
// defaultConfig returns the default configuration
|
||||||
@@ -119,6 +130,10 @@ func defaultConfig() *Config {
|
|||||||
DSN: "data/sqlite.db",
|
DSN: "data/sqlite.db",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Backend = Backend{
|
||||||
|
Default: LocalBackendName, // local backend is the default (using wgcrtl)
|
||||||
|
}
|
||||||
|
|
||||||
cfg.Web = WebConfig{
|
cfg.Web = WebConfig{
|
||||||
RequestLogging: false,
|
RequestLogging: false,
|
||||||
ExternalUrl: "http://localhost:8888",
|
ExternalUrl: "http://localhost:8888",
|
||||||
@@ -169,6 +184,7 @@ func defaultConfig() *Config {
|
|||||||
|
|
||||||
cfg.Auth.WebAuthn.Enabled = true
|
cfg.Auth.WebAuthn.Enabled = true
|
||||||
cfg.Auth.MinPasswordLength = 16
|
cfg.Auth.MinPasswordLength = 16
|
||||||
|
cfg.Auth.HideLoginForm = false
|
||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
@@ -197,6 +213,10 @@ func GetConfig() (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg.Web.Sanitize()
|
cfg.Web.Sanitize()
|
||||||
|
err := cfg.Backend.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
@@ -62,4 +62,7 @@ const (
|
|||||||
|
|
||||||
LockedReasonAdmin = "locked by admin"
|
LockedReasonAdmin = "locked by admin"
|
||||||
LockedReasonApi = "locked by admin"
|
LockedReasonApi = "locked by admin"
|
||||||
|
|
||||||
|
ConfigStyleRaw = "raw"
|
||||||
|
ConfigStyleWgQuick = "wgquick"
|
||||||
)
|
)
|
||||||
|
32
internal/domain/controller.go
Normal file
32
internal/domain/controller.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
// ControllerType defines the type of controller used to manage interfaces.
|
||||||
|
|
||||||
|
const (
|
||||||
|
ControllerTypeMikrotik = "mikrotik"
|
||||||
|
ControllerTypeLocal = "wgctrl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller extras can be used to store additional information available for specific controllers only.
|
||||||
|
|
||||||
|
type MikrotikInterfaceExtras struct {
|
||||||
|
Id string // internal mikrotik ID
|
||||||
|
Comment string
|
||||||
|
Disabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type MikrotikPeerExtras struct {
|
||||||
|
Id string // internal mikrotik ID
|
||||||
|
Name string
|
||||||
|
Comment string
|
||||||
|
IsResponder bool
|
||||||
|
Disabled bool
|
||||||
|
ClientEndpoint string
|
||||||
|
ClientAddress string
|
||||||
|
ClientDns string
|
||||||
|
ClientKeepalive int
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalPeerExtras struct {
|
||||||
|
Disabled bool
|
||||||
|
}
|
@@ -10,6 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ var allowedFileNameRegex = regexp.MustCompile("[^a-zA-Z0-9-_]+")
|
|||||||
|
|
||||||
type InterfaceIdentifier string
|
type InterfaceIdentifier string
|
||||||
type InterfaceType string
|
type InterfaceType string
|
||||||
|
type InterfaceBackend string
|
||||||
|
|
||||||
type Interface struct {
|
type Interface struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
@@ -49,11 +52,12 @@ type Interface struct {
|
|||||||
SaveConfig bool // automatically persist config changes to the wgX.conf file
|
SaveConfig bool // automatically persist config changes to the wgX.conf file
|
||||||
|
|
||||||
// WG Portal specific
|
// WG Portal specific
|
||||||
DisplayName string // a nice display name/ description for the interface
|
DisplayName string // a nice display name/ description for the interface
|
||||||
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
|
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
|
||||||
DriverType string // the interface driver type (linux, software, ...)
|
Backend InterfaceBackend // the backend that is used to manage the interface (wgctrl, mikrotik, ...)
|
||||||
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
|
DriverType string // the interface driver type (linux, software, ...)
|
||||||
DisabledReason string // the reason why the interface has been disabled
|
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||||
|
DisabledReason string // the reason why the interface has been disabled
|
||||||
|
|
||||||
// Default settings for the peer, used for new peers, those settings will be published to ConfigOption options of
|
// Default settings for the peer, used for new peers, those settings will be published to ConfigOption options of
|
||||||
// the peer config
|
// the peer config
|
||||||
@@ -204,9 +208,31 @@ type PhysicalInterface struct {
|
|||||||
|
|
||||||
BytesUpload uint64
|
BytesUpload uint64
|
||||||
BytesDownload uint64
|
BytesDownload uint64
|
||||||
|
|
||||||
|
backendExtras any // additional backend-specific extras, e.g., domain.MikrotikInterfaceExtras
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PhysicalInterface) GetExtras() any {
|
||||||
|
return p.backendExtras
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PhysicalInterface) SetExtras(extras any) {
|
||||||
|
switch extras.(type) {
|
||||||
|
case MikrotikInterfaceExtras: // OK
|
||||||
|
default: // we only support MikrotikInterfaceExtras for now
|
||||||
|
panic(fmt.Sprintf("unsupported interface backend extras type %T", extras))
|
||||||
|
}
|
||||||
|
|
||||||
|
p.backendExtras = extras
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
|
func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
|
||||||
|
networks := make([]Cidr, 0, len(pi.Addresses))
|
||||||
|
for _, addr := range pi.Addresses {
|
||||||
|
networks = append(networks, addr.NetworkAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new basic interface with the data from the physical interface
|
||||||
iface := &Interface{
|
iface := &Interface{
|
||||||
Identifier: pi.Identifier,
|
Identifier: pi.Identifier,
|
||||||
KeyPair: pi.KeyPair,
|
KeyPair: pi.KeyPair,
|
||||||
@@ -226,11 +252,11 @@ func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
|
|||||||
Type: InterfaceTypeAny,
|
Type: InterfaceTypeAny,
|
||||||
DriverType: pi.DeviceType,
|
DriverType: pi.DeviceType,
|
||||||
Disabled: nil,
|
Disabled: nil,
|
||||||
PeerDefNetworkStr: "",
|
PeerDefNetworkStr: CidrsToString(networks),
|
||||||
PeerDefDnsStr: "",
|
PeerDefDnsStr: "",
|
||||||
PeerDefDnsSearchStr: "",
|
PeerDefDnsSearchStr: "",
|
||||||
PeerDefEndpoint: "",
|
PeerDefEndpoint: "",
|
||||||
PeerDefAllowedIPsStr: "",
|
PeerDefAllowedIPsStr: CidrsToString(networks),
|
||||||
PeerDefMtu: pi.Mtu,
|
PeerDefMtu: pi.Mtu,
|
||||||
PeerDefPersistentKeepalive: 0,
|
PeerDefPersistentKeepalive: 0,
|
||||||
PeerDefFirewallMark: 0,
|
PeerDefFirewallMark: 0,
|
||||||
@@ -241,6 +267,23 @@ func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
|
|||||||
PeerDefPostDown: "",
|
PeerDefPostDown: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pi.GetExtras() == nil {
|
||||||
|
return iface
|
||||||
|
}
|
||||||
|
|
||||||
|
// enrich the data with controller-specific extras
|
||||||
|
now := time.Now()
|
||||||
|
switch pi.ImportSource {
|
||||||
|
case ControllerTypeMikrotik:
|
||||||
|
extras := pi.GetExtras().(MikrotikInterfaceExtras)
|
||||||
|
iface.DisplayName = extras.Comment
|
||||||
|
if extras.Disabled {
|
||||||
|
iface.Disabled = &now
|
||||||
|
} else {
|
||||||
|
iface.Disabled = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return iface
|
return iface
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +296,15 @@ func MergeToPhysicalInterface(pi *PhysicalInterface, i *Interface) {
|
|||||||
pi.FirewallMark = i.FirewallMark
|
pi.FirewallMark = i.FirewallMark
|
||||||
pi.DeviceUp = !i.IsDisabled()
|
pi.DeviceUp = !i.IsDisabled()
|
||||||
pi.Addresses = i.Addresses
|
pi.Addresses = i.Addresses
|
||||||
|
|
||||||
|
switch pi.ImportSource {
|
||||||
|
case ControllerTypeMikrotik:
|
||||||
|
extras := MikrotikInterfaceExtras{
|
||||||
|
Comment: i.DisplayName,
|
||||||
|
Disabled: i.IsDisabled(),
|
||||||
|
}
|
||||||
|
pi.SetExtras(extras)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoutingTableInfo struct {
|
type RoutingTableInfo struct {
|
||||||
@@ -279,3 +331,30 @@ func (r RoutingTableInfo) GetRoutingTable() int {
|
|||||||
|
|
||||||
return r.Table
|
return r.Table
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IpFamily int
|
||||||
|
|
||||||
|
const (
|
||||||
|
IpFamilyIPv4 IpFamily = unix.AF_INET
|
||||||
|
IpFamilyIPv6 IpFamily = unix.AF_INET6
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f IpFamily) String() string {
|
||||||
|
switch f {
|
||||||
|
case IpFamilyIPv4:
|
||||||
|
return "IPv4"
|
||||||
|
case IpFamilyIPv6:
|
||||||
|
return "IPv6"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteRule represents a routing table rule.
|
||||||
|
type RouteRule struct {
|
||||||
|
InterfaceId InterfaceIdentifier
|
||||||
|
IpFamily IpFamily
|
||||||
|
FwMark uint32
|
||||||
|
Table int
|
||||||
|
HasDefault bool
|
||||||
|
}
|
||||||
|
@@ -129,7 +129,7 @@ func (p *Peer) GenerateDisplayName(prefix string) {
|
|||||||
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
|
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OverwriteUserEditableFields overwrites the user editable fields of the peer with the values from the userPeer
|
// OverwriteUserEditableFields overwrites the user-editable fields of the peer with the values from the userPeer
|
||||||
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
|
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
|
||||||
p.DisplayName = userPeer.DisplayName
|
p.DisplayName = userPeer.DisplayName
|
||||||
if cfg.Core.EditableKeys {
|
if cfg.Core.EditableKeys {
|
||||||
@@ -182,9 +182,12 @@ type PhysicalPeer struct {
|
|||||||
|
|
||||||
BytesUpload uint64 // upload bytes are the number of bytes that the remote peer has sent to the server
|
BytesUpload uint64 // upload bytes are the number of bytes that the remote peer has sent to the server
|
||||||
BytesDownload uint64 // upload bytes are the number of bytes that the remote peer has received from the server
|
BytesDownload uint64 // upload bytes are the number of bytes that the remote peer has received from the server
|
||||||
|
|
||||||
|
ImportSource string // import source (wgctrl, file, ...)
|
||||||
|
backendExtras any // additional backend-specific extras, e.g., domain.MikrotikPeerExtras
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
func (p *PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
||||||
if p.PresharedKey == "" {
|
if p.PresharedKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -196,7 +199,7 @@ func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
|||||||
return &key
|
return &key
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PhysicalPeer) GetEndpointAddress() *net.UDPAddr {
|
func (p *PhysicalPeer) GetEndpointAddress() *net.UDPAddr {
|
||||||
if p.Endpoint == "" {
|
if p.Endpoint == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -208,7 +211,7 @@ func (p PhysicalPeer) GetEndpointAddress() *net.UDPAddr {
|
|||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PhysicalPeer) GetPersistentKeepaliveTime() *time.Duration {
|
func (p *PhysicalPeer) GetPersistentKeepaliveTime() *time.Duration {
|
||||||
if p.PersistentKeepalive == 0 {
|
if p.PersistentKeepalive == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -217,7 +220,7 @@ func (p PhysicalPeer) GetPersistentKeepaliveTime() *time.Duration {
|
|||||||
return &keepAliveDuration
|
return &keepAliveDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PhysicalPeer) GetAllowedIPs() []net.IPNet {
|
func (p *PhysicalPeer) GetAllowedIPs() []net.IPNet {
|
||||||
allowedIPs := make([]net.IPNet, len(p.AllowedIPs))
|
allowedIPs := make([]net.IPNet, len(p.AllowedIPs))
|
||||||
for i, ip := range p.AllowedIPs {
|
for i, ip := range p.AllowedIPs {
|
||||||
allowedIPs[i] = *ip.IpNet()
|
allowedIPs[i] = *ip.IpNet()
|
||||||
@@ -226,6 +229,21 @@ func (p PhysicalPeer) GetAllowedIPs() []net.IPNet {
|
|||||||
return allowedIPs
|
return allowedIPs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PhysicalPeer) GetExtras() any {
|
||||||
|
return p.backendExtras
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PhysicalPeer) SetExtras(extras any) {
|
||||||
|
switch extras.(type) {
|
||||||
|
case MikrotikPeerExtras: // OK
|
||||||
|
case LocalPeerExtras: // OK
|
||||||
|
default: // we only support MikrotikPeerExtras and LocalPeerExtras for now
|
||||||
|
panic(fmt.Sprintf("unsupported peer backend extras type %T", extras))
|
||||||
|
}
|
||||||
|
|
||||||
|
p.backendExtras = extras
|
||||||
|
}
|
||||||
|
|
||||||
func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
|
func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
|
||||||
peer := &Peer{
|
peer := &Peer{
|
||||||
Endpoint: NewConfigOption(pp.Endpoint, true),
|
Endpoint: NewConfigOption(pp.Endpoint, true),
|
||||||
@@ -244,6 +262,44 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pp.GetExtras() == nil {
|
||||||
|
return peer
|
||||||
|
}
|
||||||
|
|
||||||
|
// enrich the data with controller-specific extras
|
||||||
|
now := time.Now()
|
||||||
|
switch pp.ImportSource {
|
||||||
|
case ControllerTypeMikrotik:
|
||||||
|
extras := pp.GetExtras().(MikrotikPeerExtras)
|
||||||
|
peer.Notes = extras.Comment
|
||||||
|
peer.DisplayName = extras.Name
|
||||||
|
if extras.ClientEndpoint != "" { // if the client endpoint is set, we assume that this is a client peer
|
||||||
|
peer.Endpoint = NewConfigOption(extras.ClientEndpoint, true)
|
||||||
|
peer.Interface.Type = InterfaceTypeClient
|
||||||
|
peer.Interface.Addresses, _ = CidrsFromString(extras.ClientAddress)
|
||||||
|
peer.Interface.DnsStr = NewConfigOption(extras.ClientDns, true)
|
||||||
|
peer.PersistentKeepalive = NewConfigOption(extras.ClientKeepalive, true)
|
||||||
|
} else {
|
||||||
|
peer.Interface.Type = InterfaceTypeServer
|
||||||
|
}
|
||||||
|
if extras.Disabled {
|
||||||
|
peer.Disabled = &now
|
||||||
|
peer.DisabledReason = "Disabled by Mikrotik controller"
|
||||||
|
} else {
|
||||||
|
peer.Disabled = nil
|
||||||
|
peer.DisabledReason = ""
|
||||||
|
}
|
||||||
|
case ControllerTypeLocal:
|
||||||
|
extras := pp.GetExtras().(LocalPeerExtras)
|
||||||
|
if extras.Disabled {
|
||||||
|
peer.Disabled = &now
|
||||||
|
peer.DisabledReason = "Disabled by Local controller"
|
||||||
|
} else {
|
||||||
|
peer.Disabled = nil
|
||||||
|
peer.DisabledReason = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return peer
|
return peer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,9 +321,30 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
|
|||||||
pp.PresharedKey = p.PresharedKey
|
pp.PresharedKey = p.PresharedKey
|
||||||
pp.PublicKey = p.Interface.PublicKey
|
pp.PublicKey = p.Interface.PublicKey
|
||||||
pp.PersistentKeepalive = p.PersistentKeepalive.GetValue()
|
pp.PersistentKeepalive = p.PersistentKeepalive.GetValue()
|
||||||
|
|
||||||
|
switch pp.ImportSource {
|
||||||
|
case ControllerTypeMikrotik:
|
||||||
|
extras := MikrotikPeerExtras{
|
||||||
|
Id: "",
|
||||||
|
Name: p.DisplayName,
|
||||||
|
Comment: p.Notes,
|
||||||
|
IsResponder: false,
|
||||||
|
Disabled: p.IsDisabled(),
|
||||||
|
ClientEndpoint: p.Endpoint.GetValue(),
|
||||||
|
ClientAddress: CidrsToString(p.Interface.Addresses),
|
||||||
|
ClientDns: p.Interface.DnsStr.GetValue(),
|
||||||
|
ClientKeepalive: p.PersistentKeepalive.GetValue(),
|
||||||
|
}
|
||||||
|
pp.SetExtras(extras)
|
||||||
|
case ControllerTypeLocal:
|
||||||
|
extras := LocalPeerExtras{
|
||||||
|
Disabled: p.IsDisabled(),
|
||||||
|
}
|
||||||
|
pp.SetExtras(extras)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerCreationRequest struct {
|
type PeerCreationRequest struct {
|
||||||
UserIdentifiers []string
|
UserIdentifiers []string
|
||||||
Suffix string
|
Prefix string
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,27 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type PeerStatus struct {
|
type PeerStatus struct {
|
||||||
PeerId PeerIdentifier `gorm:"primaryKey;column:identifier"`
|
PeerId PeerIdentifier `gorm:"primaryKey;column:identifier" json:"PeerId"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"-"`
|
||||||
|
|
||||||
IsPingable bool `gorm:"column:pingable"`
|
IsConnected bool `gorm:"column:connected" json:"IsConnected"` // indicates if the peer is connected based on the last handshake or ping
|
||||||
LastPing *time.Time `gorm:"column:last_ping"`
|
|
||||||
|
|
||||||
BytesReceived uint64 `gorm:"column:received"`
|
IsPingable bool `gorm:"column:pingable" json:"IsPingable"`
|
||||||
BytesTransmitted uint64 `gorm:"column:transmitted"`
|
LastPing *time.Time `gorm:"column:last_ping" json:"LastPing"`
|
||||||
|
|
||||||
LastHandshake *time.Time `gorm:"column:last_handshake"`
|
BytesReceived uint64 `gorm:"column:received" json:"BytesReceived"`
|
||||||
Endpoint string `gorm:"column:endpoint"`
|
BytesTransmitted uint64 `gorm:"column:transmitted" json:"BytesTransmitted"`
|
||||||
LastSessionStart *time.Time `gorm:"column:last_session_start"`
|
|
||||||
|
LastHandshake *time.Time `gorm:"column:last_handshake" json:"LastHandshake"`
|
||||||
|
Endpoint string `gorm:"column:endpoint" json:"Endpoint"`
|
||||||
|
LastSessionStart *time.Time `gorm:"column:last_session_start" json:"LastSessionStart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s PeerStatus) IsConnected() bool {
|
func (s *PeerStatus) CalcConnected() {
|
||||||
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
||||||
|
|
||||||
handshakeValid := false
|
handshakeValid := false
|
||||||
@@ -25,7 +29,7 @@ func (s PeerStatus) IsConnected() bool {
|
|||||||
handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime)
|
handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.IsPingable || handshakeValid
|
s.IsConnected = s.IsPingable || handshakeValid
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceStatus struct {
|
type InterfaceStatus struct {
|
||||||
@@ -35,3 +39,25 @@ type InterfaceStatus struct {
|
|||||||
BytesReceived uint64 `gorm:"column:received"`
|
BytesReceived uint64 `gorm:"column:received"`
|
||||||
BytesTransmitted uint64 `gorm:"column:transmitted"`
|
BytesTransmitted uint64 `gorm:"column:transmitted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PingerResult struct {
|
||||||
|
PacketsRecv int
|
||||||
|
PacketsSent int
|
||||||
|
Rtts []time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PingerResult) IsPingable() bool {
|
||||||
|
return r.PacketsRecv > 0 && r.PacketsSent > 0 && len(r.Rtts) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PingerResult) AverageRtt() time.Duration {
|
||||||
|
if len(r.Rtts) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var total time.Duration
|
||||||
|
for _, rtt := range r.Rtts {
|
||||||
|
total += rtt
|
||||||
|
}
|
||||||
|
return total / time.Duration(len(r.Rtts))
|
||||||
|
}
|
||||||
|
@@ -66,8 +66,9 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := tt.status.IsConnected(); got != tt.want {
|
tt.status.CalcConnected()
|
||||||
t.Errorf("IsConnected() = %v, want %v", got, tt.want)
|
if got := tt.status.IsConnected; got != tt.want {
|
||||||
|
t.Errorf("IsConnected = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -12,8 +12,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetupLogging initializes the global logger with the given level and format
|
// GetLoggingHandler initializes a slog.Handler based on the provided logging level and format options.
|
||||||
func SetupLogging(level string, pretty, json bool) {
|
func GetLoggingHandler(level string, pretty, json bool) slog.Handler {
|
||||||
var logLevel = new(slog.LevelVar)
|
var logLevel = new(slog.LevelVar)
|
||||||
|
|
||||||
switch strings.ToLower(level) {
|
switch strings.ToLower(level) {
|
||||||
@@ -46,6 +46,13 @@ func SetupLogging(level string, pretty, json bool) {
|
|||||||
handler = slog.NewTextHandler(output, opts)
|
handler = slog.NewTextHandler(output, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupLogging initializes the global logger with the given level and format
|
||||||
|
func SetupLogging(level string, pretty, json bool) {
|
||||||
|
handler := GetLoggingHandler(level, pretty, json)
|
||||||
|
|
||||||
logger := slog.New(handler)
|
logger := slog.New(handler)
|
||||||
|
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
435
internal/lowlevel/mikrotik.go
Normal file
435
internal/lowlevel/mikrotik.go
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
package lowlevel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// region models
|
||||||
|
|
||||||
|
const (
|
||||||
|
MikrotikApiStatusOk = "success"
|
||||||
|
MikrotikApiStatusError = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MikrotikApiErrorCodeUnknown = iota + 600
|
||||||
|
MikrotikApiErrorCodeRequestPreparationFailed
|
||||||
|
MikrotikApiErrorCodeRequestFailed
|
||||||
|
MikrotikApiErrorCodeResponseDecodeFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
type MikrotikApiResponse[T any] struct {
|
||||||
|
Status string
|
||||||
|
Code int
|
||||||
|
Data T `json:"data,omitempty"`
|
||||||
|
Error *MikrotikApiError `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MikrotikApiError struct {
|
||||||
|
Code int `json:"error,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Details string `json:"detail,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MikrotikApiError) String() string {
|
||||||
|
if e == nil {
|
||||||
|
return "no error"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("API error %d: %s - %s", e.Code, e.Message, e.Details)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenericJsonObject map[string]any
|
||||||
|
type EmptyResponse struct{}
|
||||||
|
|
||||||
|
func (JsonObject GenericJsonObject) GetString(key string) string {
|
||||||
|
if value, ok := JsonObject[key]; ok {
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
return strValue
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%v", value) // Convert to string if not already
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JsonObject GenericJsonObject) GetInt(key string) int {
|
||||||
|
if value, ok := JsonObject[key]; ok {
|
||||||
|
if intValue, ok := value.(int); ok {
|
||||||
|
return intValue
|
||||||
|
} else {
|
||||||
|
if floatValue, ok := value.(float64); ok {
|
||||||
|
return int(floatValue) // Convert float64 to int
|
||||||
|
}
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
if intValue, err := strconv.Atoi(strValue); err == nil {
|
||||||
|
return intValue // Convert string to int if possible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JsonObject GenericJsonObject) GetBool(key string) bool {
|
||||||
|
if value, ok := JsonObject[key]; ok {
|
||||||
|
if boolValue, ok := value.(bool); ok {
|
||||||
|
return boolValue
|
||||||
|
} else {
|
||||||
|
if intValue, ok := value.(int); ok {
|
||||||
|
return intValue == 1 // Convert int to bool (1 is true, 0 is false)
|
||||||
|
}
|
||||||
|
if floatValue, ok := value.(float64); ok {
|
||||||
|
return int(floatValue) == 1 // Convert float64 to bool (1.0 is true, 0.0 is false)
|
||||||
|
}
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
boolValue, err := strconv.ParseBool(strValue)
|
||||||
|
if err == nil {
|
||||||
|
return boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type MikrotikRequestOptions struct {
|
||||||
|
Filters map[string]string `json:"filters,omitempty"`
|
||||||
|
PropList []string `json:"proplist,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *MikrotikRequestOptions) GetPath(base string) string {
|
||||||
|
if o == nil {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := url.Parse(base)
|
||||||
|
if err != nil {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
query := path.Query()
|
||||||
|
for k, v := range o.Filters {
|
||||||
|
query.Set(k, v)
|
||||||
|
}
|
||||||
|
if len(o.PropList) > 0 {
|
||||||
|
query.Set(".proplist", strings.Join(o.PropList, ","))
|
||||||
|
}
|
||||||
|
path.RawQuery = query.Encode()
|
||||||
|
return path.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// region models
|
||||||
|
|
||||||
|
// region API-client
|
||||||
|
|
||||||
|
type MikrotikApiClient struct {
|
||||||
|
coreCfg *config.Config
|
||||||
|
cfg *config.BackendMikrotik
|
||||||
|
|
||||||
|
client *http.Client
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMikrotikApiClient(coreCfg *config.Config, cfg *config.BackendMikrotik) (*MikrotikApiClient, error) {
|
||||||
|
c := &MikrotikApiClient{
|
||||||
|
coreCfg: coreCfg,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.setup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.debugLog("Mikrotik api client created", "api_url", cfg.ApiUrl)
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) setup() error {
|
||||||
|
m.client = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: !m.cfg.ApiVerifyTls,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timeout: m.cfg.GetApiTimeout(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.cfg.Debug {
|
||||||
|
m.log = slog.New(internal.GetLoggingHandler("debug",
|
||||||
|
m.coreCfg.Advanced.LogPretty,
|
||||||
|
m.coreCfg.Advanced.LogJson).
|
||||||
|
WithAttrs([]slog.Attr{
|
||||||
|
{
|
||||||
|
Key: "mikrotik-bid", Value: slog.StringValue(m.cfg.Id),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) debugLog(msg string, args ...any) {
|
||||||
|
if m.log != nil {
|
||||||
|
m.log.Debug("[MT-API] "+msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) getFullPath(command string) string {
|
||||||
|
path, err := url.JoinPath(m.cfg.ApiUrl, command)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) prepareGetRequest(ctx context.Context, fullUrl string) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if m.cfg.ApiUser != "" && m.cfg.ApiPassword != "" {
|
||||||
|
req.SetBasicAuth(m.cfg.ApiUser, m.cfg.ApiPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) prepareDeleteRequest(ctx context.Context, fullUrl string) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fullUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if m.cfg.ApiUser != "" && m.cfg.ApiPassword != "" {
|
||||||
|
req.SetBasicAuth(m.cfg.ApiUser, m.cfg.ApiPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) preparePayloadRequest(
|
||||||
|
ctx context.Context,
|
||||||
|
method string,
|
||||||
|
fullUrl string,
|
||||||
|
payload GenericJsonObject,
|
||||||
|
) (*http.Request, error) {
|
||||||
|
// marshal the payload to JSON
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, fullUrl, bytes.NewReader(payloadBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if m.cfg.ApiUser != "" && m.cfg.ApiPassword != "" {
|
||||||
|
req.SetBasicAuth(m.cfg.ApiUser, m.cfg.ApiPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func errToApiResponse[T any](code int, message string, err error) MikrotikApiResponse[T] {
|
||||||
|
return MikrotikApiResponse[T]{
|
||||||
|
Status: MikrotikApiStatusError,
|
||||||
|
Code: code,
|
||||||
|
Error: &MikrotikApiError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Details: err.Error(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHttpResponse[T any](resp *http.Response, err error) MikrotikApiResponse[T] {
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[T](MikrotikApiErrorCodeRequestFailed, "failed to execute request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
err := Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to close response body", "error", err)
|
||||||
|
}
|
||||||
|
}(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
|
var data T
|
||||||
|
|
||||||
|
// if the type of T is EmptyResponse, we can return an empty response with just the status
|
||||||
|
if _, ok := any(data).(EmptyResponse); ok {
|
||||||
|
return MikrotikApiResponse[T]{Status: MikrotikApiStatusOk, Code: resp.StatusCode}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
return errToApiResponse[T](MikrotikApiErrorCodeResponseDecodeFailed, "failed to decode response", err)
|
||||||
|
}
|
||||||
|
return MikrotikApiResponse[T]{Status: MikrotikApiStatusOk, Code: resp.StatusCode, Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiErr MikrotikApiError
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
|
||||||
|
return errToApiResponse[T](resp.StatusCode, "unknown error, unparsable response", err)
|
||||||
|
} else {
|
||||||
|
return MikrotikApiResponse[T]{Status: MikrotikApiStatusError, Code: resp.StatusCode, Error: &apiErr}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) Query(
|
||||||
|
ctx context.Context,
|
||||||
|
command string,
|
||||||
|
opts *MikrotikRequestOptions,
|
||||||
|
) MikrotikApiResponse[[]GenericJsonObject] {
|
||||||
|
apiCtx, cancel := context.WithTimeout(ctx, m.cfg.GetApiTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullUrl := opts.GetPath(m.getFullPath(command))
|
||||||
|
|
||||||
|
req, err := m.prepareGetRequest(apiCtx, fullUrl)
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[[]GenericJsonObject](MikrotikApiErrorCodeRequestPreparationFailed,
|
||||||
|
"failed to create request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
m.debugLog("executing API query", "url", fullUrl)
|
||||||
|
response := parseHttpResponse[[]GenericJsonObject](m.client.Do(req))
|
||||||
|
m.debugLog("retrieved API query result", "url", fullUrl, "duration", time.Since(start).String())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) Get(
|
||||||
|
ctx context.Context,
|
||||||
|
command string,
|
||||||
|
opts *MikrotikRequestOptions,
|
||||||
|
) MikrotikApiResponse[GenericJsonObject] {
|
||||||
|
apiCtx, cancel := context.WithTimeout(ctx, m.cfg.GetApiTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullUrl := opts.GetPath(m.getFullPath(command))
|
||||||
|
|
||||||
|
req, err := m.prepareGetRequest(apiCtx, fullUrl)
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[GenericJsonObject](MikrotikApiErrorCodeRequestPreparationFailed,
|
||||||
|
"failed to create request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
m.debugLog("executing API get", "url", fullUrl)
|
||||||
|
response := parseHttpResponse[GenericJsonObject](m.client.Do(req))
|
||||||
|
m.debugLog("retrieved API get result", "url", fullUrl, "duration", time.Since(start).String())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) Create(
|
||||||
|
ctx context.Context,
|
||||||
|
command string,
|
||||||
|
payload GenericJsonObject,
|
||||||
|
) MikrotikApiResponse[GenericJsonObject] {
|
||||||
|
apiCtx, cancel := context.WithTimeout(ctx, m.cfg.GetApiTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullUrl := m.getFullPath(command)
|
||||||
|
|
||||||
|
req, err := m.preparePayloadRequest(apiCtx, http.MethodPut, fullUrl, payload)
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[GenericJsonObject](MikrotikApiErrorCodeRequestPreparationFailed,
|
||||||
|
"failed to create request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
m.debugLog("executing API put", "url", fullUrl)
|
||||||
|
response := parseHttpResponse[GenericJsonObject](m.client.Do(req))
|
||||||
|
m.debugLog("retrieved API put result", "url", fullUrl, "duration", time.Since(start).String())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) Update(
|
||||||
|
ctx context.Context,
|
||||||
|
command string,
|
||||||
|
payload GenericJsonObject,
|
||||||
|
) MikrotikApiResponse[GenericJsonObject] {
|
||||||
|
apiCtx, cancel := context.WithTimeout(ctx, m.cfg.GetApiTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullUrl := m.getFullPath(command)
|
||||||
|
|
||||||
|
req, err := m.preparePayloadRequest(apiCtx, http.MethodPatch, fullUrl, payload)
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[GenericJsonObject](MikrotikApiErrorCodeRequestPreparationFailed,
|
||||||
|
"failed to create request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
m.debugLog("executing API patch", "url", fullUrl)
|
||||||
|
response := parseHttpResponse[GenericJsonObject](m.client.Do(req))
|
||||||
|
m.debugLog("retrieved API patch result", "url", fullUrl, "duration", time.Since(start).String())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) Delete(
|
||||||
|
ctx context.Context,
|
||||||
|
command string,
|
||||||
|
) MikrotikApiResponse[EmptyResponse] {
|
||||||
|
apiCtx, cancel := context.WithTimeout(ctx, m.cfg.GetApiTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullUrl := m.getFullPath(command)
|
||||||
|
|
||||||
|
req, err := m.prepareDeleteRequest(apiCtx, fullUrl)
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[EmptyResponse](MikrotikApiErrorCodeRequestPreparationFailed,
|
||||||
|
"failed to create request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
m.debugLog("executing API delete", "url", fullUrl)
|
||||||
|
response := parseHttpResponse[EmptyResponse](m.client.Do(req))
|
||||||
|
m.debugLog("retrieved API delete result", "url", fullUrl, "duration", time.Since(start).String())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) ExecList(
|
||||||
|
ctx context.Context,
|
||||||
|
command string,
|
||||||
|
payload GenericJsonObject,
|
||||||
|
) MikrotikApiResponse[[]GenericJsonObject] {
|
||||||
|
apiCtx, cancel := context.WithTimeout(ctx, m.cfg.GetApiTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullUrl := m.getFullPath(command)
|
||||||
|
|
||||||
|
req, err := m.preparePayloadRequest(apiCtx, http.MethodPost, fullUrl, payload)
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[[]GenericJsonObject](MikrotikApiErrorCodeRequestPreparationFailed,
|
||||||
|
"failed to create request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
m.debugLog("executing API post", "url", fullUrl)
|
||||||
|
response := parseHttpResponse[[]GenericJsonObject](m.client.Do(req))
|
||||||
|
m.debugLog("retrieved API post result", "url", fullUrl, "duration", time.Since(start).String())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion API-client
|
@@ -80,8 +80,10 @@ nav:
|
|||||||
- Examples: documentation/configuration/examples.md
|
- Examples: documentation/configuration/examples.md
|
||||||
- Usage:
|
- Usage:
|
||||||
- General: documentation/usage/general.md
|
- General: documentation/usage/general.md
|
||||||
|
- Backends: documentation/usage/backends.md
|
||||||
- LDAP: documentation/usage/ldap.md
|
- LDAP: documentation/usage/ldap.md
|
||||||
- Security: documentation/usage/security.md
|
- Security: documentation/usage/security.md
|
||||||
|
- Webhooks: documentation/usage/webhooks.md
|
||||||
- REST API: documentation/rest-api/api-doc.md
|
- REST API: documentation/rest-api/api-doc.md
|
||||||
- Upgrade: documentation/upgrade/v1.md
|
- Upgrade: documentation/upgrade/v1.md
|
||||||
- Monitoring: documentation/monitoring/prometheus.md
|
- Monitoring: documentation/monitoring/prometheus.md
|
||||||
|
Reference in New Issue
Block a user