mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-13 14:31:15 +00:00
V2 alpha - initial version (#172)
Initial alpha codebase for version 2 of WireGuard Portal. This version is considered unstable and incomplete (for example, no public REST API)! Use with care! Fixes/Implements the following issues: - OAuth support #154, #1 - New Web UI with internationalisation support #98, #107, #89, #62 - Postgres Support #49 - Improved Email handling #47, #119 - DNS Search Domain support #46 - Bugfixes #94, #48 --------- Co-authored-by: Fabian Wechselberger <wechselbergerf@hotmail.com>
This commit is contained in:
20
internal/domain/audit.go
Normal file
20
internal/domain/audit.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type AuditSeverityLevel string
|
||||
|
||||
const AuditSeverityLevelLow AuditSeverityLevel = "low"
|
||||
const AuditSeverityLevelMedium AuditSeverityLevel = "medium"
|
||||
const AuditSeverityLevelHigh AuditSeverityLevel = "high"
|
||||
|
||||
type AuditEntry struct {
|
||||
UniqueId uint64 `gorm:"primaryKey;autoIncrement:true;column:id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;index:idx_au_created"`
|
||||
|
||||
Severity AuditSeverityLevel `gorm:"column:severity;index:idx_au_severity"`
|
||||
|
||||
Origin string `gorm:"column:origin"` // origin: for example user auth, stats, ...
|
||||
|
||||
Message string `gorm:"column:message"`
|
||||
}
|
51
internal/domain/auth.go
Normal file
51
internal/domain/auth.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type LoginProvider string
|
||||
|
||||
type LoginProviderInfo struct {
|
||||
Identifier string
|
||||
Name string
|
||||
ProviderUrl string
|
||||
CallbackUrl string
|
||||
}
|
||||
|
||||
type AuthenticatorUserInfo struct {
|
||||
Identifier UserIdentifier
|
||||
Email string
|
||||
Firstname string
|
||||
Lastname string
|
||||
Phone string
|
||||
Department string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
type AuthenticatorType string
|
||||
|
||||
const (
|
||||
AuthenticatorTypeOAuth AuthenticatorType = "oauth"
|
||||
AuthenticatorTypeOidc AuthenticatorType = "oidc"
|
||||
)
|
||||
|
||||
type OauthAuthenticator interface {
|
||||
GetName() string
|
||||
GetType() AuthenticatorType
|
||||
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
|
||||
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
|
||||
GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]interface{}, error)
|
||||
ParseUserInfo(raw map[string]interface{}) (*AuthenticatorUserInfo, error)
|
||||
RegistrationEnabled() bool
|
||||
}
|
||||
|
||||
type LdapAuthenticator interface {
|
||||
GetName() string
|
||||
PlaintextAuthentication(userId UserIdentifier, plainPassword string) error
|
||||
GetUserInfo(ctx context.Context, username UserIdentifier) (map[string]interface{}, error)
|
||||
ParseUserInfo(raw map[string]interface{}) (*AuthenticatorUserInfo, error)
|
||||
RegistrationEnabled() bool
|
||||
}
|
37
internal/domain/base.go
Normal file
37
internal/domain/base.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseModel struct {
|
||||
CreatedBy string
|
||||
UpdatedBy string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type PrivateString string
|
||||
|
||||
func (PrivateString) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`""`), nil
|
||||
}
|
||||
|
||||
func (PrivateString) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
const (
|
||||
DisabledReasonExpired = "expired"
|
||||
DisabledReasonDeleted = "deleted"
|
||||
DisabledReasonUserEdit = "user edit action"
|
||||
DisabledReasonUserCreate = "user create action"
|
||||
DisabledReasonAdminEdit = "admin edit action"
|
||||
DisabledReasonAdminCreate = "admin create action"
|
||||
DisabledReasonApiEdit = "api edit action"
|
||||
DisabledReasonApiCreate = "api create action"
|
||||
DisabledReasonLdapMissing = "missing in ldap"
|
||||
DisabledReasonUserMissing = "missing user"
|
||||
DisabledReasonMigrationDummy = "migration dummy user"
|
||||
DisabledReasonInterfaceMissing = "missing WireGuard interface"
|
||||
)
|
74
internal/domain/context.go
Normal file
74
internal/domain/context.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const CtxUserInfo = "userInfo"
|
||||
|
||||
const (
|
||||
CtxSystemAdminId = "_WG_SYS_ADMIN_"
|
||||
CtxUnknownUserId = "_WG_SYS_UNKNOWN_"
|
||||
)
|
||||
|
||||
type ContextUserInfo struct {
|
||||
Id UserIdentifier
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
func (u *ContextUserInfo) String() string {
|
||||
return fmt.Sprintf("%s|%t", u.Id, u.IsAdmin)
|
||||
}
|
||||
|
||||
func (u *ContextUserInfo) UserId() string {
|
||||
return string(u.Id)
|
||||
}
|
||||
|
||||
func DefaultContextUserInfo() *ContextUserInfo {
|
||||
return &ContextUserInfo{
|
||||
Id: CtxUnknownUserId,
|
||||
IsAdmin: false,
|
||||
}
|
||||
}
|
||||
|
||||
func SystemAdminContextUserInfo() *ContextUserInfo {
|
||||
return &ContextUserInfo{
|
||||
Id: CtxSystemAdminId,
|
||||
IsAdmin: true,
|
||||
}
|
||||
}
|
||||
|
||||
func SetUserInfoFromGin(c *gin.Context) context.Context {
|
||||
ginUserInfo, exists := c.Get(CtxUserInfo)
|
||||
|
||||
info := DefaultContextUserInfo()
|
||||
if exists {
|
||||
if ginInfo, ok := ginUserInfo.(*ContextUserInfo); ok {
|
||||
info = ginInfo
|
||||
}
|
||||
}
|
||||
|
||||
ctx := SetUserInfo(c.Request.Context(), info)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
|
||||
ctx = context.WithValue(ctx, CtxUserInfo, info)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func GetUserInfo(ctx context.Context) *ContextUserInfo {
|
||||
rawInfo := ctx.Value(CtxUserInfo)
|
||||
if rawInfo == nil {
|
||||
return DefaultContextUserInfo()
|
||||
}
|
||||
|
||||
if info, ok := rawInfo.(*ContextUserInfo); ok {
|
||||
return info
|
||||
}
|
||||
|
||||
return DefaultContextUserInfo()
|
||||
}
|
67
internal/domain/crypto.go
Normal file
67
internal/domain/crypto.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
PrivateKey string
|
||||
PublicKey string
|
||||
}
|
||||
|
||||
func (p KeyPair) GetPrivateKeyBytes() []byte {
|
||||
data, _ := base64.StdEncoding.DecodeString(p.PrivateKey)
|
||||
return data
|
||||
}
|
||||
|
||||
func (p KeyPair) GetPublicKeyBytes() []byte {
|
||||
data, _ := base64.StdEncoding.DecodeString(p.PublicKey)
|
||||
return data
|
||||
}
|
||||
|
||||
func (p KeyPair) GetPrivateKey() wgtypes.Key {
|
||||
key, _ := wgtypes.ParseKey(p.PrivateKey)
|
||||
return key
|
||||
}
|
||||
|
||||
func (p KeyPair) GetPublicKey() wgtypes.Key {
|
||||
key, _ := wgtypes.ParseKey(p.PublicKey)
|
||||
return key
|
||||
}
|
||||
|
||||
type PreSharedKey string
|
||||
|
||||
func NewFreshKeypair() (KeyPair, error) {
|
||||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return KeyPair{}, err
|
||||
}
|
||||
|
||||
return KeyPair{
|
||||
PrivateKey: privateKey.String(),
|
||||
PublicKey: privateKey.PublicKey().String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewPreSharedKey() (PreSharedKey, error) {
|
||||
preSharedKey, err := wgtypes.GenerateKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return PreSharedKey(preSharedKey.String()), nil
|
||||
}
|
||||
|
||||
func KeyBytesToString(key []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(key)
|
||||
}
|
||||
|
||||
func PublicKeyFromPrivateKey(key string) string {
|
||||
privKey, err := wgtypes.ParseKey(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return privKey.PublicKey().String()
|
||||
}
|
6
internal/domain/errors.go
Normal file
6
internal/domain/errors.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package domain
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotFound = errors.New("record not found")
|
||||
var ErrNotUnique = errors.New("record not unique")
|
246
internal/domain/interface.go
Normal file
246
internal/domain/interface.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/sirupsen/logrus"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
InterfaceTypeServer InterfaceType = "server"
|
||||
InterfaceTypeClient InterfaceType = "client"
|
||||
InterfaceTypeAny InterfaceType = "any"
|
||||
)
|
||||
|
||||
type InterfaceIdentifier string
|
||||
type InterfaceType string
|
||||
|
||||
type Interface struct {
|
||||
BaseModel
|
||||
|
||||
// WireGuard specific (for the [interface] section of the config file)
|
||||
|
||||
Identifier InterfaceIdentifier `gorm:"primaryKey"` // device name, for example: wg0
|
||||
KeyPair // private/public Key of the server interface
|
||||
ListenPort int // the listening port, for example: 51820
|
||||
|
||||
Addresses []Cidr `gorm:"many2many:interface_addresses;"` // the interface ip addresses
|
||||
DnsStr string // the dns server that should be set if the interface is up, comma separated
|
||||
DnsSearchStr string // the dns search option string that should be set if the interface is up, will be appended to DnsStr
|
||||
|
||||
Mtu int // the device MTU
|
||||
FirewallMark int32 // a firewall mark
|
||||
RoutingTable string // the routing table number or "off" if the routing table should not be managed
|
||||
|
||||
PreUp string // action that is executed before the device is up
|
||||
PostUp string // action that is executed after the device is up
|
||||
PreDown string // action that is executed before the device is down
|
||||
PostDown string // action that is executed after the device is down
|
||||
|
||||
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
|
||||
|
||||
// Default settings for the peer, used for new peers, those settings will be published to ConfigOption options of
|
||||
// the peer config
|
||||
|
||||
PeerDefNetworkStr string // the default subnets from which peers will get their IP addresses, comma seperated
|
||||
PeerDefDnsStr string // the default dns server for the peer
|
||||
PeerDefDnsSearchStr string // the default dns search options for the peer
|
||||
PeerDefEndpoint string // the default endpoint for the peer
|
||||
PeerDefAllowedIPsStr string // the default allowed IP string for the peer
|
||||
PeerDefMtu int // the default device MTU
|
||||
PeerDefPersistentKeepalive int // the default persistent keep-alive Value
|
||||
PeerDefFirewallMark int32 // default firewall mark
|
||||
PeerDefRoutingTable string // the default routing table
|
||||
|
||||
PeerDefPreUp string // default action that is executed before the device is up
|
||||
PeerDefPostUp string // default action that is executed after the device is up
|
||||
PeerDefPreDown string // default action that is executed before the device is down
|
||||
PeerDefPostDown string // default action that is executed after the device is down
|
||||
}
|
||||
|
||||
func (i *Interface) IsValid() bool {
|
||||
return true // TODO: implement check
|
||||
}
|
||||
|
||||
func (i *Interface) IsDisabled() bool {
|
||||
if i == nil {
|
||||
return true
|
||||
}
|
||||
return i.Disabled != nil
|
||||
}
|
||||
|
||||
func (i *Interface) AddressStr() string {
|
||||
return CidrsToString(i.Addresses)
|
||||
}
|
||||
|
||||
func (i *Interface) CopyCalculatedAttributes(src *Interface) {
|
||||
i.BaseModel = src.BaseModel
|
||||
}
|
||||
|
||||
func (i *Interface) GetConfigFileName() string {
|
||||
reg := regexp.MustCompile("[^a-zA-Z0-9-_]+")
|
||||
|
||||
filename := fmt.Sprintf("%s", internal.TruncateString(string(i.Identifier), 8))
|
||||
filename = reg.ReplaceAllString(filename, "")
|
||||
filename += ".conf"
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
func (i *Interface) GetAllowedIPs(peers []Peer) []Cidr {
|
||||
var allowedCidrs []Cidr
|
||||
|
||||
for _, peer := range peers {
|
||||
allowedCidrs = append(allowedCidrs, peer.Interface.Addresses...)
|
||||
if peer.ExtraAllowedIPsStr != "" {
|
||||
extraIPs, err := CidrsFromString(peer.ExtraAllowedIPsStr)
|
||||
if err == nil {
|
||||
allowedCidrs = append(allowedCidrs, extraIPs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allowedCidrs
|
||||
}
|
||||
|
||||
func (i *Interface) ManageRoutingTable() bool {
|
||||
routingTableStr := strings.ToLower(i.RoutingTable)
|
||||
return routingTableStr != "off"
|
||||
}
|
||||
|
||||
// GetRoutingTable returns the routing table number or
|
||||
//
|
||||
// -1 if RoutingTable was set to "off" or an error occurred
|
||||
func (i *Interface) GetRoutingTable() int {
|
||||
routingTableStr := strings.ToLower(i.RoutingTable)
|
||||
switch {
|
||||
case routingTableStr == "":
|
||||
return 0
|
||||
case routingTableStr == "off":
|
||||
return -1
|
||||
case strings.HasPrefix(routingTableStr, "0x"):
|
||||
numberStr := strings.ReplaceAll(routingTableStr, "0x", "")
|
||||
routingTable, err := strconv.ParseUint(numberStr, 16, 64)
|
||||
if err != nil {
|
||||
logrus.Errorf("invalid hex routing table %s: %v", routingTableStr, err)
|
||||
return -1
|
||||
}
|
||||
if routingTable > math.MaxInt32 {
|
||||
logrus.Errorf("invalid routing table %s, too big", routingTableStr)
|
||||
return -1
|
||||
}
|
||||
return int(routingTable)
|
||||
default:
|
||||
routingTable, err := strconv.Atoi(routingTableStr)
|
||||
if err != nil {
|
||||
logrus.Errorf("invalid routing table %s: %v", routingTableStr, err)
|
||||
return -1
|
||||
}
|
||||
return routingTable
|
||||
}
|
||||
}
|
||||
|
||||
type PhysicalInterface struct {
|
||||
Identifier InterfaceIdentifier // device name, for example: wg0
|
||||
KeyPair // private/public Key of the server interface
|
||||
ListenPort int // the listening port, for example: 51820
|
||||
|
||||
Addresses []Cidr // the interface ip addresses
|
||||
|
||||
Mtu int // the device MTU
|
||||
FirewallMark int32 // a firewall mark
|
||||
|
||||
DeviceUp bool // device status
|
||||
|
||||
ImportSource string // import source (wgctrl, file, ...)
|
||||
DeviceType string // device type (Linux kernel, userspace, ...)
|
||||
|
||||
BytesUpload uint64
|
||||
BytesDownload uint64
|
||||
}
|
||||
|
||||
func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
|
||||
iface := &Interface{
|
||||
Identifier: pi.Identifier,
|
||||
KeyPair: pi.KeyPair,
|
||||
ListenPort: pi.ListenPort,
|
||||
Addresses: pi.Addresses,
|
||||
DnsStr: "",
|
||||
DnsSearchStr: "",
|
||||
Mtu: pi.Mtu,
|
||||
FirewallMark: pi.FirewallMark,
|
||||
RoutingTable: "",
|
||||
PreUp: "",
|
||||
PostUp: "",
|
||||
PreDown: "",
|
||||
PostDown: "",
|
||||
SaveConfig: false,
|
||||
DisplayName: string(pi.Identifier),
|
||||
Type: InterfaceTypeAny,
|
||||
DriverType: pi.DeviceType,
|
||||
Disabled: nil,
|
||||
PeerDefNetworkStr: "",
|
||||
PeerDefDnsStr: "",
|
||||
PeerDefDnsSearchStr: "",
|
||||
PeerDefEndpoint: "",
|
||||
PeerDefAllowedIPsStr: "",
|
||||
PeerDefMtu: pi.Mtu,
|
||||
PeerDefPersistentKeepalive: 0,
|
||||
PeerDefFirewallMark: 0,
|
||||
PeerDefRoutingTable: "",
|
||||
PeerDefPreUp: "",
|
||||
PeerDefPostUp: "",
|
||||
PeerDefPreDown: "",
|
||||
PeerDefPostDown: "",
|
||||
}
|
||||
|
||||
return iface
|
||||
}
|
||||
|
||||
func MergeToPhysicalInterface(pi *PhysicalInterface, i *Interface) {
|
||||
pi.Identifier = i.Identifier
|
||||
pi.PublicKey = i.PublicKey
|
||||
pi.PrivateKey = i.PrivateKey
|
||||
pi.ListenPort = i.ListenPort
|
||||
pi.Mtu = i.Mtu
|
||||
pi.FirewallMark = i.FirewallMark
|
||||
pi.DeviceUp = !i.IsDisabled()
|
||||
pi.Addresses = i.Addresses
|
||||
}
|
||||
|
||||
type RoutingTableInfo struct {
|
||||
FwMark int
|
||||
Table int
|
||||
}
|
||||
|
||||
func (r RoutingTableInfo) String() string {
|
||||
return fmt.Sprintf("%d -> %d", r.FwMark, r.Table)
|
||||
}
|
||||
|
||||
func (r RoutingTableInfo) ManagementEnabled() bool {
|
||||
if r.Table == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r RoutingTableInfo) GetRoutingTable() int {
|
||||
if r.Table <= 0 {
|
||||
return r.FwMark // use the dynamic routing table which has the same number as the firewall mark
|
||||
}
|
||||
|
||||
return r.Table
|
||||
}
|
201
internal/domain/ip.go
Normal file
201
internal/domain/ip.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/vishvananda/netlink"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Cidr struct {
|
||||
Cidr string `gorm:"primaryKey;column:cidr"` // Sqlite/GORM does not support composite primary keys...
|
||||
Addr string `gorm:"column:addr"`
|
||||
NetLength int `gorm:"column:net_len"`
|
||||
}
|
||||
|
||||
func (c Cidr) Prefix() netip.Prefix {
|
||||
return netip.PrefixFrom(netip.MustParseAddr(c.Addr), c.NetLength)
|
||||
}
|
||||
|
||||
func (c Cidr) String() string {
|
||||
return c.Prefix().String()
|
||||
}
|
||||
|
||||
func (c Cidr) IsValid() bool {
|
||||
return c.Prefix().IsValid()
|
||||
}
|
||||
|
||||
func CidrFromString(str string) (Cidr, error) {
|
||||
prefix, err := netip.ParsePrefix(strings.TrimSpace(str))
|
||||
if err != nil {
|
||||
return Cidr{}, err
|
||||
}
|
||||
return CidrFromPrefix(prefix), nil
|
||||
}
|
||||
|
||||
func CidrsFromString(str string) ([]Cidr, error) {
|
||||
strParts := strings.Split(str, ",")
|
||||
cidrs := make([]Cidr, len(strParts))
|
||||
|
||||
for i, cidrStr := range strParts {
|
||||
cidr, err := CidrFromString(cidrStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cidrs[i] = cidr
|
||||
}
|
||||
|
||||
return cidrs, nil
|
||||
}
|
||||
|
||||
func CidrsMust(cidrs []Cidr, err error) []Cidr {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return cidrs
|
||||
}
|
||||
|
||||
func CidrsFromArray(strs []string) ([]Cidr, error) {
|
||||
cidrs := make([]Cidr, len(strs))
|
||||
|
||||
for i, cidrStr := range strs {
|
||||
cidr, err := CidrFromString(cidrStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cidrs[i] = cidr
|
||||
}
|
||||
|
||||
return cidrs, nil
|
||||
}
|
||||
|
||||
func CidrFromPrefix(prefix netip.Prefix) Cidr {
|
||||
return Cidr{
|
||||
Cidr: prefix.String(),
|
||||
Addr: prefix.Addr().String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
|
||||
func CidrFromIpNet(ipNet net.IPNet) Cidr {
|
||||
prefix, _ := CidrFromString(ipNet.String())
|
||||
return prefix
|
||||
}
|
||||
|
||||
func CidrFromNetlinkAddr(addr netlink.Addr) Cidr {
|
||||
prefix, _ := CidrFromString(addr.IPNet.String())
|
||||
return prefix
|
||||
}
|
||||
|
||||
func (c Cidr) IpNet() *net.IPNet {
|
||||
ip, cidr, _ := net.ParseCIDR(c.String())
|
||||
cidr.IP = ip
|
||||
return cidr
|
||||
}
|
||||
|
||||
func (c Cidr) NetlinkAddr() *netlink.Addr {
|
||||
return &netlink.Addr{
|
||||
IPNet: c.IpNet(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Cidr) IsV4() bool {
|
||||
return c.Prefix().Addr().Is4()
|
||||
}
|
||||
|
||||
// BroadcastAddr returns the last address in the given network (for IPv6), or the broadcast address.
|
||||
func (c Cidr) BroadcastAddr() Cidr {
|
||||
prefix := c.Prefix()
|
||||
if !prefix.IsValid() {
|
||||
return Cidr{}
|
||||
}
|
||||
a16 := prefix.Addr().As16()
|
||||
var off uint8
|
||||
var bits uint8 = 128
|
||||
if prefix.Addr().Is4() {
|
||||
off = 12
|
||||
bits = 32
|
||||
}
|
||||
for b := uint8(prefix.Bits()); b < bits; b++ {
|
||||
byteNum, bitInByte := b/8, 7-(b%8)
|
||||
a16[off+byteNum] |= 1 << uint(bitInByte)
|
||||
}
|
||||
if prefix.Addr().Is4() {
|
||||
addr := netip.AddrFrom16(a16).Unmap()
|
||||
return Cidr{
|
||||
Cidr: netip.PrefixFrom(addr, prefix.Bits()).String(),
|
||||
Addr: addr.String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
} else {
|
||||
addr := netip.AddrFrom16(a16) // doesn't unmap
|
||||
return Cidr{
|
||||
Cidr: netip.PrefixFrom(addr, prefix.Bits()).String(),
|
||||
Addr: addr.String(), // doesn't unmap
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkAddr returns the network address in the given prefix.
|
||||
func (c Cidr) NetworkAddr() Cidr {
|
||||
prefix := c.Prefix()
|
||||
if !prefix.IsValid() {
|
||||
return Cidr{}
|
||||
}
|
||||
|
||||
return CidrFromPrefix(prefix.Masked())
|
||||
}
|
||||
|
||||
func (c Cidr) FirstAddr() Cidr {
|
||||
prefix := c.Prefix()
|
||||
firstAddr := prefix.Masked().Addr().Next()
|
||||
return Cidr{
|
||||
Cidr: netip.PrefixFrom(firstAddr, c.NetLength).String(),
|
||||
Addr: firstAddr.String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Cidr) NextAddr() Cidr {
|
||||
prefix := c.Prefix()
|
||||
nextAddr := prefix.Addr().Next()
|
||||
return Cidr{
|
||||
Cidr: netip.PrefixFrom(nextAddr, c.NetLength).String(),
|
||||
Addr: nextAddr.String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Cidr) HostAddr() Cidr {
|
||||
return Cidr{
|
||||
Cidr: netip.PrefixFrom(c.Prefix().Addr(), c.Prefix().Addr().BitLen()).String(),
|
||||
Addr: c.Addr,
|
||||
NetLength: c.Prefix().Addr().BitLen(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Cidr) NextSubnet() Cidr {
|
||||
prefix := c.Prefix()
|
||||
nextAddr := c.BroadcastAddr().Prefix().Addr().Next()
|
||||
return Cidr{
|
||||
Cidr: netip.PrefixFrom(nextAddr, c.NetLength).String(),
|
||||
Addr: nextAddr.String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
|
||||
func CidrsToString(slice []Cidr) string {
|
||||
return strings.Join(CidrsToStringSlice(slice), ",")
|
||||
}
|
||||
|
||||
func CidrsToStringSlice(slice []Cidr) []string {
|
||||
cidrs := make([]string, len(slice))
|
||||
|
||||
for i, cidr := range slice {
|
||||
cidrs[i] = cidr.String()
|
||||
}
|
||||
|
||||
return cidrs
|
||||
}
|
204
internal/domain/ip_test.go
Normal file
204
internal/domain/ip_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCidrFromString(t *testing.T) {
|
||||
type args struct {
|
||||
str string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Cidr
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "IPv4",
|
||||
args: args{str: "1.2.3.4/24"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.4/24")),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv4 Network",
|
||||
args: args{str: "1.2.3.0/24"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.0/24")),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv4 error",
|
||||
args: args{str: "1.1/24"},
|
||||
want: Cidr{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 short",
|
||||
args: args{str: "fe00:1234::1/64"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00:1234::1/64")),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv6",
|
||||
args: args{str: "2A02:810A:900:333E:3B74:D237:E076:8B36/128"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("2A02:810A:900:333E:3B74:D237:E076:8B36/128")),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv6 Network",
|
||||
args: args{str: "fe00::/56"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00::/56")),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := CidrFromString(tt.args.str)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CidrFromString() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CidrFromString() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCidr_BroadcastAddr(t *testing.T) {
|
||||
type fields struct {
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want Cidr
|
||||
}{
|
||||
{
|
||||
name: "V4",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.255/24")),
|
||||
},
|
||||
{
|
||||
name: "V6",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("fe00:d3ad:b33f:c0d3::/64")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00:d3ad:b33f:c0d3:ffff:ffff:ffff:ffff/64")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := CidrFromPrefix(tt.fields.Prefix)
|
||||
if got := c.BroadcastAddr(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("BroadcastAddr() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCidr_NetworkAddr(t *testing.T) {
|
||||
type fields struct {
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want Cidr
|
||||
}{
|
||||
|
||||
{
|
||||
name: "V4",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.0/24")),
|
||||
},
|
||||
{
|
||||
name: "V6",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("fe00:d3ad:b33f:c0d3::1234/64")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00:d3ad:b33f:c0d3::/64")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := CidrFromPrefix(tt.fields.Prefix)
|
||||
if got := c.NetworkAddr(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NetworkAddr() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCidr_NextAddr(t *testing.T) {
|
||||
type fields struct {
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want Cidr
|
||||
}{
|
||||
{
|
||||
name: "V4 normal",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.5/24")),
|
||||
},
|
||||
{
|
||||
name: "V4 broadcast",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.254/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.255/24")),
|
||||
},
|
||||
{
|
||||
name: "V4 overflow",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.255/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.4.0/24")),
|
||||
},
|
||||
{
|
||||
name: "V6 normal",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("fe00::1/64")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00::2/64")),
|
||||
},
|
||||
{
|
||||
name: "V6 overflow",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("fe00::ffff:ffff:ffff:ffff/64")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00:0:0:1::/64")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := CidrFromPrefix(tt.fields.Prefix)
|
||||
if got := c.NextAddr(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NextAddr() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCidr_NextSubnet(t *testing.T) {
|
||||
type fields struct {
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want Cidr
|
||||
}{
|
||||
{
|
||||
name: "V4",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.4.0/24")),
|
||||
},
|
||||
{
|
||||
name: "V4 bigger subnet",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/16")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.3.0.0/16")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := CidrFromPrefix(tt.fields.Prefix)
|
||||
if got := c.NextSubnet(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NextSubnet() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
18
internal/domain/mail.go
Normal file
18
internal/domain/mail.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package domain
|
||||
|
||||
import "io"
|
||||
|
||||
type MailOptions struct {
|
||||
ReplyTo string // defaults to the sender
|
||||
HtmlBody string // if html body is empty, a text-only email will be sent
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Attachments []MailAttachment
|
||||
}
|
||||
|
||||
type MailAttachment struct {
|
||||
Name string
|
||||
ContentType string
|
||||
Data io.Reader
|
||||
Embedded bool
|
||||
}
|
113
internal/domain/options.go
Normal file
113
internal/domain/options.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package domain
|
||||
|
||||
type StringConfigOption struct {
|
||||
Value string `gorm:"column:v"`
|
||||
Overridable bool `gorm:"column:o"`
|
||||
}
|
||||
|
||||
func (o StringConfigOption) GetValue() string {
|
||||
return o.Value
|
||||
}
|
||||
|
||||
func (o *StringConfigOption) SetValue(value string) {
|
||||
o.Value = value
|
||||
}
|
||||
|
||||
func (o *StringConfigOption) TrySetValue(value string) bool {
|
||||
if o.Overridable {
|
||||
o.Value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewStringConfigOption(value string, overridable bool) StringConfigOption {
|
||||
return StringConfigOption{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
||||
|
||||
type IntConfigOption struct {
|
||||
Value int `gorm:"column:v"`
|
||||
Overridable bool `gorm:"column:o"`
|
||||
}
|
||||
|
||||
func (o IntConfigOption) GetValue() int {
|
||||
return o.Value
|
||||
}
|
||||
|
||||
func (o *IntConfigOption) SetValue(value int) {
|
||||
o.Value = value
|
||||
}
|
||||
|
||||
func (o *IntConfigOption) TrySetValue(value int) bool {
|
||||
if o.Overridable {
|
||||
o.Value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewIntConfigOption(value int, overridable bool) IntConfigOption {
|
||||
return IntConfigOption{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
||||
|
||||
type Int32ConfigOption struct {
|
||||
Value int32 `gorm:"column:v"`
|
||||
Overridable bool `gorm:"column:o"`
|
||||
}
|
||||
|
||||
func (o Int32ConfigOption) GetValue() int32 {
|
||||
return o.Value
|
||||
}
|
||||
|
||||
func (o *Int32ConfigOption) SetValue(value int32) {
|
||||
o.Value = value
|
||||
}
|
||||
|
||||
func (o *Int32ConfigOption) TrySetValue(value int32) bool {
|
||||
if o.Overridable {
|
||||
o.Value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewInt32ConfigOption(value int32, overridable bool) Int32ConfigOption {
|
||||
return Int32ConfigOption{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
||||
|
||||
type BoolConfigOption struct {
|
||||
Value bool `gorm:"column:v"`
|
||||
Overridable bool `gorm:"column:o"`
|
||||
}
|
||||
|
||||
func (o BoolConfigOption) GetValue() bool {
|
||||
return o.Value
|
||||
}
|
||||
|
||||
func (o *BoolConfigOption) SetValue(value bool) {
|
||||
o.Value = value
|
||||
}
|
||||
|
||||
func (o *BoolConfigOption) TrySetValue(value bool) bool {
|
||||
if o.Overridable {
|
||||
o.Value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewBoolConfigOption(value bool, overridable bool) BoolConfigOption {
|
||||
return BoolConfigOption{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
243
internal/domain/peer.go
Normal file
243
internal/domain/peer.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type PeerIdentifier string
|
||||
|
||||
func (i PeerIdentifier) IsPublicKey() bool {
|
||||
_, err := wgtypes.ParseKey(string(i))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (i PeerIdentifier) ToPublicKey() wgtypes.Key {
|
||||
publicKey, _ := wgtypes.ParseKey(string(i))
|
||||
return publicKey
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
BaseModel
|
||||
|
||||
// WireGuard specific (for the [peer] section of the config file)
|
||||
|
||||
Endpoint StringConfigOption `gorm:"embedded;embeddedPrefix:endpoint_"` // the endpoint address
|
||||
EndpointPublicKey StringConfigOption `gorm:"embedded;embeddedPrefix:endpoint_pubkey_"` // the endpoint public key
|
||||
AllowedIPsStr StringConfigOption `gorm:"embedded;embeddedPrefix:allowed_ips_str_"` // all allowed ip subnets, comma seperated
|
||||
ExtraAllowedIPsStr string // all allowed ip subnets on the server side, comma seperated
|
||||
PresharedKey PreSharedKey // the pre-shared Key of the peer
|
||||
PersistentKeepalive IntConfigOption `gorm:"embedded;embeddedPrefix:persistent_keep_alive_"` // the persistent keep-alive interval
|
||||
|
||||
// WG Portal specific
|
||||
|
||||
DisplayName string // a nice display name/ description for the peer
|
||||
Identifier PeerIdentifier `gorm:"primaryKey;column:identifier"` // peer unique identifier
|
||||
UserIdentifier UserIdentifier `gorm:"index;column:user_identifier"` // the owner
|
||||
InterfaceIdentifier InterfaceIdentifier `gorm:"index;column:interface_identifier"` // the interface id
|
||||
Disabled *time.Time `gorm:"column:disabled"` // if this field is set, the peer is disabled
|
||||
DisabledReason string // the reason why the peer has been disabled
|
||||
ExpiresAt *time.Time `gorm:"column:expires_at"` // expiry dates for peers
|
||||
Notes string `form:"notes" binding:"omitempty"` // a note field for peers
|
||||
|
||||
// Interface settings for the peer, used to generate the [interface] section in the peer config file
|
||||
Interface PeerInterfaceConfig `gorm:"embedded"`
|
||||
}
|
||||
|
||||
func (p *Peer) IsDisabled() bool {
|
||||
return p.Disabled != nil
|
||||
}
|
||||
|
||||
func (p *Peer) IsExpired() bool {
|
||||
if p.ExpiresAt == nil {
|
||||
return false
|
||||
}
|
||||
if p.ExpiresAt.Before(time.Now()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Peer) CheckAliveAddress() string {
|
||||
if p.Interface.CheckAliveAddress != "" {
|
||||
return p.Interface.CheckAliveAddress
|
||||
}
|
||||
|
||||
if len(p.Interface.Addresses) > 0 {
|
||||
return p.Interface.Addresses[0].Addr // take the first peer address
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Peer) CopyCalculatedAttributes(src *Peer) {
|
||||
p.BaseModel = src.BaseModel
|
||||
}
|
||||
|
||||
func (p *Peer) GetConfigFileName() string {
|
||||
filename := ""
|
||||
reg := regexp.MustCompile("[^a-zA-Z0-9-_]+")
|
||||
|
||||
if p.DisplayName != "" {
|
||||
filename = p.DisplayName
|
||||
filename = strings.ReplaceAll(filename, " ", "_")
|
||||
filename = reg.ReplaceAllString(filename, "")
|
||||
filename = internal.TruncateString(filename, 16)
|
||||
filename += ".conf"
|
||||
} else {
|
||||
filename = fmt.Sprintf("wg_%s", internal.TruncateString(string(p.Identifier), 8))
|
||||
filename = reg.ReplaceAllString(filename, "")
|
||||
filename += ".conf"
|
||||
}
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
func (p *Peer) ApplyInterfaceDefaults(in *Interface) {
|
||||
p.Endpoint.TrySetValue(in.PeerDefEndpoint)
|
||||
p.EndpointPublicKey.TrySetValue(in.PublicKey)
|
||||
p.AllowedIPsStr.TrySetValue(in.PeerDefAllowedIPsStr)
|
||||
p.PersistentKeepalive.TrySetValue(in.PeerDefPersistentKeepalive)
|
||||
p.Interface.DnsStr.TrySetValue(in.PeerDefDnsStr)
|
||||
p.Interface.DnsSearchStr.TrySetValue(in.PeerDefDnsSearchStr)
|
||||
p.Interface.Mtu.TrySetValue(in.PeerDefMtu)
|
||||
p.Interface.FirewallMark.TrySetValue(in.PeerDefFirewallMark)
|
||||
p.Interface.RoutingTable.TrySetValue(in.PeerDefRoutingTable)
|
||||
p.Interface.PreUp.TrySetValue(in.PeerDefPreUp)
|
||||
p.Interface.PostUp.TrySetValue(in.PeerDefPostUp)
|
||||
p.Interface.PreDown.TrySetValue(in.PeerDefPreDown)
|
||||
p.Interface.PostDown.TrySetValue(in.PeerDefPostDown)
|
||||
}
|
||||
|
||||
type PeerInterfaceConfig struct {
|
||||
KeyPair // private/public Key of the peer
|
||||
|
||||
Type InterfaceType `gorm:"column:iface_type"` // the interface type (server, client, any)
|
||||
|
||||
Addresses []Cidr `gorm:"many2many:peer_addresses;"` // the interface ip addresses
|
||||
CheckAliveAddress string `gorm:"column:check_alive_address"` // optional ip address or DNS name that is used for ping checks
|
||||
DnsStr StringConfigOption `gorm:"embedded;embeddedPrefix:iface_dns_str_"` // the dns server that should be set if the interface is up, comma separated
|
||||
DnsSearchStr StringConfigOption `gorm:"embedded;embeddedPrefix:iface_dns_search_str_"` // the dns search option string that should be set if the interface is up, will be appended to DnsStr
|
||||
Mtu IntConfigOption `gorm:"embedded;embeddedPrefix:iface_mtu_"` // the device MTU
|
||||
FirewallMark Int32ConfigOption `gorm:"embedded;embeddedPrefix:iface_firewall_mark_"` // a firewall mark
|
||||
RoutingTable StringConfigOption `gorm:"embedded;embeddedPrefix:iface_routing_table_"` // the routing table
|
||||
|
||||
PreUp StringConfigOption `gorm:"embedded;embeddedPrefix:iface_pre_up_"` // action that is executed before the device is up
|
||||
PostUp StringConfigOption `gorm:"embedded;embeddedPrefix:iface_post_up_"` // action that is executed after the device is up
|
||||
PreDown StringConfigOption `gorm:"embedded;embeddedPrefix:iface_pre_down_"` // action that is executed before the device is down
|
||||
PostDown StringConfigOption `gorm:"embedded;embeddedPrefix:iface_post_down_"` // action that is executed after the device is down
|
||||
}
|
||||
|
||||
func (p *PeerInterfaceConfig) AddressStr() string {
|
||||
return CidrsToString(p.Addresses)
|
||||
}
|
||||
|
||||
type PhysicalPeer struct {
|
||||
Identifier PeerIdentifier // peer unique identifier
|
||||
|
||||
Endpoint string // the endpoint address
|
||||
AllowedIPs []Cidr // all allowed ip subnets
|
||||
KeyPair // private/public Key of the peer, for imports it only contains the public key as the private key is not known to the server
|
||||
PresharedKey PreSharedKey // the pre-shared Key of the peer
|
||||
PersistentKeepalive int // the persistent keep-alive interval
|
||||
|
||||
LastHandshake time.Time
|
||||
ProtocolVersion int
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
||||
if p.PresharedKey == "" {
|
||||
return nil
|
||||
}
|
||||
key, err := wgtypes.ParseKey(string(p.PresharedKey))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &key
|
||||
}
|
||||
|
||||
func (p PhysicalPeer) GetEndpointAddress() *net.UDPAddr {
|
||||
if p.Endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
addr, err := net.ResolveUDPAddr("udp", p.Endpoint)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
func (p PhysicalPeer) GetPersistentKeepaliveTime() *time.Duration {
|
||||
if p.PersistentKeepalive == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keepAliveDuration := time.Duration(p.PersistentKeepalive) * time.Second
|
||||
return &keepAliveDuration
|
||||
}
|
||||
|
||||
func (p PhysicalPeer) GetAllowedIPs() []net.IPNet {
|
||||
allowedIPs := make([]net.IPNet, len(p.AllowedIPs))
|
||||
for i, ip := range p.AllowedIPs {
|
||||
allowedIPs[i] = *ip.IpNet()
|
||||
}
|
||||
|
||||
return allowedIPs
|
||||
}
|
||||
|
||||
func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
|
||||
peer := &Peer{
|
||||
Endpoint: StringConfigOption{Value: pp.Endpoint, Overridable: true},
|
||||
EndpointPublicKey: StringConfigOption{Value: "", Overridable: true},
|
||||
AllowedIPsStr: StringConfigOption{Value: "", Overridable: true},
|
||||
ExtraAllowedIPsStr: "",
|
||||
PresharedKey: pp.PresharedKey,
|
||||
PersistentKeepalive: IntConfigOption{Value: pp.PersistentKeepalive, Overridable: true},
|
||||
DisplayName: string(pp.Identifier),
|
||||
Identifier: pp.Identifier,
|
||||
UserIdentifier: "",
|
||||
InterfaceIdentifier: "",
|
||||
Disabled: nil,
|
||||
Interface: PeerInterfaceConfig{
|
||||
KeyPair: pp.KeyPair,
|
||||
},
|
||||
}
|
||||
|
||||
return peer
|
||||
}
|
||||
|
||||
func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
|
||||
pp.Identifier = p.Identifier
|
||||
pp.Endpoint = p.Endpoint.GetValue()
|
||||
if p.Interface.Type == InterfaceTypeServer {
|
||||
allowedIPs, _ := CidrsFromString(p.AllowedIPsStr.GetValue())
|
||||
extraAllowedIPs, _ := CidrsFromString(p.ExtraAllowedIPsStr)
|
||||
pp.AllowedIPs = append(allowedIPs, extraAllowedIPs...)
|
||||
} else {
|
||||
allowedIPs := p.Interface.Addresses
|
||||
extraAllowedIPs, _ := CidrsFromString(p.ExtraAllowedIPsStr)
|
||||
pp.AllowedIPs = append(allowedIPs, extraAllowedIPs...)
|
||||
}
|
||||
pp.PresharedKey = p.PresharedKey
|
||||
pp.PublicKey = p.Interface.PublicKey
|
||||
pp.PersistentKeepalive = p.PersistentKeepalive.GetValue()
|
||||
}
|
||||
|
||||
type PeerCreationRequest struct {
|
||||
UserIdentifiers []string
|
||||
Suffix string
|
||||
}
|
37
internal/domain/statistics.go
Normal file
37
internal/domain/statistics.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type PeerStatus struct {
|
||||
PeerId PeerIdentifier `gorm:"primaryKey;column:identifier"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
|
||||
IsPingable bool `gorm:"column:pingable"`
|
||||
LastPing *time.Time `gorm:"column:last_ping"`
|
||||
|
||||
BytesReceived uint64 `gorm:"column:received"`
|
||||
BytesTransmitted uint64 `gorm:"column:transmitted"`
|
||||
|
||||
LastHandshake *time.Time `gorm:"column:last_handshake"`
|
||||
Endpoint string `gorm:"column:endpoint"`
|
||||
LastSessionStart *time.Time `gorm:"column:last_session_start"`
|
||||
}
|
||||
|
||||
func (s PeerStatus) IsConnected() bool {
|
||||
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
||||
|
||||
handshakeValid := false
|
||||
if s.LastHandshake != nil {
|
||||
handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime)
|
||||
}
|
||||
|
||||
return s.IsPingable || handshakeValid
|
||||
}
|
||||
|
||||
type InterfaceStatus struct {
|
||||
InterfaceId InterfaceIdentifier `gorm:"primaryKey;column:identifier"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
|
||||
BytesReceived uint64 `gorm:"column:received"`
|
||||
BytesTransmitted uint64 `gorm:"column:transmitted"`
|
||||
}
|
139
internal/domain/user.go
Normal file
139
internal/domain/user.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
UserSourceLdap UserSource = "ldap" // LDAP / ActiveDirectory
|
||||
UserSourceDatabase UserSource = "db" // sqlite / mysql database
|
||||
UserSourceOauth UserSource = "oauth" // oauth / open id connect
|
||||
)
|
||||
|
||||
type UserIdentifier string
|
||||
|
||||
type UserSource string
|
||||
|
||||
// User is the user model that gets linked to peer entries, by default an empty user model with only the email address is created
|
||||
type User struct {
|
||||
BaseModel
|
||||
|
||||
// required fields
|
||||
Identifier UserIdentifier `gorm:"primaryKey;column:identifier"`
|
||||
Email string `form:"email" binding:"required,email"`
|
||||
Source UserSource
|
||||
ProviderName string
|
||||
IsAdmin bool
|
||||
|
||||
// optional fields
|
||||
Firstname string `form:"firstname" binding:"omitempty"`
|
||||
Lastname string `form:"lastname" binding:"omitempty"`
|
||||
Phone string `form:"phone" binding:"omitempty"`
|
||||
Department string `form:"department" binding:"omitempty"`
|
||||
Notes string `form:"notes" binding:"omitempty"`
|
||||
|
||||
// optional, integrated password authentication
|
||||
Password PrivateString `form:"password" binding:"omitempty"`
|
||||
Disabled *time.Time `gorm:"index;column:disabled"` // if this field is set, the user is disabled (WireGuard peers are disabled as well)
|
||||
DisabledReason string // the reason why the user has been disabled
|
||||
Locked *time.Time `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login (WireGuard peers still can connect)
|
||||
LockedReason string // the reason why the user has been locked
|
||||
|
||||
LinkedPeerCount int `gorm:"-"`
|
||||
}
|
||||
|
||||
// IsDisabled returns true if the user is disabled. In such a case,
|
||||
// no login is possible and WireGuard peers associated with the user are disabled.
|
||||
func (u *User) IsDisabled() bool {
|
||||
return u.Disabled != nil
|
||||
}
|
||||
|
||||
// IsLocked returns true if the user is locked. In such a case, no login is possible, WireGuard connections still work.
|
||||
func (u *User) IsLocked() bool {
|
||||
return u.Locked != nil
|
||||
}
|
||||
|
||||
func (u *User) CanChangePassword() error {
|
||||
if u.Source == UserSourceDatabase {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("password change only allowed for database source")
|
||||
}
|
||||
|
||||
func (u *User) EditAllowed(new *User) error {
|
||||
if u.Source == UserSourceDatabase {
|
||||
return nil
|
||||
}
|
||||
|
||||
// for users which are not database users, only the notes field and the disabled flag can be updated
|
||||
updateOk := true
|
||||
updateOk = updateOk && u.Identifier == new.Identifier
|
||||
updateOk = updateOk && u.Source == new.Source
|
||||
updateOk = updateOk && u.IsAdmin == new.IsAdmin
|
||||
updateOk = updateOk && u.Email == new.Email
|
||||
updateOk = updateOk && u.Firstname == new.Firstname
|
||||
updateOk = updateOk && u.Lastname == new.Lastname
|
||||
updateOk = updateOk && u.Phone == new.Phone
|
||||
updateOk = updateOk && u.Department == new.Department
|
||||
|
||||
if !updateOk {
|
||||
return errors.New("edit only allowed for database source")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) DeleteAllowed() error {
|
||||
if u.Source == UserSourceDatabase {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("delete only allowed for database source")
|
||||
}
|
||||
|
||||
func (u *User) CheckPassword(password string) error {
|
||||
if u.Source != UserSourceDatabase {
|
||||
return errors.New("invalid user source")
|
||||
}
|
||||
|
||||
if u.IsDisabled() {
|
||||
return errors.New("user disabled")
|
||||
}
|
||||
|
||||
if u.Password == "" {
|
||||
return errors.New("empty user password")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)); err != nil {
|
||||
return errors.New("wrong password")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) HashPassword() error {
|
||||
if u.Password == "" {
|
||||
return nil // nothing to hash
|
||||
}
|
||||
|
||||
if _, err := bcrypt.Cost([]byte(u.Password)); err == nil {
|
||||
return nil // password already hashed
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = PrivateString(hash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) CopyCalculatedAttributes(src *User) {
|
||||
u.BaseModel = src.BaseModel
|
||||
u.LinkedPeerCount = src.LinkedPeerCount
|
||||
}
|
Reference in New Issue
Block a user