wip: basic CRUD for peer (#426)

This commit is contained in:
Christoph Haas 2025-06-06 22:21:47 +02:00
parent 0724505ea1
commit f086ba2605
No known key found for this signature in database
6 changed files with 188 additions and 22 deletions

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"slices" "slices"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/config"
@ -210,7 +211,7 @@ func (c MikrotikController) GetPeers(ctx context.Context, deviceId domain.Interf
PropList: []string{ PropList: []string{
".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment", ".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment",
"current-endpoint-address", "current-endpoint-port", "last-handshake", "persistent-keepalive", "current-endpoint-address", "current-endpoint-port", "last-handshake", "persistent-keepalive",
"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder", "public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder", "client-dns",
}, },
Filters: map[string]string{ Filters: map[string]string{
"interface": string(deviceId), "interface": string(deviceId),
@ -241,7 +242,7 @@ func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
error, error,
) { ) {
keepAliveSeconds := 0 keepAliveSeconds := 0
duration, err := time.ParseDuration(peer.GetString("client-keepalive")) duration, err := time.ParseDuration(peer.GetString("persistent-keepalive"))
if err == nil { if err == nil {
keepAliveSeconds = int(duration.Seconds()) keepAliveSeconds = int(duration.Seconds())
} }
@ -261,6 +262,12 @@ func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
allowedAddresses, _ := domain.CidrsFromString(peer.GetString("allowed-address")) allowedAddresses, _ := domain.CidrsFromString(peer.GetString("allowed-address"))
clientKeepAliveSeconds := 0
duration, err = time.ParseDuration(peer.GetString("client-keepalive"))
if err == nil {
clientKeepAliveSeconds = int(duration.Seconds())
}
peerModel := domain.PhysicalPeer{ peerModel := domain.PhysicalPeer{
Identifier: domain.PeerIdentifier(peer.GetString("public-key")), Identifier: domain.PeerIdentifier(peer.GetString("public-key")),
Endpoint: currentEndpoint, Endpoint: currentEndpoint,
@ -279,12 +286,15 @@ func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
} }
peerModel.SetExtras(domain.MikrotikPeerExtras{ peerModel.SetExtras(domain.MikrotikPeerExtras{
Id: peer.GetString(".id"),
Name: peer.GetString("name"), Name: peer.GetString("name"),
Comment: peer.GetString("comment"), Comment: peer.GetString("comment"),
IsResponder: peer.GetBool("responder"), IsResponder: peer.GetBool("responder"),
Disabled: peer.GetBool("disabled"),
ClientEndpoint: peer.GetString("client-endpoint"), ClientEndpoint: peer.GetString("client-endpoint"),
ClientAddress: peer.GetString("client-address"), ClientAddress: peer.GetString("client-address"),
Disabled: peer.GetBool("disabled"), ClientDns: peer.GetString("client-dns"),
ClientKeepalive: clientKeepAliveSeconds,
}) })
return peerModel, nil return peerModel, nil
@ -477,9 +487,12 @@ func (c MikrotikController) DeleteInterface(ctx context.Context, id domain.Inter
"name": string(id), "name": string(id),
}, },
}) })
if wgReply.Status != lowlevel.MikrotikApiStatusOk || len(wgReply.Data) == 0 { if wgReply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("unable to find WireGuard interface %s: %v", id, wgReply.Error) return fmt.Errorf("unable to find WireGuard interface %s: %v", id, wgReply.Error)
} }
if len(wgReply.Data) == 0 {
return nil // interface does not exist, nothing to delete
}
deviceId := wgReply.Data[0].GetString(".id") deviceId := wgReply.Data[0].GetString(".id")
deleteReply := c.client.Delete(ctx, "/interface/wireguard/"+deviceId) deleteReply := c.client.Delete(ctx, "/interface/wireguard/"+deviceId)
@ -491,21 +504,136 @@ func (c MikrotikController) DeleteInterface(ctx context.Context, id domain.Inter
} }
func (c MikrotikController) SavePeer( func (c MikrotikController) SavePeer(
_ context.Context, ctx context.Context,
deviceId domain.InterfaceIdentifier, deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier, id domain.PeerIdentifier,
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error), updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
) error { ) error {
// TODO implement me physicalPeer, err := c.getOrCreatePeer(ctx, deviceId, id)
if err != nil {
return err
}
peerId := physicalPeer.GetExtras().(domain.MikrotikPeerExtras).Id
physicalPeer, err = updateFunc(physicalPeer)
if err != nil {
return err
}
newExtras := physicalPeer.GetExtras().(domain.MikrotikPeerExtras)
newExtras.Id = peerId // ensure the ID is not changed
physicalPeer.SetExtras(newExtras)
if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
return err
}
return nil
}
func (c MikrotikController) getOrCreatePeer(
ctx context.Context,
deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier,
) (*domain.PhysicalPeer, error) {
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",
},
Filters: map[string]string{
"public-key": string(id),
"interface": string(deviceId),
},
})
if wgReply.Status == lowlevel.MikrotikApiStatusOk && len(wgReply.Data) > 0 {
existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0])
if err != nil {
return nil, err
}
return &existingPeer, nil
}
// create a new peer if it does not exist
createReply := c.client.Create(ctx, "/interface/wireguard/peers", lowlevel.GenericJsonObject{
"name": fmt.Sprintf("tmp-wg-%s", id[0:8]),
"interface": string(deviceId),
"public-key": string(id), // public key will be set later
"allowed-address": "169.254.254.254/32", // allowed addresses will be set later
})
if createReply.Status == lowlevel.MikrotikApiStatusOk {
newPeer, err := c.convertWireGuardPeer(createReply.Data)
if err != nil {
return nil, err
}
return &newPeer, nil
}
return nil, fmt.Errorf("failed to create peer %s for interface %s: %v", id, deviceId, createReply.Error)
}
func (c MikrotikController) updatePeer(
ctx context.Context,
deviceId domain.InterfaceIdentifier,
pp *domain.PhysicalPeer,
) error {
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]
}
wgReply := c.client.Update(ctx, "/interface/wireguard/peers/"+peerId, lowlevel.GenericJsonObject{
"name": extras.Name,
"comment": extras.Comment,
"preshared-key": pp.PresharedKey,
"public-key": pp.KeyPair.PublicKey,
"private-key": pp.KeyPair.PrivateKey,
"persistent-keepalive": (time.Duration(pp.PersistentKeepalive) * time.Second).String(),
"disabled": strconv.FormatBool(extras.Disabled),
"responder": strconv.FormatBool(extras.IsResponder),
"client-endpoint": extras.ClientEndpoint,
"client-address": extras.ClientAddress,
"client-keepalive": (time.Duration(extras.ClientKeepalive) * time.Second).String(),
"client-dns": extras.ClientDns,
"endpoint-address": endpoint,
"endpoint-port": endpointPort,
})
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("failed to update peer %s on interface %s: %v", pp.Identifier, deviceId, wgReply.Error)
}
return nil return nil
} }
func (c MikrotikController) DeletePeer( func (c MikrotikController) DeletePeer(
_ context.Context, ctx context.Context,
deviceId domain.InterfaceIdentifier, deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier, id domain.PeerIdentifier,
) error { ) error {
// TODO implement me wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
PropList: []string{".id"},
Filters: map[string]string{
"public-key": string(id),
"interface": string(deviceId),
},
})
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("unable to find WireGuard peer %s for interface %s: %v", id, deviceId, wgReply.Error)
}
if len(wgReply.Data) == 0 {
return nil // peer does not exist, nothing to delete
}
peerId := wgReply.Data[0].GetString(".id")
deleteReply := c.client.Delete(ctx, "/interface/wireguard/peers/"+peerId)
if deleteReply.Status != lowlevel.MikrotikApiStatusOk {
return fmt.Errorf("failed to delete WireGuard peer %s for interface %s: %v", id, deviceId, deleteReply.Error)
}
return nil return nil
} }

