mirror of
				https://github.com/h44z/wg-portal.git
				synced 2025-11-03 23:56:18 +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