wip: create different backend handlers (#426)

This commit is contained in:
Christoph Haas 2025-05-31 22:15:09 +02:00
parent e934232e0b
commit ea6da4114f
No known key found for this signature in database
9 changed files with 153 additions and 44 deletions

View File

@ -80,6 +80,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Identifier = interfaces.Prepared.Identifier
formData.value.DisplayName = interfaces.Prepared.DisplayName
formData.value.Mode = interfaces.Prepared.Mode
formData.value.Backend = interfaces.Prepared.Backend
formData.value.PublicKey = interfaces.Prepared.PublicKey
formData.value.PrivateKey = interfaces.Prepared.PrivateKey
@ -118,6 +119,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Identifier = selectedInterface.value.Identifier
formData.value.DisplayName = selectedInterface.value.DisplayName
formData.value.Mode = selectedInterface.value.Mode
formData.value.Backend = selectedInterface.value.Backend
formData.value.PublicKey = selectedInterface.value.PublicKey
formData.value.PrivateKey = selectedInterface.value.PrivateKey

View File

@ -134,7 +134,7 @@ func (c LocalController) convertWireGuardInterface(device *wgtypes.Device) (doma
Mtu: 0,
FirewallMark: uint32(device.FirewallMark),
DeviceUp: false,
ImportSource: "wgctrl",
ImportSource: domain.ControllerTypeLocal,
DeviceType: device.Type.String(),
BytesUpload: 0,
BytesDownload: 0,
@ -199,6 +199,7 @@ func (c LocalController) convertWireGuardPeer(peer *wgtypes.Peer) (domain.Physic
ProtocolVersion: peer.ProtocolVersion,
BytesUpload: uint64(peer.ReceiveBytes),
BytesDownload: uint64(peer.TransmitBytes),
ImportSource: domain.ControllerTypeLocal,
}
for _, addr := range peer.AllowedIPs {

View File

@ -40,7 +40,7 @@ func (c MikrotikController) GetId() domain.InterfaceBackend {
func (c MikrotikController) GetInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
PropList: []string{
".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running",
".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running", "comment",
},
})
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
@ -167,12 +167,17 @@ func (c MikrotikController) convertWireGuardInterface(
Mtu: wg.GetInt("mtu"),
FirewallMark: 0,
DeviceUp: wg.GetBool("running"),
ImportSource: "mikrotik",
DeviceType: "Mikrotik",
ImportSource: domain.ControllerTypeMikrotik,
DeviceType: domain.ControllerTypeMikrotik,
BytesUpload: uint64(iface.GetInt("tx-byte")),
BytesDownload: uint64(iface.GetInt("rx-byte")),
}
pi.SetExtras(domain.MikrotikInterfaceExtras{
Comment: wg.GetString("comment"),
Disabled: wg.GetBool("disabled"),
})
return pi, nil
}
@ -210,7 +215,10 @@ func (c MikrotikController) GetPeers(ctx context.Context, deviceId domain.Interf
return peers, nil
}
func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject) (domain.PhysicalPeer, error) {
func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject) (
domain.PhysicalPeer,
error,
) {
keepAliveSeconds := 0
duration, err := time.ParseDuration(peer.GetString("client-keepalive"))
if err == nil {
@ -246,15 +254,17 @@ func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
ProtocolVersion: 0, // Mikrotik does not support protocol versioning, so we set it to 0
BytesUpload: uint64(peer.GetInt("rx")),
BytesDownload: uint64(peer.GetInt("tx")),
BackendExtras: make(map[string]interface{}),
ImportSource: domain.ControllerTypeMikrotik,
}
peerModel.BackendExtras["MT-NAME"] = peer.GetString("name")
peerModel.BackendExtras["MT-COMMENT"] = peer.GetString("comment")
peerModel.BackendExtras["MT-RESPONDER"] = peer.GetString("responder")
peerModel.BackendExtras["MT-ENDPOINT"] = peer.GetString("client-endpoint")
peerModel.BackendExtras["MT-IP"] = peer.GetString("client-address")
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"),
})
return peerModel, nil
}

View File

