Browse Source

Merge pull request #2674 from gravitl/release-v0.21.2

v0.21.2
Abhishek K 1 year ago
parent
commit
5af7a6a50f

+ 1 - 0
.github/ISSUE_TEMPLATE/bug-report.yml

@@ -31,6 +31,7 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.21.2
         - v0.21.1
         - v0.21.0
         - v0.20.6

+ 2 - 2
README.md

@@ -16,7 +16,7 @@
 
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.21.1-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.21.2-informational?style=flat-square" />
   </a>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />
@@ -57,7 +57,7 @@ These are the instructions for deploying a Netmaker server on your own cloud VM
 
 1. Get a cloud VM with Ubuntu 22.04 and a public IP.
 2. Open ports 443, 80, 3479, 8089 and 51821-51830/udp on the VM firewall and in cloud security settings.
-3. (recommended) Prepare DNS - Set a wildcard subdomain in your DNS settings for Netmaker, e.g. *.netmaker.example.com, which points to your VM's pubic IP.
+3. (recommended) Prepare DNS - Set a wildcard subdomain in your DNS settings for Netmaker, e.g. *.netmaker.example.com, which points to your VM's public IP.
 4. Run the script: 
 
 `sudo wget -qO /root/nm-quick.sh https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh && sudo chmod +x /root/nm-quick.sh && sudo /root/nm-quick.sh`  

+ 11 - 2
auth/host_session.go

@@ -15,6 +15,7 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slog"
 )
 
 // SessionHandler - called by the HTTP router when user
@@ -202,7 +203,7 @@ func SessionHandler(conn *websocket.Conn) {
 		if err = conn.WriteMessage(messageType, reponseData); err != nil {
 			logger.Log(0, "error during message writing:", err.Error())
 		}
-		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host)
+		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil)
 	case <-timeout: // the read from req.answerCh has timed out
 		if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
 			logger.Log(0, "error during timeout message writing:", err.Error())
@@ -221,7 +222,7 @@ func SessionHandler(conn *websocket.Conn) {
 }
 
 // CheckNetRegAndHostUpdate - run through networks and send a host update
-func CheckNetRegAndHostUpdate(networks []string, h *models.Host) {
+func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uuid.UUID) {
 	// publish host update through MQ
 	for i := range networks {
 		network := networks[i]
@@ -231,6 +232,14 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host) {
 				logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, network, err.Error())
 				continue
 			}
