Mikrotik integration (#467)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled

Allow MikroTik routes as WireGuard backends
This commit is contained in:
h44z
2025-08-10 14:42:02 +02:00
committed by GitHub
parent a86f83a219
commit 112f6bfb77
40 changed files with 3150 additions and 205 deletions

View File

@@ -0,0 +1,32 @@
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 {
Id string // internal mikrotik ID
Comment string
Disabled bool
}
type MikrotikPeerExtras struct {
Id string // internal mikrotik ID
Name string
Comment string
IsResponder bool
Disabled bool
ClientEndpoint string
ClientAddress string
ClientDns string
ClientKeepalive int
}
type LocalPeerExtras struct {
Disabled bool
}

View File

@@ -10,6 +10,8 @@ import (
"strings"
"time"
"golang.org/x/sys/unix"
"github.com/h44z/wg-portal/internal"
)
@@ -23,6 +25,7 @@ var allowedFileNameRegex = regexp.MustCompile("[^a-zA-Z0-9-_]+")
type InterfaceIdentifier string
type InterfaceType string
type InterfaceBackend string
type Interface struct {
BaseModel
@@ -49,11 +52,12 @@ type Interface struct {
SaveConfig bool // automatically persist config changes to the wgX.conf file
// WG Portal specific
DisplayName string // a nice display name/ description for the interface
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
DriverType string // the interface driver type (linux, software, ...)
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
DisabledReason string // the reason why the interface has been disabled
DisplayName string // a nice display name/ description for the interface
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
Backend InterfaceBackend // the backend that is used to manage the interface (wgctrl, mikrotik, ...)
DriverType string // the interface driver type (linux, software, ...)
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
DisabledReason string // the reason why the interface has been disabled
// Default settings for the peer, used for new peers, those settings will be published to ConfigOption options of
// the peer config
@@ -204,9 +208,31 @@ 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 {
networks := make([]Cidr, 0, len(pi.Addresses))
for _, addr := range pi.Addresses {
networks = append(networks, addr.NetworkAddr())
}
// create a new basic interface with the data from the physical interface
iface := &Interface{
Identifier: pi.Identifier,
KeyPair: pi.KeyPair,
@@ -226,11 +252,11 @@ func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
Type: InterfaceTypeAny,
DriverType: pi.DeviceType,
Disabled: nil,
PeerDefNetworkStr: "",
PeerDefNetworkStr: CidrsToString(networks),
PeerDefDnsStr: "",
PeerDefDnsSearchStr: "",
PeerDefEndpoint: "",
PeerDefAllowedIPsStr: "",
PeerDefAllowedIPsStr: CidrsToString(networks),
PeerDefMtu: pi.Mtu,
PeerDefPersistentKeepalive: 0,
PeerDefFirewallMark: 0,
@@ -241,6 +267,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
}
@@ -253,6 +296,15 @@ func MergeToPhysicalInterface(pi *PhysicalInterface, i *Interface) {
pi.FirewallMark = i.FirewallMark
pi.DeviceUp = !i.IsDisabled()
pi.Addresses = i.Addresses
switch pi.ImportSource {
case ControllerTypeMikrotik:
extras := MikrotikInterfaceExtras{
Comment: i.DisplayName,
Disabled: i.IsDisabled(),
}
pi.SetExtras(extras)
}
}
type RoutingTableInfo struct {
@@ -279,3 +331,30 @@ func (r RoutingTableInfo) GetRoutingTable() int {
return r.Table
}
type IpFamily int
const (
IpFamilyIPv4 IpFamily = unix.AF_INET
IpFamilyIPv6 IpFamily = unix.AF_INET6
)
func (f IpFamily) String() string {
switch f {
case IpFamilyIPv4:
return "IPv4"
case IpFamilyIPv6:
return "IPv6"
default:
return "unknown"
}
}
// RouteRule represents a routing table rule.
type RouteRule struct {
InterfaceId InterfaceIdentifier
IpFamily IpFamily
FwMark uint32
Table int
HasDefault bool
}

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,9 +182,12 @@ 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
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
}
@@ -196,7 +199,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
}
@@ -208,7 +211,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
}
@@ -217,7 +220,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()
@@ -226,6 +229,21 @@ 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
case LocalPeerExtras: // OK
default: // we only support MikrotikPeerExtras and LocalPeerExtras 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),
@@ -244,6 +262,44 @@ 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
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"
} else {
peer.Disabled = nil
peer.DisabledReason = ""
}
case ControllerTypeLocal:
extras := pp.GetExtras().(LocalPeerExtras)
if extras.Disabled {
peer.Disabled = &now
peer.DisabledReason = "Disabled by Local controller"
} else {
peer.Disabled = nil
peer.DisabledReason = ""
}
}
return peer
}
@@ -265,6 +321,27 @@ 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)
case ControllerTypeLocal:
extras := LocalPeerExtras{
Disabled: p.IsDisabled(),
}
pp.SetExtras(extras)
}
}
type PeerCreationRequest struct {

View File

@@ -1,6 +1,8 @@
package domain
import "time"
import (
"time"
)
type PeerStatus struct {
PeerId PeerIdentifier `gorm:"primaryKey;column:identifier" json:"PeerId"`
@@ -37,3 +39,25 @@ type InterfaceStatus struct {
BytesReceived uint64 `gorm:"column:received"`
BytesTransmitted uint64 `gorm:"column:transmitted"`
}
type PingerResult struct {
PacketsRecv int
PacketsSent int
Rtts []time.Duration
}
func (r PingerResult) IsPingable() bool {
return r.PacketsRecv > 0 && r.PacketsSent > 0 && len(r.Rtts) > 0
}
func (r PingerResult) AverageRtt() time.Duration {
if len(r.Rtts) == 0 {
return 0
}
var total time.Duration
for _, rtt := range r.Rtts {
total += rtt
}
return total / time.Duration(len(r.Rtts))
}