wip: create different backend handlers (#426)

This commit is contained in:
Christoph Haas
2025-05-30 23:19:48 +02:00
parent 33dcc80078
commit 15d035ec10
12 changed files with 1229 additions and 103 deletions

View File

@@ -0,0 +1,161 @@
package wireguard
import (
"context"
"fmt"
"log/slog"
"maps"
"slices"
"github.com/h44z/wg-portal/internal/adapters/wgcontroller"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
type InterfaceController interface {
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
SaveInterface(
_ context.Context,
id domain.InterfaceIdentifier,
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
) error
DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error
SavePeer(
_ context.Context,
deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier,
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
) error
DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error
}
type backendInstance struct {
Config config.BackendBase // Config is the configuration for the backend instance.
Implementation InterfaceController
}
type ControllerManager struct {
cfg *config.Config
controllers map[domain.InterfaceBackend]backendInstance
}
func NewControllerManager(cfg *config.Config) (*ControllerManager, error) {
c := &ControllerManager{
cfg: cfg,
controllers: make(map[domain.InterfaceBackend]backendInstance),
}
err := c.init()
if err != nil {
return nil, err
}
return c, nil
}
func (c *ControllerManager) init() error {
if err := c.registerLocalController(); err != nil {
return err
}
if err := c.registerMikrotikControllers(); err != nil {
return err
}
c.logRegisteredControllers()
return nil
}
func (c *ControllerManager) registerLocalController() error {
localController, err := wgcontroller.NewLocalController(c.cfg)
if err != nil {
return fmt.Errorf("failed to create local WireGuard controller: %w", err)
}
c.controllers[config.LocalBackendName] = backendInstance{
Config: config.BackendBase{
Id: config.LocalBackendName,
DisplayName: "Local WireGuard Controller",
},
Implementation: localController,
}
return nil
}
func (c *ControllerManager) registerMikrotikControllers() error {
for _, backendConfig := range c.cfg.Backend.Mikrotik {
if backendConfig.Id == config.LocalBackendName {
slog.Warn("skipping registration of Mikrotik controller with reserved ID", "id", config.LocalBackendName)
continue
}
controller, err := wgcontroller.NewMikrotikController() // TODO: Pass backendConfig to the controller constructor
if err != nil {
return fmt.Errorf("failed to create Mikrotik controller for backend %s: %w", backendConfig.Id, err)
}
c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{
Config: backendConfig.BackendBase,
Implementation: controller,
}
}
return nil
}
func (c *ControllerManager) logRegisteredControllers() {
for backend, controller := range c.controllers {
slog.Debug("backend controller registered",
"backend", backend, "type", fmt.Sprintf("%T", controller.Implementation))
}
}
func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) InterfaceController {
return c.getController(backend, "")
}
func (c *ControllerManager) GetController(iface domain.Interface) InterfaceController {
return c.getController(iface.Backend, iface.Identifier)
}
func (c *ControllerManager) getController(
backend domain.InterfaceBackend,
ifaceId domain.InterfaceIdentifier,
) InterfaceController {
if backend == "" {
// If no backend is specified, use the local controller.
// This might be the case for interfaces created in previous WireGuard Portal versions.
backend = config.LocalBackendName
}
controller, exists := c.controllers[backend]
if !exists {
controller, exists = c.controllers[config.LocalBackendName] // Fallback to local controller
if !exists {
// If the local controller is also not found, panic
panic(fmt.Sprintf("%s interface controller for backend %s not found", ifaceId, backend))
}
slog.Warn("controller for backend not found, using local controller",
"backend", backend, "interface", ifaceId)
}
return controller.Implementation
}
func (c *ControllerManager) GetAllControllers() []InterfaceController {
var backendInstances = make([]InterfaceController, 0, len(c.controllers))
for instance := range maps.Values(c.controllers) {
backendInstances = append(backendInstances, instance.Implementation)
}
return backendInstances
}
func (c *ControllerManager) GetControllerNames() []config.BackendBase {
var names []config.BackendBase
for _, id := range slices.Sorted(maps.Keys(c.controllers)) {
names = append(names, c.controllers[id].Config)
}
return names
}

View File

@@ -53,7 +53,7 @@ type StatisticsCollector struct {
pingJobs chan domain.Peer
db StatisticsDatabaseRepo
wg StatisticsInterfaceController
wg *ControllerManager
ms StatisticsMetricsServer
}
@@ -62,7 +62,7 @@ func NewStatisticsCollector(
cfg *config.Config,
bus StatisticsEventBus,
db StatisticsDatabaseRepo,
wg StatisticsInterfaceController,
wg *ControllerManager,
ms StatisticsMetricsServer,
) (*StatisticsCollector, error) {
c := &StatisticsCollector{
@@ -113,7 +113,7 @@ func (c *StatisticsCollector) collectInterfaceData(ctx context.Context) {
}
for _, in := range interfaces {
physicalInterface, err := c.wg.GetInterface(ctx, in.Identifier)
physicalInterface, err := c.wg.GetController(in).GetInterface(ctx, in.Identifier)
if err != nil {
slog.Warn("failed to load physical interface for data collection", "interface", in.Identifier,
"error", err)
@@ -165,7 +165,7 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
}
for _, in := range interfaces {
peers, err := c.wg.GetPeers(ctx, in.Identifier)
peers, err := c.wg.GetController(in).GetPeers(ctx, in.Identifier)
if err != nil {
slog.Warn("failed to fetch peers for data collection", "interface", in.Identifier, "error", err)
continue

View File

@@ -37,25 +37,6 @@ type InterfaceAndPeerDatabaseRepo interface {
GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error)
}
type InterfaceController interface {
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
SaveInterface(
_ context.Context,
id domain.InterfaceIdentifier,
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
) error
DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error
SavePeer(
_ context.Context,
deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier,
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
) error
DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error
}
type WgQuickController interface {
ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error
SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error
@@ -75,7 +56,7 @@ type Manager struct {
cfg *config.Config
bus EventBus
db InterfaceAndPeerDatabaseRepo
wg InterfaceController
wg *ControllerManager
quick WgQuickController
userLockMap *sync.Map
@@ -84,7 +65,7 @@ type Manager struct {
func NewWireGuardManager(
cfg *config.Config,
bus EventBus,
wg InterfaceController,
wg *ControllerManager,
quick WgQuickController,
db InterfaceAndPeerDatabaseRepo,
) (*Manager, error) {

View File

@@ -21,12 +21,17 @@ func (m Manager) GetImportableInterfaces(ctx context.Context) ([]domain.Physical
return nil, err
}
physicalInterfaces, err := m.wg.GetInterfaces(ctx)
if 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 physicalInterfaces, nil
return allPhysicalInterfaces, nil
}
// GetInterfaceAndPeers returns the interface and all peers for the given interface identifier.
@@ -109,47 +114,49 @@ func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.Inter
return 0, err
}
physicalInterfaces, err := m.wg.GetInterfaces(ctx)
if err != nil {
return 0, err
}
// if no filter is given, exclude already existing interfaces
var excludedInterfaces []domain.InterfaceIdentifier
if len(filter) == 0 {
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil {
return 0, err
}
for _, existingInterface := range existingInterfaces {
excludedInterfaces = append(excludedInterfaces, existingInterface.Identifier)
}
}
imported := 0
for _, physicalInterface := range physicalInterfaces {
if slices.Contains(excludedInterfaces, physicalInterface.Identifier) {
continue
}
if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) {
continue
}
slog.Info("importing new interface", "interface", physicalInterface.Identifier)
physicalPeers, err := m.wg.GetPeers(ctx, physicalInterface.Identifier)
for _, wgBackend := range m.wg.GetAllControllers() {
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
if err != nil {
return 0, err
}
err = m.importInterface(ctx, &physicalInterface, physicalPeers)
if err != nil {
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
// if no filter is given, exclude already existing interfaces
var excludedInterfaces []domain.InterfaceIdentifier
if len(filter) == 0 {
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil {
return 0, err
}
for _, existingInterface := range existingInterfaces {
excludedInterfaces = append(excludedInterfaces, existingInterface.Identifier)
}
}
slog.Info("imported new interface", "interface", physicalInterface.Identifier, "peers", len(physicalPeers))
imported++
for _, physicalInterface := range physicalInterfaces {
if slices.Contains(excludedInterfaces, physicalInterface.Identifier) {
continue
}
if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) {
continue
}
slog.Info("importing new interface", "interface", physicalInterface.Identifier)
physicalPeers, err := wgBackend.GetPeers(ctx, physicalInterface.Identifier)
if err != nil {
return 0, err
}
err = m.importInterface(ctx, &physicalInterface, physicalPeers)
if err != nil {
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
}
slog.Info("imported new interface", "interface", physicalInterface.Identifier, "peers", len(physicalPeers))
imported++
}
}
return imported, nil
@@ -213,7 +220,7 @@ func (m Manager) RestoreInterfaceState(
return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err)
}
_, err = m.wg.GetInterface(ctx, iface.Identifier)
_, err = m.wg.GetController(iface).GetInterface(ctx, iface.Identifier)
if err != nil && !iface.IsDisabled() {
slog.Debug("creating missing interface", "interface", iface.Identifier)
@@ -261,17 +268,19 @@ func (m Manager) RestoreInterfaceState(
for _, peer := range peers {
switch {
case iface.IsDisabled(): // if interface is disabled, delete all peers
if err := m.wg.DeletePeer(ctx, iface.Identifier, peer.Identifier); err != nil {
if err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
peer.Identifier); err != nil {
return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w",
peer.Identifier, iface.Identifier, err)
}
case peer.IsDisabled(): // if peer is disabled, delete it
if err := m.wg.DeletePeer(ctx, iface.Identifier, peer.Identifier); err != nil {
if err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
peer.Identifier); err != nil {
return fmt.Errorf("failed to remove disbaled peer %s from interface %s: %w",
peer.Identifier, iface.Identifier, err)
}
default: // update peer
err := m.wg.SavePeer(ctx, iface.Identifier, peer.Identifier,
err := m.wg.GetController(iface).SavePeer(ctx, iface.Identifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, &peer)
return pp, nil
@@ -284,7 +293,7 @@ func (m Manager) RestoreInterfaceState(
}
// remove non-wgportal peers
physicalPeers, _ := m.wg.GetPeers(ctx, iface.Identifier)
physicalPeers, _ := m.wg.GetController(iface).GetPeers(ctx, iface.Identifier)
for _, physicalPeer := range physicalPeers {
isWgPortalPeer := false
for _, peer := range peers {
@@ -294,7 +303,8 @@ func (m Manager) RestoreInterfaceState(
}
}
if !isWgPortalPeer {
err := m.wg.DeletePeer(ctx, iface.Identifier, domain.PeerIdentifier(physicalPeer.PublicKey))
err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
domain.PeerIdentifier(physicalPeer.PublicKey))
if err != nil {
return fmt.Errorf("failed to remove non-wgportal peer %s from interface %s: %w",
physicalPeer.PublicKey, iface.Identifier, err)
@@ -459,7 +469,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
existingInterface.Disabled = &now // simulate a disabled interface
existingInterface.DisabledReason = domain.DisabledReasonDeleted
physicalInterface, _ := m.wg.GetInterface(ctx, id)
physicalInterface, _ := m.wg.GetController(*existingInterface).GetInterface(ctx, id)
if err := m.handleInterfacePreSaveHooks(true, existingInterface); err != nil {
return fmt.Errorf("pre-delete hooks failed: %w", err)
@@ -473,7 +483,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
return fmt.Errorf("peer deletion failure: %w", err)
}
if err := m.wg.DeleteInterface(ctx, id); err != nil {
if err := m.wg.GetController(*existingInterface).DeleteInterface(ctx, id); err != nil {
return fmt.Errorf("wireguard deletion failure: %w", err)
}
@@ -522,7 +532,7 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
err := m.db.SaveInterface(ctx, iface.Identifier, func(i *domain.Interface) (*domain.Interface, error) {
iface.CopyCalculatedAttributes(i)
err := m.wg.SaveInterface(ctx, iface.Identifier,
err := m.wg.GetController(*iface).SaveInterface(ctx, iface.Identifier,
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
domain.MergeToPhysicalInterface(pi, iface)
return pi, nil
@@ -538,7 +548,7 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
}
if iface.IsDisabled() {
physicalInterface, _ := m.wg.GetInterface(ctx, iface.Identifier)
physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier)
fwMark := iface.FirewallMark
if physicalInterface != nil && fwMark == 0 {
fwMark = physicalInterface.FirewallMark
@@ -576,7 +586,7 @@ func (m Manager) hasInterfaceStateChanged(ctx context.Context, iface *domain.Int
return true // interface in db has changed
}
wgInterface, err := m.wg.GetInterface(ctx, iface.Identifier)
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
}
@@ -844,12 +854,12 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
}
func (m Manager) deleteInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) error {
allPeers, err := m.db.GetInterfacePeers(ctx, id)
iface, allPeers, err := m.db.GetInterfaceAndPeers(ctx, id)
if err != nil {
return err
}
for _, peer := range allPeers {
err = m.wg.DeletePeer(ctx, id, peer.Identifier)
err = m.wg.GetController(*iface).DeletePeer(ctx, id, peer.Identifier)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("wireguard peer deletion failure for %s: %w", peer.Identifier, err)
}

View File

@@ -352,7 +352,12 @@ func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
return fmt.Errorf("delete not allowed: %w", err)
}
err = m.wg.DeletePeer(ctx, peer.InterfaceIdentifier, id)
iface, err := m.db.GetInterface(ctx, peer.InterfaceIdentifier)
if err != nil {
return fmt.Errorf("unable to find interface %s: %w", peer.InterfaceIdentifier, err)
}
err = m.wg.GetController(*iface).DeletePeer(ctx, peer.InterfaceIdentifier, id)
if err != nil {
return fmt.Errorf("wireguard failed to delete peer %s: %w", id, err)
}
@@ -414,14 +419,18 @@ func (m Manager) GetUserPeerStats(ctx context.Context, id domain.UserIdentifier)
func (m Manager) savePeers(ctx context.Context, peers ...*domain.Peer) error {
interfaces := make(map[domain.InterfaceIdentifier]struct{})
for i := range peers {
peer := peers[i]
var err error
for _, peer := range peers {
iface, err := m.db.GetInterface(ctx, peer.InterfaceIdentifier)
if err != nil {
return fmt.Errorf("unable to find interface %s: %w", peer.InterfaceIdentifier, err)
}
if peer.IsDisabled() || peer.IsExpired() {
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
peer.CopyCalculatedAttributes(p)
if err := m.wg.DeletePeer(ctx, peer.InterfaceIdentifier, peer.Identifier); err != nil {
if err := m.wg.GetController(*iface).DeletePeer(ctx, peer.InterfaceIdentifier,
peer.Identifier); err != nil {
return nil, fmt.Errorf("failed to delete wireguard peer %s: %w", peer.Identifier, err)
}
@@ -431,7 +440,7 @@ func (m Manager) savePeers(ctx context.Context, peers ...*domain.Peer) error {
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
peer.CopyCalculatedAttributes(p)
err := m.wg.SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
err := m.wg.GetController(*iface).SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, peer)
return pp, nil