netmaker/logic/util.go
Abhishek K 74fef9fbc6
NM-122: Auto Relay, auto assignment of Gw (#3697)
* add auto realy handlers and logic funcs

* add pro func connectors

* Add auto relayed peer ips on peer update, set auto relay on gw creation

* add network id to signal, add autorelay nodes to peerudpate

* add autorelay peer update logic

* add nodes to peer update

* revert node model change

* reset auto relayed peers on the relay node on reset, add auto relay nodes to pull

* add logic api to update auto relay node

* add autoassigngw field to node, add logic to swith relay node in relayme udpate api

* add gw nodes to pull

* intilaise gw map

* HA relay functionality

* add autoassign gw option to enrollment key

* publish intant action to auto assign gw

* fix static checks

* unset relay if auto assign removed

* add host node model to auto relay info

* add host node model to auto relay info

* only use hostNode model for gws info

* handle autoassigned gw peer in the update

* handle autoassigned gw peer in the update

* handle peer updates for autoassigned gw peer

* unset auto assigned peer if relayed or failedovered
2025-10-28 09:53:31 +04:00

303 lines
7 KiB
Go

// package for logicing client and server code
package logic
import (
"crypto/rand"
"encoding/base32"
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"reflect"
"regexp"
"strings"
"time"
"unicode"
"github.com/blang/semver"
"github.com/c-robinson/iplib"
"github.com/gravitl/netmaker/database"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/models"
)
// IsBase64 - checks if a string is in base64 format
// This is used to validate public keys (make sure they're base64 encoded like all public keys should be).
func IsBase64(s string) bool {
_, err := base64.StdEncoding.DecodeString(s)
return err == nil
}
// CheckEndpoint - checks if an endpoint is valid
func CheckEndpoint(endpoint string) bool {
endpointarr := strings.Split(endpoint, ":")
return len(endpointarr) == 2
}
// FileExists - checks if local file exists
func FileExists(f string) bool {
info, err := os.Stat(f)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// IsAddressInCIDR - util to see if an address is in a cidr or not
func IsAddressInCIDR(address net.IP, cidr string) bool {
var _, currentCIDR, cidrErr = net.ParseCIDR(cidr)
if cidrErr != nil {
return false
}
return currentCIDR.Contains(address)
}
// SetNetworkNodesLastModified - sets the network nodes last modified
func SetNetworkNodesLastModified(networkName string) error {
timestamp := time.Now().Unix()
network, err := GetParentNetwork(networkName)
if err != nil {
return err
}
network.NodesLastModified = timestamp
data, err := json.Marshal(&network)
if err != nil {
return err
}
err = database.Insert(networkName, string(data), database.NETWORKS_TABLE_NAME)
if err != nil {
return err
}
return nil
}
// RandomString - returns a random string in a charset
func RandomString(length int) string {
randombytes := make([]byte, length)
_, err := rand.Read(randombytes)
if err != nil {
logger.Log(0, "random string", err.Error())
return ""
}
return base32.StdEncoding.EncodeToString(randombytes)[:length]
}
// StringSliceContains - sees if a string slice contains a string element
func StringSliceContains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
func SetVerbosity(logLevel int) {
var level slog.Level
switch logLevel {
case 0:
level = slog.LevelInfo
case 1:
level = slog.LevelError
case 2:
level = slog.LevelWarn
case 3:
level = slog.LevelDebug
default:
level = slog.LevelInfo
}
// Create the logger with the chosen level
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
})
logger := slog.New(handler)
slog.SetDefault(logger)
}
// NormalizeCIDR - returns the first address of CIDR
func NormalizeCIDR(address string) (string, error) {
ip, IPNet, err := net.ParseCIDR(address)
if err != nil {
return "", err
}
if ip.To4() == nil {
net6 := iplib.Net6FromStr(IPNet.String())
IPNet.IP = net6.FirstAddress()
} else {
net4 := iplib.Net4FromStr(IPNet.String())
IPNet.IP = net4.NetworkAddress()
}
return IPNet.String(), nil
}
// StringDifference - returns the elements in `a` that aren't in `b`.
func StringDifference(a, b []string) []string {
mb := make(map[string]struct{}, len(b))
for _, x := range b {
mb[x] = struct{}{}
}
var diff []string
for _, x := range a {
if _, found := mb[x]; !found {
diff = append(diff, x)
}
}
return diff
}
// CheckIfFileExists - checks if file exists or not in the given path
func CheckIfFileExists(filePath string) bool {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
}
return true
}
// RemoveStringSlice - removes an element at given index i
// from a given string slice
func RemoveStringSlice(slice []string, i int) []string {
return append(slice[:i], slice[i+1:]...)
}
// RemoveAllFromSlice removes every occurrence of val from s (stable order).
func RemoveAllFromSlice[T comparable](s []T, val T) []T {
// Reuse the underlying array: write filtered items back into s[:0].
out := s[:0]
for _, v := range s {
if v != val {
out = append(out, v)
}
}
// out now contains only the kept items; capacity unchanged, len shrunk.
return out
}
// IsSlicesEqual tells whether a and b contain the same elements.
// A nil argument is equivalent to an empty slice.
func IsSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
// VersionLessThan checks if v1 < v2 semantically
// dev is the latest version
func VersionLessThan(v1, v2 string) (bool, error) {
if v1 == "dev" {
return false, nil
}
if v2 == "dev" {
return true, nil
}
semVer1 := strings.TrimFunc(v1, func(r rune) bool {
return !unicode.IsNumber(r)
})
semVer2 := strings.TrimFunc(v2, func(r rune) bool {
return !unicode.IsNumber(r)
})
sv1, err := semver.Parse(semVer1)
if err != nil {
return false, fmt.Errorf("failed to parse semver1 (%s): %w", semVer1, err)
}
sv2, err := semver.Parse(semVer2)
if err != nil {
return false, fmt.Errorf("failed to parse semver2 (%s): %w", semVer2, err)
}
return sv1.LT(sv2), nil
}
// Compare any two maps with any key and value types
func CompareMaps[K comparable, V any](a, b map[K]V) bool {
if len(a) != len(b) {
return false
}
for key, valA := range a {
valB, ok := b[key]
if !ok {
return false
}
if !reflect.DeepEqual(valA, valB) {
return false
}
}
return true
}
func UniqueStrings(input []string) []string {
seen := make(map[string]struct{})
var result []string
for _, val := range input {
if _, ok := seen[val]; !ok {
seen[val] = struct{}{}
result = append(result, val)
}
}
return result
}
func GetClientIP(r *http.Request) string {
// Trust X-Forwarded-For first
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
return strings.TrimSpace(parts[0])
}
if xrip := r.Header.Get("X-Real-IP"); xrip != "" {
return xrip
}
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return ip
}
// CompareIfaceSlices compares two slices of Iface for deep equality (order-sensitive)
func CompareIfaceSlices(a, b []models.Iface) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !compareIface(a[i], b[i]) {
return false
}
}
return true
}
func compareIface(a, b models.Iface) bool {
return a.Name == b.Name &&
a.Address.IP.Equal(b.Address.IP) &&
a.Address.Mask.String() == b.Address.Mask.String() &&
a.AddressString == b.AddressString
}
// IsFQDN checks if the given string is a valid Fully Qualified Domain Name (FQDN)
func IsFQDN(domain string) bool {
// Basic check to ensure the domain is not empty and has at least one dot (.)
if domain == "" || !strings.Contains(domain, ".") {
return false
}
// Regular expression for validating FQDN (basic check for valid characters and structure)
fqdnRegex := `^(?i)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`
re := regexp.MustCompile(fqdnRegex)
return re.MatchString(domain)
}