Add read-only support and UI indicators for MikroTik dynamic peers (#733)
Docker / Build and Push (push) Canceled after 0s
github-pages / deploy (push) Canceled after 0s
Test / make test (push) Canceled after 0s
Docker / release (push) Canceled after 0s

* Add dynamic Mikrotik peer support & UI/i18n

Introduce handling for dynamically created Mikrotik peers: add Dynamic/IsDynamic fields to domain, peer and interface models and API models; include "dynamic" in Mikrotik queries; skip updates/deletes for dynamic peers; sync dynamic flag when restoring state. Prevent modifying/deleting dynamic peers in manager. UI: disable selection/edit for dynamic peers and show badge. Implement Mikrotik interface hook execution. Add i18n keys/translations for dynamic-peer messages across languages.

* Fix Mikrotik hooks implementation and UI visibility
This commit is contained in:
Ondřej Zmatlík
2026-08-27 09:47:23 +02:00
committed by GitHub
parent 4a9bcd1b82
commit 87aef1c8ff
25 changed files with 1121 additions and 990 deletions
@@ -119,6 +119,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
names = append(names, model.SettingsBackendNames{
Id: controller.Id,
Name: displayName,
Type: controller.Type,
})
}
+1
View File
@@ -20,4 +20,5 @@ type Settings struct {
type SettingsBackendNames struct {
Id string `json:"Id"`
Name string `json:"Name"`
Type string `json:"Type"`
}
@@ -17,6 +17,7 @@ type Interface struct {
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
IsDynamic bool `json:"IsDynamic"` // specifies if the interface is a dynamic object
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
CreateDefaultPeer bool `json:"CreateDefaultPeer"` // if true, default peers will be created for this interface
@@ -65,6 +66,7 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
PublicKey: src.PublicKey,
Disabled: src.IsDisabled(),
DisabledReason: src.DisabledReason,
IsDynamic: src.IsDynamic,
SaveConfig: src.SaveConfig,
CreateDefaultPeer: src.CreateDefaultPeer,
ListenPort: src.ListenPort,
@@ -153,6 +155,7 @@ func NewDomainInterface(src *Interface) *domain.Interface {
PreDown: src.PreDown,
PostDown: src.PostDown,
SaveConfig: src.SaveConfig,
IsDynamic: src.IsDynamic,
CreateDefaultPeer: src.CreateDefaultPeer,
DisplayName: src.DisplayName,
Type: domain.InterfaceType(src.Mode),
+3
View File
@@ -49,6 +49,7 @@ type Peer struct {
DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty"` // expiry dates for peers
Notes string `json:"Notes"` // a note field for peers
IsDynamic bool `json:"IsDynamic"` // specifies if the peer is dynamically created by mikrotik
Endpoint ConfigOption[string] `json:"Endpoint"` // the endpoint address
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"` // the endpoint public key
@@ -90,6 +91,7 @@ func NewPeer(src *domain.Peer) *Peer {
DisabledReason: src.DisabledReason,
ExpiresAt: ExpiryDate{src.ExpiresAt},
Notes: src.Notes,
IsDynamic: src.IsDynamic,
Endpoint: ConfigOptionFromDomain(src.Endpoint),
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
@@ -148,6 +150,7 @@ func NewDomainPeer(src *Peer) *domain.Peer {
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
Disabled: nil, // set below
DisabledReason: src.DisabledReason,
IsDynamic: src.IsDynamic,
ExpiresAt: src.ExpiresAt.Time,
Notes: src.Notes,
Interface: domain.PeerInterfaceConfig{
@@ -63,6 +63,7 @@ func (c *ControllerManager) registerLocalController() error {
Config: config.BackendBase{
Id: config.LocalBackendName,
DisplayName: "Local WireGuard Controller",
Type: "local",
IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces,
},
Implementation: localController,
@@ -82,6 +83,7 @@ func (c *ControllerManager) registerMikrotikControllers() error {
return fmt.Errorf("failed to create Mikrotik controller for backend %s: %w", backendConfig.Id, err)
}
backendConfig.BackendBase.Type = "mikrotik"
c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{
Config: backendConfig.BackendBase,
Implementation: controller,
@@ -102,6 +104,7 @@ func (c *ControllerManager) registerPfsenseControllers() error {
return fmt.Errorf("failed to create pfSense controller for backend %s: %w", backendConfig.Id, err)
}
backendConfig.BackendBase.Type = "pfsense"
c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{
Config: backendConfig.BackendBase,
Implementation: controller,
+12 -2
View File
@@ -307,9 +307,19 @@ func (m Manager) RestoreInterfaceState(
physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier)
for _, physicalPeer := range physicalPeers {
isWgPortalPeer := false
for _, peer := range peers {
if peer.Identifier == domain.PeerIdentifier(physicalPeer.PublicKey) {
for i := range peers {
if peers[i].Identifier == domain.PeerIdentifier(physicalPeer.PublicKey) {
isWgPortalPeer = true
// Sync IsDynamic flag if it was changed on the backend
physConverted := domain.ConvertPhysicalPeer(&physicalPeer)
if peers[i].IsDynamic != physConverted.IsDynamic {
peers[i].IsDynamic = physConverted.IsDynamic
_ = m.db.SavePeer(ctx, peers[i].Identifier, func(p *domain.Peer) (*domain.Peer, error) {
p.IsDynamic = physConverted.IsDynamic
return p, nil
})
}
break
}
}
+10 -2
View File
@@ -620,7 +620,11 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
return
}
func (m Manager) validatePeerModifications(ctx context.Context, _, _ *domain.Peer) error {
func (m Manager) validatePeerModifications(ctx context.Context, old, _ *domain.Peer) error {
if old != nil && old.IsDynamic {
return fmt.Errorf("cannot modify dynamic peer: %w", domain.ErrInvalidData)
}
currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
@@ -649,7 +653,11 @@ func (m Manager) validatePeerCreation(ctx context.Context, _, new *domain.Peer)
return nil
}
func (m Manager) validatePeerDeletion(ctx context.Context, _ *domain.Peer) error {
func (m Manager) validatePeerDeletion(ctx context.Context, peer *domain.Peer) error {
if peer != nil && peer.IsDynamic {
return fmt.Errorf("cannot delete dynamic peer: %w", domain.ErrInvalidData)
}
currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {