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"
"slices"
"strconv"
"strings"
"time"
"github.com/h44z/wg-portal/internal/config"
@ -210,7 +211,7 @@ func (c MikrotikController) GetPeers(ctx context.Context, deviceId domain.Interf
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",
"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder", "client-dns",
},
Filters: map[string]string{
"interface": string(deviceId),
@ -241,7 +242,7 @@ func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
error,
) {
keepAliveSeconds := 0
duration, err := time.ParseDuration(peer.GetString("client-keepalive"))
duration, err := time.ParseDuration(peer.GetString("persistent-keepalive"))
if err == nil {
keepAliveSeconds = int(duration.Seconds())
}
@ -261,6 +262,12 @@ func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
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{
Identifier: domain.PeerIdentifier(peer.GetString("public-key")),
Endpoint: currentEndpoint,
@ -279,12 +286,15 @@ func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
}
peerModel.SetExtras(domain.MikrotikPeerExtras{
Name: peer.GetString("name"),
Comment: peer.GetString("comment"),
IsResponder: peer.GetBool("responder"),
ClientEndpoint: peer.GetString("client-endpoint"),
ClientAddress: peer.GetString("client-address"),
Disabled: peer.GetBool("disabled"),
Id: peer.GetString(".id"),
Name: peer.GetString("name"),
Comment: peer.GetString("comment"),
IsResponder: peer.GetBool("responder"),
Disabled: peer.GetBool("disabled"),
ClientEndpoint: peer.GetString("client-endpoint"),
ClientAddress: peer.GetString("client-address"),
ClientDns: peer.GetString("client-dns"),
ClientKeepalive: clientKeepAliveSeconds,
})
return peerModel, nil
@ -477,9 +487,12 @@ func (c MikrotikController) DeleteInterface(ctx context.Context, id domain.Inter
"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)
}
if len(wgReply.Data) == 0 {
return nil // interface does not exist, nothing to delete
}
deviceId := wgReply.Data[0].GetString(".id")
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(
_ context.Context,
ctx context.Context,
deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier,
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, 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
}
func (c MikrotikController) DeletePeer(
_ context.Context,
ctx context.Context,
deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier,
) 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
}

View File

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

View File

@ -787,6 +787,17 @@ func (m Manager) importInterface(
iface.Backend = backend.GetId()
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)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
return err

View File

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

View File

@ -271,7 +271,15 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
extras := pp.GetExtras().(MikrotikPeerExtras)
peer.Notes = extras.Comment
peer.DisplayName = extras.Name
peer.Endpoint = NewConfigOption(extras.ClientEndpoint, true)
if extras.ClientEndpoint != "" { // if the client endpoint is set, we assume that this is a client peer
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 {
peer.Disabled = &now
peer.DisabledReason = "Disabled by Mikrotik controller"
@ -302,6 +310,22 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
pp.PresharedKey = p.PresharedKey
pp.PublicKey = p.Interface.PublicKey
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 {

View File

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