+			if relayNodeId != uuid.Nil && !newNode.IsRelayed {
+				newNode.IsRelayed = true
+				newNode.RelayedBy = relayNodeId.String()
+				slog.Info(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
+				if err := logic.UpsertNode(newNode); err != nil {
+					slog.Error("failed to update node", "nodeid", relayNodeId.String())
+				}
+			}
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
 			hostactions.AddAction(models.HostUpdate{
 				Action: models.JoinHostToNetwork,

+ 1 - 1
compose/docker-compose.netclient.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netclient:
     container_name: netclient
-    image: 'gravitl/netclient:v0.21.1'
+    image: 'gravitl/netclient:v0.21.2'
     hostname: netmaker-1
     network_mode: host
     restart: on-failure

+ 1 - 1
controllers/docs.go

@@ -10,7 +10,7 @@
 //
 //	Schemes: https
 //	BasePath: /
-//	Version: 0.21.1
+//	Version: 0.21.2
 //	Host: api.demo.netmaker.io
 //
 //	Consumes:

+ 66 - 1
controllers/enrollmentkeys.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 
 	"github.com/gravitl/netmaker/auth"
@@ -26,6 +27,8 @@ func enrollmentKeyHandlers(r *mux.Router) {
 		Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/host/register/{token}", http.HandlerFunc(handleHostRegister)).
 		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/enrollment-keys/{keyID}", logic.SecurityCheck(true, http.HandlerFunc(updateEnrollmentKey))).
+		Methods(http.MethodPut)
 }
 
 // swagger:route GET /api/v1/enrollment-keys enrollmentKeys getEnrollmentKeys
@@ -113,12 +116,23 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		newTime = time.Unix(enrollmentKeyBody.Expiration, 0)
 	}
 
+	relayId := uuid.Nil
+	if enrollmentKeyBody.Relay != "" {
+		relayId, err = uuid.Parse(enrollmentKeyBody.Relay)
+		if err != nil {
+			logger.Log(0, r.Header.Get("user"), "error parsing relay id: ", err.Error())
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	}
+
 	newEnrollmentKey, err := logic.CreateEnrollmentKey(
 		enrollmentKeyBody.UsesRemaining,
 		newTime,
 		enrollmentKeyBody.Networks,
 		enrollmentKeyBody.Tags,
 		enrollmentKeyBody.Unlimited,
+		relayId,
 	)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
@@ -136,6 +150,57 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 }
 
+// swagger:route PUT /api/v1/enrollment-keys/:id enrollmentKeys updateEnrollmentKey
+//
+// Updates an EnrollmentKey for hosts to use on Netmaker server. Updates only the relay to use.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: EnrollmentKey
+func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
+	var enrollmentKeyBody models.APIEnrollmentKey
+	params := mux.Vars(r)
+	keyId := params["keyID"]
+
+	err := json.NewDecoder(r.Body).Decode(&enrollmentKeyBody)
+	if err != nil {
+		slog.Error("error decoding request body", "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	relayId := uuid.Nil
+	if enrollmentKeyBody.Relay != "" {
+		relayId, err = uuid.Parse(enrollmentKeyBody.Relay)
+		if err != nil {
+			slog.Error("error parsing relay id", "error", err)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	}
+
+	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId)
+	if err != nil {
+		slog.Error("failed to update enrollment key", "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	if err = logic.Tokenize(newEnrollmentKey, servercfg.GetAPIHost()); err != nil {
+		slog.Error("failed to update enrollment key", "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	slog.Info("updated enrollment key", "id", keyId)
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(newEnrollmentKey)
+}
+
 // swagger:route POST /api/v1/enrollment-keys/{token} enrollmentKeys handleHostRegister
 //
 // Handles a Netclient registration with server and add nodes accordingly.
@@ -286,5 +351,5 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)
 	// notify host of changes, peer and node updates
-	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost)
+	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay)
 }

+ 59 - 46
controllers/hosts.go

@@ -7,6 +7,7 @@ import (
 	"net/http"
 
 	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -123,11 +124,12 @@ func pull(w http.ResponseWriter, r *http.Request) {
 
 	serverConf.TrafficKey = key
 	response := models.HostPull{
-		Host:         *host,
-		Nodes:        logic.GetHostNodes(host),
-		ServerConfig: serverConf,
-		Peers:        hPU.Peers,
-		PeerIDs:      hPU.PeerIDs,
+		Host:            *host,
+		Nodes:           logic.GetHostNodes(host),
+		ServerConfig:    serverConf,
+		Peers:           hPU.Peers,
+		PeerIDs:         hPU.PeerIDs,
+		HostNetworkInfo: hPU.HostNetworkInfo,
 	}
 
 	logger.Log(1, hostID, "completed a pull")
@@ -226,6 +228,19 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	for _, nodeID := range currHost.Nodes {
+		node, err := logic.GetNodeByID(nodeID)
+		if err != nil {
+			slog.Error("failed to get node", "nodeid", nodeID, "error", err)
+			continue
+		}
+		var gwClients []models.ExtClient
+		if node.IsIngressGateway {
+			gwClients = logic.GetGwExtclients(node.ID.String(), node.Network)
+		}
+		go mq.PublishMqUpdatesForDeletedNode(node, false, gwClients)
+
+	}
 	if err = logic.RemoveHost(currHost, forceDelete); err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to delete a host:", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -285,6 +300,7 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 			Node:   *newNode,
 		})
 		mq.PublishPeerUpdate()
+		mq.HandleNewNodeDNS(currHost, newNode)
 	}()
 	logger.Log(2, r.Header.Get("user"), fmt.Sprintf("added host %s to network %s", currHost.Name, network))
 	w.WriteHeader(http.StatusOK)
@@ -314,6 +330,24 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 	// confirm host exists
 	currHost, err := logic.GetHost(hostid)
 	if err != nil {
+		if database.IsEmptyRecord(err) {
+			// check if there is any daemon nodes that needs to be deleted
+			node, err := logic.GetNodeByHostRef(hostid, network)
+			if err != nil {
+				slog.Error("couldn't get node for host", "hostid", hostid, "network", network, "error", err)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+				return
+			}
+			if err = logic.DeleteNodeByID(&node); err != nil {
+				slog.Error("failed to force delete daemon node",
+					"nodeid", node.ID.String(), "hostid", hostid, "network", network, "error", err)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to force delete daemon node: "+err.Error()), "internal"))
+				return
+			}
+			logic.ReturnSuccessResponse(w, r, "force deleted daemon node successfully")
+			return
+		}
+
 		logger.Log(0, r.Header.Get("user"), "failed to find host:", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
@@ -321,58 +355,37 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 
 	node, err := logic.UpdateHostNetwork(currHost, network, false)
 	if err != nil {
+		if node == nil && forceDelete {
+			// force cleanup the node
+			node, err := logic.GetNodeByHostRef(hostid, network)
+			if err != nil {
+				slog.Error("couldn't get node for host", "hostid", hostid, "network", network, "error", err)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+				return
+			}
+			if err = logic.DeleteNodeByID(&node); err != nil {
+				slog.Error("failed to force delete daemon node",
+					"nodeid", node.ID.String(), "hostid", hostid, "network", network, "error", err)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to force delete daemon node: "+err.Error()), "internal"))
+				return
+			}
+			logic.ReturnSuccessResponse(w, r, "force deleted daemon node successfully")
+			return
+		}
 		logger.Log(0, r.Header.Get("user"), "failed to remove host from network:", hostid, network, err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if node.IsRelayed {
-		// cleanup node from relayednodes on relay node
-		relayNode, err := logic.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
-			logic.UpsertNode(&relayNode)
-		}
-	}
-	if node.IsRelay {
-		// unset all the relayed nodes
-		logic.SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
-	}
+	var gwClients []models.ExtClient
 	if node.IsIngressGateway {
-		// delete ext clients belonging to ingress gateway
-		go func(node models.Node) {
-			if err = logic.DeleteGatewayExtClients(node.ID.String(), node.Network); err != nil {
-				slog.Error("failed to delete extclients", "gatewayid", node.ID.String(), "network", node.Network, "error", err.Error())
-			}
-		}(*node)
+		gwClients = logic.GetGwExtclients(node.ID.String(), node.Network)
 	}
 	logger.Log(1, "deleting node", node.ID.String(), "from host", currHost.Name)
 	if err := logic.DeleteNode(node, forceDelete); err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete node"), "internal"))
 		return
 	}
-	node.Action = models.NODE_DELETE
-	node.PendingDelete = true
-	go func() {
-		// notify node change
-		if err := mq.NodeUpdate(node); err != nil {
-			slog.Error("error publishing node update to node", "node", node.ID, "error", err)
-		}
-		// notify of peer change
-		err = mq.PublishDeletedNodePeerUpdate(node)
-		if err != nil {
-			logger.Log(1, "error publishing peer update ", err.Error())
-		}
-		if err := mq.PublishDNSDelete(node, currHost); err != nil {
-			logger.Log(1, "error publishing dns update", err.Error())
-		}
-	}()
+	go mq.PublishMqUpdatesForDeletedNode(*node, true, gwClients)
 	logger.Log(2, r.Header.Get("user"), fmt.Sprintf("removed host %s from network %s", currHost.Name, network))
 	w.WriteHeader(http.StatusOK)
 }

+ 4 - 44
controllers/node.go

@@ -614,6 +614,7 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 				if err := mq.NodeUpdate(&node); err != nil {
 					slog.Error("error publishing node update to node", "node", node.ID, "error", err)
 				}
+				mq.PublishDeleteAllExtclientsDNS(node.Network, removedClients)
 			}()
 		}
 	}
@@ -725,35 +726,10 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 	}
 	forceDelete := r.URL.Query().Get("force") == "true"
 	fromNode := r.Header.Get("requestfrom") == "node"
-	if node.IsRelayed {
-		// cleanup node from relayednodes on relay node
-		relayNode, err := logic.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
-			logic.UpsertNode(&relayNode)
-		}
-	}
-	if node.IsRelay {
-		// unset all the relayed nodes
-		logic.SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
-	}
+	var gwClients []models.ExtClient
 	if node.IsIngressGateway {
-		// delete ext clients belonging to ingress gatewa
-		go func(node models.Node) {
-			if err = logic.DeleteGatewayExtClients(node.ID.String(), node.Network); err != nil {
-				slog.Error("failed to delete extclients", "gatewayid", node.ID.String(), "network", node.Network, "error", err.Error())
-			}
-		}(node)
-
+		gwClients = logic.GetGwExtclients(node.ID.String(), node.Network)
 	}
