mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-04 15:36:18 +00:00
Merge branch 'master' into mikrotik_integration
# Conflicts: # internal/app/api/v0/handlers/endpoint_config.go # internal/app/api/v0/model/models.go # internal/app/wireguard/statistics.go # internal/app/wireguard/wireguard_interfaces.go
This commit is contained in:
@@ -133,5 +133,5 @@ func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerS
|
||||
}
|
||||
m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
|
||||
m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
|
||||
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected()))
|
||||
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected))
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
@@ -16,8 +17,9 @@ import (
|
||||
|
||||
// WgRepo implements all low-level WireGuard interactions.
|
||||
type WgRepo struct {
|
||||
wg lowlevel.WireGuardClient
|
||||
nl lowlevel.NetlinkClient
|
||||
wg lowlevel.WireGuardClient
|
||||
nl lowlevel.NetlinkClient
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewWireGuardRepository creates a new WgRepo instance.
|
||||
@@ -31,8 +33,9 @@ func NewWireGuardRepository() *WgRepo {
|
||||
nl := &lowlevel.NetlinkManager{}
|
||||
|
||||
repo := &WgRepo{
|
||||
wg: wg,
|
||||
nl: nl,
|
||||
wg: wg,
|
||||
nl: nl,
|
||||
log: slog.Default().With(slog.String("adapter", "wireguard")),
|
||||
}
|
||||
|
||||
return repo
|
||||
@@ -40,8 +43,10 @@ func NewWireGuardRepository() *WgRepo {
|
||||
|
||||
// GetInterfaces returns all existing WireGuard interfaces.
|
||||
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||
r.log.Debug("getting all interfaces")
|
||||
devices, err := r.wg.Devices()
|
||||
if err != nil {
|
||||
r.log.Error("failed to get devices", "error", 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.
|
||||
// If no interface is found, an error os.ErrNotExist is returned.
|
||||
func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||
r.log.Debug("getting interface", "id", id)
|
||||
return r.getInterface(id)
|
||||
}
|
||||
|
||||
// GetPeers returns all peers associated with the given interface id.
|
||||
// If the requested interface is found, an error os.ErrNotExist is returned.
|
||||
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))
|
||||
if err != nil {
|
||||
r.log.Error("failed to get device", "deviceId", deviceId, "error", err)
|
||||
return nil, fmt.Errorf("device error: %w", err)
|
||||
}
|
||||
|
||||
@@ -90,6 +98,7 @@ func (r *WgRepo) GetPeer(
|
||||
deviceId domain.InterfaceIdentifier,
|
||||
id domain.PeerIdentifier,
|
||||
) (*domain.PhysicalPeer, error) {
|
||||
r.log.Debug("getting peer", "deviceId", deviceId, "peerId", id)
|
||||
return r.getPeer(deviceId, id)
|
||||
}
|
||||
|
||||
@@ -174,25 +183,31 @@ func (r *WgRepo) SaveInterface(
|
||||
id domain.InterfaceIdentifier,
|
||||
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
||||
) error {
|
||||
r.log.Debug("saving interface", "id", id)
|
||||
physicalInterface, err := r.getOrCreateInterface(id)
|
||||
if err != nil {
|
||||
r.log.Error("failed to get or create interface", "id", id, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if updateFunc != nil {
|
||||
physicalInterface, err = updateFunc(physicalInterface)
|
||||
if err != nil {
|
||||
r.log.Error("interface update function failed", "id", id, "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.updateLowLevelInterface(physicalInterface); err != nil {
|
||||
r.log.Error("failed to update low level interface", "id", id, "error", err)
|
||||
return err
|
||||
}
|
||||
if err := r.updateWireGuardInterface(physicalInterface); err != nil {
|
||||
r.log.Error("failed to update wireguard interface", "id", id, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
r.log.Debug("successfully saved interface", "id", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -323,10 +338,13 @@ func (r *WgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
|
||||
// DeleteInterface deletes the interface with the given id.
|
||||
// If the requested interface is found, no error is returned.
|
||||
func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
||||
r.log.Debug("deleting interface", "id", id)
|
||||
if err := r.deleteLowLevelInterface(id); err != nil {
|
||||
r.log.Error("failed to delete low level interface", "id", id, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
r.log.Debug("successfully deleted interface", "id", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -356,20 +374,25 @@ func (r *WgRepo) SavePeer(
|
||||
id domain.PeerIdentifier,
|
||||
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
||||
) error {
|
||||
r.log.Debug("saving peer", "deviceId", deviceId, "peerId", id)
|
||||
physicalPeer, err := r.getOrCreatePeer(deviceId, id)
|
||||
if err != nil {
|
||||
r.log.Error("failed to get or create peer", "deviceId", deviceId, "peerId", id, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
physicalPeer, err = updateFunc(physicalPeer)
|
||||
if err != nil {
|
||||
r.log.Error("peer update function failed", "deviceId", deviceId, "peerId", id, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.updatePeer(deviceId, physicalPeer); err != nil {
|
||||
r.log.Error("failed to update peer", "deviceId", deviceId, "peerId", id, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
r.log.Debug("successfully saved peer", "deviceId", deviceId, "peerId", id)
|
||||
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}})
|
||||
if err != nil {
|
||||
r.log.Error("failed to configure device for peer update", "deviceId", deviceId, "peerId", pp.Identifier, "error", 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.
|
||||
// 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 {
|
||||
r.log.Debug("deleting peer", "deviceId", deviceId, "peerId", id)
|
||||
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)
|
||||
if err != nil {
|
||||
r.log.Error("failed to delete peer", "deviceId", deviceId, "peerId", id, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
r.log.Debug("successfully deleted peer", "deviceId", deviceId, "peerId", id)
|
||||
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}})
|
||||
if err != nil {
|
||||
r.log.Error("failed to configure device for peer deletion", "deviceId", deviceId, "peerId", id, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@@ -57,6 +57,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login/{provider}/callback": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Handle the OAuth callback.",
|
||||
"operationId": "auth_handleOauthCallbackGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login/{provider}/init": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Initiate the OAuth login flow.",
|
||||
"operationId": "auth_handleOauthInitiateGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"post": {
|
||||
"produces": [
|
||||
@@ -275,52 +321,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/{provider}/callback": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Handle the OAuth callback.",
|
||||
"operationId": "auth_handleOauthCallbackGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/{provider}/init": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Initiate the OAuth login flow.",
|
||||
"operationId": "auth_handleOauthInitiateGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/frontend.js": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -819,6 +819,12 @@
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.PeerMailRequest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The configuration style",
|
||||
"name": "style",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -858,6 +864,12 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The configuration style",
|
||||
"name": "style",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -899,6 +911,12 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The configuration style",
|
||||
"name": "style",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -2231,9 +2249,15 @@
|
||||
"ApiAdminOnly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"LoginFormVisible": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"MailLinkOnly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"MinPasswordLength": {
|
||||
"type": "integer"
|
||||
},
|
||||
"PersistentConfigSupported": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@@ -381,8 +381,12 @@ definitions:
|
||||
properties:
|
||||
ApiAdminOnly:
|
||||
type: boolean
|
||||
LoginFormVisible:
|
||||
type: boolean
|
||||
MailLinkOnly:
|
||||
type: boolean
|
||||
MinPasswordLength:
|
||||
type: integer
|
||||
PersistentConfigSupported:
|
||||
type: boolean
|
||||
SelfProvisioning:
|
||||
@@ -472,7 +476,22 @@ paths:
|
||||
summary: Get all available audit entries. Ordered by timestamp.
|
||||
tags:
|
||||
- Audit
|
||||
/auth/{provider}/callback:
|
||||
/auth/login:
|
||||
post:
|
||||
operationId: auth_handleLoginPost
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Get all available external login providers.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/login/{provider}/callback:
|
||||
get:
|
||||
operationId: auth_handleOauthCallbackGet
|
||||
produces:
|
||||
@@ -487,7 +506,7 @@ paths:
|
||||
summary: Handle the OAuth callback.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/{provider}/init:
|
||||
/auth/login/{provider}/init:
|
||||
get:
|
||||
operationId: auth_handleOauthInitiateGet
|
||||
produces:
|
||||
@@ -502,21 +521,6 @@ paths:
|
||||
summary: Initiate the OAuth login flow.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/login:
|
||||
post:
|
||||
operationId: auth_handleLoginPost
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Get all available external login providers.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/logout:
|
||||
post:
|
||||
operationId: auth_handleLogoutPost
|
||||
@@ -1068,6 +1072,10 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.PeerMailRequest'
|
||||
- description: The configuration style
|
||||
in: query
|
||||
name: style
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -1093,6 +1101,10 @@ paths:
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: The configuration style
|
||||
in: query
|
||||
name: style
|
||||
type: string
|
||||
produces:
|
||||
- image/png
|
||||
- application/json
|
||||
@@ -1121,6 +1133,10 @@ paths:
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: The configuration style
|
||||
in: query
|
||||
name: style
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@@ -100,6 +100,7 @@ func (s *Server) Run(ctx context.Context, listenAddress string) {
|
||||
srvContext, cancelFn := context.WithCancel(ctx)
|
||||
go func() {
|
||||
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 != "" {
|
||||
err = srv.ListenAndServeTLS(s.cfg.Web.CertFile, s.cfg.Web.KeyFile)
|
||||
} else {
|
||||
|
@@ -27,12 +27,12 @@ type PeerServicePeerManager interface {
|
||||
}
|
||||
|
||||
type PeerServiceConfigFileManager interface {
|
||||
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||
GetPeerConfigQrCode(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, style string) (io.Reader, error)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -95,16 +95,24 @@ func (p PeerService) DeletePeer(ctx context.Context, id domain.PeerIdentifier) e
|
||||
return p.peers.DeletePeer(ctx, id)
|
||||
}
|
||||
|
||||
func (p PeerService) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
|
||||
return p.configFile.GetPeerConfig(ctx, id)
|
||||
func (p PeerService) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) {
|
||||
return p.configFile.GetPeerConfig(ctx, id, style)
|
||||
}
|
||||
|
||||
func (p PeerService) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
|
||||
return p.configFile.GetPeerConfigQrCode(ctx, id)
|
||||
func (p PeerService) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (
|
||||
io.Reader,
|
||||
error,
|
||||
) {
|
||||
return p.configFile.GetPeerConfigQrCode(ctx, id, style)
|
||||
}
|
||||
|
||||
func (p PeerService) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error {
|
||||
return p.mailer.SendPeerEmail(ctx, linkOnly, peers...)
|
||||
func (p PeerService) SendPeerEmail(
|
||||
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) {
|
||||
|
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -189,7 +190,7 @@ func (e AuthEndpoint) handleSessionInfoGet() http.HandlerFunc {
|
||||
// @Summary Initiate the OAuth login flow.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.LoginProviderInfo
|
||||
// @Router /auth/{provider}/init [get]
|
||||
// @Router /auth/login/{provider}/init [get]
|
||||
func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
@@ -234,6 +235,8 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
|
||||
authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
|
||||
if err != nil {
|
||||
slog.Debug("failed to create oauth auth code URL",
|
||||
"provider", provider, "error", err)
|
||||
if autoRedirect && e.isValidReturnUrl(returnTo) {
|
||||
redirectToReturn()
|
||||
} else {
|
||||
@@ -268,7 +271,7 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
// @Summary Handle the OAuth callback.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.LoginProviderInfo
|
||||
// @Router /auth/{provider}/callback [get]
|
||||
// @Router /auth/login/{provider}/callback [get]
|
||||
func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
@@ -306,6 +309,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
oauthState := request.Query(r, "state")
|
||||
|
||||
if provider != currentSession.OauthProvider {
|
||||
slog.Debug("invalid oauth provider in callback",
|
||||
"expected", currentSession.OauthProvider, "got", provider, "state", oauthState)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
} else {
|
||||
@@ -315,6 +320,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
if oauthState != currentSession.OauthState {
|
||||
slog.Debug("invalid oauth state in callback",
|
||||
"expected", currentSession.OauthState, "got", oauthState, "provider", provider)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
} else {
|
||||
@@ -324,11 +331,13 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
loginCtx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
|
||||
loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
|
||||
user, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
|
||||
oauthCode)
|
||||
cancel()
|
||||
if err != nil {
|
||||
slog.Debug("failed to process oauth code",
|
||||
"provider", provider, "state", oauthState, "error", err)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
} else {
|
||||
|
@@ -124,11 +124,14 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
if sessionUser.Id == domain.CtxUnknownUserId || sessionUser.Id == "" {
|
||||
respond.JSON(w, http.StatusOK, model.Settings{
|
||||
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
||||
AvailableBackends: []model.SettingsBackendNames{}, // return an empty list instead of null
|
||||
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
|
||||
})
|
||||
} else {
|
||||
respond.JSON(w, http.StatusOK, model.Settings{
|
||||
@@ -139,6 +142,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
||||
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
||||
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(ctx context.Context, id domain.PeerIdentifier) error
|
||||
// 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(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(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(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.
|
||||
// @Produce json
|
||||
// @Param id path string true "The peer identifier"
|
||||
// @Param style query string false "The configuration style"
|
||||
// @Success 200 {object} string
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
@@ -369,7 +370,9 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc {
|
||||
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 {
|
||||
respond.JSON(w, http.StatusInternalServerError, model.Error{
|
||||
Code: http.StatusInternalServerError, Message: err.Error(),
|
||||
@@ -397,6 +400,7 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc {
|
||||
// @Produce png
|
||||
// @Produce json
|
||||
// @Param id path string true "The peer identifier"
|
||||
// @Param style query string false "The configuration style"
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
@@ -411,7 +415,9 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc {
|
||||
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 {
|
||||
respond.JSON(w, http.StatusInternalServerError, model.Error{
|
||||
Code: http.StatusInternalServerError, Message: err.Error(),
|
||||
@@ -438,6 +444,7 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc {
|
||||
// @Summary Send peer configuration via email.
|
||||
// @Produce json
|
||||
// @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"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
@@ -460,11 +467,13 @@ func (e PeerEndpoint) handleEmailPost() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
configStyle := e.getConfigStyle(r)
|
||||
|
||||
peerIds := make([]domain.PeerIdentifier, len(req.Identifiers))
|
||||
for i := range req.Identifiers {
|
||||
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,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
@@ -504,3 +513,11 @@ func (e PeerEndpoint) handleStatsGet() http.HandlerFunc {
|
||||
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
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ type Settings struct {
|
||||
WebAuthnEnabled bool `json:"WebAuthnEnabled"`
|
||||
MinPasswordLength int `json:"MinPasswordLength"`
|
||||
AvailableBackends []SettingsBackendNames `json:"AvailableBackends"`
|
||||
LoginFormVisible bool `json:"LoginFormVisible"`
|
||||
}
|
||||
|
||||
type SettingsBackendNames struct {
|
||||
|
@@ -198,7 +198,7 @@ func NewPeerStats(enabled bool, src []domain.PeerStatus) *PeerStats {
|
||||
|
||||
for _, srcStat := range src {
|
||||
stats[string(srcStat.PeerId)] = PeerStatData{
|
||||
IsConnected: srcStat.IsConnected(),
|
||||
IsConnected: srcStat.IsConnected,
|
||||
IsPingable: srcStat.IsPingable,
|
||||
LastPing: srcStat.LastPing,
|
||||
BytesReceived: srcStat.BytesReceived,
|
||||
|
@@ -23,8 +23,8 @@ type ProvisioningServicePeerManagerRepo interface {
|
||||
}
|
||||
|
||||
type ProvisioningServiceConfigFileManagerRepo interface {
|
||||
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||
GetPeerConfigQrCode(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, style string) (io.Reader, error)
|
||||
}
|
||||
|
||||
type ProvisioningService struct {
|
||||
@@ -96,7 +96,7 @@ func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.Pe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
|
||||
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier, domain.ConfigStyleWgQuick)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.Pee
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
|
||||
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier, domain.ConfigStyleWgQuick)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -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
|
||||
callbackUrlPrefix string
|
||||
|
||||
callbackUrl *url.URL
|
||||
|
||||
users UserManager
|
||||
}
|
||||
|
||||
@@ -102,82 +104,136 @@ func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserM
|
||||
error,
|
||||
) {
|
||||
a := &Authenticator{
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
users: users,
|
||||
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
users: users,
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
err := a.setupExternalAuthProviders(ctx)
|
||||
parsedExtUrl, err := url.Parse(a.callbackUrlPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to parse external URL: %w", err)
|
||||
}
|
||||
a.callbackUrl = parsedExtUrl
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) setupExternalAuthProviders(ctx context.Context) error {
|
||||
extUrl, err := url.Parse(a.callbackUrlPrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse external url: %w", err)
|
||||
}
|
||||
// StartBackgroundJobs starts the background jobs for the authenticator.
|
||||
// It sets up the external authentication providers (OIDC, OAuth, LDAP) and retries in case of errors.
|
||||
func (a *Authenticator) StartBackgroundJobs(ctx context.Context) {
|
||||
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))
|
||||
a.ldapAuthenticators = make(map[string]AuthenticatorLdap, len(a.cfg.Ldap))
|
||||
ticker := time.NewTicker(30 * time.Second) // Ticker for delay between retries
|
||||
defer ticker.Stop()
|
||||
|
||||
for i := range a.cfg.OpenIDConnect { // OIDC
|
||||
providerCfg := &a.cfg.OpenIDConnect[i]
|
||||
for {
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
provider, err := newOidcAuthenticator(ctx, redirectUrl.String(), providerCfg)
|
||||
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
|
||||
}
|
||||
for i := range a.cfg.OAuth { // PLAIN OAUTH
|
||||
providerCfg := &a.cfg.OAuth[i]
|
||||
for i := range oauth { // PLAIN OAUTH
|
||||
providerCfg := &oauth[i]
|
||||
providerId := strings.ToLower(providerCfg.ProviderName)
|
||||
|
||||
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")
|
||||
|
||||
provider, err := newPlainOauthAuthenticator(ctx, redirectUrl.String(), providerCfg)
|
||||
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
|
||||
}
|
||||
for i := range a.cfg.Ldap { // LDAP
|
||||
providerCfg := &a.cfg.Ldap[i]
|
||||
for i := range ldap { // LDAP
|
||||
providerCfg := &ldap[i]
|
||||
providerId := strings.ToLower(providerCfg.URL)
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
return nil
|
||||
return failedOidc, failedOauth, failedLdap
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) {
|
||||
return nil, fmt.Errorf("user %s is not in allowed domains", userInfo.Email)
|
||||
}
|
||||
|
||||
ctx = domain.SetUserInfo(ctx,
|
||||
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
|
||||
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(),
|
||||
@@ -450,10 +510,6 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
|
||||
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() {
|
||||
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
|
||||
Ctx: ctx,
|
||||
|
@@ -46,7 +46,7 @@ type TemplateRenderer interface {
|
||||
// GetInterfaceConfig returns the configuration file for the given interface.
|
||||
GetInterfaceConfig(iface *domain.Interface, peers []domain.Peer) (io.Reader, error)
|
||||
// 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 {
|
||||
@@ -186,7 +186,7 @@ func (m Manager) GetInterfaceConfig(ctx context.Context, id domain.InterfaceIden
|
||||
|
||||
// GetPeerConfig returns the configuration file for the given peer.
|
||||
// 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)
|
||||
if err != nil {
|
||||
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 m.tplHandler.GetPeerConfig(peer)
|
||||
return m.tplHandler.GetPeerConfig(peer, style)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
cfgData, err := m.tplHandler.GetPeerConfig(peer)
|
||||
cfgData, err := m.tplHandler.GetPeerConfig(peer, style)
|
||||
if err != nil {
|
||||
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.
|
||||
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
|
||||
|
||||
err := c.templates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]any{
|
||||
"Peer": peer,
|
||||
"Style": style,
|
||||
"Peer": peer,
|
||||
"Portal": map[string]any{
|
||||
"Version": "unknown",
|
||||
},
|
||||
|
@@ -1,6 +1,8 @@
|
||||
# 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
|
||||
{{- end}}
|
||||
# Lines starting with the -WGP- tag are used by
|
||||
# the WireGuard Portal configuration parser.
|
||||
|
||||
@@ -21,22 +23,27 @@
|
||||
|
||||
# Core settings
|
||||
PrivateKey = {{ .Peer.Interface.KeyPair.PrivateKey }}
|
||||
{{- if eq .Style "wgquick"}}
|
||||
Address = {{ CidrsToString .Peer.Interface.Addresses }}
|
||||
{{- end}}
|
||||
|
||||
# Misc. settings (optional)
|
||||
{{- if eq .Style "wgquick"}}
|
||||
{{- if .Peer.Interface.DnsStr.GetValue}}
|
||||
DNS = {{ .Peer.Interface.DnsStr.GetValue }} {{- if .Peer.Interface.DnsSearchStr.GetValue}}, {{ .Peer.Interface.DnsSearchStr.GetValue }} {{- end}}
|
||||
{{- end}}
|
||||
{{- if ne .Peer.Interface.Mtu.GetValue 0}}
|
||||
MTU = {{ .Peer.Interface.Mtu.GetValue }}
|
||||
{{- end}}
|
||||
{{- if ne .Peer.Interface.FirewallMark.GetValue 0}}
|
||||
FwMark = {{ .Peer.Interface.FirewallMark.GetValue }}
|
||||
{{- end}}
|
||||
{{- if ne .Peer.Interface.RoutingTable.GetValue ""}}
|
||||
Table = {{ .Peer.Interface.RoutingTable.GetValue }}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- if ne .Peer.Interface.FirewallMark.GetValue 0}}
|
||||
FwMark = {{ .Peer.Interface.FirewallMark.GetValue }}
|
||||
{{- end}}
|
||||
|
||||
{{- if eq .Style "wgquick"}}
|
||||
# Interface hooks (optional)
|
||||
{{- if .Peer.Interface.PreUp.GetValue}}
|
||||
PreUp = {{ .Peer.Interface.PreUp.GetValue }}
|
||||
@@ -50,6 +57,7 @@ PreDown = {{ .Peer.Interface.PreDown.GetValue }}
|
||||
{{- if .Peer.Interface.PostDown.GetValue}}
|
||||
PostDown = {{ .Peer.Interface.PostDown.GetValue }}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{ .Peer.EndpointPublicKey.GetValue }}
|
||||
|
@@ -36,6 +36,7 @@ const TopicPeerDeleted = "peer:deleted"
|
||||
const TopicPeerUpdated = "peer:updated"
|
||||
const TopicPeerInterfaceUpdated = "peer:interface:updated"
|
||||
const TopicPeerIdentifierUpdated = "peer:identifier:updated"
|
||||
const TopicPeerStateChanged = "peer:state:changed"
|
||||
|
||||
// endregion peer-events
|
||||
|
||||
|
@@ -21,9 +21,9 @@ type ConfigFileManager interface {
|
||||
// GetInterfaceConfig returns the configuration for the given interface.
|
||||
GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error)
|
||||
// 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(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
|
||||
}
|
||||
|
||||
type UserDatabaseRepo interface {
|
||||
@@ -71,7 +71,7 @@ func NewMailManager(
|
||||
users UserDatabaseRepo,
|
||||
wg WireguardDatabaseRepo,
|
||||
) (*Manager, error) {
|
||||
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl)
|
||||
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle)
|
||||
if err != nil {
|
||||
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.
|
||||
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 {
|
||||
peer, err := m.wg.GetPeer(ctx, peerId)
|
||||
if err != nil {
|
||||
@@ -123,7 +123,7 @@ func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...doma
|
||||
continue
|
||||
}
|
||||
|
||||
err = m.sendPeerEmail(ctx, linkOnly, user, peer)
|
||||
err = m.sendPeerEmail(ctx, linkOnly, style, user, peer)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
configName := peer.GetConfigFileName()
|
||||
|
||||
@@ -148,12 +154,12 @@ func (m Manager) sendPeerEmail(ctx context.Context, linkOnly bool, user *domain.
|
||||
}
|
||||
|
||||
} else {
|
||||
peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier)
|
||||
peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier, style)
|
||||
if err != nil {
|
||||
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 {
|
||||
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.
|
||||
type TemplateHandler struct {
|
||||
portalUrl string
|
||||
portalName string
|
||||
htmlTemplates *htmlTemplate.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")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse html template files: %w", err)
|
||||
@@ -34,6 +35,7 @@ func newTemplateHandler(portalUrl string) (*TemplateHandler, error) {
|
||||
|
||||
handler := &TemplateHandler{
|
||||
portalUrl: portalUrl,
|
||||
portalName: portalName,
|
||||
htmlTemplates: htmlTemplateCache,
|
||||
textTemplates: txtTemplateCache,
|
||||
}
|
||||
@@ -81,6 +83,7 @@ func (c TemplateHandler) GetConfigMailWithAttachment(user *domain.User, cfgName,
|
||||
"ConfigFileName": cfgName,
|
||||
"QrcodePngName": qrName,
|
||||
"PortalUrl": c.portalUrl,
|
||||
"PortalName": c.portalName,
|
||||
})
|
||||
if err != nil {
|
||||
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,
|
||||
"QrcodePngName": qrName,
|
||||
"PortalUrl": c.portalUrl,
|
||||
"PortalName": c.portalName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gohtml: %w", err)
|
||||
|
@@ -19,7 +19,7 @@
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
||||
<!--<![endif]-->
|
||||
<title>Email Template</title>
|
||||
<title>{{$.PortalName}}</title>
|
||||
<!--[if gte mso 9]>
|
||||
<style type="text/css" media="all">
|
||||
sup { font-size: 100% !important; }
|
||||
@@ -143,7 +143,7 @@
|
||||
<td align="left">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<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>
|
||||
</table>
|
||||
</td>
|
||||
@@ -167,10 +167,10 @@
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</table>
|
||||
</td>
|
||||
|
@@ -20,5 +20,5 @@ You can download and install the WireGuard VPN client from:
|
||||
https://www.wireguard.com/install/
|
||||
|
||||
|
||||
This mail was generated using WireGuard Portal.
|
||||
This mail was generated by {{$.PortalName}}.
|
||||
{{$.PortalUrl}}
|
@@ -19,7 +19,7 @@
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
||||
<!--<![endif]-->
|
||||
<title>Email Template</title>
|
||||
<title>{{$.PortalName}}</title>
|
||||
<!--[if gte mso 9]>
|
||||
<style type="text/css" media="all">
|
||||
sup { font-size: 100% !important; }
|
||||
@@ -143,7 +143,7 @@
|
||||
<td align="left">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<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>
|
||||
</table>
|
||||
</td>
|
||||
@@ -167,10 +167,10 @@
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</table>
|
||||
</td>
|
||||
|
@@ -20,5 +20,5 @@ You can download and install the WireGuard VPN client from:
|
||||
https://www.wireguard.com/install/
|
||||
|
||||
|
||||
This mail was generated using WireGuard Portal.
|
||||
This mail was generated by {{$.PortalName}}.
|
||||
{{$.PortalUrl}}
|
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"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/domain"
|
||||
)
|
||||
@@ -64,6 +65,7 @@ func (m Manager) connectToMessageBus() {
|
||||
_ = m.bus.Subscribe(app.TopicPeerCreated, m.handlePeerCreateEvent)
|
||||
_ = m.bus.Subscribe(app.TopicPeerUpdated, m.handlePeerUpdateEvent)
|
||||
_ = 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.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) {
|
||||
m.handleGenericEvent(WebhookEventCreate, user)
|
||||
m.handleGenericEvent(WebhookEventCreate, models.NewUser(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) {
|
||||
m.handleGenericEvent(WebhookEventDelete, user)
|
||||
m.handleGenericEvent(WebhookEventDelete, models.NewUser(user))
|
||||
}
|
||||
|
||||
func (m Manager) handlePeerCreateEvent(peer domain.Peer) {
|
||||
m.handleGenericEvent(WebhookEventCreate, peer)
|
||||
m.handleGenericEvent(WebhookEventCreate, models.NewPeer(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) {
|
||||
m.handleGenericEvent(WebhookEventDelete, peer)
|
||||
m.handleGenericEvent(WebhookEventDelete, models.NewPeer(peer))
|
||||
}
|
||||
|
||||
func (m Manager) handleInterfaceCreateEvent(iface domain.Interface) {
|
||||
m.handleGenericEvent(WebhookEventCreate, iface)
|
||||
m.handleGenericEvent(WebhookEventCreate, models.NewInterface(iface))
|
||||
}
|
||||
|
||||
func (m Manager) handleInterfaceUpdateEvent(iface domain.Interface) {
|
||||
m.handleGenericEvent(WebhookEventUpdate, iface)
|
||||
m.handleGenericEvent(WebhookEventUpdate, models.NewInterface(iface))
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -168,15 +178,18 @@ func (m Manager) createWebhookData(action WebhookEvent, payload any) (*WebhookDa
|
||||
}
|
||||
|
||||
switch v := payload.(type) {
|
||||
case domain.User:
|
||||
case models.User:
|
||||
d.Entity = WebhookEntityUser
|
||||
d.Identifier = string(v.Identifier)
|
||||
case domain.Peer:
|
||||
d.Identifier = v.Identifier
|
||||
case models.Peer:
|
||||
d.Entity = WebhookEntityPeer
|
||||
d.Identifier = string(v.Identifier)
|
||||
case domain.Interface:
|
||||
d.Identifier = v.Identifier
|
||||
case models.Interface:
|
||||
d.Entity = WebhookEntityInterface
|
||||
d.Identifier = string(v.Identifier)
|
||||
d.Identifier = v.Identifier
|
||||
case models.PeerMetrics:
|
||||
d.Entity = WebhookEntityPeerMetric
|
||||
d.Identifier = v.Peer.Identifier
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported payload type: %T", v)
|
||||
}
|
||||
|
@@ -34,15 +34,18 @@ func (d *WebhookData) Serialize() (io.Reader, error) {
|
||||
type WebhookEntity = string
|
||||
|
||||
const (
|
||||
WebhookEntityUser WebhookEntity = "user"
|
||||
WebhookEntityPeer WebhookEntity = "peer"
|
||||
WebhookEntityInterface WebhookEntity = "interface"
|
||||
WebhookEntityUser WebhookEntity = "user"
|
||||
WebhookEntityPeer WebhookEntity = "peer"
|
||||
WebhookEntityPeerMetric WebhookEntity = "peer_metric"
|
||||
WebhookEntityInterface WebhookEntity = "interface"
|
||||
)
|
||||
|
||||
type WebhookEvent = string
|
||||
|
||||
const (
|
||||
WebhookEventCreate WebhookEvent = "create"
|
||||
WebhookEventUpdate WebhookEvent = "update"
|
||||
WebhookEventDelete WebhookEvent = "delete"
|
||||
WebhookEventCreate WebhookEvent = "create"
|
||||
WebhookEventUpdate WebhookEvent = "update"
|
||||
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,
|
||||
}
|
||||
}
|
@@ -36,6 +36,8 @@ type StatisticsMetricsServer interface {
|
||||
type StatisticsEventBus interface {
|
||||
// Subscribe subscribes to a topic
|
||||
Subscribe(topic string, fn interface{}) error
|
||||
// Publish sends a message to the message bus.
|
||||
Publish(topic string, args ...any)
|
||||
}
|
||||
|
||||
type pingJob struct {
|
||||
@@ -53,6 +55,8 @@ type StatisticsCollector struct {
|
||||
db StatisticsDatabaseRepo
|
||||
wg *ControllerManager
|
||||
ms StatisticsMetricsServer
|
||||
|
||||
peerChangeEvent chan domain.PeerIdentifier
|
||||
}
|
||||
|
||||
// NewStatisticsCollector creates a new statistics collector.
|
||||
@@ -169,8 +173,12 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
for _, peer := range peers {
|
||||
var connectionStateChanged bool
|
||||
var newPeerStatus domain.PeerStatus
|
||||
err = c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
||||
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
||||
wasConnected := p.IsConnected
|
||||
|
||||
var lastHandshake *time.Time
|
||||
if !peer.LastHandshake.IsZero() {
|
||||
lastHandshake = &peer.LastHandshake
|
||||
@@ -184,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.Endpoint = peer.Endpoint
|
||||
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
|
||||
go c.updatePeerMetrics(ctx, *p)
|
||||
@@ -195,6 +210,17 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,12 +327,18 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
||||
for job := range c.pingJobs {
|
||||
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)
|
||||
|
||||
now := time.Now()
|
||||
err := c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
||||
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
||||
wasConnected := p.IsConnected
|
||||
|
||||
if peerPingable {
|
||||
p.IsPingable = true
|
||||
p.LastPing = &now
|
||||
@@ -314,6 +346,13 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
||||
p.IsPingable = false
|
||||
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
|
||||
go c.updatePeerMetrics(ctx, *p)
|
||||
@@ -325,6 +364,11 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
||||
} else {
|
||||
slog.Debug("updated peer ping status", "peer", peer.Identifier)
|
||||
}
|
||||
|
||||
if connectionStateChanged {
|
||||
// publish event if connection state changed
|
||||
c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus, peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -471,7 +471,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -500,7 +500,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -519,9 +519,9 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -561,7 +561,7 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -576,32 +576,13 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
||||
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)
|
||||
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 true // interface in db has changed
|
||||
}
|
||||
|
||||
wgInterface, err := m.wg.GetController(*iface).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
|
||||
return !oldInterface.IsDisabled(), !iface.IsDisabled()
|
||||
}
|
||||
|
||||
func (m Manager) handleInterfacePreSaveActions(iface *domain.Interface) error {
|
||||
@@ -617,12 +598,14 @@ func (m Manager) handleInterfacePreSaveActions(iface *domain.Interface) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) handleInterfacePreSaveHooks(stateChanged bool, iface *domain.Interface) error {
|
||||
if !stateChanged {
|
||||
func (m Manager) handleInterfacePreSaveHooks(iface *domain.Interface, oldEnabled, newEnabled bool) error {
|
||||
if oldEnabled == newEnabled {
|
||||
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 {
|
||||
return fmt.Errorf("failed to execute pre-up hook: %w", err)
|
||||
}
|
||||
@@ -634,12 +617,14 @@ func (m Manager) handleInterfacePreSaveHooks(stateChanged bool, iface *domain.In
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) handleInterfacePostSaveHooks(stateChanged bool, iface *domain.Interface) error {
|
||||
if !stateChanged {
|
||||
func (m Manager) handleInterfacePostSaveHooks(iface *domain.Interface, oldEnabled, newEnabled bool) error {
|
||||
if oldEnabled == newEnabled {
|
||||
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 {
|
||||
return fmt.Errorf("failed to execute post-up hook: %w", err)
|
||||
}
|
||||
|
@@ -188,6 +188,30 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
||||
|
||||
sessionUser := domain.GetUserInfo(ctx)
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch peers for user %s: %w", peer.UserIdentifier, err)
|
||||
}
|
||||
// Count enabled peers (disabled IS NULL)
|
||||
peerCount := 0
|
||||
for _, p := range peers {
|
||||
if !p.IsDisabled() {
|
||||
peerCount++
|
||||
}
|
||||
}
|
||||
totalAllowedPeers := 1 + m.cfg.Advanced.LimitAdditionalUserPeers // 1 default peer + x additional peers
|
||||
if peerCount >= totalAllowedPeers {
|
||||
slog.WarnContext(ctx, "peer creation blocked due to limit",
|
||||
"user", peer.UserIdentifier,
|
||||
"current_count", peerCount,
|
||||
"allowed_count", totalAllowedPeers)
|
||||
return nil, fmt.Errorf("peer limit reached (%d peers allowed): %w", totalAllowedPeers, domain.ErrNoPermission)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return nil, fmt.Errorf("unable to load existing 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.
|
||||
// It is encouraged to set this value to at least 16 characters.
|
||||
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.
|
||||
|
@@ -29,18 +29,19 @@ type Config struct {
|
||||
} `yaml:"core"`
|
||||
|
||||
Advanced struct {
|
||||
LogLevel string `yaml:"log_level"`
|
||||
LogPretty bool `yaml:"log_pretty"`
|
||||
LogJson bool `yaml:"log_json"`
|
||||
StartListenPort int `yaml:"start_listen_port"`
|
||||
StartCidrV4 string `yaml:"start_cidr_v4"`
|
||||
StartCidrV6 string `yaml:"start_cidr_v6"`
|
||||
UseIpV6 bool `yaml:"use_ip_v6"`
|
||||
ConfigStoragePath string `yaml:"config_storage_path"` // keep empty to disable config export to file
|
||||
ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"`
|
||||
RulePrioOffset int `yaml:"rule_prio_offset"`
|
||||
RouteTableOffset int `yaml:"route_table_offset"`
|
||||
ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API
|
||||
LogLevel string `yaml:"log_level"`
|
||||
LogPretty bool `yaml:"log_pretty"`
|
||||
LogJson bool `yaml:"log_json"`
|
||||
StartListenPort int `yaml:"start_listen_port"`
|
||||
StartCidrV4 string `yaml:"start_cidr_v4"`
|
||||
StartCidrV6 string `yaml:"start_cidr_v6"`
|
||||
UseIpV6 bool `yaml:"use_ip_v6"`
|
||||
ConfigStoragePath string `yaml:"config_storage_path"` // keep empty to disable config export to file
|
||||
ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"`
|
||||
RulePrioOffset int `yaml:"rule_prio_offset"`
|
||||
RouteTableOffset int `yaml:"route_table_offset"`
|
||||
ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API
|
||||
LimitAdditionalUserPeers int `yaml:"limit_additional_user_peers"`
|
||||
} `yaml:"advanced"`
|
||||
|
||||
Backend Backend `yaml:"backend"`
|
||||
@@ -78,6 +79,7 @@ func (c *Config) LogStartupValues() {
|
||||
"reEnablePeerAfterUserEnable", c.Core.ReEnablePeerAfterUserEnable,
|
||||
"deletePeerAfterUserDeleted", c.Core.DeletePeerAfterUserDeleted,
|
||||
"selfProvisioningAllowed", c.Core.SelfProvisioningAllowed,
|
||||
"limitAdditionalUserPeers", c.Advanced.LimitAdditionalUserPeers,
|
||||
"importExisting", c.Core.ImportExisting,
|
||||
"restoreState", c.Core.RestoreState,
|
||||
"useIpV6", c.Advanced.UseIpV6,
|
||||
@@ -95,6 +97,9 @@ func (c *Config) LogStartupValues() {
|
||||
"oidcProviders", len(c.Auth.OpenIDConnect),
|
||||
"oauthProviders", len(c.Auth.OAuth),
|
||||
"ldapProviders", len(c.Auth.Ldap),
|
||||
"webauthnEnabled", c.Auth.WebAuthn.Enabled,
|
||||
"minPasswordLength", c.Auth.MinPasswordLength,
|
||||
"hideLoginForm", c.Auth.HideLoginForm,
|
||||
)
|
||||
|
||||
slog.Debug("Config Backend",
|
||||
@@ -149,6 +154,7 @@ func defaultConfig() *Config {
|
||||
cfg.Advanced.RulePrioOffset = 20000
|
||||
cfg.Advanced.RouteTableOffset = 20000
|
||||
cfg.Advanced.ApiAdminOnly = true
|
||||
cfg.Advanced.LimitAdditionalUserPeers = 0
|
||||
|
||||
cfg.Statistics.UsePingChecks = true
|
||||
cfg.Statistics.PingCheckWorkers = 10
|
||||
@@ -178,6 +184,7 @@ func defaultConfig() *Config {
|
||||
|
||||
cfg.Auth.WebAuthn.Enabled = true
|
||||
cfg.Auth.MinPasswordLength = 16
|
||||
cfg.Auth.HideLoginForm = false
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
@@ -62,4 +62,7 @@ const (
|
||||
|
||||
LockedReasonAdmin = "locked by admin"
|
||||
LockedReasonApi = "locked by admin"
|
||||
|
||||
ConfigStyleRaw = "raw"
|
||||
ConfigStyleWgQuick = "wgquick"
|
||||
)
|
||||
|
@@ -136,6 +136,7 @@ func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
|
||||
p.Interface.PublicKey = userPeer.Interface.PublicKey
|
||||
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
|
||||
p.PresharedKey = userPeer.PresharedKey
|
||||
p.Identifier = userPeer.Identifier
|
||||
}
|
||||
p.Interface.Mtu = userPeer.Interface.Mtu
|
||||
p.PersistentKeepalive = userPeer.PersistentKeepalive
|
||||
|
@@ -5,21 +5,23 @@ import (
|
||||
)
|
||||
|
||||
type PeerStatus struct {
|
||||
PeerId PeerIdentifier `gorm:"primaryKey;column:identifier"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
PeerId PeerIdentifier `gorm:"primaryKey;column:identifier" json:"PeerId"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"-"`
|
||||
|
||||
IsPingable bool `gorm:"column:pingable"`
|
||||
LastPing *time.Time `gorm:"column:last_ping"`
|
||||
IsConnected bool `gorm:"column:connected" json:"IsConnected"` // indicates if the peer is connected based on the last handshake or ping
|
||||
|
||||
BytesReceived uint64 `gorm:"column:received"`
|
||||
BytesTransmitted uint64 `gorm:"column:transmitted"`
|
||||
IsPingable bool `gorm:"column:pingable" json:"IsPingable"`
|
||||
LastPing *time.Time `gorm:"column:last_ping" json:"LastPing"`
|
||||
|
||||
LastHandshake *time.Time `gorm:"column:last_handshake"`
|
||||
Endpoint string `gorm:"column:endpoint"`
|
||||
LastSessionStart *time.Time `gorm:"column:last_session_start"`
|
||||
BytesReceived uint64 `gorm:"column:received" json:"BytesReceived"`
|
||||
BytesTransmitted uint64 `gorm:"column:transmitted" json:"BytesTransmitted"`
|
||||
|
||||
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
|
||||
|
||||
handshakeValid := false
|
||||
@@ -27,7 +29,7 @@ func (s PeerStatus) IsConnected() bool {
|
||||
handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime)
|
||||
}
|
||||
|
||||
return s.IsPingable || handshakeValid
|
||||
s.IsConnected = s.IsPingable || handshakeValid
|
||||
}
|
||||
|
||||
type InterfaceStatus struct {
|
||||
|
@@ -66,8 +66,9 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.status.IsConnected(); got != tt.want {
|
||||
t.Errorf("IsConnected() = %v, want %v", got, tt.want)
|
||||
tt.status.CalcConnected()
|
||||
if got := tt.status.IsConnected; got != tt.want {
|
||||
t.Errorf("IsConnected = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user