| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809 | package logicimport (	"context"	"encoding/json"	"errors"	"fmt"	"net"	"sort"	"sync"	"time"	validator "github.com/go-playground/validator/v10"	"github.com/google/uuid"	"github.com/gravitl/netmaker/database"	"github.com/gravitl/netmaker/logger"	"github.com/gravitl/netmaker/logic/acls"	"github.com/gravitl/netmaker/logic/acls/nodeacls"	"github.com/gravitl/netmaker/models"	"github.com/gravitl/netmaker/servercfg"	"github.com/gravitl/netmaker/validation"	"github.com/seancfoley/ipaddress-go/ipaddr"	"golang.org/x/exp/slog")var (	nodeCacheMutex = &sync.RWMutex{}	nodesCacheMap  = make(map[string]models.Node))func getNodeFromCache(nodeID string) (node models.Node, ok bool) {	nodeCacheMutex.RLock()	node, ok = nodesCacheMap[nodeID]	nodeCacheMutex.RUnlock()	return}func getNodesFromCache() (nodes []models.Node) {	nodeCacheMutex.RLock()	for _, node := range nodesCacheMap {		nodes = append(nodes, node)	}	nodeCacheMutex.RUnlock()	return}func deleteNodeFromCache(nodeID string) {	nodeCacheMutex.Lock()	delete(nodesCacheMap, nodeID)	nodeCacheMutex.Unlock()}func storeNodeInCache(node models.Node) {	nodeCacheMutex.Lock()	nodesCacheMap[node.ID.String()] = node	nodeCacheMutex.Unlock()}func loadNodesIntoCache(nMap map[string]models.Node) {	nodeCacheMutex.Lock()	nodesCacheMap = nMap	nodeCacheMutex.Unlock()}func ClearNodeCache() {	nodeCacheMutex.Lock()	nodesCacheMap = make(map[string]models.Node)	nodeCacheMutex.Unlock()}const (	// RELAY_NODE_ERR - error to return if relay node is unfound	RELAY_NODE_ERR = "could not find relay for node"	// NodePurgeTime time to wait for node to response to a NODE_DELETE actions	NodePurgeTime = time.Second * 10	// NodePurgeCheckTime is how often to check nodes for Pending Delete	NodePurgeCheckTime = time.Second * 30)// GetNetworkNodes - gets the nodes of a networkfunc GetNetworkNodes(network string) ([]models.Node, error) {	allnodes, err := GetAllNodes()	if err != nil {		return []models.Node{}, err	}	return GetNetworkNodesMemory(allnodes, network), nil}// GetHostNodes - fetches all nodes part of the hostfunc GetHostNodes(host *models.Host) []models.Node {	nodes := []models.Node{}	for _, nodeID := range host.Nodes {		node, err := GetNodeByID(nodeID)		if err == nil {			nodes = append(nodes, node)		}	}	return nodes}// GetNetworkNodesMemory - gets all nodes belonging to a network from list in memoryfunc GetNetworkNodesMemory(allNodes []models.Node, network string) []models.Node {	var nodes = []models.Node{}	for i := range allNodes {		node := allNodes[i]		if node.Network == network {			nodes = append(nodes, node)		}	}	return nodes}// UpdateNodeCheckin - updates the checkin time of a nodefunc UpdateNodeCheckin(node *models.Node) error {	node.SetLastCheckIn()	data, err := json.Marshal(node)	if err != nil {		return err	}	err = database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)	if err != nil {		return err	}	if servercfg.CacheEnabled() {		storeNodeInCache(*node)	}	return nil}// UpsertNode - updates node in the DBfunc UpsertNode(newNode *models.Node) error {	newNode.SetLastModified()	data, err := json.Marshal(newNode)	if err != nil {		return err	}	err = database.Insert(newNode.ID.String(), string(data), database.NODES_TABLE_NAME)	if err != nil {		return err	}	if servercfg.CacheEnabled() {		storeNodeInCache(*newNode)	}	return nil}// UpdateNode - takes a node and updates another node with it's valuesfunc UpdateNode(currentNode *models.Node, newNode *models.Node) error {	if newNode.Address.IP.String() != currentNode.Address.IP.String() {		if network, err := GetParentNetwork(newNode.Network); err == nil {			if !IsAddressInCIDR(newNode.Address.IP, network.AddressRange) {				return fmt.Errorf("invalid address provided; out of network range for node %s", newNode.ID)			}		}	}	nodeACLDelta := currentNode.DefaultACL != newNode.DefaultACL	newNode.Fill(currentNode, servercfg.IsPro)	// check for un-settable server values	if err := ValidateNode(newNode, true); err != nil {		return err	}	if newNode.ID == currentNode.ID {		if nodeACLDelta {			if err := UpdateProNodeACLs(newNode); err != nil {				logger.Log(1, "failed to apply node level ACLs during creation of node", newNode.ID.String(), "-", err.Error())				return err			}		}		newNode.SetLastModified()		if data, err := json.Marshal(newNode); err != nil {			return err		} else {			err = database.Insert(newNode.ID.String(), string(data), database.NODES_TABLE_NAME)			if err != nil {				return err			}			if servercfg.CacheEnabled() {				storeNodeInCache(*newNode)			}			return nil		}	}	return fmt.Errorf("failed to update node " + currentNode.ID.String() + ", cannot change ID.")}// DeleteNode - marks node for deletion (and adds to zombie list) if called by UI or deletes node if called by nodefunc DeleteNode(node *models.Node, purge bool) error {	alreadyDeleted := node.PendingDelete || node.Action == models.NODE_DELETE	node.Action = models.NODE_DELETE	//delete ext clients if node is ingress gw	if node.IsIngressGateway {		if err := DeleteGatewayExtClients(node.ID.String(), node.Network); err != nil {			slog.Error("failed to delete ext clients", "nodeid", node.ID.String(), "error", err.Error())		}		host, err := GetHost(node.HostID.String())		if err == nil {			go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)		}	}	if node.IsRelayed {		// cleanup node from relayednodes on relay node		relayNode, err := GetNodeByID(node.RelayedBy)		if err == nil {			relayedNodes := []string{}			for _, relayedNodeID := range relayNode.RelayedNodes {				if relayedNodeID == node.ID.String() {					continue				}				relayedNodes = append(relayedNodes, relayedNodeID)			}			relayNode.RelayedNodes = relayedNodes			UpsertNode(&relayNode)		}	}	if node.FailedOverBy != uuid.Nil {		ResetFailedOverPeer(node)	}	if node.IsRelay {		// unset all the relayed nodes		SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)	}	if node.InternetGwID != "" {		inetNode, err := GetNodeByID(node.InternetGwID)		if err == nil {			clientNodeIDs := []string{}			for _, inetNodeClientID := range inetNode.InetNodeReq.InetNodeClientIDs {				if inetNodeClientID == node.ID.String() {					continue				}				clientNodeIDs = append(clientNodeIDs, inetNodeClientID)			}			inetNode.InetNodeReq.InetNodeClientIDs = clientNodeIDs			UpsertNode(&inetNode)		}	}	if node.IsInternetGateway {		UnsetInternetGw(node)	}	if !purge && !alreadyDeleted {		newnode := *node		newnode.PendingDelete = true		if err := UpdateNode(node, &newnode); err != nil {			return err		}		newZombie <- node.ID		return nil	}	if alreadyDeleted {		logger.Log(1, "forcibly deleting node", node.ID.String())	}	host, err := GetHost(node.HostID.String())	if err != nil {		logger.Log(1, "no host found for node", node.ID.String(), "deleting..")		if delErr := DeleteNodeByID(node); delErr != nil {			logger.Log(0, "failed to delete node", node.ID.String(), delErr.Error())		}		return err	}	if err := DissasociateNodeFromHost(node, host); err != nil {		return err	}	return nil}// GetNodeByHostRef - gets the node by host id and networkfunc GetNodeByHostRef(hostid, network string) (node models.Node, err error) {	nodes, err := GetNetworkNodes(network)	if err != nil {		return models.Node{}, err	}	for _, node := range nodes {		if node.HostID.String() == hostid && node.Network == network {			return node, nil		}	}	return models.Node{}, errors.New("node not found")}// DeleteNodeByID - deletes a node from databasefunc DeleteNodeByID(node *models.Node) error {	var err error	var key = node.ID.String()	if err = database.DeleteRecord(database.NODES_TABLE_NAME, key); err != nil {		if !database.IsEmptyRecord(err) {			return err		}	}	if servercfg.CacheEnabled() {		deleteNodeFromCache(node.ID.String())	}	if servercfg.IsDNSMode() {		SetDNS()	}	_, err = nodeacls.RemoveNodeACL(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()))	if err != nil {		// ignoring for now, could hit a nil pointer if delete called twice		logger.Log(2, "attempted to remove node ACL for node", node.ID.String())	}	// removeZombie <- node.ID	if err = DeleteMetrics(node.ID.String()); err != nil {		logger.Log(1, "unable to remove metrics from DB for node", node.ID.String(), err.Error())	}	//recycle ip address	if node.Address.IP != nil {		RemoveIpFromAllocatedIpMap(node.Network, node.Address.IP.String())	}	if node.Address6.IP != nil {		RemoveIpFromAllocatedIpMap(node.Network, node.Address6.IP.String())	}	return nil}// IsNodeIDUnique - checks if node id is uniquefunc IsNodeIDUnique(node *models.Node) (bool, error) {	_, err := database.FetchRecord(database.NODES_TABLE_NAME, node.ID.String())	return database.IsEmptyRecord(err), err}// ValidateNode - validates node valuesfunc ValidateNode(node *models.Node, isUpdate bool) error {	v := validator.New()	_ = v.RegisterValidation("id_unique", func(fl validator.FieldLevel) bool {		if isUpdate {			return true		}		isFieldUnique, _ := IsNodeIDUnique(node)		return isFieldUnique	})	_ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {		_, err := GetNetworkByNode(node)		return err == nil	})	_ = v.RegisterValidation("checkyesornoorunset", func(f1 validator.FieldLevel) bool {		return validation.CheckYesOrNoOrUnset(f1)	})	err := v.Struct(node)	return err}// GetAllNodes - returns all nodes in the DBfunc GetAllNodes() ([]models.Node, error) {	var nodes []models.Node	if servercfg.CacheEnabled() {		nodes = getNodesFromCache()		if len(nodes) != 0 {			return nodes, nil		}	}	nodesMap := make(map[string]models.Node)	if servercfg.CacheEnabled() {		defer loadNodesIntoCache(nodesMap)	}	collection, err := database.FetchRecords(database.NODES_TABLE_NAME)	if err != nil {		if database.IsEmptyRecord(err) {			return []models.Node{}, nil		}		return []models.Node{}, err	}	for _, value := range collection {		var node models.Node		// ignore legacy nodes in database		if err := json.Unmarshal([]byte(value), &node); err != nil {			logger.Log(3, "legacy node detected: ", err.Error())			continue		}		// add node to our array		nodes = append(nodes, node)		nodesMap[node.ID.String()] = node	}	return nodes, nil}func AddStaticNodestoList(nodes []models.Node) []models.Node {	netMap := make(map[string]struct{})	for _, node := range nodes {		if _, ok := netMap[node.Network]; ok {			continue		}		if node.IsIngressGateway {			nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network))...)			netMap[node.Network] = struct{}{}		}	}	return nodes}// GetNetworkByNode - gets the network model from a nodefunc GetNetworkByNode(node *models.Node) (models.Network, error) {	var network = models.Network{}	networkData, err := database.FetchRecord(database.NETWORKS_TABLE_NAME, node.Network)	if err != nil {		return network, err	}	if err = json.Unmarshal([]byte(networkData), &network); err != nil {		return models.Network{}, err	}	return network, nil}// SetNodeDefaults - sets the defaults of a node to avoid empty fieldsfunc SetNodeDefaults(node *models.Node, resetConnected bool) {	parentNetwork, _ := GetNetworkByNode(node)	_, cidr, err := net.ParseCIDR(parentNetwork.AddressRange)	if err == nil {		node.NetworkRange = *cidr	}	_, cidr, err = net.ParseCIDR(parentNetwork.AddressRange6)	if err == nil {		node.NetworkRange6 = *cidr	}	if node.DefaultACL == "" {		node.DefaultACL = parentNetwork.DefaultACL	}	if node.FailOverPeers == nil {		node.FailOverPeers = make(map[string]struct{})	}	node.SetLastModified()	if node.LastCheckIn.IsZero() {		node.SetLastCheckIn()	}	if resetConnected {		node.SetDefaultConnected()	}	node.SetExpirationDateTime()	if node.Tags == nil {		node.Tags = make(map[models.TagID]struct{})	}}// GetRecordKey - get record key// depricatedfunc GetRecordKey(id string, network string) (string, error) {	if id == "" || network == "" {		return "", errors.New("unable to get record key")	}	return id + "###" + network, nil}func GetNodeByID(uuid string) (models.Node, error) {	if servercfg.CacheEnabled() {		if node, ok := getNodeFromCache(uuid); ok {			return node, nil		}	}	var record, err = database.FetchRecord(database.NODES_TABLE_NAME, uuid)	if err != nil {		return models.Node{}, err	}	var node models.Node	if err = json.Unmarshal([]byte(record), &node); err != nil {		return models.Node{}, err	}	if servercfg.CacheEnabled() {		storeNodeInCache(node)	}	return node, nil}// GetDeletedNodeByID - get a deleted nodefunc GetDeletedNodeByID(uuid string) (models.Node, error) {	var node models.Node	record, err := database.FetchRecord(database.DELETED_NODES_TABLE_NAME, uuid)	if err != nil {		return models.Node{}, err	}	if err = json.Unmarshal([]byte(record), &node); err != nil {		return models.Node{}, err	}	SetNodeDefaults(&node, true)	return node, nil}// FindRelay - returns the node that is the relay for a relayed nodefunc FindRelay(node *models.Node) *models.Node {	relay, err := GetNodeByID(node.RelayedBy)	if err != nil {		logger.Log(0, "FindRelay: "+err.Error())		return nil	}	return &relay}// GetAllNodesAPI - get all nodes for api usagefunc GetAllNodesAPI(nodes []models.Node) []models.ApiNode {	apiNodes := []models.ApiNode{}	for i := range nodes {		newApiNode := nodes[i].ConvertToAPINode()		apiNodes = append(apiNodes, *newApiNode)	}	return apiNodes[:]}// DeleteExpiredNodes - goroutine which deletes nodes which are expiredfunc DeleteExpiredNodes(ctx context.Context, peerUpdate chan *models.Node) {	// Delete Expired Nodes Every Hour	ticker := time.NewTicker(time.Hour)	for {		select {		case <-ctx.Done():			ticker.Stop()			return		case <-ticker.C:			allnodes, err := GetAllNodes()			if err != nil {				slog.Error("failed to retrieve all nodes", "error", err.Error())				return			}			for _, node := range allnodes {				node := node				if time.Now().After(node.ExpirationDateTime) {					peerUpdate <- &node					slog.Info("deleting expired node", "nodeid", node.ID.String())				}			}		}	}}// createNode - creates a node in databasefunc createNode(node *models.Node) error {	// lock because we need unique IPs and having it concurrent makes parallel calls result in same "unique" IPs	addressLock.Lock()	defer addressLock.Unlock()	host, err := GetHost(node.HostID.String())	if err != nil {		return err	}	if !node.DNSOn {		if servercfg.IsDNSMode() {			node.DNSOn = true		} else {			node.DNSOn = false		}	}	SetNodeDefaults(node, true)	defaultACLVal := acls.Allowed	parentNetwork, err := GetNetwork(node.Network)	if err == nil {		if parentNetwork.DefaultACL != "yes" {			defaultACLVal = acls.NotAllowed		}	}	if node.DefaultACL == "" {		node.DefaultACL = "unset"	}	if node.Address.IP == nil {		if parentNetwork.IsIPv4 == "yes" {			if node.Address.IP, err = UniqueAddress(node.Network, false); err != nil {				return err			}			_, cidr, err := net.ParseCIDR(parentNetwork.AddressRange)			if err != nil {				return err			}			node.Address.Mask = net.CIDRMask(cidr.Mask.Size())		}	} else if !IsIPUnique(node.Network, node.Address.String(), database.NODES_TABLE_NAME, false) {		return fmt.Errorf("invalid address: ipv4 " + node.Address.String() + " is not unique")	}	if node.Address6.IP == nil {		if parentNetwork.IsIPv6 == "yes" {			if node.Address6.IP, err = UniqueAddress6(node.Network, false); err != nil {				return err			}			_, cidr, err := net.ParseCIDR(parentNetwork.AddressRange6)			if err != nil {				return err			}			node.Address6.Mask = net.CIDRMask(cidr.Mask.Size())		}	} else if !IsIPUnique(node.Network, node.Address6.String(), database.NODES_TABLE_NAME, true) {		return fmt.Errorf("invalid address: ipv6 " + node.Address6.String() + " is not unique")	}	node.ID = uuid.New()	//Create a JWT for the node	tokenString, _ := CreateJWT(node.ID.String(), host.MacAddress.String(), node.Network)	if tokenString == "" {		//logic.ReturnErrorResponse(w, r, errorResponse)		return err	}	err = ValidateNode(node, false)	if err != nil {		return err	}	CheckZombies(node)	nodebytes, err := json.Marshal(&node)	if err != nil {		return err	}	err = database.Insert(node.ID.String(), string(nodebytes), database.NODES_TABLE_NAME)	if err != nil {		return err	}	if servercfg.CacheEnabled() {		storeNodeInCache(*node)	}	if _, ok := allocatedIpMap[node.Network]; ok {		if node.Address.IP != nil {			AddIpToAllocatedIpMap(node.Network, node.Address.IP)		}		if node.Address6.IP != nil {			AddIpToAllocatedIpMap(node.Network, node.Address6.IP)		}	}	_, err = nodeacls.CreateNodeACL(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), defaultACLVal)	if err != nil {		logger.Log(1, "failed to create node ACL for node,", node.ID.String(), "err:", err.Error())		return err	}	if err = UpdateProNodeACLs(node); err != nil {		logger.Log(1, "failed to apply node level ACLs during creation of node", node.ID.String(), "-", err.Error())		return err	}	if err = UpdateMetrics(node.ID.String(), &models.Metrics{Connectivity: make(map[string]models.Metric)}); err != nil {		logger.Log(1, "failed to initialize metrics for node", node.ID.String(), err.Error())	}	SetNetworkNodesLastModified(node.Network)	if servercfg.IsDNSMode() {		err = SetDNS()	}	return err}// SortApiNodes - Sorts slice of ApiNodes by their ID alphabetically with numbers firstfunc SortApiNodes(unsortedNodes []models.ApiNode) {	sort.Slice(unsortedNodes, func(i, j int) bool {		return unsortedNodes[i].ID < unsortedNodes[j].ID	})}func ValidateParams(nodeid, netid string) (models.Node, error) {	node, err := GetNodeByID(nodeid)	if err != nil {		slog.Error("error fetching node", "node", nodeid, "error", err.Error())		return node, fmt.Errorf("error fetching node during parameter validation: %v", err)	}	if node.Network != netid {		slog.Error("network url param does not match node id", "url nodeid", netid, "node", node.Network)		return node, fmt.Errorf("network url param does not match node network")	}	return node, nil}func ValidateEgressRange(gateway models.EgressGatewayRequest) error {	network, err := GetNetworkSettings(gateway.NetID)	if err != nil {		slog.Error("error getting network with netid", "error", gateway.NetID, err.Error)		return errors.New("error getting network with netid:  " + gateway.NetID + " " + err.Error())	}	ipv4Net := network.AddressRange	ipv6Net := network.AddressRange6	for _, v := range gateway.Ranges {		if ipv4Net != "" {			if ContainsCIDR(ipv4Net, v) {				slog.Error("egress range should not be the same as or contained in the netmaker network address", "error", v, ipv4Net)				return errors.New("egress range should not be the same as or contained in the netmaker network address" + v + " " + ipv4Net)			}		}		if ipv6Net != "" {			if ContainsCIDR(ipv6Net, v) {				slog.Error("egress range should not be the same as or contained in the netmaker network address", "error", v, ipv6Net)				return errors.New("egress range should not be the same as or contained in the netmaker network address" + v + " " + ipv6Net)			}		}	}	return nil}func ContainsCIDR(net1, net2 string) bool {	one, two := ipaddr.NewIPAddressString(net1),		ipaddr.NewIPAddressString(net2)	return one.Contains(two) || two.Contains(one)}// GetAllFailOvers - gets all the nodes that are failoversfunc GetAllFailOvers() ([]models.Node, error) {	nodes, err := GetAllNodes()	if err != nil {		return nil, err	}	igs := make([]models.Node, 0)	for _, node := range nodes {		if node.IsFailOver {			igs = append(igs, node)		}	}	return igs, nil}func GetTagMapWithNodes(netID models.NetworkID) (tagNodesMap map[models.TagID][]models.Node) {	tagNodesMap = make(map[models.TagID][]models.Node)	nodes, _ := GetNetworkNodes(netID.String())	for _, nodeI := range nodes {		if nodeI.Tags == nil {			continue		}		for nodeTagID := range nodeI.Tags {			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)		}	}	return AddTagMapWithStaticNodes(netID, tagNodesMap)}func AddTagMapWithStaticNodes(netID models.NetworkID,	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {	extclients, err := GetNetworkExtClients(netID.String())	if err != nil {		return tagNodesMap	}	for _, extclient := range extclients {		if extclient.Tags == nil || extclient.RemoteAccessClientID != "" {			continue		}		for tagID := range extclient.Tags {			tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{				IsStatic:   true,				StaticNode: extclient,			})		}	}	return tagNodesMap}func GetNodesWithTag(tagID models.TagID) map[string]models.Node {	nMap := make(map[string]models.Node)	tag, err := GetTag(tagID)	if err != nil {		return nMap	}	nodes, _ := GetNetworkNodes(tag.Network.String())	for _, nodeI := range nodes {		if nodeI.Tags == nil {			continue		}		if _, ok := nodeI.Tags[tagID]; ok {			nMap[nodeI.ID.String()] = nodeI		}	}	return AddStaticNodesWithTag(tag, nMap)}func AddStaticNodesWithTag(tag models.Tag, nMap map[string]models.Node) map[string]models.Node {	extclients, err := GetNetworkExtClients(tag.Network.String())	if err != nil {		return nMap	}	for _, extclient := range extclients {		if extclient.RemoteAccessClientID != "" {			continue		}		if _, ok := extclient.Tags[tag.ID]; ok {			nMap[extclient.ClientID] = models.Node{				IsStatic:   true,				StaticNode: extclient,			}		}	}	return nMap}func GetStaticNodeWithTag(tagID models.TagID) map[string]models.Node {	nMap := make(map[string]models.Node)	tag, err := GetTag(tagID)	if err != nil {		return nMap	}	extclients, err := GetNetworkExtClients(tag.Network.String())	if err != nil {		return nMap	}	for _, extclient := range extclients {		nMap[extclient.ClientID] = models.Node{			IsStatic:   true,			StaticNode: extclient,		}	}	return nMap}
 |