-
 	purge := forceDelete || fromNode
 	if err := logic.DeleteNode(&node, purge); err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete node"), "internal"))
@@ -762,23 +738,7 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 
 	logic.ReturnSuccessResponse(w, r, nodeid+" deleted.")
 	logger.Log(1, r.Header.Get("user"), "Deleted node", nodeid, "from network", params["network"])
-	go func() { // notify of peer change
-		if !fromNode {
-			if err := mq.NodeUpdate(&node); err != nil {
-				slog.Error("error publishing node update to node", "node", node.ID, "error", err)
-			}
-		}
-		if err := mq.PublishDeletedNodePeerUpdate(&node); err != nil {
-			logger.Log(1, "error publishing peer update ", err.Error())
-		}
-		host, err := logic.GetHost(node.HostID.String())
-		if err != nil {
-			logger.Log(1, "failed to retrieve host for node", node.ID.String(), err.Error())
-		}
-		if err := mq.PublishDNSDelete(&node, host); err != nil {
-			logger.Log(1, "error publishing dns update", err.Error())
-		}
-	}()
+	go mq.PublishMqUpdatesForDeletedNode(node, !fromNode, gwClients)
 }
 
 func validateParams(nodeid, netid string) (models.Node, error) {

+ 7 - 7
go.mod

@@ -6,7 +6,7 @@ require (
 	github.com/eclipse/paho.mqtt.golang v1.4.3
 	github.com/go-playground/validator/v10 v10.15.5
 	github.com/golang-jwt/jwt/v4 v4.5.0
-	github.com/google/uuid v1.3.1
+	github.com/google/uuid v1.4.0
 	github.com/gorilla/handlers v1.5.1
 	github.com/gorilla/mux v1.8.0
 	github.com/lib/pq v1.10.9
@@ -15,10 +15,10 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.8.4
 	github.com/txn2/txeh v1.5.5
-	golang.org/x/crypto v0.13.0
-	golang.org/x/net v0.15.0 // indirect
-	golang.org/x/oauth2 v0.12.0
-	golang.org/x/sys v0.12.0 // indirect
+	golang.org/x/crypto v0.14.0
+	golang.org/x/net v0.17.0 // indirect
+	golang.org/x/oauth2 v0.13.0
+	golang.org/x/sys v0.13.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	google.golang.org/protobuf v1.31.0 // indirect
@@ -32,7 +32,7 @@ require (
 )
 
 require (
-	github.com/coreos/go-oidc/v3 v3.6.0
+	github.com/coreos/go-oidc/v3 v3.7.0
 	github.com/gorilla/websocket v1.5.0
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 )
@@ -67,5 +67,5 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	golang.org/x/sync v0.1.0 // indirect
-	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/appengine v1.6.8 // indirect
 )

+ 35 - 17
go.sum

@@ -7,8 +7,8 @@ filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5E
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/c-robinson/iplib v1.0.7 h1:Dh9AINAlkc+NsNzZuFiVs+pi3AjN+0B7mu01KHdJKHU=
 github.com/c-robinson/iplib v1.0.7/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
-github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
-github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
+github.com/coreos/go-oidc/v3 v3.7.0 h1:FTdj0uexT4diYIPlF4yoFVI5MRO1r5+SEcIpEw9vC0o=
+github.com/coreos/go-oidc/v3 v3.7.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -34,15 +34,15 @@ github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7N
 github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
-github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
 github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
@@ -101,34 +101,52 @@ github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
-golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
-golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
-golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
+golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=

+ 1 - 1
k8s/client/netclient-daemonset.yaml

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       containers:
       - name: netclient
-        image: gravitl/netclient:v0.21.1
+        image: gravitl/netclient:v0.21.2
         env:
         - name: TOKEN
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/client/netclient.yaml

@@ -28,7 +28,7 @@ spec:
       #           - "<node label value>"
       containers:
       - name: netclient
-        image: gravitl/netclient:v0.21.1
+        image: gravitl/netclient:v0.21.2
         env:
         - name: TOKEN
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/server/netmaker-ui.yaml

@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: netmaker-ui
-        image: gravitl/netmaker-ui:v0.21.1
+        image: gravitl/netmaker-ui:v0.21.2
         ports:
         - containerPort: 443
         env:

+ 2 - 1
logic/dns.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"encoding/json"
+	"fmt"
 	"os"
 	"regexp"
 	"sort"
@@ -87,7 +88,7 @@ func GetNodeDNS(network string) ([]models.DNSEntry, error) {
 			continue
 		}
 		var entry = models.DNSEntry{}
-		entry.Name = host.Name
+		entry.Name = fmt.Sprintf("%s.%s", host.Name, network)
 		entry.Network = network
 		if node.Address.IP != nil {
 			entry.Address = node.Address.IP.String()

+ 47 - 3
logic/enrollmentkey.go

@@ -7,8 +7,10 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slices"
 )
 
 // EnrollmentErrors - struct for holding EnrollmentKey error messages
@@ -29,12 +31,12 @@ var EnrollmentErrors = struct {
 }
 
 // CreateEnrollmentKey - creates a new enrollment key in db
-func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, unlimited bool) (k *models.EnrollmentKey, err error) {
+func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, unlimited bool, relay uuid.UUID) (*models.EnrollmentKey, error) {
 	newKeyID, err := getUniqueEnrollmentID()
 	if err != nil {
 		return nil, err
 	}
-	k = &models.EnrollmentKey{
+	k := &models.EnrollmentKey{
 		Value:         newKeyID,
 		Expiration:    time.Time{},
 		UsesRemaining: 0,
@@ -42,6 +44,7 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 		Networks:      []string{},
 		Tags:          []string{},
 		Type:          models.Undefined,
+		Relay:         relay,
 	}
 	if uses > 0 {
 		k.UsesRemaining = uses
@@ -61,10 +64,51 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 	if ok := k.Validate(); !ok {
 		return nil, EnrollmentErrors.InvalidCreate
 	}
+	if relay != uuid.Nil {
+		relayNode, err := GetNodeByID(relay.String())
+		if err != nil {
+			return nil, err
+		}
+		if !slices.Contains(k.Networks, relayNode.Network) {
+			return nil, errors.New("relay node not in key's networks")
+		}
+		if !relayNode.IsRelay {
+			return nil, errors.New("relay node is not a relay")
+		}
+	}
 	if err = upsertEnrollmentKey(k); err != nil {
 		return nil, err
 	}
-	return
+	return k, nil
+}
+
+// UpdateEnrollmentKey - updates an existing enrollment key's associated relay
+func UpdateEnrollmentKey(keyId string, relayId uuid.UUID) (*models.EnrollmentKey, error) {
+	key, err := GetEnrollmentKey(keyId)
+	if err != nil {
+		return nil, err
+	}
+
+	if relayId != uuid.Nil {
+		relayNode, err := GetNodeByID(relayId.String())
+		if err != nil {
+			return nil, err
+		}
+		if !slices.Contains(key.Networks, relayNode.Network) {
+			return nil, errors.New("relay node not in key's networks")
+		}
+		if !relayNode.IsRelay {
+			return nil, errors.New("relay node is not a relay")
+		}
+	}
+
+	key.Relay = relayId
+
+	if err = upsertEnrollmentKey(key); err != nil {
+		return nil, err
+	}
+
+	return key, nil
 }
 
 // GetAllEnrollmentKeys - fetches all enrollment keys from DB

+ 14 - 13
logic/enrollmentkey_test.go

@@ -4,6 +4,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/models"
 	"github.com/stretchr/testify/assert"
@@ -13,35 +14,35 @@ func TestCreateEnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	t.Run("Can_Not_Create_Key", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, false, uuid.Nil)
 		assert.Nil(t, newKey)
 		assert.NotNil(t, err)
 		assert.Equal(t, err, EnrollmentErrors.InvalidCreate)
 	})
 	t.Run("Can_Create_Key_Uses", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, false)
