mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-15 07:11:15 +00:00
Mikrotik integration (#467)
Allow MikroTik routes as WireGuard backends
This commit is contained in:
32
internal/domain/controller.go
Normal file
32
internal/domain/controller.go
Normal 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
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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))
|
||||
}
|
||||
|
Reference in New Issue
Block a user