Mikrotik improvements (#521)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled

* allow to specify ignored interfaces (#514)

* only set endpoint info for "responder" peers (#516)
This commit is contained in:
h44z
2025-09-09 21:43:16 +02:00
committed by GitHub
parent 6d2a5fa6de
commit 765fb09770
7 changed files with 125 additions and 66 deletions

View File

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

View File

@@ -184,6 +184,11 @@ The current MikroTik backend is in **BETA** and may not support all features.
- **Description:** The default backend to use for managing WireGuard interfaces. - **Description:** The default backend to use for managing WireGuard interfaces.
Valid options are: `local`, or other backend id's configured in the `mikrotik` section. Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
### `ignored_local_interfaces`
- **Default:** *(empty)*
- **Description:** A list of interface names to exclude when enumerating local interfaces.
This is useful if you want to prevent certain interfaces from being imported from the local system.
### Mikrotik ### Mikrotik
The `mikrotik` array contains a list of MikroTik backend definitions. Each entry describes how to connect to a MikroTik RouterOS instance that hosts WireGuard interfaces. The `mikrotik` array contains a list of MikroTik backend definitions. Each entry describes how to connect to a MikroTik RouterOS instance that hosts WireGuard interfaces.
@@ -225,6 +230,11 @@ Below are the properties for each entry inside `backend.mikrotik`:
- **Default:** `5` - **Default:** `5`
- **Description:** Maximum number of concurrent API requests the backend will issue when enumerating interfaces and their details. If `0` or negative, a sane default of `5` is used. - **Description:** Maximum number of concurrent API requests the backend will issue when enumerating interfaces and their details. If `0` or negative, a sane default of `5` is used.
#### `ignored_interfaces`
- **Default:** *(empty)*
- **Description:** A list of interface names to exclude during interface enumeration.
This is useful if you want to prevent specific interfaces from being imported from the MikroTik device.
#### `debug` #### `debug`
- **Default:** `false` - **Default:** `false`
- **Description:** Enable verbose debug logging for the MikroTik backend. - **Description:** Enable verbose debug logging for the MikroTik backend.

View File

@@ -3,14 +3,13 @@ package wgcontroller
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"log/slog"
"github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
"github.com/h44z/wg-portal/internal/lowlevel" "github.com/h44z/wg-portal/internal/lowlevel"
@@ -678,12 +677,16 @@ func (c *MikrotikController) updatePeer(
extras := pp.GetExtras().(domain.MikrotikPeerExtras) extras := pp.GetExtras().(domain.MikrotikPeerExtras)
peerId := extras.Id peerId := extras.Id
endpoint := pp.Endpoint endpoint := "" // by default, we have no endpoint (the peer does not initiate a connection)
endpointPort := "51820" // default port if not set endpointPort := "0" // by default, we have no endpoint port (the peer does not initiate a connection)
if !extras.IsResponder { // if the peer is not only a responder, it needs the endpoint to initiate a connection
endpoint = pp.Endpoint
endpointPort = "51820" // default port if not set
if s := strings.Split(endpoint, ":"); len(s) == 2 { if s := strings.Split(endpoint, ":"); len(s) == 2 {
endpoint = s[0] endpoint = s[0]
endpointPort = s[1] endpointPort = s[1]
} }
}
allowedAddressStr := domain.CidrsToString(pp.AllowedIPs) allowedAddressStr := domain.CidrsToString(pp.AllowedIPs)
slog.Debug("updating Mikrotik peer", slog.Debug("updating Mikrotik peer",

View File

@@ -84,6 +84,7 @@ func (c *ControllerManager) registerLocalController() error {
Config: config.BackendBase{ Config: config.BackendBase{
Id: config.LocalBackendName, Id: config.LocalBackendName,
DisplayName: "Local WireGuard Controller", DisplayName: "Local WireGuard Controller",
IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces,
}, },
Implementation: localController, Implementation: localController,
} }
@@ -118,17 +119,17 @@ func (c *ControllerManager) logRegisteredControllers() {
} }
func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) InterfaceController { func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) InterfaceController {
return c.getController(backend, "") return c.getController(backend, "").Implementation
} }
func (c *ControllerManager) GetController(iface domain.Interface) InterfaceController { func (c *ControllerManager) GetController(iface domain.Interface) InterfaceController {
return c.getController(iface.Backend, iface.Identifier) return c.getController(iface.Backend, iface.Identifier).Implementation
} }
func (c *ControllerManager) getController( func (c *ControllerManager) getController(
backend domain.InterfaceBackend, backend domain.InterfaceBackend,
ifaceId domain.InterfaceIdentifier, ifaceId domain.InterfaceIdentifier,
) InterfaceController { ) backendInstance {
if backend == "" { if backend == "" {
// If no backend is specified, use the local controller. // If no backend is specified, use the local controller.
// This might be the case for interfaces created in previous WireGuard Portal versions. // This might be the case for interfaces created in previous WireGuard Portal versions.
@@ -145,13 +146,13 @@ func (c *ControllerManager) getController(
slog.Warn("controller for backend not found, using local controller", slog.Warn("controller for backend not found, using local controller",
"backend", backend, "interface", ifaceId) "backend", backend, "interface", ifaceId)
} }
return controller.Implementation return controller
} }
func (c *ControllerManager) GetAllControllers() []InterfaceController { func (c *ControllerManager) GetAllControllers() []backendInstance {
var backendInstances = make([]InterfaceController, 0, len(c.controllers)) var backendInstances = make([]backendInstance, 0, len(c.controllers))
for instance := range maps.Values(c.controllers) { for instance := range maps.Values(c.controllers) {
backendInstances = append(backendInstances, instance.Implementation) backendInstances = append(backendInstances, instance)
} }
return backendInstances return backendInstances
} }

View File

@@ -15,26 +15,6 @@ import (
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
// GetImportableInterfaces returns all physical interfaces that are available on the system.
// This function also returns interfaces that are already available in the database.
func (m Manager) GetImportableInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
var allPhysicalInterfaces []domain.PhysicalInterface
for _, wgBackend := range m.wg.GetAllControllers() {
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
if err != nil {
return nil, err
}
allPhysicalInterfaces = append(allPhysicalInterfaces, physicalInterfaces...)
}
return allPhysicalInterfaces, nil
}
// GetInterfaceAndPeers returns the interface and all peers for the given interface identifier. // GetInterfaceAndPeers returns the interface and all peers for the given interface identifier.
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) ( func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.Interface, *domain.Interface,
@@ -110,52 +90,62 @@ func (m Manager) GetUserInterfaces(ctx context.Context, _ domain.UserIdentifier)
} }
// ImportNewInterfaces imports all new physical interfaces that are available on the system. // ImportNewInterfaces imports all new physical interfaces that are available on the system.
// If a filter is set, only interfaces that match the filter will be imported.
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) { func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil { if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return 0, err return 0, err
} }
imported := 0 var existingInterfaceIds []domain.InterfaceIdentifier
for _, wgBackend := range m.wg.GetAllControllers() {
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
if err != nil {
return 0, err
}
// if no filter is given, exclude already existing interfaces
var excludedInterfaces []domain.InterfaceIdentifier
if len(filter) == 0 {
existingInterfaces, err := m.db.GetAllInterfaces(ctx) existingInterfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil { if err != nil {
return 0, err return 0, err
} }
for _, existingInterface := range existingInterfaces { for _, existingInterface := range existingInterfaces {
excludedInterfaces = append(excludedInterfaces, existingInterface.Identifier) existingInterfaceIds = append(existingInterfaceIds, existingInterface.Identifier)
}
} }
for _, physicalInterface := range physicalInterfaces { imported := 0
if slices.Contains(excludedInterfaces, physicalInterface.Identifier) { for _, wgBackend := range m.wg.GetAllControllers() {
continue physicalInterfaces, err := wgBackend.Implementation.GetInterfaces(ctx)
}
if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) {
continue
}
slog.Info("importing new interface", "interface", physicalInterface.Identifier)
physicalPeers, err := wgBackend.GetPeers(ctx, physicalInterface.Identifier)
if err != nil { if err != nil {
return 0, err return 0, err
} }
err = m.importInterface(ctx, wgBackend, &physicalInterface, physicalPeers) for _, physicalInterface := range physicalInterfaces {
if slices.Contains(wgBackend.Config.IgnoredInterfaces, string(physicalInterface.Identifier)) {
slog.Info("ignoring interface due to backend filter restrictions",
"interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces,
"backend", wgBackend.Config.Id)
continue // skip ignored interfaces
}
if slices.Contains(existingInterfaceIds, physicalInterface.Identifier) {
continue // skip interfaces that already exist
}
if len(filter) > 0 && !slices.Contains(filter, physicalInterface.Identifier) {
slog.Info("ignoring interface due to filter restrictions",
"interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces,
"backend", wgBackend.Config.Id)
continue
}
slog.Info("importing new interface",
"interface", physicalInterface.Identifier, "backend", wgBackend.Config.Id)
physicalPeers, err := wgBackend.Implementation.GetPeers(ctx, physicalInterface.Identifier)
if err != nil {
return 0, err
}
err = m.importInterface(ctx, wgBackend.Implementation, &physicalInterface, physicalPeers)
if err != nil { if err != nil {
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err) return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
} }
slog.Info("imported new interface", "interface", physicalInterface.Identifier, "peers", len(physicalPeers)) slog.Info("imported new interface",
"interface", physicalInterface.Identifier, "peers", len(physicalPeers), "backend", wgBackend.Config.Id)
imported++ imported++
} }
} }
@@ -221,9 +211,11 @@ func (m Manager) RestoreInterfaceState(
return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err) return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err)
} }
_, err = m.wg.GetController(iface).GetInterface(ctx, iface.Identifier) controller := m.wg.GetController(iface)
_, err = controller.GetInterface(ctx, iface.Identifier)
if err != nil && !iface.IsDisabled() { if err != nil && !iface.IsDisabled() {
slog.Debug("creating missing interface", "interface", iface.Identifier) slog.Debug("creating missing interface", "interface", iface.Identifier, "backend", controller.GetId())
// temporarily disable interface in database so that the current state is reflected correctly // temporarily disable interface in database so that the current state is reflected correctly
_ = m.db.SaveInterface(ctx, iface.Identifier, _ = m.db.SaveInterface(ctx, iface.Identifier,
@@ -250,7 +242,8 @@ func (m Manager) RestoreInterfaceState(
return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err) return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err)
} }
} else { } else {
slog.Debug("restoring interface state", "interface", iface.Identifier, "disabled", iface.IsDisabled()) slog.Debug("restoring interface state",
"interface", iface.Identifier, "disabled", iface.IsDisabled(), "backend", controller.GetId())
// try to move interface to stored state // try to move interface to stored state
_, err = m.saveInterface(ctx, &iface) _, err = m.saveInterface(ctx, &iface)
@@ -278,13 +271,13 @@ func (m Manager) RestoreInterfaceState(
for _, peer := range peers { for _, peer := range peers {
switch { switch {
case iface.IsDisabled() && iface.Backend == config.LocalBackendName: // if interface is disabled, delete all peers case iface.IsDisabled() && iface.Backend == config.LocalBackendName: // if interface is disabled, delete all peers
if err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier, if err := controller.DeletePeer(ctx, iface.Identifier,
peer.Identifier); err != nil { peer.Identifier); err != nil {
return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w", return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w",
peer.Identifier, iface.Identifier, err) peer.Identifier, iface.Identifier, err)
} }
default: // update peer default: // update peer
err := m.wg.GetController(iface).SavePeer(ctx, iface.Identifier, peer.Identifier, err := controller.SavePeer(ctx, iface.Identifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) { func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, &peer) domain.MergeToPhysicalPeer(pp, &peer)
return pp, nil return pp, nil
@@ -297,7 +290,7 @@ func (m Manager) RestoreInterfaceState(
} }
// remove non-wgportal peers // remove non-wgportal peers
physicalPeers, _ := m.wg.GetController(iface).GetPeers(ctx, iface.Identifier) physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier)
for _, physicalPeer := range physicalPeers { for _, physicalPeer := range physicalPeers {
isWgPortalPeer := false isWgPortalPeer := false
for _, peer := range peers { for _, peer := range peers {
@@ -307,7 +300,7 @@ func (m Manager) RestoreInterfaceState(
} }
} }
if !isWgPortalPeer { if !isWgPortalPeer {
err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier, err := controller.DeletePeer(ctx, iface.Identifier,
domain.PeerIdentifier(physicalPeer.PublicKey)) domain.PeerIdentifier(physicalPeer.PublicKey))
if err != nil { if err != nil {
return fmt.Errorf("failed to remove non-wgportal peer %s from interface %s: %w", return fmt.Errorf("failed to remove non-wgportal peer %s from interface %s: %w",
@@ -551,6 +544,30 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
return nil, fmt.Errorf("failed to save interface: %w", err) return nil, fmt.Errorf("failed to save interface: %w", err)
} }
// update the interface type of peers in db
peers, err := m.db.GetInterfacePeers(ctx, iface.Identifier)
if err != nil {
return nil, fmt.Errorf("failed to load peers for interface %s: %w", iface.Identifier, err)
}
for _, peer := range peers {
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
switch iface.Type {
case domain.InterfaceTypeAny:
peer.Interface.Type = domain.InterfaceTypeAny
case domain.InterfaceTypeClient:
peer.Interface.Type = domain.InterfaceTypeServer
case domain.InterfaceTypeServer:
peer.Interface.Type = domain.InterfaceTypeClient
}
return &peer, nil
})
if err != nil {
return nil, fmt.Errorf("failed to update peer %s for interface %s: %w", peer.Identifier,
iface.Identifier, err)
}
}
if iface.IsDisabled() { if iface.IsDisabled() {
physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier) physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier)
fwMark := iface.FirewallMark fwMark := iface.FirewallMark

View File

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

View File

@@ -328,7 +328,7 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
Id: "", Id: "",
Name: p.DisplayName, Name: p.DisplayName,
Comment: p.Notes, Comment: p.Notes,
IsResponder: false, IsResponder: p.Interface.Type == InterfaceTypeClient,
Disabled: p.IsDisabled(), Disabled: p.IsDisabled(),
ClientEndpoint: p.Endpoint.GetValue(), ClientEndpoint: p.Endpoint.GetValue(),
ClientAddress: CidrsToString(p.Interface.Addresses), ClientAddress: CidrsToString(p.Interface.Addresses),