Compare commits

...

7 Commits

Author SHA1 Message Date
Christoph Haas
e65cab4857 add funding info, prepare release v2.1 2025-10-04 14:14:08 +02:00
Christoph Haas
75ec234a72 add dark/light image to doc 2025-10-04 13:42:52 +02:00
Christoph Haas
4729bccdd3 add dark/light image to doc 2025-09-17 22:55:17 +02:00
Christoph Haas
afb38b685c improve logging of LDAP login process (#529)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-09-17 22:33:54 +02:00
h44z
7cd7d13dc7 fix peer creation if custom public key is set (#523) (#528)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-09-15 22:54:34 +02:00
dependabot[bot]
d945e313b2 chore(deps): bump github.com/go-webauthn/webauthn from 0.13.4 to 0.14.0 (#526)
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.13.4 to 0.14.0.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.13.4...v0.14.0)

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

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


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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 22:48:44 +02:00
16 changed files with 332 additions and 18 deletions

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

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

View File

@@ -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
a copy of this software and associated documentation files (the

View File

@@ -21,7 +21,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Pos
## Features
* 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
* QR-Code for convenient mobile client configuration
* 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
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* Supports multiple WireGuard backends (wgctrl or MikroTik [BETA])
* Supports multiple WireGuard backends (wgctrl or MikroTik)
* Peer Expiry Feature
* Handles route and DNS settings like wg-quick does
* 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>
## Contributors and Sponsors
Thanks so much for all your contributions! Theyre truly appreciated and help keep WireGuard Portal moving ahead.
<a href="https://github.com/h44z/wg-portal/graphs/contributors">
<img src="https://contrib.rocks/image?repo=h44z/wg-portal" />
</a>
Want to support the project? You can buy me a coffee or join as a contributor - every bit of support helps!
[Become a sponsor!](https://github.com/sponsors/h44z)
> [!IMPORTANT]
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -300,6 +300,59 @@
background: var(--md-accent-fg-color--transparent);
}
.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>
<!-- Hero for landing page -->
@@ -326,11 +379,34 @@
<div class="md-container">
<div class="tx-hero__image">
<img
src="{{config.site_url}}/assets/images/screenshot.png"
alt=""
draggable="false"
>
<div>
<img-comparison-slider hover="hover">
<figure slot="first" class="before">
<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>

6
go.mod
View File

@@ -10,7 +10,7 @@ require (
github.com/go-ldap/ldap/v3 v3.4.11
github.com/go-pkgz/routegroup v1.5.3
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/prometheus-community/pro-bing v0.7.0
github.com/prometheus/client_golang v1.23.2
@@ -29,7 +29,7 @@ require (
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlserver v1.6.1
gorm.io/gorm v1.30.5
gorm.io/gorm v1.31.0
)
require (
@@ -64,7 +64,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // 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-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect

14
go.sum
View File

@@ -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-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-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ=
github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI=
github.com/go-webauthn/x v0.1.24 h1:6LaWf2zzWqbyKT8IyQkhje1/1KCGhlEkMz4V1tDnt/A=
github.com/go-webauthn/x v0.1.24/go.mod h1:2o5XKJ+X1AKqYKGgHdKflGnoQFQZ6flJ2IFCBKSbSOw=
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
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.2.1/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=
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/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/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
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/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
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.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
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/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=

View File

@@ -364,6 +364,7 @@ func (a *Authenticator) passwordAuthentication(
}
ldapUserInfo, err = ldapAuth.ParseUserInfo(rawUserInfo)
if err != nil {
slog.Error("failed to parse ldap user info", "identifier", identifier, "error", err)
continue
}
@@ -376,10 +377,13 @@ func (a *Authenticator) passwordAuthentication(
}
if userSource == "" {
slog.Warn("no user source found for user", "identifier", identifier, "ldapProviderCount", a.ldapAuthenticators)
return nil, errors.New("user not found")
}
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")
}

View File

@@ -188,6 +188,8 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
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
if m.cfg.Core.SelfProvisioningAllowed && !sessionUser.IsAdmin && m.cfg.Advanced.LimitAdditionalUserPeers > 0 {
peers, err := m.db.GetUserPeers(ctx, peer.UserIdentifier)

View File

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

View File

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