diff --git a/docs/documentation/configuration/examples.md b/docs/documentation/configuration/examples.md index 41a4034..3909d48 100644 --- a/docs/documentation/configuration/examples.md +++ b/docs/documentation/configuration/examples.md @@ -11,6 +11,24 @@ core: create_default_peer: 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: site_title: My WireGuard Server site_company_name: My Company @@ -195,3 +213,5 @@ auth: registration_enabled: 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)). diff --git a/docs/documentation/configuration/overview.md b/docs/documentation/configuration/overview.md index 7c0000f..0345338 100644 --- a/docs/documentation/configuration/overview.md +++ b/docs/documentation/configuration/overview.md @@ -184,6 +184,11 @@ The current MikroTik backend is in **BETA** and may not support all features. - **Description:** The default backend to use for managing WireGuard interfaces. 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 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` - **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` - **Default:** `false` - **Description:** Enable verbose debug logging for the MikroTik backend. diff --git a/internal/adapters/wgcontroller/mikrotik.go b/internal/adapters/wgcontroller/mikrotik.go index 8498d34..ac98094 100644 --- a/internal/adapters/wgcontroller/mikrotik.go +++ b/internal/adapters/wgcontroller/mikrotik.go @@ -3,14 +3,13 @@ package wgcontroller import ( "context" "fmt" + "log/slog" "slices" "strconv" "strings" "sync" "time" - "log/slog" - "github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/lowlevel" @@ -678,11 +677,15 @@ func (c *MikrotikController) updatePeer( extras := pp.GetExtras().(domain.MikrotikPeerExtras) peerId := extras.Id - endpoint := pp.Endpoint - endpointPort := "51820" // default port if not set - if s := strings.Split(endpoint, ":"); len(s) == 2 { - endpoint = s[0] - endpointPort = s[1] + endpoint := "" // by default, we have no endpoint (the peer does not initiate a connection) + 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 { + endpoint = s[0] + endpointPort = s[1] + } } allowedAddressStr := domain.CidrsToString(pp.AllowedIPs) diff --git a/internal/app/wireguard/controller_manager.go b/internal/app/wireguard/controller_manager.go index ab1eaa9..2eea6af 100644 --- a/internal/app/wireguard/controller_manager.go +++ b/internal/app/wireguard/controller_manager.go @@ -82,8 +82,9 @@ func (c *ControllerManager) registerLocalController() error { c.controllers[config.LocalBackendName] = backendInstance{ Config: config.BackendBase{ - Id: config.LocalBackendName, - DisplayName: "Local WireGuard Controller", + Id: config.LocalBackendName, + DisplayName: "Local WireGuard Controller", + IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces, }, Implementation: localController, } @@ -118,17 +119,17 @@ func (c *ControllerManager) logRegisteredControllers() { } 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 { - return c.getController(iface.Backend, iface.Identifier) + return c.getController(iface.Backend, iface.Identifier).Implementation } func (c *ControllerManager) getController( backend domain.InterfaceBackend, ifaceId domain.InterfaceIdentifier, -) InterfaceController { +) backendInstance { if backend == "" { // If no backend is specified, use the local controller. // 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", "backend", backend, "interface", ifaceId) } - return controller.Implementation + return controller } -func (c *ControllerManager) GetAllControllers() []InterfaceController { - var backendInstances = make([]InterfaceController, 0, len(c.controllers)) +func (c *ControllerManager) GetAllControllers() []backendInstance { + var backendInstances = make([]backendInstance, 0, len(c.controllers)) for instance := range maps.Values(c.controllers) { - backendInstances = append(backendInstances, instance.Implementation) + backendInstances = append(backendInstances, instance) } return backendInstances } diff --git a/internal/app/wireguard/wireguard_interfaces.go b/internal/app/wireguard/wireguard_interfaces.go index 1a09822..368d1eb 100644 --- a/internal/app/wireguard/wireguard_interfaces.go +++ b/internal/app/wireguard/wireguard_interfaces.go @@ -15,26 +15,6 @@ import ( "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. func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) ( *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. +// 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) { if err := domain.ValidateAdminAccessRights(ctx); err != nil { return 0, err } + var existingInterfaceIds []domain.InterfaceIdentifier + existingInterfaces, err := m.db.GetAllInterfaces(ctx) + if err != nil { + return 0, err + } + for _, existingInterface := range existingInterfaces { + existingInterfaceIds = append(existingInterfaceIds, existingInterface.Identifier) + } + imported := 0 for _, wgBackend := range m.wg.GetAllControllers() { - physicalInterfaces, err := wgBackend.GetInterfaces(ctx) + physicalInterfaces, err := wgBackend.Implementation.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) - } - } - for _, physicalInterface := range physicalInterfaces { - if slices.Contains(excludedInterfaces, physicalInterface.Identifier) { + if slices.Contains(wgBackend.Config.IgnoredInterfaces, string(physicalInterface.Identifier)) { + slog.Info("ignoring interface due to backend filter restrictions", + "interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces, + "backend", wgBackend.Config.Id) + continue // skip ignored interfaces + } + + if slices.Contains(existingInterfaceIds, physicalInterface.Identifier) { + continue // skip interfaces that already exist + } + + if len(filter) > 0 && !slices.Contains(filter, physicalInterface.Identifier) { + slog.Info("ignoring interface due to filter restrictions", + "interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces, + "backend", wgBackend.Config.Id) continue } - if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) { - continue - } + slog.Info("importing new interface", + "interface", physicalInterface.Identifier, "backend", wgBackend.Config.Id) - slog.Info("importing new interface", "interface", physicalInterface.Identifier) - - physicalPeers, err := wgBackend.GetPeers(ctx, physicalInterface.Identifier) + physicalPeers, err := wgBackend.Implementation.GetPeers(ctx, physicalInterface.Identifier) if err != nil { return 0, err } - err = m.importInterface(ctx, wgBackend, &physicalInterface, physicalPeers) + err = m.importInterface(ctx, wgBackend.Implementation, &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)) + slog.Info("imported new interface", + "interface", physicalInterface.Identifier, "peers", len(physicalPeers), "backend", wgBackend.Config.Id) imported++ } } @@ -221,9 +211,11 @@ func (m Manager) RestoreInterfaceState( 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() { - 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 _ = 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) } } 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 _, err = m.saveInterface(ctx, &iface) @@ -278,13 +271,13 @@ func (m Manager) RestoreInterfaceState( for _, peer := range peers { switch { 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 { return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w", peer.Identifier, iface.Identifier, err) } 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) { domain.MergeToPhysicalPeer(pp, &peer) return pp, nil @@ -297,7 +290,7 @@ func (m Manager) RestoreInterfaceState( } // remove non-wgportal peers - physicalPeers, _ := m.wg.GetController(iface).GetPeers(ctx, iface.Identifier) + physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier) for _, physicalPeer := range physicalPeers { isWgPortalPeer := false for _, peer := range peers { @@ -307,7 +300,7 @@ func (m Manager) RestoreInterfaceState( } } if !isWgPortalPeer { - err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier, + err := controller.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", @@ -551,6 +544,30 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) ( 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() { physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier) fwMark := iface.FirewallMark diff --git a/internal/config/backend.go b/internal/config/backend.go index f81adad..fa8ff2e 100644 --- a/internal/config/backend.go +++ b/internal/config/backend.go @@ -10,6 +10,12 @@ const LocalBackendName = "local" type Backend struct { 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"` } @@ -42,6 +48,8 @@ func (b *Backend) Validate() error { type BackendBase struct { Id string `yaml:"id"` // A unique id 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. diff --git a/internal/domain/peer.go b/internal/domain/peer.go index 519d551..c47f63e 100644 --- a/internal/domain/peer.go +++ b/internal/domain/peer.go @@ -328,7 +328,7 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) { Id: "", Name: p.DisplayName, Comment: p.Notes, - IsResponder: false, + IsResponder: p.Interface.Type == InterfaceTypeClient, Disabled: p.IsDisabled(), ClientEndpoint: p.Endpoint.GetValue(), ClientAddress: CidrsToString(p.Interface.Addresses),