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
+63 -10
View File
@@ -296,7 +296,7 @@ func (c *MikrotikController) GetPeers(ctx context.Context, deviceId domain.Inter
PropList: []string{
".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment",
"current-endpoint-address", "current-endpoint-port", "last-handshake", "persistent-keepalive",
"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder", "client-dns",
"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder", "client-dns", "dynamic",
},
Filters: map[string]string{
"interface": string(deviceId),
@@ -376,6 +376,7 @@ func (c *MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObjec
Comment: peer.GetString("comment"),
IsResponder: peer.GetBool("responder"),
Disabled: peer.GetBool("disabled"),
Dynamic: peer.GetBool("dynamic"),
ClientEndpoint: peer.GetString("client-endpoint"),
ClientAddress: peer.GetString("client-address"),
ClientDns: peer.GetString("client-dns"),
@@ -614,15 +615,24 @@ func (c *MikrotikController) SavePeer(
return err
}
peerId := physicalPeer.GetExtras().(domain.MikrotikPeerExtras).Id
oldExtras := physicalPeer.GetExtras().(domain.MikrotikPeerExtras)
peerId := oldExtras.Id
dynamic := oldExtras.Dynamic
physicalPeer, err = updateFunc(physicalPeer)
if err != nil {
return err
}
newExtras := physicalPeer.GetExtras().(domain.MikrotikPeerExtras)
newExtras.Id = peerId // ensure the ID is not changed
newExtras.Id = peerId // ensure the ID is not changed
newExtras.Dynamic = dynamic // ensure the dynamic flag is preserved
physicalPeer.SetExtras(newExtras)
if newExtras.Dynamic {
slog.Debug("skipping update for dynamic Mikrotik peer", "peer", id, "interface", deviceId)
return nil
}
if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
return err
}
@@ -638,7 +648,7 @@ func (c *MikrotikController) getOrCreatePeer(
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
PropList: []string{
".id", "name", "public-key", "private-key", "preshared-key", "persistent-keepalive", "client-address",
"client-endpoint", "client-keepalive", "allowed-address", "client-dns", "comment", "disabled", "responder",
"client-endpoint", "client-keepalive", "allowed-address", "client-dns", "comment", "disabled", "responder", "dynamic",
},
Filters: map[string]string{
"public-key": string(id),
@@ -742,7 +752,7 @@ func (c *MikrotikController) DeletePeer(
defer mutex.Unlock()
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
PropList: []string{".id"},
PropList: []string{".id", "dynamic"},
Filters: map[string]string{
"public-key": string(id),
"interface": string(deviceId),
@@ -754,6 +764,10 @@ func (c *MikrotikController) DeletePeer(
if len(wgReply.Data) == 0 {
return nil // peer does not exist, nothing to delete
}
if wgReply.Data[0].GetBool("dynamic") {
slog.Debug("skipping deletion of dynamic Mikrotik peer", "peer", id, "interface", deviceId)
return nil
}
peerId := wgReply.Data[0].GetString(".id")
deleteReply := c.client.Delete(ctx, "/interface/wireguard/peers/"+peerId)
@@ -769,12 +783,51 @@ func (c *MikrotikController) DeletePeer(
// region wg-quick-related
func (c *MikrotikController) ExecuteInterfaceHook(
_ context.Context,
_ domain.InterfaceIdentifier,
_ string,
ctx context.Context,
id domain.InterfaceIdentifier,
hookCmd string,
) error {
// TODO implement me
slog.Error("interface hooks are not yet supported for Mikrotik backends, please open an issue on GitHub")
if hookCmd == "" {
return nil
}
scriptName := fmt.Sprintf("wg-portal-hook-%s-%d", id, time.Now().UnixNano())
// Replace %i with the interface ID to mimic wg-quick behavior
scriptSource := strings.ReplaceAll(hookCmd, "%i", string(id))
// Inject the interface ID as a local variable for convenience in RouterOS scripts
scriptSource = fmt.Sprintf(":local WGInterface \"%s\";\n%s", id, scriptSource)
slog.Debug("executing Mikrotik script for interface hook", "interface", id, "script", scriptSource)
createReply := c.client.Create(ctx, "/system/script", lowlevel.GenericJsonObject{
"name": scriptName,
"source": scriptSource,
"policy": "ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon",
})
if createReply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("failed to create temporary script for hook: %v", createReply.Error)
}
scriptId := createReply.Data.GetString(".id")
if scriptId == "" {
scriptId = scriptName
}
defer func() {
cleanupReply := c.client.Delete(ctx, "/system/script/"+scriptId)
if cleanupReply.Status != lowlevel.MikrotikApiStatusOk {
slog.Warn("failed to remove temporary Mikrotik script", "scriptId", scriptId, "error", cleanupReply.Error)
}
}()
runReply := c.client.ExecList(ctx, "/system/script/run", lowlevel.GenericJsonObject{
"number": scriptId,
})
if runReply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("failed to execute hook script %s: %v", scriptName, runReply.Error)
}
return nil
}