mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-08 09:16:18 +00:00
Compare commits
8 Commits
mikrotik_i
...
doc_improv
Author | SHA1 | Date | |
---|---|---|---|
|
e65cab4857 | ||
|
75ec234a72 | ||
|
4729bccdd3 | ||
|
afb38b685c | ||
|
7cd7d13dc7 | ||
|
d945e313b2 | ||
|
c5fe82ab11 | ||
|
765fb09770 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: h44z # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2020-2023 Christoph Haas
|
Copyright (c) 2020-2025 Christoph Haas
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
a copy of this software and associated documentation files (the
|
||||||
|
15
README.md
15
README.md
@@ -21,7 +21,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Pos
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Self-hosted - the whole application is a single binary
|
* Self-hosted - the whole application is a single binary
|
||||||
* Responsive multi-language web UI written in Vue.js
|
* Responsive multi-language web UI with dark-mode written in Vue.js
|
||||||
* Automatically selects IP from the network pool assigned to the client
|
* Automatically selects IP from the network pool assigned to the client
|
||||||
* QR-Code for convenient mobile client configuration
|
* QR-Code for convenient mobile client configuration
|
||||||
* Sends email to the client with QR-code and client config
|
* Sends email to the client with QR-code and client config
|
||||||
@@ -32,7 +32,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Pos
|
|||||||
* Docker ready
|
* Docker ready
|
||||||
* Can be used with existing WireGuard setups
|
* Can be used with existing WireGuard setups
|
||||||
* Support for multiple WireGuard interfaces
|
* Support for multiple WireGuard interfaces
|
||||||
* Supports multiple WireGuard backends (wgctrl or MikroTik [BETA])
|
* Supports multiple WireGuard backends (wgctrl or MikroTik)
|
||||||
* Peer Expiry Feature
|
* Peer Expiry Feature
|
||||||
* Handles route and DNS settings like wg-quick does
|
* Handles route and DNS settings like wg-quick does
|
||||||
* Exposes Prometheus metrics for monitoring and alerting
|
* Exposes Prometheus metrics for monitoring and alerting
|
||||||
@@ -62,6 +62,17 @@ For the complete documentation visit [wgportal.org](https://wgportal.org).
|
|||||||
|
|
||||||
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
|
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
|
||||||
|
|
||||||
|
## Contributors and Sponsors
|
||||||
|
|
||||||
|
Thanks so much for all your contributions! They’re truly appreciated and help keep WireGuard Portal moving ahead.
|
||||||
|
|
||||||
|
<a href="https://github.com/h44z/wg-portal/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=h44z/wg-portal" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Want to support the project? You can buy me a coffee or join as a contributor - every bit of support helps!
|
||||||
|
[Become a sponsor!](https://github.com/sponsors/h44z)
|
||||||
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
||||||
|
@@ -7,7 +7,7 @@ If you believe you've found a security issue in one of the supported versions of
|
|||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|---------|--------------------|
|
|---------|--------------------|
|
||||||
| v2.x | :white_check_mark: |
|
| v2.x | :white_check_mark: |
|
||||||
| v1.x | :white_check_mark: |
|
| v1.x | :x: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
BIN
docs/assets/images/wgportal_dark.png
Normal file
BIN
docs/assets/images/wgportal_dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
BIN
docs/assets/images/wgportal_light.png
Normal file
BIN
docs/assets/images/wgportal_light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 129 KiB |
@@ -11,6 +11,24 @@ core:
|
|||||||
create_default_peer: true
|
create_default_peer: true
|
||||||
self_provisioning_allowed: true
|
self_provisioning_allowed: true
|
||||||
|
|
||||||
|
backend:
|
||||||
|
# default backend decides where new interfaces are created
|
||||||
|
default: mikrotik
|
||||||
|
|
||||||
|
mikrotik:
|
||||||
|
- id: mikrotik # unique id, not "local"
|
||||||
|
display_name: RouterOS RB5009 # optional nice name
|
||||||
|
api_url: https://10.10.10.10/rest
|
||||||
|
api_user: wgportal
|
||||||
|
api_password: a-super-secret-password
|
||||||
|
api_verify_tls: false # set to false only if using self-signed during testing
|
||||||
|
api_timeout: 30s # maximum request duration
|
||||||
|
concurrency: 5 # limit parallel REST calls to device
|
||||||
|
debug: false # verbose logging for this backend
|
||||||
|
ignored_interfaces: # ignore these interfaces during import
|
||||||
|
- wgTest1
|
||||||
|
- wgTest2
|
||||||
|
|
||||||
web:
|
web:
|
||||||
site_title: My WireGuard Server
|
site_title: My WireGuard Server
|
||||||
site_company_name: My Company
|
site_company_name: My Company
|
||||||
@@ -195,3 +213,5 @@ auth:
|
|||||||
registration_enabled: true
|
registration_enabled: true
|
||||||
log_user_info: true
|
log_user_info: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For more information, check out the usage documentation (e.g. [General Configuration](../usage/general.md) or [Backends Configuration](../usage/backends.md)).
|
||||||
|
@@ -184,6 +184,11 @@ The current MikroTik backend is in **BETA** and may not support all features.
|
|||||||
- **Description:** The default backend to use for managing WireGuard interfaces.
|
- **Description:** The default backend to use for managing WireGuard interfaces.
|
||||||
Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
|
Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
|
||||||
|
|
||||||
|
### `ignored_local_interfaces`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of interface names to exclude when enumerating local interfaces.
|
||||||
|
This is useful if you want to prevent certain interfaces from being imported from the local system.
|
||||||
|
|
||||||
### Mikrotik
|
### Mikrotik
|
||||||
|
|
||||||
The `mikrotik` array contains a list of MikroTik backend definitions. Each entry describes how to connect to a MikroTik RouterOS instance that hosts WireGuard interfaces.
|
The `mikrotik` array contains a list of MikroTik backend definitions. Each entry describes how to connect to a MikroTik RouterOS instance that hosts WireGuard interfaces.
|
||||||
@@ -225,6 +230,11 @@ Below are the properties for each entry inside `backend.mikrotik`:
|
|||||||
- **Default:** `5`
|
- **Default:** `5`
|
||||||
- **Description:** Maximum number of concurrent API requests the backend will issue when enumerating interfaces and their details. If `0` or negative, a sane default of `5` is used.
|
- **Description:** Maximum number of concurrent API requests the backend will issue when enumerating interfaces and their details. If `0` or negative, a sane default of `5` is used.
|
||||||
|
|
||||||
|
#### `ignored_interfaces`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of interface names to exclude during interface enumeration.
|
||||||
|
This is useful if you want to prevent specific interfaces from being imported from the MikroTik device.
|
||||||
|
|
||||||
#### `debug`
|
#### `debug`
|
||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
- **Description:** Enable verbose debug logging for the MikroTik backend.
|
- **Description:** Enable verbose debug logging for the MikroTik backend.
|
||||||
|
2
docs/javascript/img-comparison-slider.js
Normal file
2
docs/javascript/img-comparison-slider.js
Normal file
File diff suppressed because one or more lines are too long
1
docs/javascript/img-comparison-slider.js.map
Normal file
1
docs/javascript/img-comparison-slider.js.map
Normal file
File diff suppressed because one or more lines are too long
15
docs/stylesheets/img-comparison-slider.css
Normal file
15
docs/stylesheets/img-comparison-slider.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
img-comparison-slider {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
img-comparison-slider [slot='second'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img-comparison-slider.rendered {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
img-comparison-slider.rendered [slot='second'] {
|
||||||
|
display: unset;
|
||||||
|
}
|
@@ -300,6 +300,59 @@
|
|||||||
background: var(--md-accent-fg-color--transparent);
|
background: var(--md-accent-fg-color--transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.before,
|
||||||
|
.after {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after figcaption {
|
||||||
|
background: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
color: #000000;
|
||||||
|
opacity: 0.9;
|
||||||
|
padding: 9px;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.before figcaption {
|
||||||
|
background: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
color: #ffffff;
|
||||||
|
opacity: 0.9;
|
||||||
|
padding: 9px;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.before figcaption {
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
.after figcaption {
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
.custom-animated-handle {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-with-animated-handle:hover .custom-animated-handle {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
.md-typeset img-comparison-slider figure {
|
||||||
|
margin: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-overlay {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Hero for landing page -->
|
<!-- Hero for landing page -->
|
||||||
@@ -326,11 +379,34 @@
|
|||||||
|
|
||||||
<div class="md-container">
|
<div class="md-container">
|
||||||
<div class="tx-hero__image">
|
<div class="tx-hero__image">
|
||||||
<img
|
<div>
|
||||||
src="{{config.site_url}}/assets/images/screenshot.png"
|
<img-comparison-slider hover="hover">
|
||||||
alt=""
|
<figure slot="first" class="before">
|
||||||
draggable="false"
|
<img src="{{config.site_url}}/assets/images/wgportal_light.png" alt="Light Mode"/>
|
||||||
>
|
<figcaption>Light Mode</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure slot="second" class="after">
|
||||||
|
<img src="{{config.site_url}}/assets/images/wgportal_dark.png" alt="Dark Mode"/>
|
||||||
|
<figcaption>Dark Mode</figcaption>
|
||||||
|
</figure>
|
||||||
|
<svg slot="handle" class="custom-animated-handle" xmlns="http://www.w3.org/2000/svg" width="100" viewBox="-8 -3 16 6">
|
||||||
|
<!-- Left arrow (dark) -->
|
||||||
|
<path d="M -5 -2 L -7 0 L -5 2 M -5 -2 L -5 2"
|
||||||
|
stroke="#1a1a1a"
|
||||||
|
fill="#1a1a1a"
|
||||||
|
stroke-width="1"
|
||||||
|
vector-effect="non-scaling-stroke">
|
||||||
|
</path>
|
||||||
|
<!-- Right arrow (white) -->
|
||||||
|
<path d="M 5 -2 L 7 0 L 5 2 M 5 -2 L 5 2"
|
||||||
|
stroke="#fff"
|
||||||
|
fill="#fff"
|
||||||
|
stroke-width="1"
|
||||||
|
vector-effect="non-scaling-stroke">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</img-comparison-slider>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
6
go.mod
6
go.mod
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/go-ldap/ldap/v3 v3.4.11
|
github.com/go-ldap/ldap/v3 v3.4.11
|
||||||
github.com/go-pkgz/routegroup v1.5.3
|
github.com/go-pkgz/routegroup v1.5.3
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/go-webauthn/webauthn v0.13.4
|
github.com/go-webauthn/webauthn v0.14.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/prometheus-community/pro-bing v0.7.0
|
github.com/prometheus-community/pro-bing v0.7.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
@@ -29,7 +29,7 @@ require (
|
|||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/driver/sqlserver v1.6.1
|
gorm.io/driver/sqlserver v1.6.1
|
||||||
gorm.io/gorm v1.30.5
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -64,7 +64,7 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/go-test/deep v1.1.1 // indirect
|
github.com/go-test/deep v1.1.1 // indirect
|
||||||
github.com/go-webauthn/x v0.1.24 // indirect
|
github.com/go-webauthn/x v0.1.25 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
|
14
go.sum
14
go.sum
@@ -106,10 +106,10 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
|
|||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ=
|
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
|
||||||
github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI=
|
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
|
||||||
github.com/go-webauthn/x v0.1.24 h1:6LaWf2zzWqbyKT8IyQkhje1/1KCGhlEkMz4V1tDnt/A=
|
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
|
||||||
github.com/go-webauthn/x v0.1.24/go.mod h1:2o5XKJ+X1AKqYKGgHdKflGnoQFQZ6flJ2IFCBKSbSOw=
|
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
@@ -255,6 +255,8 @@ github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
|
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
|
||||||
@@ -397,8 +399,8 @@ gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXD
|
|||||||
gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY=
|
gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY=
|
||||||
gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
|
gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
|
||||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
modernc.org/cc/v4 v4.26.4 h1:jPhG8oNjtTYuP2FA4YefTJ/wioNUGALmGuEWt7SUR6s=
|
modernc.org/cc/v4 v4.26.4 h1:jPhG8oNjtTYuP2FA4YefTJ/wioNUGALmGuEWt7SUR6s=
|
||||||
modernc.org/cc/v4 v4.26.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.26.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||||
|
@@ -3,14 +3,13 @@ package wgcontroller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"github.com/h44z/wg-portal/internal/lowlevel"
|
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||||
@@ -678,11 +677,15 @@ func (c *MikrotikController) updatePeer(
|
|||||||
extras := pp.GetExtras().(domain.MikrotikPeerExtras)
|
extras := pp.GetExtras().(domain.MikrotikPeerExtras)
|
||||||
peerId := extras.Id
|
peerId := extras.Id
|
||||||
|
|
||||||
endpoint := pp.Endpoint
|
endpoint := "" // by default, we have no endpoint (the peer does not initiate a connection)
|
||||||
endpointPort := "51820" // default port if not set
|
endpointPort := "0" // by default, we have no endpoint port (the peer does not initiate a connection)
|
||||||
if s := strings.Split(endpoint, ":"); len(s) == 2 {
|
if !extras.IsResponder { // if the peer is not only a responder, it needs the endpoint to initiate a connection
|
||||||
endpoint = s[0]
|
endpoint = pp.Endpoint
|
||||||
endpointPort = s[1]
|
endpointPort = "51820" // default port if not set
|
||||||
|
if s := strings.Split(endpoint, ":"); len(s) == 2 {
|
||||||
|
endpoint = s[0]
|
||||||
|
endpointPort = s[1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedAddressStr := domain.CidrsToString(pp.AllowedIPs)
|
allowedAddressStr := domain.CidrsToString(pp.AllowedIPs)
|
||||||
|
@@ -364,6 +364,7 @@ func (a *Authenticator) passwordAuthentication(
|
|||||||
}
|
}
|
||||||
ldapUserInfo, err = ldapAuth.ParseUserInfo(rawUserInfo)
|
ldapUserInfo, err = ldapAuth.ParseUserInfo(rawUserInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
slog.Error("failed to parse ldap user info", "identifier", identifier, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,10 +377,13 @@ func (a *Authenticator) passwordAuthentication(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userSource == "" {
|
if userSource == "" {
|
||||||
|
slog.Warn("no user source found for user", "identifier", identifier, "ldapProviderCount", a.ldapAuthenticators)
|
||||||
return nil, errors.New("user not found")
|
return nil, errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSource == domain.UserSourceLdap && ldapProvider == nil {
|
if userSource == domain.UserSourceLdap && ldapProvider == nil {
|
||||||
|
slog.Warn("no ldap provider found for user",
|
||||||
|
"identifier", identifier, "ldapProviderCount", a.ldapAuthenticators)
|
||||||
return nil, errors.New("ldap provider not found")
|
return nil, errors.New("ldap provider not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -82,8 +82,9 @@ func (c *ControllerManager) registerLocalController() error {
|
|||||||
|
|
||||||
c.controllers[config.LocalBackendName] = backendInstance{
|
c.controllers[config.LocalBackendName] = backendInstance{
|
||||||
Config: config.BackendBase{
|
Config: config.BackendBase{
|
||||||
Id: config.LocalBackendName,
|
Id: config.LocalBackendName,
|
||||||
DisplayName: "Local WireGuard Controller",
|
DisplayName: "Local WireGuard Controller",
|
||||||
|
IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces,
|
||||||
},
|
},
|
||||||
Implementation: localController,
|
Implementation: localController,
|
||||||
}
|
}
|
||||||
@@ -118,17 +119,17 @@ func (c *ControllerManager) logRegisteredControllers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) InterfaceController {
|
func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) InterfaceController {
|
||||||
return c.getController(backend, "")
|
return c.getController(backend, "").Implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControllerManager) GetController(iface domain.Interface) InterfaceController {
|
func (c *ControllerManager) GetController(iface domain.Interface) InterfaceController {
|
||||||
return c.getController(iface.Backend, iface.Identifier)
|
return c.getController(iface.Backend, iface.Identifier).Implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControllerManager) getController(
|
func (c *ControllerManager) getController(
|
||||||
backend domain.InterfaceBackend,
|
backend domain.InterfaceBackend,
|
||||||
ifaceId domain.InterfaceIdentifier,
|
ifaceId domain.InterfaceIdentifier,
|
||||||
) InterfaceController {
|
) backendInstance {
|
||||||
if backend == "" {
|
if backend == "" {
|
||||||
// If no backend is specified, use the local controller.
|
// If no backend is specified, use the local controller.
|
||||||
// This might be the case for interfaces created in previous WireGuard Portal versions.
|
// This might be the case for interfaces created in previous WireGuard Portal versions.
|
||||||
@@ -145,13 +146,13 @@ func (c *ControllerManager) getController(
|
|||||||
slog.Warn("controller for backend not found, using local controller",
|
slog.Warn("controller for backend not found, using local controller",
|
||||||
"backend", backend, "interface", ifaceId)
|
"backend", backend, "interface", ifaceId)
|
||||||
}
|
}
|
||||||
return controller.Implementation
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControllerManager) GetAllControllers() []InterfaceController {
|
func (c *ControllerManager) GetAllControllers() []backendInstance {
|
||||||
var backendInstances = make([]InterfaceController, 0, len(c.controllers))
|
var backendInstances = make([]backendInstance, 0, len(c.controllers))
|
||||||
for instance := range maps.Values(c.controllers) {
|
for instance := range maps.Values(c.controllers) {
|
||||||
backendInstances = append(backendInstances, instance.Implementation)
|
backendInstances = append(backendInstances, instance)
|
||||||
}
|
}
|
||||||
return backendInstances
|
return backendInstances
|
||||||
}
|
}
|
||||||
|
@@ -15,26 +15,6 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetImportableInterfaces returns all physical interfaces that are available on the system.
|
|
||||||
// This function also returns interfaces that are already available in the database.
|
|
||||||
func (m Manager) GetImportableInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
|
|
||||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var allPhysicalInterfaces []domain.PhysicalInterface
|
|
||||||
for _, wgBackend := range m.wg.GetAllControllers() {
|
|
||||||
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
allPhysicalInterfaces = append(allPhysicalInterfaces, physicalInterfaces...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allPhysicalInterfaces, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInterfaceAndPeers returns the interface and all peers for the given interface identifier.
|
// GetInterfaceAndPeers returns the interface and all peers for the given interface identifier.
|
||||||
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
|
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
*domain.Interface,
|
*domain.Interface,
|
||||||
@@ -110,52 +90,62 @@ func (m Manager) GetUserInterfaces(ctx context.Context, _ domain.UserIdentifier)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ImportNewInterfaces imports all new physical interfaces that are available on the system.
|
// ImportNewInterfaces imports all new physical interfaces that are available on the system.
|
||||||
|
// If a filter is set, only interfaces that match the filter will be imported.
|
||||||
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
|
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
|
||||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingInterfaceIds []domain.InterfaceIdentifier
|
||||||
|
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
for _, existingInterface := range existingInterfaces {
|
||||||
|
existingInterfaceIds = append(existingInterfaceIds, existingInterface.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
imported := 0
|
imported := 0
|
||||||
for _, wgBackend := range m.wg.GetAllControllers() {
|
for _, wgBackend := range m.wg.GetAllControllers() {
|
||||||
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
|
physicalInterfaces, err := wgBackend.Implementation.GetInterfaces(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no filter is given, exclude already existing interfaces
|
|
||||||
var excludedInterfaces []domain.InterfaceIdentifier
|
|
||||||
if len(filter) == 0 {
|
|
||||||
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
for _, existingInterface := range existingInterfaces {
|
|
||||||
excludedInterfaces = append(excludedInterfaces, existingInterface.Identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, physicalInterface := range physicalInterfaces {
|
for _, physicalInterface := range physicalInterfaces {
|
||||||
if slices.Contains(excludedInterfaces, physicalInterface.Identifier) {
|
if slices.Contains(wgBackend.Config.IgnoredInterfaces, string(physicalInterface.Identifier)) {
|
||||||
|
slog.Info("ignoring interface due to backend filter restrictions",
|
||||||
|
"interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces,
|
||||||
|
"backend", wgBackend.Config.Id)
|
||||||
|
continue // skip ignored interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(existingInterfaceIds, physicalInterface.Identifier) {
|
||||||
|
continue // skip interfaces that already exist
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter) > 0 && !slices.Contains(filter, physicalInterface.Identifier) {
|
||||||
|
slog.Info("ignoring interface due to filter restrictions",
|
||||||
|
"interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces,
|
||||||
|
"backend", wgBackend.Config.Id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) {
|
slog.Info("importing new interface",
|
||||||
continue
|
"interface", physicalInterface.Identifier, "backend", wgBackend.Config.Id)
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("importing new interface", "interface", physicalInterface.Identifier)
|
physicalPeers, err := wgBackend.Implementation.GetPeers(ctx, physicalInterface.Identifier)
|
||||||
|
|
||||||
physicalPeers, err := wgBackend.GetPeers(ctx, physicalInterface.Identifier)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.importInterface(ctx, wgBackend, &physicalInterface, physicalPeers)
|
err = m.importInterface(ctx, wgBackend.Implementation, &physicalInterface, physicalPeers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
|
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("imported new interface", "interface", physicalInterface.Identifier, "peers", len(physicalPeers))
|
slog.Info("imported new interface",
|
||||||
|
"interface", physicalInterface.Identifier, "peers", len(physicalPeers), "backend", wgBackend.Config.Id)
|
||||||
imported++
|
imported++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,9 +211,11 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err)
|
return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = m.wg.GetController(iface).GetInterface(ctx, iface.Identifier)
|
controller := m.wg.GetController(iface)
|
||||||
|
|
||||||
|
_, err = controller.GetInterface(ctx, iface.Identifier)
|
||||||
if err != nil && !iface.IsDisabled() {
|
if err != nil && !iface.IsDisabled() {
|
||||||
slog.Debug("creating missing interface", "interface", iface.Identifier)
|
slog.Debug("creating missing interface", "interface", iface.Identifier, "backend", controller.GetId())
|
||||||
|
|
||||||
// temporarily disable interface in database so that the current state is reflected correctly
|
// temporarily disable interface in database so that the current state is reflected correctly
|
||||||
_ = m.db.SaveInterface(ctx, iface.Identifier,
|
_ = m.db.SaveInterface(ctx, iface.Identifier,
|
||||||
@@ -250,7 +242,8 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err)
|
return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
slog.Debug("restoring interface state", "interface", iface.Identifier, "disabled", iface.IsDisabled())
|
slog.Debug("restoring interface state",
|
||||||
|
"interface", iface.Identifier, "disabled", iface.IsDisabled(), "backend", controller.GetId())
|
||||||
|
|
||||||
// try to move interface to stored state
|
// try to move interface to stored state
|
||||||
_, err = m.saveInterface(ctx, &iface)
|
_, err = m.saveInterface(ctx, &iface)
|
||||||
@@ -278,13 +271,13 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
switch {
|
switch {
|
||||||
case iface.IsDisabled() && iface.Backend == config.LocalBackendName: // if interface is disabled, delete all peers
|
case iface.IsDisabled() && iface.Backend == config.LocalBackendName: // if interface is disabled, delete all peers
|
||||||
if err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
|
if err := controller.DeletePeer(ctx, iface.Identifier,
|
||||||
peer.Identifier); err != nil {
|
peer.Identifier); err != nil {
|
||||||
return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w",
|
return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w",
|
||||||
peer.Identifier, iface.Identifier, err)
|
peer.Identifier, iface.Identifier, err)
|
||||||
}
|
}
|
||||||
default: // update peer
|
default: // update peer
|
||||||
err := m.wg.GetController(iface).SavePeer(ctx, iface.Identifier, peer.Identifier,
|
err := controller.SavePeer(ctx, iface.Identifier, peer.Identifier,
|
||||||
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
|
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
|
||||||
domain.MergeToPhysicalPeer(pp, &peer)
|
domain.MergeToPhysicalPeer(pp, &peer)
|
||||||
return pp, nil
|
return pp, nil
|
||||||
@@ -297,7 +290,7 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove non-wgportal peers
|
// remove non-wgportal peers
|
||||||
physicalPeers, _ := m.wg.GetController(iface).GetPeers(ctx, iface.Identifier)
|
physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier)
|
||||||
for _, physicalPeer := range physicalPeers {
|
for _, physicalPeer := range physicalPeers {
|
||||||
isWgPortalPeer := false
|
isWgPortalPeer := false
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
@@ -307,7 +300,7 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !isWgPortalPeer {
|
if !isWgPortalPeer {
|
||||||
err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
|
err := controller.DeletePeer(ctx, iface.Identifier,
|
||||||
domain.PeerIdentifier(physicalPeer.PublicKey))
|
domain.PeerIdentifier(physicalPeer.PublicKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to remove non-wgportal peer %s from interface %s: %w",
|
return fmt.Errorf("failed to remove non-wgportal peer %s from interface %s: %w",
|
||||||
@@ -551,6 +544,30 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
|||||||
return nil, fmt.Errorf("failed to save interface: %w", err)
|
return nil, fmt.Errorf("failed to save interface: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update the interface type of peers in db
|
||||||
|
peers, err := m.db.GetInterfacePeers(ctx, iface.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load peers for interface %s: %w", iface.Identifier, err)
|
||||||
|
}
|
||||||
|
for _, peer := range peers {
|
||||||
|
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
|
||||||
|
switch iface.Type {
|
||||||
|
case domain.InterfaceTypeAny:
|
||||||
|
peer.Interface.Type = domain.InterfaceTypeAny
|
||||||
|
case domain.InterfaceTypeClient:
|
||||||
|
peer.Interface.Type = domain.InterfaceTypeServer
|
||||||
|
case domain.InterfaceTypeServer:
|
||||||
|
peer.Interface.Type = domain.InterfaceTypeClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return &peer, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update peer %s for interface %s: %w", peer.Identifier,
|
||||||
|
iface.Identifier, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if iface.IsDisabled() {
|
if iface.IsDisabled() {
|
||||||
physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier)
|
physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier)
|
||||||
fwMark := iface.FirewallMark
|
fwMark := iface.FirewallMark
|
||||||
|
@@ -188,6 +188,8 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
|||||||
|
|
||||||
sessionUser := domain.GetUserInfo(ctx)
|
sessionUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
|
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // ensure that identifier corresponds to the public key
|
||||||
|
|
||||||
// Enforce peer limit for non-admin users if LimitAdditionalUserPeers is set
|
// Enforce peer limit for non-admin users if LimitAdditionalUserPeers is set
|
||||||
if m.cfg.Core.SelfProvisioningAllowed && !sessionUser.IsAdmin && m.cfg.Advanced.LimitAdditionalUserPeers > 0 {
|
if m.cfg.Core.SelfProvisioningAllowed && !sessionUser.IsAdmin && m.cfg.Advanced.LimitAdditionalUserPeers > 0 {
|
||||||
peers, err := m.db.GetUserPeers(ctx, peer.UserIdentifier)
|
peers, err := m.db.GetUserPeers(ctx, peer.UserIdentifier)
|
||||||
|
194
internal/app/wireguard/wireguard_peers_test.go
Normal file
194
internal/app/wireguard/wireguard_peers_test.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package wireguard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Test mocks ---
|
||||||
|
|
||||||
|
type mockBus struct{}
|
||||||
|
|
||||||
|
func (f *mockBus) Publish(topic string, args ...any) {}
|
||||||
|
func (f *mockBus) Subscribe(topic string, fn interface{}) error { return nil }
|
||||||
|
|
||||||
|
type mockController struct{}
|
||||||
|
|
||||||
|
func (f *mockController) GetId() domain.InterfaceBackend { return "local" }
|
||||||
|
func (f *mockController) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *mockController) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.PhysicalInterface,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
return &domain.PhysicalInterface{Identifier: id}, nil
|
||||||
|
}
|
||||||
|
func (f *mockController) GetPeers(_ context.Context, _ domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *mockController) SaveInterface(
|
||||||
|
_ context.Context,
|
||||||
|
_ domain.InterfaceIdentifier,
|
||||||
|
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
||||||
|
) error {
|
||||||
|
_, _ = updateFunc(&domain.PhysicalInterface{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *mockController) DeleteInterface(_ context.Context, _ domain.InterfaceIdentifier) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *mockController) SavePeer(
|
||||||
|
_ context.Context,
|
||||||
|
_ domain.InterfaceIdentifier,
|
||||||
|
_ domain.PeerIdentifier,
|
||||||
|
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
||||||
|
) error {
|
||||||
|
_, _ = updateFunc(&domain.PhysicalPeer{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *mockController) DeletePeer(_ context.Context, _ domain.InterfaceIdentifier, _ domain.PeerIdentifier) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *mockController) PingAddresses(_ context.Context, _ string) (*domain.PingerResult, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockDB struct {
|
||||||
|
savedPeers map[domain.PeerIdentifier]*domain.Peer
|
||||||
|
iface *domain.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *mockDB) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
|
||||||
|
if f.iface != nil && f.iface.Identifier == id {
|
||||||
|
return f.iface, nil
|
||||||
|
}
|
||||||
|
return &domain.Interface{Identifier: id}, nil
|
||||||
|
}
|
||||||
|
func (f *mockDB) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.Interface,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
return f.iface, nil, nil
|
||||||
|
}
|
||||||
|
func (f *mockDB) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *mockDB) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) { return nil, nil }
|
||||||
|
func (f *mockDB) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *mockDB) SaveInterface(
|
||||||
|
ctx context.Context,
|
||||||
|
id domain.InterfaceIdentifier,
|
||||||
|
updateFunc func(in *domain.Interface) (*domain.Interface, error),
|
||||||
|
) error {
|
||||||
|
if f.iface == nil {
|
||||||
|
f.iface = &domain.Interface{Identifier: id}
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
f.iface, err = updateFunc(f.iface)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
func (f *mockDB) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *mockDB) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *mockDB) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *mockDB) SavePeer(
|
||||||
|
ctx context.Context,
|
||||||
|
id domain.PeerIdentifier,
|
||||||
|
updateFunc func(in *domain.Peer) (*domain.Peer, error),
|
||||||
|
) error {
|
||||||
|
if f.savedPeers == nil {
|
||||||
|
f.savedPeers = make(map[domain.PeerIdentifier]*domain.Peer)
|
||||||
|
}
|
||||||
|
existing := f.savedPeers[id]
|
||||||
|
if existing == nil {
|
||||||
|
existing = &domain.Peer{Identifier: id}
|
||||||
|
}
|
||||||
|
updated, err := updateFunc(existing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.savedPeers[updated.Identifier] = updated
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *mockDB) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error { return nil }
|
||||||
|
func (f *mockDB) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
func (f *mockDB) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (
|
||||||
|
map[domain.Cidr][]domain.Cidr,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
return map[domain.Cidr][]domain.Cidr{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test ---
|
||||||
|
|
||||||
|
func TestCreatePeer_SetsIdentifier_FromPublicKey(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
cfg := &config.Config{}
|
||||||
|
cfg.Core.SelfProvisioningAllowed = true
|
||||||
|
cfg.Core.EditableKeys = true
|
||||||
|
cfg.Advanced.LimitAdditionalUserPeers = 0
|
||||||
|
|
||||||
|
bus := &mockBus{}
|
||||||
|
|
||||||
|
// Prepare a controller manager with our mock controller
|
||||||
|
ctrlMgr := &ControllerManager{
|
||||||
|
controllers: map[domain.InterfaceBackend]backendInstance{
|
||||||
|
config.LocalBackendName: {Implementation: &mockController{}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
db := &mockDB{iface: &domain.Interface{Identifier: "wg0", Type: domain.InterfaceTypeServer}}
|
||||||
|
|
||||||
|
m := Manager{
|
||||||
|
cfg: cfg,
|
||||||
|
bus: bus,
|
||||||
|
db: db,
|
||||||
|
wg: ctrlMgr,
|
||||||
|
}
|
||||||
|
|
||||||
|
userId := domain.UserIdentifier("user@example.com")
|
||||||
|
ctx := domain.SetUserInfo(context.Background(), &domain.ContextUserInfo{Id: userId, IsAdmin: false})
|
||||||
|
|
||||||
|
pubKey := "TEST_PUBLIC_KEY_ABC123"
|
||||||
|
|
||||||
|
input := &domain.Peer{
|
||||||
|
Identifier: "should_be_overwritten",
|
||||||
|
UserIdentifier: userId,
|
||||||
|
InterfaceIdentifier: domain.InterfaceIdentifier("wg0"),
|
||||||
|
Interface: domain.PeerInterfaceConfig{
|
||||||
|
KeyPair: domain.KeyPair{PublicKey: pubKey},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
out, err := m.CreatePeer(ctx, input)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreatePeer returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedId := domain.PeerIdentifier(pubKey)
|
||||||
|
if out.Identifier != expectedId {
|
||||||
|
t.Fatalf("expected Identifier to be set from public key %q, got %q", expectedId, out.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the saved peer in DB also has the expected identifier
|
||||||
|
if db.savedPeers[expectedId] == nil {
|
||||||
|
t.Fatalf("expected peer with identifier %q to be saved in DB", expectedId)
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,12 @@ const LocalBackendName = "local"
|
|||||||
type Backend struct {
|
type Backend struct {
|
||||||
Default string `yaml:"default"` // The default backend to use (defaults to the internal backend)
|
Default string `yaml:"default"` // The default backend to use (defaults to the internal backend)
|
||||||
|
|
||||||
|
// Local Backend-specific configuration
|
||||||
|
|
||||||
|
IgnoredLocalInterfaces []string `yaml:"ignored_local_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
|
||||||
|
|
||||||
|
// External Backend-specific configuration
|
||||||
|
|
||||||
Mikrotik []BackendMikrotik `yaml:"mikrotik"`
|
Mikrotik []BackendMikrotik `yaml:"mikrotik"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +48,8 @@ func (b *Backend) Validate() error {
|
|||||||
type BackendBase struct {
|
type BackendBase struct {
|
||||||
Id string `yaml:"id"` // A unique id for the backend
|
Id string `yaml:"id"` // A unique id for the backend
|
||||||
DisplayName string `yaml:"display_name"` // A display name for the backend
|
DisplayName string `yaml:"display_name"` // A display name for the backend
|
||||||
|
|
||||||
|
IgnoredInterfaces []string `yaml:"ignored_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDisplayName returns the display name of the backend.
|
// GetDisplayName returns the display name of the backend.
|
||||||
|
@@ -328,7 +328,7 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
|
|||||||
Id: "",
|
Id: "",
|
||||||
Name: p.DisplayName,
|
Name: p.DisplayName,
|
||||||
Comment: p.Notes,
|
Comment: p.Notes,
|
||||||
IsResponder: false,
|
IsResponder: p.Interface.Type == InterfaceTypeClient,
|
||||||
Disabled: p.IsDisabled(),
|
Disabled: p.IsDisabled(),
|
||||||
ClientEndpoint: p.Endpoint.GetValue(),
|
ClientEndpoint: p.Endpoint.GetValue(),
|
||||||
ClientAddress: CidrsToString(p.Interface.Addresses),
|
ClientAddress: CidrsToString(p.Interface.Addresses),
|
||||||
|
@@ -6,8 +6,12 @@ repo_name: h44z/wg-portal
|
|||||||
repo_url: https://github.com/h44z/wg-portal
|
repo_url: https://github.com/h44z/wg-portal
|
||||||
copyright: Copyright © 2023-2025 WireGuard Portal Project
|
copyright: Copyright © 2023-2025 WireGuard Portal Project
|
||||||
|
|
||||||
|
extra_javascript:
|
||||||
|
- javascript/img-comparison-slider.js
|
||||||
|
|
||||||
extra_css:
|
extra_css:
|
||||||
- stylesheets/extra.css
|
- stylesheets/extra.css
|
||||||
|
- stylesheets/img-comparison-slider.css
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
|
Reference in New Issue
Block a user