View File

@ -46,7 +46,7 @@ func Initialize(
users: users, users: users,
} }
startupContext, cancel := context.WithTimeout(context.Background(), 30*time.Second) startupContext, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel() defer cancel()
// Switch to admin user context // Switch to admin user context

View File

@ -787,6 +787,17 @@ func (m Manager) importInterface(
iface.Backend = backend.GetId() iface.Backend = backend.GetId()
iface.PeerDefAllowedIPsStr = iface.AddressStr() iface.PeerDefAllowedIPsStr = iface.AddressStr()
// try to predict the interface type based on the number of peers
switch len(peers) {
case 0:
iface.Type = domain.InterfaceTypeAny // no peers means this is an unknown interface
case 1:
iface.Type = domain.InterfaceTypeClient // one peer means this is a client interface
default: // multiple peers means this is a server interface
iface.Type = domain.InterfaceTypeServer
}
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier) existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
if err != nil && !errors.Is(err, domain.ErrNotFound) { if err != nil && !errors.Is(err, domain.ErrNotFound) {
return err return err

View File

@ -16,10 +16,13 @@ type MikrotikInterfaceExtras struct {
} }
type MikrotikPeerExtras struct { type MikrotikPeerExtras struct {
Id string // internal mikrotik ID
Name string Name string
Comment string Comment string
IsResponder bool IsResponder bool
Disabled bool
ClientEndpoint string ClientEndpoint string
ClientAddress string ClientAddress string
Disabled bool ClientDns string
ClientKeepalive int
} }

View File

@ -271,7 +271,15 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
extras := pp.GetExtras().(MikrotikPeerExtras) extras := pp.GetExtras().(MikrotikPeerExtras)
peer.Notes = extras.Comment peer.Notes = extras.Comment
peer.DisplayName = extras.Name peer.DisplayName = extras.Name
if extras.ClientEndpoint != "" { // if the client endpoint is set, we assume that this is a client peer
peer.Endpoint = NewConfigOption(extras.ClientEndpoint, true) peer.Endpoint = NewConfigOption(extras.ClientEndpoint, true)
peer.Interface.Type = InterfaceTypeClient
peer.Interface.Addresses, _ = CidrsFromString(extras.ClientAddress)
peer.Interface.DnsStr = NewConfigOption(extras.ClientDns, true)
peer.PersistentKeepalive = NewConfigOption(extras.ClientKeepalive, true)
} else {
peer.Interface.Type = InterfaceTypeServer
}
if extras.Disabled { if extras.Disabled {
peer.Disabled = &now peer.Disabled = &now
peer.DisabledReason = "Disabled by Mikrotik controller" peer.DisabledReason = "Disabled by Mikrotik controller"
@ -302,6 +310,22 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
pp.PresharedKey = p.PresharedKey pp.PresharedKey = p.PresharedKey
pp.PublicKey = p.Interface.PublicKey pp.PublicKey = p.Interface.PublicKey
pp.PersistentKeepalive = p.PersistentKeepalive.GetValue() pp.PersistentKeepalive = p.PersistentKeepalive.GetValue()
switch pp.ImportSource {
case ControllerTypeMikrotik:
extras := MikrotikPeerExtras{
Id: "",
Name: p.DisplayName,
Comment: p.Notes,
IsResponder: false,
Disabled: p.IsDisabled(),
ClientEndpoint: p.Endpoint.GetValue(),
ClientAddress: CidrsToString(p.Interface.Addresses),
ClientDns: p.Interface.DnsStr.GetValue(),
ClientKeepalive: p.PersistentKeepalive.GetValue(),
}
pp.SetExtras(extras)
}
} }
type PeerCreationRequest struct { type PeerCreationRequest struct {

View File

@ -42,7 +42,7 @@ type MikrotikApiResponse[T any] struct {
type MikrotikApiError struct { type MikrotikApiError struct {
Code int `json:"error,omitempty"` Code int `json:"error,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"detail,omitempty"`
} }
func (e *MikrotikApiError) String() string { func (e *MikrotikApiError) String() string {