+		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
 		assert.Nil(t, err)
 		assert.Equal(t, 1, newKey.UsesRemaining)
 		assert.True(t, newKey.IsValid())
 	})
 	t.Run("Can_Create_Key_Time", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, false)
+		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, false, uuid.Nil)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 	})
 	t.Run("Can_Create_Key_Unlimited", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, true)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, true, uuid.Nil)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 	})
 	t.Run("Can_Create_Key_WithNetworks", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Networks) == 2)
 	})
 	t.Run("Can_Create_Key_WithTags", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, true)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, true, uuid.Nil)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Tags) == 2)
@@ -61,7 +62,7 @@ func TestCreateEnrollmentKey(t *testing.T) {
 func TestDelete_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
 	t.Run("Can_Delete_Key", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		err := DeleteEnrollmentKey(newKey.Value)
@@ -82,7 +83,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 func TestDecrement_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false)
+	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
 	t.Run("Check_initial_uses", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		assert.Equal(t, newKey.UsesRemaining, 1)
@@ -106,9 +107,9 @@ func TestDecrement_EnrollmentKey(t *testing.T) {
 func TestUsability_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false)
-	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, false)
-	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, true)
+	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
+	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, false, uuid.Nil)
+	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, true, uuid.Nil)
 	t.Run("Check if valid use key can be used", func(t *testing.T) {
 		assert.Equal(t, key1.UsesRemaining, 1)
 		ok := TryToUseEnrollmentKey(key1)
@@ -144,7 +145,7 @@ func removeAllEnrollments() {
 func TestTokenize_EnrollmentKeys(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
 	const defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5"
 	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
@@ -177,7 +178,7 @@ func TestTokenize_EnrollmentKeys(t *testing.T) {
 func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
 	const b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
 

+ 16 - 1
logic/extpeers.go

@@ -107,7 +107,7 @@ func GetNetworkExtClients(network string) ([]models.ExtClient, error) {
 		if err != nil {
 			continue
 		}
-		key, err := GetRecordKey(extclient.ClientID, network)
+		key, err := GetRecordKey(extclient.ClientID, extclient.Network)
 		if err == nil {
 			storeExtClientInCache(key, extclient)
 		}
@@ -137,6 +137,21 @@ func GetExtClient(clientid string, network string) (models.ExtClient, error) {
 	return extclient, err
 }
 
+// GetGwExtclients - return all ext clients attached to the passed gw id
+func GetGwExtclients(nodeID, network string) []models.ExtClient {
+	gwClients := []models.ExtClient{}
+	clients, err := GetNetworkExtClients(network)
+	if err != nil {
+		return gwClients
+	}
+	for _, client := range clients {
+		if client.IngressGatewayID == nodeID {
+			gwClients = append(gwClients, client)
+		}
+	}
+	return gwClients
+}
+
 // GetExtClient - gets a single ext client on a network
 func GetExtClientByPubKey(publicKey string, network string) (*models.ExtClient, error) {
 	netClients, err := GetNetworkExtClients(network)

+ 1 - 2
logic/hosts.go

@@ -199,7 +199,6 @@ func UpdateHost(newHost, currentHost *models.Host) {
 	newHost.Nodes = currentHost.Nodes
 	newHost.PublicKey = currentHost.PublicKey
 	newHost.TrafficKeyPublic = currentHost.TrafficKeyPublic
-
 	// changeable fields
 	if len(newHost.Version) == 0 {
 		newHost.Version = currentHost.Version
@@ -393,7 +392,7 @@ func DissasociateNodeFromHost(n *models.Node, h *models.Host) error {
 			}
 		}
 	}()
-	if err := deleteNodeByID(n); err != nil {
+	if err := DeleteNodeByID(n); err != nil {
 		return err
 	}
 

+ 24 - 1
logic/networks.go

@@ -23,7 +23,6 @@ func GetNetworks() ([]models.Network, error) {
 	var networks []models.Network
 
 	collection, err := database.FetchRecords(database.NETWORKS_TABLE_NAME)
-
 	if err != nil {
 		return networks, err
 	}
@@ -72,6 +71,9 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 		}
 		network.AddressRange6 = normalizedRange
 	}
+	if !IsNetworkCIDRUnique(network.GetNetworkNetworkCIDR4(), network.GetNetworkNetworkCIDR6()) {
+		return models.Network{}, errors.New("network cidr already in use")
+	}
 
 	network.SetDefaults()
 	network.SetNodesLastModified()
@@ -101,6 +103,27 @@ func GetNetworkNonServerNodeCount(networkName string) (int, error) {
 	return len(nodes), err
 }
 
+func IsNetworkCIDRUnique(cidr4 *net.IPNet, cidr6 *net.IPNet) bool {
+	networks, err := GetNetworks()
+	if err != nil {
+		return database.IsEmptyRecord(err)
+	}
+	for _, network := range networks {
+		if intersect(network.GetNetworkNetworkCIDR4(), cidr4) ||
+			intersect(network.GetNetworkNetworkCIDR6(), cidr6) {
+			return false
+		}
+	}
+	return true
+}
+
+func intersect(n1, n2 *net.IPNet) bool {
+	if n1 == nil || n2 == nil {
+		return false
+	}
+	return n2.Contains(n1.IP) || n1.Contains(n2.IP)
+}
+
 // GetParentNetwork - get parent network
 func GetParentNetwork(networkname string) (models.Network, error) {
 

+ 45 - 9
logic/nodes.go

@@ -183,6 +183,33 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 func 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())
+		}
+	}
+	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.IsRelay {
+		// unset all the relayed nodes
+		SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
+	}
+
 	if !purge && !alreadyDeleted {
 		newnode := *node
 		newnode.PendingDelete = true
@@ -198,7 +225,7 @@ func DeleteNode(node *models.Node, purge bool) error {
 	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 {
+		if delErr := DeleteNodeByID(node); delErr != nil {
 			logger.Log(0, "failed to delete node", node.ID.String(), delErr.Error())
 		}
 		return err
@@ -215,16 +242,25 @@ func DeleteNode(node *models.Node, purge bool) error {
 	return nil
 }
 
-// deleteNodeByID - deletes a node from database
-func deleteNodeByID(node *models.Node) error {
-	var err error
-	var key = node.ID.String()
-	//delete any ext clients as required
-	if node.IsIngressGateway {
-		if err := DeleteGatewayExtClients(node.ID.String(), node.Network); err != nil {
-			logger.Log(0, "failed to deleted ext clients", err.Error())
+// GetNodeByHostRef - gets the node by host id and network
+func 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 database
+func 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

+ 0 - 2
logic/peers.go

@@ -38,8 +38,6 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		HostNetworkInfo: models.HostInfoMap{},
 	}
 
-	// endpoint detection always comes from the server
-	hostPeerUpdate.EndpointDetection = servercfg.EndpointDetectionEnabled()
 	slog.Debug("peer update for host", "hostId", host.ID.String())
 	peerIndexMap := make(map[string]int)
 	for _, nodeID := range host.Nodes {

+ 6 - 2
logic/zombie.go

@@ -107,6 +107,7 @@ func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 							logger.Log(1, "error deleting zombie node", zombies[i].String(), err.Error())
 							continue
 						}
+						node.PendingDelete = true
 						node.Action = models.NODE_DELETE
 						peerUpdate <- &node
 						logger.Log(1, "deleting zombie node", node.ID.String())
@@ -120,14 +121,17 @@ func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 					host, err := GetHost(hostZombies[i].String())
 					if err != nil {
 						logger.Log(1, "error retrieving zombie host", err.Error())
-						logger.Log(1, "deleting ", host.ID.String(), " from zombie list")
-						zombies = append(zombies[:i], zombies[i+1:]...)
+						if host != nil {
+							logger.Log(1, "deleting ", host.ID.String(), " from zombie list")
+						}
+						hostZombies = append(hostZombies[:i], hostZombies[i+1:]...)
 						continue
 					}
 					if len(host.Nodes) == 0 {
 						if err := RemoveHost(host, true); err != nil {
 							logger.Log(0, "error deleting zombie host", host.ID.String(), err.Error())
 						}
+						hostZombies = append(hostZombies[:i], hostZombies[i+1:]...)
 					}
 				}
 			}

+ 11 - 7
main.go

@@ -28,7 +28,8 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var version = "v0.21.1"
+
+var version = "v0.21.2"
 
 // Start DB Connection and start API Request Handler
 func main() {
@@ -48,6 +49,7 @@ func main() {
 	defer stop()
 	var waitGroup sync.WaitGroup
 	startControllers(&waitGroup, ctx) // start the api endpoint and mq and stun
+	startHooks()
 	<-ctx.Done()
 	waitGroup.Wait()
 }
@@ -63,6 +65,14 @@ func setupConfig(absoluteConfigPath string) {
 	}
 }
 
+func startHooks() {
+	err := logic.TimerCheckpoint()
+	if err != nil {
+		logger.Log(1, "Timer error occurred: ", err.Error())
+	}
+	logic.EnterpriseCheck()
+}
+
 func initialize() { // Client Mode Prereq Check
 	var err error
 
@@ -82,12 +92,6 @@ func initialize() { // Client Mode Prereq Check
 
 	logic.SetJWTSecret()
 
-	err = logic.TimerCheckpoint()
-	if err != nil {
-		logger.Log(1, "Timer error occurred: ", err.Error())
-	}
-	logic.EnterpriseCheck()
-
 	var authProvider = auth.InitializeAuthProvider()
 	if authProvider != "" {
 		logger.Log(0, "OAuth provider,", authProvider+",", "initialized")

+ 3 - 0
models/api_host.go

@@ -32,6 +32,7 @@ type ApiHost struct {
 	RelayedHosts        []string `json:"relay_hosts"           yaml:"relay_hosts"           bson:"relay_hosts"`
 	NatType             string   `json:"nat_type"              yaml:"nat_type"`
 	PersistentKeepalive int      `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
+	AutoUpdate          bool     `json:"autoupdate"              yaml:"autoupdate"`
 }
 
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
@@ -60,6 +61,7 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a.IsDefault = h.IsDefault
 	a.NatType = h.NatType
 	a.PersistentKeepalive = int(h.PersistentKeepalive.Seconds())
+	a.AutoUpdate = h.AutoUpdate
 	return &a
 }
 
@@ -98,5 +100,6 @@ func (a *ApiHost) ConvertAPIHostToNMHost(currentHost *Host) *Host {
 	h.NatType = currentHost.NatType
 	h.TurnEndpoint = currentHost.TurnEndpoint
 	h.PersistentKeepalive = time.Duration(a.PersistentKeepalive) * time.Second
+	h.AutoUpdate = a.AutoUpdate
 	return &h
 }

+ 4 - 0
models/enrollment_key.go

@@ -2,6 +2,8 @@ package models
 
 import (
 	"time"
+
+	"github.com/google/uuid"
 )
 
 const (
@@ -39,6 +41,7 @@ type EnrollmentKey struct {
 	Tags          []string  `json:"tags"`
 	Token         string    `json:"token,omitempty"` // B64 value of EnrollmentToken
 	Type          KeyType   `json:"type"`
+	Relay         uuid.UUID `json:"relay"`
 }
 
 // APIEnrollmentKey - used to create enrollment keys via API
@@ -49,6 +52,7 @@ type APIEnrollmentKey struct {
 	Unlimited     bool     `json:"unlimited"`
 	Tags          []string `json:"tags"`
 	Type          KeyType  `json:"type"`
+	Relay         string   `json:"relay"`
 }
 
 // RegisterResponse - the response to a successful enrollment register

+ 1 - 0
models/host.go

@@ -160,4 +160,5 @@ type RegisterMsg struct {
 	User         string `json:"user,omitempty"`
 	Password     string `json:"password,omitempty"`
 	JoinAll      bool   `json:"join_all,omitempty"`
+	Relay        string `json:"relay,omitempty"`
 }

+ 11 - 12
models/mqtt.go

@@ -8,18 +8,17 @@ import (
 
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
-	Host              Host                 `json:"host" bson:"host" yaml:"host"`
-	NodeAddrs         []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
-	Server            string               `json:"server" bson:"server" yaml:"server"`
-	ServerVersion     string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
-	ServerAddrs       []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
-	NodePeers         []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
-	Peers             []wgtypes.PeerConfig
-	PeerIDs           PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
-	EndpointDetection bool                  `json:"endpointdetection" yaml:"endpointdetection"`
-	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
-	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate          FwUpdate              `json:"fw_update"`
+	Host            Host                 `json:"host" bson:"host" yaml:"host"`
+	NodeAddrs       []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
+	Server          string               `json:"server" bson:"server" yaml:"server"`
+	ServerVersion   string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
+	ServerAddrs     []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
+	NodePeers       []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
+	Peers           []wgtypes.PeerConfig
+	PeerIDs         PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
+	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate        FwUpdate              `json:"fw_update"`
 }
 
 // IngressInfo - struct for ingress info

+ 16 - 0
models/network.go

@@ -1,6 +1,7 @@
 package models
 
 import (
+	"net"
 	"time"
 )
 
@@ -81,3 +82,18 @@ func (network *Network) SetDefaults() {
 		network.DefaultACL = "yes"
 	}
 }
+
+func (network *Network) GetNetworkNetworkCIDR4() *net.IPNet {
+	if network.AddressRange == "" {
+		return nil
+	}
+	_, netCidr, _ := net.ParseCIDR(network.AddressRange)
+	return netCidr
+}
+func (network *Network) GetNetworkNetworkCIDR6() *net.IPNet {
+	if network.AddressRange6 == "" {
+		return nil
+	}
+	_, netCidr, _ := net.ParseCIDR(network.AddressRange6)
+	return netCidr
+}

+ 6 - 5
models/structs.go

@@ -223,11 +223,12 @@ type TrafficKeys struct {
 
 // HostPull - response of a host's pull
 type HostPull struct {
-	Host         Host                 `json:"host" yaml:"host"`
-	Nodes        []Node               `json:"nodes" yaml:"nodes"`
-	Peers        []wgtypes.PeerConfig `json:"peers" yaml:"peers"`
-	ServerConfig ServerConfig         `json:"server_config" yaml:"server_config"`
-	PeerIDs      PeerMap              `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
+	Host            Host                 `json:"host" yaml:"host"`
+	Nodes           []Node               `json:"nodes" yaml:"nodes"`
+	Peers           []wgtypes.PeerConfig `json:"peers" yaml:"peers"`
+	ServerConfig    ServerConfig         `json:"server_config" yaml:"server_config"`
+	PeerIDs         PeerMap              `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
+	HostNetworkInfo HostInfoMap          `json:"host_network_info,omitempty"  yaml:"host_network_info,omitempty"`
 }
 
 // NodeGet - struct for a single node get response

+ 33 - 3
mq/handlers.go

@@ -60,7 +60,21 @@ func UpdateNode(client mqtt.Client, msg mqtt.Message) {
 		return
 	}
 	if ifaceDelta { // reduce number of unneeded updates, by only sending on iface changes
-		if err = PublishPeerUpdate(); err != nil {
+		if !newNode.Connected {
+			err = PublishDeletedNodePeerUpdate(&newNode)
+			host, err := logic.GetHost(newNode.HostID.String())
+			if err != nil {
+				slog.Error("failed to get host for the node", "nodeid", newNode.ID.String(), "error", err)
+				return
+			}
+			allNodes, err := logic.GetAllNodes()
+			if err == nil {
+				PublishSingleHostPeerUpdate(host, allNodes, nil, nil)
+			}
+		} else {
+			err = PublishPeerUpdate()
+		}
+		if err != nil {
 			slog.Warn("error updating peers when node informed the server of an interface change", "nodeid", currentNode.ID, "error", err)
 		}
 	}
@@ -116,7 +130,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 					slog.Error("failed peers publish after join acknowledged", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 					return
 				}
-				if err = handleNewNodeDNS(&hu.Host, &hu.Node); err != nil {
+				if err = HandleNewNodeDNS(&hu.Host, &hu.Node); err != nil {
 					slog.Error("failed to send dns update after node added to host", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 					return
 				}
@@ -159,6 +173,22 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 				return
 			}
 		}
+
+		// notify of deleted peer change
+		go func(host models.Host) {
+			for _, nodeID := range host.Nodes {
+				node, err := logic.GetNodeByID(nodeID)
+				if err == nil {
+					var gwClients []models.ExtClient
+					if node.IsIngressGateway {
+						gwClients = logic.GetGwExtclients(node.ID.String(), node.Network)
+					}
+					go PublishMqUpdatesForDeletedNode(node, false, gwClients)
+				}
+
+			}
+		}(*currentHost)
+
 		if err := logic.DisassociateAllNodesFromHost(currentHost.ID.String()); err != nil {
 			slog.Error("failed to delete all nodes of host", "id", currentHost.ID, "error", err)
 			return
@@ -217,7 +247,7 @@ func ClientPeerUpdate(client mqtt.Client, msg mqtt.Message) {
 	slog.Info("sent peer updates after signal received from", "id", id)
 }
 
-func handleNewNodeDNS(host *models.Host, node *models.Node) error {
+func HandleNewNodeDNS(host *models.Host, node *models.Node) error {
 	dns := models.DNSUpdate{
 		Action: models.DNSInsert,
 		Name:   host.Name + "." + node.Network,

+ 64 - 4
mq/publishers.go

@@ -10,6 +10,7 @@ import (
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slog"
 )
 
 // PublishPeerUpdate --- determines and publishes a peer update to all the hosts
@@ -95,10 +96,6 @@ func PublishSingleHostPeerUpdate(host *models.Host, allNodes []models.Node, dele
 	if err != nil {
 		return err
 	}
-	if len(peerUpdate.Peers) == 0 { // no peers to send
-		return nil
-	}
-
 	data, err := json.Marshal(&peerUpdate)
 	if err != nil {
 		return err
@@ -169,6 +166,27 @@ func ServerStartNotify() error {
 	return nil
 }
 
+// PublishDNSUpdatev1 - published dns updates to all nodes passed
+func PublishDNSUpdatev1(network string, dns models.DNSUpdate, nodes []models.Node) error {
+	for _, node := range nodes {
+		host, err := logic.GetHost(node.HostID.String())
+		if err != nil {
+			logger.Log(0, "error retrieving host for dns update", node.HostID.String(), err.Error())
+			continue
+		}
+		data, err := json.Marshal(dns)
+		if err != nil {
+			logger.Log(0, "failed to encode dns data for node", node.ID.String(), err.Error())
+		}
+		if err := publish(host, "dns/update/"+host.ID.String()+"/"+servercfg.GetServer(), data); err != nil {
+			logger.Log(0, "error publishing dns update to host", host.ID.String(), err.Error())
+			continue
+		}
+		logger.Log(3, "published dns update to host", host.ID.String())
+	}
+	return nil
+}
+
 // PublishDNSUpdate publishes a dns update to all nodes on a network
 func PublishDNSUpdate(network string, dns models.DNSUpdate) error {
 	nodes, err := logic.GetNetworkNodes(network)
@@ -215,6 +233,32 @@ func PublishAllDNS(newnode *models.Node) error {
 	return nil
 }
 
+// PublishMqUpdatesForDeletedNode - published all the required updates for deleted node
+func PublishMqUpdatesForDeletedNode(node models.Node, sendNodeUpdate bool, gwClients []models.ExtClient) {
+	// notify of peer change
+	node.PendingDelete = true
+	node.Action = models.NODE_DELETE
+	if sendNodeUpdate {
+		if err := NodeUpdate(&node); err != nil {
+			slog.Error("error publishing node update to node", "node", node.ID, "error", err)
+		}
+	}
+	if err := PublishDeletedNodePeerUpdate(&node); err != nil {
+		logger.Log(1, "error publishing peer update ", err.Error())
+	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logger.Log(1, "failed to retrieve host for node", node.ID.String(), err.Error())
+	}
+	if err := PublishDNSDelete(&node, host); err != nil {
+		logger.Log(1, "error publishing dns update", err.Error())
+	}
+	if err := PublishDeleteAllExtclientsDNS(node.Network, gwClients); err != nil {
+		logger.Log(1, "error publishing ext dns update", err.Error())
+	}
+
+}
+
 // PublishDNSDelete publish a dns update deleting a node to all hosts on a network
 func PublishDNSDelete(node *models.Node, host *models.Host) error {
 	dns := models.DNSUpdate{
@@ -299,6 +343,22 @@ func PublishExtClientDNSUpdate(old, new models.ExtClient, network string) error
 	return nil
 }
 
+// PublishDeleteAllExtclientsDNS - publish to delete all passed ext clients dns entries
+func PublishDeleteAllExtclientsDNS(network string, clients []models.ExtClient) error {
+	nodes, err := logic.GetNetworkNodes(network)
+	if err != nil {
+		return err
+	}
+	for _, client := range clients {
+		dns := models.DNSUpdate{
+			Action: models.DNSDeleteByName,
+			Name:   client.ClientID + "." + client.Network,
+		}
+		go PublishDNSUpdatev1(client.Network, dns, nodes)
+	}
+	return nil
+}
+
 // PublishDeleteExtClientDNS publish dns update to delete extclient entry
 func PublishDeleteExtClientDNS(client *models.ExtClient) error {
 	dns := models.DNSUpdate{

+ 1 - 1
mq/util.go

@@ -34,7 +34,7 @@ func decryptMsgWithHost(host *models.Host, msg []byte) ([]byte, error) {
 
 func DecryptMsg(node *models.Node, msg []byte) ([]byte, error) {
 	if len(msg) <= 24 { // make sure message is of appropriate length
-		return nil, fmt.Errorf("recieved invalid message from broker %v", msg)
+		return nil, fmt.Errorf("received invalid message from broker %v", msg)
 	}
 	host, err := logic.GetHost(node.HostID.String())
 	if err != nil {

+ 28 - 13
pro/license.go

@@ -213,28 +213,43 @@ func validateLicenseKey(encryptedData []byte, publicKey *[32]byte) ([]byte, erro
 	req.Header.Add("Content-Type", "application/json")
 	req.Header.Add("Accept", "application/json")
 	client := &http.Client{}
-	var body []byte
 	validateResponse, err := client.Do(req)
 	if err != nil { // check cache
-		body, err = getCachedResponse()
-		if err != nil {
-			return nil, err
-		}
 		slog.Warn("proceeding with cached response, Netmaker API may be down")
-	} else {
-		defer validateResponse.Body.Close()
-		if validateResponse.StatusCode != 200 {
-			return nil, fmt.Errorf("could not validate license, got status code %d", validateResponse.StatusCode)
-		} // if you received a 200 cache the response locally
+		return getCachedResponse()
+	}
+	defer validateResponse.Body.Close()
+	code := validateResponse.StatusCode
 
-		body, err = io.ReadAll(validateResponse.Body)
+	// if we received a 200, cache the response locally
+	if code == http.StatusOK {
+		body, err := io.ReadAll(validateResponse.Body)
 		if err != nil {
+			slog.Warn("failed to parse response", "error", err)
 			return nil, err
 		}
-		cacheResponse(body)
+		if err := cacheResponse(body); err != nil {
+			slog.Warn("failed to cache response", "error", err)
+		}
+		return body, nil
+	}
+
+	// at this point the backend returned some undesired state
+
+	// inform failure via logs
+	body, _ := io.ReadAll(validateResponse.Body)
+	err = fmt.Errorf("could not validate license with validation backend (status={%d}, body={%s})",
+		validateResponse.StatusCode, string(body))
+	slog.Warn(err.Error())
+
+	// try to use cache if we had a temporary error
+	if code == http.StatusServiceUnavailable || code == http.StatusGatewayTimeout {
+		slog.Warn("proceeding with cached response, Netmaker API may be down")
+		return getCachedResponse()
 	}
 
-	return body, err
+	// at this point the error is irreversible, return it
+	return nil, err
 }
 
 func getAccountsHost() string {

+ 1 - 0
pro/logic/relays.go

@@ -149,6 +149,7 @@ func RelayUpdates(currentNode, newNode *models.Node) bool {
 	return relayUpdates
 }
 
+// UpdateRelayed - updates a relay's relayed nodes, and sends updates to the relayed nodes over MQ
 func UpdateRelayed(currentNode, newNode *models.Node) {
 	updatenodes := updateRelayNodes(currentNode.ID.String(), currentNode.RelayedNodes, newNode.RelayedNodes)
 	if len(updatenodes) > 0 {

+ 8 - 9
release.md

@@ -1,20 +1,19 @@
 
-# Netmaker v0.21.1
+# Netmaker v0.21.2
 
 ## Whats New
-- Remote access client session management, refer users section in docs for more details
-- Can now create generic DNS entries
-- Upgrade client version to match server version from UI
-- Moved PersistentKeepAlive setting from node to host level
+- Auto Relay via Enrollment key
+- Local Routing Improvements
 ## What's Fixed
-- Extclients DNS now properly set from ingress dns value provided
-- Allow role update of OAuth user
-- Fixed zombie node issue
+- Inconsistency in DNS Entries for Network has been fixed
+- Unique network CIDR validation
+- Fixed extclient caching decrepancies
+- Deleted node peer update fixes when disconnected from network
+- Force Deletion of Daemon Nodes that are stuck in removing state
 ## known issues
 - Windows installer does not install WireGuard
 - netclient-gui will continously display error dialog if netmaker server is offline
 - Mac IPv6 addresses/route issues
-- Docker client can not re-join after complete deletion
 - netclient-gui network tab blank after disconnect
 
 

+ 0 - 1
scripts/netmaker.default.env

@@ -19,7 +19,6 @@ NETMAKER_TENANT_ID=
 LICENSE_KEY=
 SERVER_IMAGE_TAG=
 UI_IMAGE_TAG=
-NETCLIENT_ENDPOINT_DETECTION=disabled
 # used for HA - identifies this server vs other servers
 NODE_ID=netmaker-server-1
 METRICS_EXPORTER=off

+ 4 - 4
scripts/nm-quick.sh

@@ -309,7 +309,7 @@ save_config() { (
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "STUN_PORT" "VERBOSITY"
 		"TURN_PORT" "USE_TURN" "DEBUG_MODE" "TURN_API_PORT" "REST_BACKEND"
-		"DISABLE_REMOTE_IP_CHECK" "NETCLIENT_ENDPOINT_DETECTION" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
+		"DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
 		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE")
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
@@ -370,7 +370,6 @@ local_install_setup() { (
 	else
 		cp docker/Caddyfile "$SCRIPT_DIR/Caddyfile"
 	fi
-	cp scripts/nm-certs.sh "$SCRIPT_DIR/nm-certs.sh"
 	cp scripts/netmaker.default.env "$SCRIPT_DIR/netmaker.default.env"
 	cp docker/mosquitto.conf "$SCRIPT_DIR/mosquitto.conf"
 	cp docker/wait.sh "$SCRIPT_DIR/wait.sh"
@@ -776,8 +775,9 @@ install_netmaker() {
 	export COMPOSE_HTTP_TIMEOUT=120
 
 	# start docker and rebuild containers / networks
-	docker-compose -f "$SCRIPT_DIR"/docker-compose.yml up -d --force-recreate
-
+	cd "${SCRIPT_DIR}"
+	docker-compose up -d --force-recreate
+	cd -
 	wait_seconds 2
 
 }

+ 1 - 1
scripts/nm-upgrade-0-17-1-to-0-19-0.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 
-LATEST="v0.21.1"
+LATEST="v0.21.2"
 INSTALL_PATH="/root"
 
 trap restore_old_netmaker_instructions

+ 1 - 1
scripts/nm-upgrade.sh

@@ -179,7 +179,7 @@ save_config() { (
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "STUN_PORT" "VERBOSITY"
 		"TURN_PORT" "USE_TURN" "DEBUG_MODE" "TURN_API_PORT" "REST_BACKEND"
-		"DISABLE_REMOTE_IP_CHECK" "NETCLIENT_ENDPOINT_DETECTION" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
+		"DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
 		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE")
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"

+ 0 - 16
servercfg/serverconf.go

@@ -53,11 +53,6 @@ func GetServerConfig() config.ServerConfig {
 	} else {
 		cfg.NetclientAutoUpdate = "disabled"
 	}
-	if EndpointDetectionEnabled() {
-		cfg.NetclientEndpointDetection = "enabled"
-	} else {
-		cfg.NetclientEndpointDetection = "disabled"
-	}
 	if IsRestBackend() {
 		cfg.RestBackend = "on"
 	}
@@ -405,17 +400,6 @@ func AutoUpdateEnabled() bool {
 	return true
 }
 
-// EndpointDetectionEnabled returns a boolean indicating whether netclient endpoint detection is enabled or disabled
-// default is enabled
-func EndpointDetectionEnabled() bool {
-	if os.Getenv("NETCLIENT_ENDPOINT_DETECTION") == "disabled" {
-		return false
-	} else if config.Config.Server.NetclientEndpointDetection == "disabled" {
-		return false
-	}
-	return true
-}
-
 // IsDNSMode - should it run with DNS
 func IsDNSMode() bool {
 	isdns := true

+ 1 - 1
swagger.yml

@@ -1149,7 +1149,7 @@ info:
 
         API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
     title: Netmaker
-    version: 0.21.1
+    version: 0.21.2
 paths:
     /api/dns:
         get: