Skip to content

Commit 765fb09

Browse files
authored
Mikrotik improvements (#521)
* allow to specify ignored interfaces (#514) * only set endpoint info for "responder" peers (#516)
1 parent 6d2a5fa commit 765fb09

File tree

7 files changed

+122
-63
lines changed

7 files changed

+122
-63
lines changed

docs/documentation/configuration/examples.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@ core:
1111
create_default_peer: true
1212
self_provisioning_allowed: true
1313

14+
backend:
15+
# default backend decides where new interfaces are created
16+
default: mikrotik
17+
18+
mikrotik:
19+
- id: mikrotik # unique id, not "local"
20+
display_name: RouterOS RB5009 # optional nice name
21+
api_url: https://10.10.10.10/rest
22+
api_user: wgportal
23+
api_password: a-super-secret-password
24+
api_verify_tls: false # set to false only if using self-signed during testing
25+
api_timeout: 30s # maximum request duration
26+
concurrency: 5 # limit parallel REST calls to device
27+
debug: false # verbose logging for this backend
28+
ignored_interfaces: # ignore these interfaces during import
29+
- wgTest1
30+
- wgTest2
31+
1432
web:
1533
site_title: My WireGuard Server
1634
site_company_name: My Company
@@ -195,3 +213,5 @@ auth:
195213
registration_enabled: true
196214
log_user_info: true
197215
```
216+
217+
For more information, check out the usage documentation (e.g. [General Configuration](../usage/general.md) or [Backends Configuration](../usage/backends.md)).

docs/documentation/configuration/overview.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ The current MikroTik backend is in **BETA** and may not support all features.
184184
- **Description:** The default backend to use for managing WireGuard interfaces.
185185
Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
186186

187+
### `ignored_local_interfaces`
188+
- **Default:** *(empty)*
189+
- **Description:** A list of interface names to exclude when enumerating local interfaces.
190+
This is useful if you want to prevent certain interfaces from being imported from the local system.
191+
187192
### Mikrotik
188193

189194
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`:
225230
- **Default:** `5`
226231
- **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.
227232

233+
#### `ignored_interfaces`
234+
- **Default:** *(empty)*
235+
- **Description:** A list of interface names to exclude during interface enumeration.
236+
This is useful if you want to prevent specific interfaces from being imported from the MikroTik device.
237+
228238
#### `debug`
229239
- **Default:** `false`
230240
- **Description:** Enable verbose debug logging for the MikroTik backend.

internal/adapters/wgcontroller/mikrotik.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ package wgcontroller
33
import (
44
"context"
55
"fmt"
6+
"log/slog"
67
"slices"
78
"strconv"
89
"strings"
910
"sync"
1011
"time"
1112

12-
"log/slog"
13-
1413
"github.com/h44z/wg-portal/internal/config"
1514
"github.com/h44z/wg-portal/internal/domain"
1615
"github.com/h44z/wg-portal/internal/lowlevel"
@@ -678,11 +677,15 @@ func (c *MikrotikController) updatePeer(
678677
extras := pp.GetExtras().(domain.MikrotikPeerExtras)
679678
peerId := extras.Id
680679

681-
endpoint := pp.Endpoint
682-
endpointPort := "51820" // default port if not set
683-
if s := strings.Split(endpoint, ":"); len(s) == 2 {
684-
endpoint = s[0]
685-
endpointPort = s[1]
680+
endpoint := "" // by default, we have no endpoint (the peer does not initiate a connection)
681+
endpointPort := "0" // by default, we have no endpoint port (the peer does not initiate a connection)
682+
if !extras.IsResponder { // if the peer is not only a responder, it needs the endpoint to initiate a connection
683+
endpoint = pp.Endpoint
684+
endpointPort = "51820" // default port if not set
685+
if s := strings.Split(endpoint, ":"); len(s) == 2 {
686+
endpoint = s[0]
687+
endpointPort = s[1]
688+
}
686689
}
687690

688691
allowedAddressStr := domain.CidrsToString(pp.AllowedIPs)

internal/app/wireguard/controller_manager.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ func (c *ControllerManager) registerLocalController() error {
8282

8383
c.controllers[config.LocalBackendName] = backendInstance{
8484
Config: config.BackendBase{
85-
Id: config.LocalBackendName,
86-
DisplayName: "Local WireGuard Controller",
85+
Id: config.LocalBackendName,
86+
DisplayName: "Local WireGuard Controller",
87+
IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces,
8788
},
8889
Implementation: localController,
8990
}
@@ -118,17 +119,17 @@ func (c *ControllerManager) logRegisteredControllers() {
118119
}
119120

120121
func (c *ControllerManager) GetControllerByName(backend domain.InterfaceBackend) InterfaceController {
121-
return c.getController(backend, "")
122+
return c.getController(backend, "").Implementation
122123
}
123124

124125
func (c *ControllerManager) GetController(iface domain.Interface) InterfaceController {
125-
return c.getController(iface.Backend, iface.Identifier)
126+
return c.getController(iface.Backend, iface.Identifier).Implementation
126127
}
127128

128129
func (c *ControllerManager) getController(
129130
backend domain.InterfaceBackend,
130131
ifaceId domain.InterfaceIdentifier,
131-
) InterfaceController {
132+
) backendInstance {
132133
if backend == "" {
133134
// If no backend is specified, use the local controller.
134135
// This might be the case for interfaces created in previous WireGuard Portal versions.
@@ -145,13 +146,13 @@ func (c *ControllerManager) getController(
145146
slog.Warn("controller for backend not found, using local controller",
146147
"backend", backend, "interface", ifaceId)
147148
}
148-
return controller.Implementation
149+
return controller
149150
}
150151

151-
func (c *ControllerManager) GetAllControllers() []InterfaceController {
152-
var backendInstances = make([]InterfaceController, 0, len(c.controllers))
152+
func (c *ControllerManager) GetAllControllers() []backendInstance {
153+
var backendInstances = make([]backendInstance, 0, len(c.controllers))
153154
for instance := range maps.Values(c.controllers) {
154-
backendInstances = append(backendInstances, instance.Implementation)
155+
backendInstances = append(backendInstances, instance)
155156
}
156157
return backendInstances
157158
}

internal/app/wireguard/wireguard_interfaces.go

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,6 @@ import (
1515
"github.com/h44z/wg-portal/internal/domain"
1616
)
1717

18-
// GetImportableInterfaces returns all physical interfaces that are available on the system.
19-
// This function also returns interfaces that are already available in the database.
20-
func (m Manager) GetImportableInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
21-
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
22-
return nil, err
23-
}
24-
25-
var allPhysicalInterfaces []domain.PhysicalInterface
26-
for _, wgBackend := range m.wg.GetAllControllers() {
27-
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
28-
if err != nil {
29-
return nil, err
30-
}
31-
32-
allPhysicalInterfaces = append(allPhysicalInterfaces, physicalInterfaces...)
33-
}
34-
35-
return allPhysicalInterfaces, nil
36-
}
37-
3818
// GetInterfaceAndPeers returns the interface and all peers for the given interface identifier.
3919
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
4020
*domain.Interface,
@@ -110,52 +90,62 @@ func (m Manager) GetUserInterfaces(ctx context.Context, _ domain.UserIdentifier)
11090
}
11191

11292
// ImportNewInterfaces imports all new physical interfaces that are available on the system.
93+
// If a filter is set, only interfaces that match the filter will be imported.
11394
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
11495
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
11596
return 0, err
11697
}
11798

99+
var existingInterfaceIds []domain.InterfaceIdentifier
100+
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
101+
if err != nil {
102+
return 0, err
103+
}
104+
for _, existingInterface := range existingInterfaces {
105+
existingInterfaceIds = append(existingInterfaceIds, existingInterface.Identifier)
106+
}
107+
118108
imported := 0
119109
for _, wgBackend := range m.wg.GetAllControllers() {
120-
physicalInterfaces, err := wgBackend.GetInterfaces(ctx)
110+
physicalInterfaces, err := wgBackend.Implementation.GetInterfaces(ctx)
121111
if err != nil {
122112
return 0, err
123113
}
124114

125-
// if no filter is given, exclude already existing interfaces
126-
var excludedInterfaces []domain.InterfaceIdentifier
127-
if len(filter) == 0 {
128-
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
129-
if err != nil {
130-
return 0, err
131-
}
132-
for _, existingInterface := range existingInterfaces {
133-
excludedInterfaces = append(excludedInterfaces, existingInterface.Identifier)
115+
for _, physicalInterface := range physicalInterfaces {
116+
if slices.Contains(wgBackend.Config.IgnoredInterfaces, string(physicalInterface.Identifier)) {
117+
slog.Info("ignoring interface due to backend filter restrictions",
118+
"interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces,
119+
"backend", wgBackend.Config.Id)
120+
continue // skip ignored interfaces
134121
}
135-
}
136122

137-
for _, physicalInterface := range physicalInterfaces {
138-
if slices.Contains(excludedInterfaces, physicalInterface.Identifier) {
139-
continue
123+
if slices.Contains(existingInterfaceIds, physicalInterface.Identifier) {
124+
continue // skip interfaces that already exist
140125
}
141126

142-
if len(filter) != 0 && !slices.Contains(filter, physicalInterface.Identifier) {
127+
if len(filter) > 0 && !slices.Contains(filter, physicalInterface.Identifier) {
128+
slog.Info("ignoring interface due to filter restrictions",
129+
"interface", physicalInterface.Identifier, "filter", wgBackend.Config.IgnoredInterfaces,
130+
"backend", wgBackend.Config.Id)
143131
continue
144132
}
145133

146-
slog.Info("importing new interface", "interface", physicalInterface.Identifier)
134+
slog.Info("importing new interface",
135+
"interface", physicalInterface.Identifier, "backend", wgBackend.Config.Id)
147136

148-
physicalPeers, err := wgBackend.GetPeers(ctx, physicalInterface.Identifier)
137+
physicalPeers, err := wgBackend.Implementation.GetPeers(ctx, physicalInterface.Identifier)
149138
if err != nil {
150139
return 0, err
151140
}
152141

153-
err = m.importInterface(ctx, wgBackend, &physicalInterface, physicalPeers)
142+
err = m.importInterface(ctx, wgBackend.Implementation, &physicalInterface, physicalPeers)
154143
if err != nil {
155144
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
156145
}
157146

158-
slog.Info("imported new interface", "interface", physicalInterface.Identifier, "peers", len(physicalPeers))
147+
slog.Info("imported new interface",
148+
"interface", physicalInterface.Identifier, "peers", len(physicalPeers), "backend", wgBackend.Config.Id)
159149
imported++
160150
}
161151
}
@@ -221,9 +211,11 @@ func (m Manager) RestoreInterfaceState(
221211
return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err)
222212
}
223213

224-
_, err = m.wg.GetController(iface).GetInterface(ctx, iface.Identifier)
214+
controller := m.wg.GetController(iface)
215+
216+
_, err = controller.GetInterface(ctx, iface.Identifier)
225217
if err != nil && !iface.IsDisabled() {
226-
slog.Debug("creating missing interface", "interface", iface.Identifier)
218+
slog.Debug("creating missing interface", "interface", iface.Identifier, "backend", controller.GetId())
227219

228220
// temporarily disable interface in database so that the current state is reflected correctly
229221
_ = m.db.SaveInterface(ctx, iface.Identifier,
@@ -250,7 +242,8 @@ func (m Manager) RestoreInterfaceState(
250242
return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err)
251243
}
252244
} else {
253-
slog.Debug("restoring interface state", "interface", iface.Identifier, "disabled", iface.IsDisabled())
245+
slog.Debug("restoring interface state",
246+
"interface", iface.Identifier, "disabled", iface.IsDisabled(), "backend", controller.GetId())
254247

255248
// try to move interface to stored state
256249
_, err = m.saveInterface(ctx, &iface)
@@ -278,13 +271,13 @@ func (m Manager) RestoreInterfaceState(
278271
for _, peer := range peers {
279272
switch {
280273
case iface.IsDisabled() && iface.Backend == config.LocalBackendName: // if interface is disabled, delete all peers
281-
if err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
274+
if err := controller.DeletePeer(ctx, iface.Identifier,
282275
peer.Identifier); err != nil {
283276
return fmt.Errorf("failed to remove peer %s for disabled interface %s: %w",
284277
peer.Identifier, iface.Identifier, err)
285278
}
286279
default: // update peer
287-
err := m.wg.GetController(iface).SavePeer(ctx, iface.Identifier, peer.Identifier,
280+
err := controller.SavePeer(ctx, iface.Identifier, peer.Identifier,
288281
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
289282
domain.MergeToPhysicalPeer(pp, &peer)
290283
return pp, nil
@@ -297,7 +290,7 @@ func (m Manager) RestoreInterfaceState(
297290
}
298291

299292
// remove non-wgportal peers
300-
physicalPeers, _ := m.wg.GetController(iface).GetPeers(ctx, iface.Identifier)
293+
physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier)
301294
for _, physicalPeer := range physicalPeers {
302295
isWgPortalPeer := false
303296
for _, peer := range peers {
@@ -307,7 +300,7 @@ func (m Manager) RestoreInterfaceState(
307300
}
308301
}
309302
if !isWgPortalPeer {
310-
err := m.wg.GetController(iface).DeletePeer(ctx, iface.Identifier,
303+
err := controller.DeletePeer(ctx, iface.Identifier,
311304
domain.PeerIdentifier(physicalPeer.PublicKey))
312305
if err != nil {
313306
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) (
551544
return nil, fmt.Errorf("failed to save interface: %w", err)
552545
}
553546

547+
// update the interface type of peers in db
548+
peers, err := m.db.GetInterfacePeers(ctx, iface.Identifier)
549+
if err != nil {
550+
return nil, fmt.Errorf("failed to load peers for interface %s: %w", iface.Identifier, err)
551+
}
552+
for _, peer := range peers {
553+
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
554+
switch iface.Type {
555+
case domain.InterfaceTypeAny:
556+
peer.Interface.Type = domain.InterfaceTypeAny
557+
case domain.InterfaceTypeClient:
558+
peer.Interface.Type = domain.InterfaceTypeServer
559+
case domain.InterfaceTypeServer:
560+
peer.Interface.Type = domain.InterfaceTypeClient
561+
}
562+
563+
return &peer, nil
564+
})
565+
if err != nil {
566+
return nil, fmt.Errorf("failed to update peer %s for interface %s: %w", peer.Identifier,
567+
iface.Identifier, err)
568+
}
569+
}
570+
554571
if iface.IsDisabled() {
555572
physicalInterface, _ := m.wg.GetController(*iface).GetInterface(ctx, iface.Identifier)
556573
fwMark := iface.FirewallMark

internal/config/backend.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ const LocalBackendName = "local"
1010
type Backend struct {
1111
Default string `yaml:"default"` // The default backend to use (defaults to the internal backend)
1212

13+
// Local Backend-specific configuration
14+
15+
IgnoredLocalInterfaces []string `yaml:"ignored_local_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
16+
17+
// External Backend-specific configuration
18+
1319
Mikrotik []BackendMikrotik `yaml:"mikrotik"`
1420
}
1521

@@ -42,6 +48,8 @@ func (b *Backend) Validate() error {
4248
type BackendBase struct {
4349
Id string `yaml:"id"` // A unique id for the backend
4450
DisplayName string `yaml:"display_name"` // A display name for the backend
51+
52+
IgnoredInterfaces []string `yaml:"ignored_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
4553
}
4654

4755
// GetDisplayName returns the display name of the backend.

internal/domain/peer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
328328
Id: "",
329329
Name: p.DisplayName,
330330
Comment: p.Notes,
331-
IsResponder: false,
331+
IsResponder: p.Interface.Type == InterfaceTypeClient,
332332
Disabled: p.IsDisabled(),
333333
ClientEndpoint: p.Endpoint.GetValue(),
334334
ClientAddress: CidrsToString(p.Interface.Addresses),

0 commit comments

Comments
 (0)