diff --git a/frontend/src/components/InterfaceEditModal.vue b/frontend/src/components/InterfaceEditModal.vue index 97dcf7d..9707f95 100644 --- a/frontend/src/components/InterfaceEditModal.vue +++ b/frontend/src/components/InterfaceEditModal.vue @@ -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 diff --git a/internal/adapters/wgcontroller/local.go b/internal/adapters/wgcontroller/local.go index b47db59..b359ae6 100644 --- a/internal/adapters/wgcontroller/local.go +++ b/internal/adapters/wgcontroller/local.go @@ -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 { diff --git a/internal/adapters/wgcontroller/mikrotik.go b/internal/adapters/wgcontroller/mikrotik.go index 356ebf2..965053f 100644 --- a/internal/adapters/wgcontroller/mikrotik.go +++ b/internal/adapters/wgcontroller/mikrotik.go @@ -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 } diff --git a/internal/app/wireguard/statistics.go b/internal/app/wireguard/statistics.go index c9098a9..76455d6 100644 --- a/internal/app/wireguard/statistics.go +++ b/internal/app/wireguard/statistics.go @@ -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) diff --git a/internal/app/wireguard/wireguard_interfaces.go b/internal/app/wireguard/wireguard_interfaces.go index 5ae95ae..be9f41a 100644 --- a/internal/app/wireguard/wireguard_interfaces.go +++ b/internal/app/wireguard/wireguard_interfaces.go @@ -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] + ")" + 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) - } + 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) { diff --git a/internal/domain/controller.go b/internal/domain/controller.go new file mode 100644 index 0000000..26d8b07 --- /dev/null +++ b/internal/domain/controller.go @@ -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 +} diff --git a/internal/domain/interface.go b/internal/domain/interface.go index 05699ec..cb57c50 100644 --- a/internal/domain/interface.go +++ b/internal/domain/interface.go @@ -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 } diff --git a/internal/domain/peer.go b/internal/domain/peer.go index 6e550ad..fc082bd 100644 --- a/internal/domain/peer.go +++ b/internal/domain/peer.go @@ -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 } diff --git a/internal/lowlevel/mikrotik.go b/internal/lowlevel/mikrotik.go index 8224463..d7ca67a 100644 --- a/internal/lowlevel/mikrotik.go +++ b/internal/lowlevel/mikrotik.go @@ -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