@ -335,6 +335,8 @@ func (c *StatisticsCollector) isPeerPingable(ctx context.Context, peer domain.Pe
return false
}
// TODO: implement ping check on Mikrotik (or any other controller)
pinger, err := probing.NewPinger(checkAddr)
if err != nil {
slog.Debug("failed to instantiate pinger", "peer", peer.Identifier, "address", checkAddr, "error", err)

View File

@ -837,28 +837,20 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
peer.Interface.PreDown = domain.NewConfigOption(in.PeerDefPreDown, true)
peer.Interface.PostDown = domain.NewConfigOption(in.PeerDefPostDown, true)
var displayName string
switch in.Type {
case domain.InterfaceTypeAny:
peer.Interface.Type = domain.InterfaceTypeAny
peer.DisplayName = "Autodetected Peer (" + peer.Interface.PublicKey[0:8] + ")"
displayName = "Autodetected Peer (" + peer.Interface.PublicKey[0:8] + ")"
case domain.InterfaceTypeClient:
peer.Interface.Type = domain.InterfaceTypeServer
peer.DisplayName = "Autodetected Endpoint (" + peer.Interface.PublicKey[0:8] + ")"
displayName = "Autodetected Endpoint (" + peer.Interface.PublicKey[0:8] + ")"
case domain.InterfaceTypeServer:
peer.Interface.Type = domain.InterfaceTypeClient
peer.DisplayName = "Autodetected Client (" + peer.Interface.PublicKey[0:8] + ")"
}
if p.BackendExtras != nil {
if val, ok := p.BackendExtras["MT-NAME"]; ok {
peer.DisplayName = val.(string)
}
if val, ok := p.BackendExtras["MT-COMMENT"]; ok {
peer.Notes = val.(string)
}
if val, ok := p.BackendExtras["MT-ENDPOINT"]; ok {
peer.Endpoint = domain.NewConfigOption(val.(string), true)
displayName = "Autodetected Client (" + peer.Interface.PublicKey[0:8] + ")"
}
if peer.DisplayName == "" {
peer.DisplayName = displayName // use auto-generated display name if not set
}
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {

View File

@ -0,0 +1,24 @@
package domain
// ControllerType defines the type of controller used to manage interfaces.
const (
ControllerTypeMikrotik = "mikrotik"
ControllerTypeLocal = "wgctrl"
)
// Controller extras can be used to store additional information available for specific controllers only.
type MikrotikInterfaceExtras struct {
Comment string
Disabled bool
}
type MikrotikPeerExtras struct {
Name string
Comment string
IsResponder bool
ClientEndpoint string
ClientAddress string
Disabled bool
}

View File

@ -208,9 +208,26 @@ type PhysicalInterface struct {
BytesUpload uint64
BytesDownload uint64
backendExtras any // additional backend-specific extras, e.g., domain.MikrotikInterfaceExtras
}
func (p *PhysicalInterface) GetExtras() any {
return p.backendExtras
}
func (p *PhysicalInterface) SetExtras(extras any) {
switch extras.(type) {
case MikrotikInterfaceExtras: // OK
default: // we only support MikrotikInterfaceExtras for now
panic(fmt.Sprintf("unsupported interface backend extras type %T", extras))
}
p.backendExtras = extras
}
func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
// create a new basic interface with the data from the physical interface
iface := &Interface{
Identifier: pi.Identifier,
KeyPair: pi.KeyPair,
@ -245,6 +262,23 @@ func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
PeerDefPostDown: "",
}
if pi.GetExtras() == nil {
return iface
}
// enrich the data with controller-specific extras
now := time.Now()
switch pi.ImportSource {
case ControllerTypeMikrotik:
extras := pi.GetExtras().(MikrotikInterfaceExtras)
iface.DisplayName = extras.Comment
if extras.Disabled {
iface.Disabled = &now
} else {
iface.Disabled = nil
}
}
return iface
}

View File

@ -129,7 +129,7 @@ func (p *Peer) GenerateDisplayName(prefix string) {
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
}
// OverwriteUserEditableFields overwrites the user editable fields of the peer with the values from the userPeer
// OverwriteUserEditableFields overwrites the user-editable fields of the peer with the values from the userPeer
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
p.DisplayName = userPeer.DisplayName
if cfg.Core.EditableKeys {
@ -182,10 +182,11 @@ type PhysicalPeer struct {
BytesUpload uint64 // upload bytes are the number of bytes that the remote peer has sent to the server
BytesDownload uint64 // upload bytes are the number of bytes that the remote peer has received from the server
BackendExtras map[string]any // additional backend specific extras, e.g. for the mikrotik backend this contains the name of the peer
ImportSource string // import source (wgctrl, file, ...)
backendExtras any // additional backend-specific extras, e.g., domain.MikrotikPeerExtras
}
func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
func (p *PhysicalPeer) GetPresharedKey() *wgtypes.Key {
if p.PresharedKey == "" {
return nil
}
@ -197,7 +198,7 @@ func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
return &key
}
func (p PhysicalPeer) GetEndpointAddress() *net.UDPAddr {
func (p *PhysicalPeer) GetEndpointAddress() *net.UDPAddr {
if p.Endpoint == "" {
return nil
}
@ -209,7 +210,7 @@ func (p PhysicalPeer) GetEndpointAddress() *net.UDPAddr {
return addr
}
func (p PhysicalPeer) GetPersistentKeepaliveTime() *time.Duration {
func (p *PhysicalPeer) GetPersistentKeepaliveTime() *time.Duration {
if p.PersistentKeepalive == 0 {
return nil
}
@ -218,7 +219,7 @@ func (p PhysicalPeer) GetPersistentKeepaliveTime() *time.Duration {
return &keepAliveDuration
}
func (p PhysicalPeer) GetAllowedIPs() []net.IPNet {
func (p *PhysicalPeer) GetAllowedIPs() []net.IPNet {
allowedIPs := make([]net.IPNet, len(p.AllowedIPs))
for i, ip := range p.AllowedIPs {
allowedIPs[i] = *ip.IpNet()
@ -227,6 +228,20 @@ func (p PhysicalPeer) GetAllowedIPs() []net.IPNet {
return allowedIPs
}
func (p *PhysicalPeer) GetExtras() any {
return p.backendExtras
}
func (p *PhysicalPeer) SetExtras(extras any) {
switch extras.(type) {
case MikrotikPeerExtras: // OK
default: // we only support MikrotikPeerExtras for now
panic(fmt.Sprintf("unsupported peer backend extras type %T", extras))
}
p.backendExtras = extras
}
func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
peer := &Peer{
Endpoint: NewConfigOption(pp.Endpoint, true),
@ -245,6 +260,27 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
},
}
if pp.GetExtras() == nil {
return peer
}
// enrich the data with controller-specific extras
now := time.Now()
switch pp.ImportSource {
case ControllerTypeMikrotik:
extras := pp.GetExtras().(MikrotikPeerExtras)
peer.Notes = extras.Comment
peer.DisplayName = extras.Name
peer.Endpoint = NewConfigOption(extras.ClientEndpoint, true)
if extras.Disabled {
peer.Disabled = &now
peer.DisabledReason = "Disabled by Mikrotik controller"
} else {
peer.Disabled = nil
peer.DisabledReason = ""
}
}
return peer
}

View File

@ -17,6 +17,20 @@ import (
"github.com/h44z/wg-portal/internal/config"
)
// region models
const (
MikrotikApiStatusOk = "success"
MikrotikApiStatusError = "error"
)
const (
MikrotikApiErrorCodeUnknown = iota + 600
MikrotikApiErrorCodeRequestPreparationFailed
MikrotikApiErrorCodeRequestFailed
MikrotikApiErrorCodeResponseDecodeFailed
)
type MikrotikApiResponse[T any] struct {
Status string
Code int
@ -113,6 +127,10 @@ func (o *MikrotikRequestOptions) GetPath(base string) string {
return path.String()
}
// region models
// region API-client
type MikrotikApiClient struct {
coreCfg *config.Config
cfg *config.BackendMikrotik
@ -192,18 +210,6 @@ func (m *MikrotikApiClient) prepareGetRequest(ctx context.Context, fullUrl strin
return req, nil
}
const (
MikrotikApiStatusOk = "success"
MikrotikApiStatusError = "error"
)
const (
MikrotikApiErrorCodeUnknown = iota + 600
MikrotikApiErrorCodeRequestPreparationFailed
MikrotikApiErrorCodeRequestFailed
MikrotikApiErrorCodeResponseDecodeFailed
)
func errToApiResponse[T any](code int, message string, err error) MikrotikApiResponse[T] {
return MikrotikApiResponse[T]{
Status: MikrotikApiStatusError,
@ -289,3 +295,5 @@ func (m *MikrotikApiClient) Get(
m.debugLog("retrieved API get result", "url", fullUrl, "duration", time.Since(start).String())
return response
}
// endregion API-client