Bläddra i källkod

Merge pull request #3724 from gravitl/release-v1.2.0

v1.2.0
Abhishek K 1 månad sedan
förälder
incheckning
e9b00001f5
85 ändrade filer med 2711 tillägg och 733 borttagningar
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 1 1
      .github/workflows/docs.yml
  3. 4 4
      .github/workflows/test.yml
  4. 2 2
      Dockerfile
  5. 1 1
      Dockerfile-quick
  6. 1 1
      README.md
  7. 40 17
      auth/host_session.go
  8. 1 1
      compose/docker-compose.netclient.yml
  9. 4 0
      controllers/acls.go
  10. 22 14
      controllers/dns.go
  11. 22 4
      controllers/egress.go
  12. 5 0
      controllers/enrollmentkeys.go
  13. 9 0
      controllers/ext_client.go
  14. 23 0
      controllers/gateway.go
  15. 51 31
      controllers/hosts.go
  16. 5 0
      controllers/inet_gws.go
  17. 72 13
      controllers/network.go
  18. 65 5
      controllers/node.go
  19. 9 51
      controllers/server.go
  20. 8 0
      controllers/user.go
  21. 1 1
      docker/Dockerfile-go-builder
  22. 27 29
      go.mod
  23. 62 60
      go.sum
  24. 1 1
      k8s/client/netclient-daemonset.yaml
  25. 1 1
      k8s/client/netclient.yaml
  26. 1 1
      k8s/server/netmaker-ui.yaml
  27. 12 12
      logic/acls.go
  28. 0 4
      logic/acls/nodeacls/retrieve.go
  29. 64 25
      logic/dns.go
  30. 75 2
      logic/egress.go
  31. 15 12
      logic/enrollmentkey.go
  32. 16 15
      logic/enrollmentkey_test.go
  33. 8 1
      logic/gateway.go
  34. 27 0
      logic/hosts.go
  35. 64 0
      logic/metrics.go
  36. 5 4
      logic/networks.go
  37. 33 2
      logic/nodes.go
  38. 65 12
      logic/peers.go
  39. 17 5
      logic/relay.go
  40. 2 2
      logic/security.go
  41. 41 1
      logic/settings.go
  42. 78 0
      logic/usage.go
  43. 13 0
      logic/util.go
  44. 0 11
      logic/wireguard.go
  45. 2 2
      main.go
  46. 88 13
      migrate/migrate.go
  47. 33 19
      models/api_node.go
  48. 1 1
      models/egress.go
  49. 23 21
      models/enrollment_key.go
  50. 17 11
      models/host.go
  51. 1 0
      models/metrics.go
  52. 12 2
      models/mqtt.go
  53. 29 23
      models/node.go
  54. 2 0
      models/settings.go
  55. 30 24
      models/structs.go
  56. 40 0
      models/usage.go
  57. 1 0
      models/user_mgmt.go
  58. 56 39
      mq/handlers.go
  59. 1 1
      mq/publishers.go
  60. 1 1
      pro/auth/headless_callback.go
  61. 1 1
      pro/auth/register_callback.go
  62. 62 19
      pro/auth/sync.go
  63. 660 0
      pro/controllers/auto_relay.go
  64. 4 0
      pro/controllers/tags.go
  65. 33 37
      pro/controllers/users.go
  66. 1 1
      pro/email/invite.go
  67. 6 2
      pro/idp/azure/azure.go
  68. 13 0
      pro/initialize.go
  69. 1 1
      pro/license.go
  70. 297 0
      pro/logic/auto_relay.go
  71. 59 21
      pro/logic/dns.go
  72. 56 0
      pro/logic/egress.go
  73. 1 21
      pro/logic/failover.go
  74. 1 0
      pro/logic/metrics.go
  75. 16 1
      pro/logic/migrate.go
  76. 23 0
      pro/logic/security.go
  77. 1 0
      pro/logic/tags.go
  78. 85 1
      pro/logic/user_mgmt.go
  79. 3 29
      pro/types.go
  80. 0 44
      pro/util.go
  81. 30 16
      release.md
  82. 13 6
      schema/dns.go
  83. 28 28
      scripts/nm-quick.sh
  84. 5 1
      servercfg/serverconf.go
  85. 1 1
      swagger.yaml

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

@@ -31,6 +31,7 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v1.2.0
         - v1.1.0
         - v1.0.0
         - v0.99.0

+ 1 - 1
.github/workflows/docs.yml

@@ -19,7 +19,7 @@ jobs:
           ref: ${{ github.event.inputs.branch || 'master' }}
 
       - name: Setup Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: go.mod
 

+ 4 - 4
.github/workflows/test.yml

@@ -13,7 +13,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v5
       - name: Setup Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
       - name: Build
@@ -27,7 +27,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v5
       - name: Setup go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
       - name: Build
@@ -44,7 +44,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v5
       - name: Setup Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
       - name: run tests
@@ -64,7 +64,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v5
       - name: Setup Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
       - name: run static checks

+ 2 - 2
Dockerfile

@@ -1,12 +1,12 @@
 #first stage - builder
-FROM gravitl/go-builder:1.23.0 AS builder
+FROM gravitl/go-builder:1.24.0 AS builder
 ARG tags 
 WORKDIR /app
 COPY . .
 
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
-FROM alpine:3.22.1
+FROM alpine:3.22.2
 
 # add a c lib
 # set the working directory

+ 1 - 1
Dockerfile-quick

@@ -1,5 +1,5 @@
 #first stage - builder
-FROM alpine:3.22.1
+FROM alpine:3.22.2
 ARG version 
 WORKDIR /app
 COPY ./netmaker /root/netmaker

+ 1 - 1
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-1.1.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-1.2.0-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" />

+ 40 - 17
auth/host_session.go

@@ -226,7 +226,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(models.EnrollmentKey{Networks: netsToAdd}, &result.Host, "")
+		go CheckNetRegAndHostUpdate(models.EnrollmentKey{Networks: netsToAdd}, &result.Host, result.User)
 	case <-timeout: // the read from req.answerCh has timed out
 		logger.Log(0, "timeout signal recv,exiting oauth socket conn")
 		break
@@ -272,24 +272,47 @@ func CheckNetRegAndHostUpdate(key models.EnrollmentKey, h *models.Host, username
 				continue
 			}
 
-			logic.LogEvent(&models.Event{
-				Action: models.JoinHostToNet,
-				Source: models.Subject{
-					ID:   key.Value,
-					Name: key.Tags[0],
-					Type: models.EnrollmentKeySub,
-				},
-				TriggeredBy: username,
-				Target: models.Subject{
-					ID:   h.ID.String(),
-					Name: h.Name,
-					Type: models.DeviceSub,
-				},
-				NetworkID: models.NetworkID(netID),
-				Origin:    models.Dashboard,
-			})
+			if len(username) > 0 {
+				logic.LogEvent(&models.Event{
+					Action: models.JoinHostToNet,
+					Source: models.Subject{
+						ID:   username,
+						Name: username,
+						Type: models.UserSub,
+					},
+					TriggeredBy: username,
+					Target: models.Subject{
+						ID:   h.ID.String(),
+						Name: h.Name,
+						Type: models.DeviceSub,
+					},
+					NetworkID: models.NetworkID(netID),
+					Origin:    models.Dashboard,
+				})
+			} else {
+				logic.LogEvent(&models.Event{
+					Action: models.JoinHostToNet,
+					Source: models.Subject{
+						ID:   key.Value,
+						Name: key.Tags[0],
+						Type: models.EnrollmentKeySub,
+					},
+					TriggeredBy: username,
+					Target: models.Subject{
+						ID:   h.ID.String(),
+						Name: h.Name,
+						Type: models.DeviceSub,
+					},
+					NetworkID: models.NetworkID(netID),
+					Origin:    models.Dashboard,
+				})
+			}
 
 			newNode, err := logic.UpdateHostNetwork(h, netID, true)
+			if servercfg.IsPro && key.AutoAssignGateway {
+				newNode.AutoAssignGateway = true
+				logic.UpsertNode(newNode)
+			}
 			if err == nil || strings.Contains(err.Error(), "host already part of network") {
 				if len(key.Groups) > 0 {
 					newNode.Tags = make(map[models.TagID]struct{})

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

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

+ 4 - 0
controllers/acls.go

@@ -426,6 +426,10 @@ func deleteAcl(w http.ResponseWriter, r *http.Request) {
 		},
 		NetworkID: acl.NetworkID,
 		Origin:    models.Dashboard,
+		Diff: models.Diff{
+			Old: acl,
+			New: nil,
+		},
 	})
 	go mq.PublishPeerUpdate(true)
 	logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name)

+ 22 - 14
controllers/dns.go

@@ -99,21 +99,25 @@ func createNs(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 	if req.MatchAll {
-		req.MatchDomains = []string{"."}
+		req.Domains = []schema.NameserverDomain{
+			{
+				Domain: ".",
+			},
+		}
 	}
 	ns := schema.Nameserver{
-		ID:           uuid.New().String(),
-		Name:         req.Name,
-		NetworkID:    req.NetworkID,
-		Description:  req.Description,
-		MatchAll:     req.MatchAll,
-		MatchDomains: req.MatchDomains,
-		Servers:      req.Servers,
-		Tags:         req.Tags,
-		Nodes:        req.Nodes,
-		Status:       true,
-		CreatedBy:    r.Header.Get("user"),
-		CreatedAt:    time.Now().UTC(),
+		ID:          uuid.New().String(),
+		Name:        req.Name,
+		NetworkID:   req.NetworkID,
+		Description: req.Description,
+		Servers:     req.Servers,
+		MatchAll:    req.MatchAll,
+		Domains:     req.Domains,
+		Tags:        req.Tags,
+		Nodes:       req.Nodes,
+		Status:      true,
+		CreatedBy:   r.Header.Get("user"),
+		CreatedAt:   time.Now().UTC(),
 	}
 
 	err = ns.Create(db.WithContext(r.Context()))
@@ -242,7 +246,7 @@ func updateNs(w http.ResponseWriter, r *http.Request) {
 	}
 	ns.Servers = updateNs.Servers
 	ns.Tags = updateNs.Tags
-	ns.MatchDomains = updateNs.MatchDomains
+	ns.Domains = updateNs.Domains
 	ns.MatchAll = updateNs.MatchAll
 	ns.Description = updateNs.Description
 	ns.Name = updateNs.Name
@@ -312,6 +316,10 @@ func deleteNs(w http.ResponseWriter, r *http.Request) {
 		},
 		NetworkID: models.NetworkID(ns.NetworkID),
 		Origin:    models.Dashboard,
+		Diff: models.Diff{
+			Old: ns,
+			New: nil,
+		},
 	})
 
 	go mq.PublishPeerUpdate(false)

+ 22 - 4
controllers/egress.go

@@ -84,8 +84,15 @@ func createEgress(w http.ResponseWriter, r *http.Request) {
 		CreatedBy:   r.Header.Get("user"),
 		CreatedAt:   time.Now().UTC(),
 	}
-	for nodeID, metric := range req.Nodes {
-		e.Nodes[nodeID] = metric
+	if len(req.Tags) > 0 {
+		for tagID, metric := range req.Tags {
+			e.Tags[tagID] = metric
+		}
+		e.Nodes = make(datatypes.JSONMap)
+	} else {
+		for nodeID, metric := range req.Nodes {
+			e.Nodes[nodeID] = metric
+		}
 	}
 	if err := logic.ValidateEgressReq(&e); err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -272,8 +279,15 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
 	}
 	e.Nodes = make(datatypes.JSONMap)
 	e.Tags = make(datatypes.JSONMap)
-	for nodeID, metric := range req.Nodes {
-		e.Nodes[nodeID] = metric
+	if len(req.Tags) > 0 {
+		for tagID, metric := range req.Tags {
+			e.Tags[tagID] = metric
+		}
+		e.Nodes = make(datatypes.JSONMap)
+	} else {
+		for nodeID, metric := range req.Nodes {
+			e.Nodes[nodeID] = metric
+		}
 	}
 	if e.Domain != req.Domain {
 		e.DomainAns = datatypes.JSONSlice[string]{}
@@ -386,6 +400,10 @@ func deleteEgress(w http.ResponseWriter, r *http.Request) {
 		},
 		NetworkID: models.NetworkID(e.Network),
 		Origin:    models.Dashboard,
+		Diff: models.Diff{
+			Old: e,
+			New: nil,
+		},
 	})
 	// delete related acl policies
 	acls := logic.ListAcls()

+ 5 - 0
controllers/enrollmentkeys.go

@@ -97,6 +97,10 @@ func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 			Type: models.EnrollmentKeySub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: key,
+			New: nil,
+		},
 	})
 	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
 	w.WriteHeader(http.StatusOK)
@@ -181,6 +185,7 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		relayId,
 		false,
 		enrollmentKeyBody.AutoEgress,
+		enrollmentKeyBody.AutoAssignGateway,
 	)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())

+ 9 - 0
controllers/ext_client.go

@@ -807,6 +807,8 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 
 	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(extclient)
+
 	go func() {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
 			slog.Error(
@@ -969,6 +971,13 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 	network := params["network"]
 	extclient, err := logic.GetExtClient(clientid, network)
 	if err != nil {
+		if database.IsEmptyRecord(err) {
+			logger.Log(0, r.Header.Get("user"),
+				"Deleted extclient client", params["clientid"], "from network", params["network"])
+			logic.ReturnSuccessResponse(w, r, params["clientid"]+" deleted.")
+			return
+		}
+
 		err = errors.New("Could not delete extclient " + params["clientid"])
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to get extclient [%s],network [%s]: %v", clientid, network, err))

+ 23 - 0
controllers/gateway.go

@@ -83,12 +83,21 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	if req.IsInternetGateway {
+		if host.DNS != "yes" {
+			host.DNS = "yes"
+			logic.UpsertHost(host)
+		}
+	}
 	for _, relayedNodeID := range relayNode.RelayedNodes {
 		relayedNode, err := logic.GetNodeByID(relayedNodeID)
 		if err == nil {
 			if relayedNode.FailedOverBy != uuid.Nil {
 				go logic.ResetFailedOverPeer(&relayedNode)
 			}
+			if len(relayedNode.AutoRelayedPeers) > 0 {
+				go logic.ResetAutoRelayedPeer(&relayedNode)
+			}
 
 		}
 	}
@@ -101,6 +110,12 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
 					mq.PublishPeerUpdate(false)
 				}()
 			}
+
+			go func() {
+				logic.ResetAutoRelayedPeer(&node)
+				mq.PublishPeerUpdate(false)
+			}()
+
 		}
 		if node.IsGw && node.IngressDNS == "" {
 			node.IngressDNS = "1.1.1.1"
@@ -190,6 +205,10 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
 	}
 	logic.UnsetInternetGw(&node)
 	node.IsGw = false
+	if node.IsAutoRelay {
+		logic.ResetAutoRelay(&node)
+	}
+	node.IsAutoRelay = false
 	logic.UpsertNode(&node)
 	logger.Log(1, r.Header.Get("user"), "deleted gw", nodeid, "on network", netid)
 
@@ -265,6 +284,10 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
 			Type: models.GatewaySub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: node,
+			New: node,
+		},
 	})
 	logic.GetNodeStatus(&node, false)
 	apiNode := node.ConvertToAPINode()

+ 51 - 31
controllers/hosts.go

@@ -13,7 +13,6 @@ import (
 	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/schema"
@@ -210,8 +209,9 @@ func pull(w http.ResponseWriter, r *http.Request) {
 			//slog.Error("failed to get node:", "id", node.ID, "error", err)
 			continue
 		}
-		if node.FailedOverBy != uuid.Nil && r.URL.Query().Get("reset_failovered") == "true" {
+		if r.URL.Query().Get("reset_failovered") == "true" {
 			logic.ResetFailedOverPeer(&node)
+			logic.ResetAutoRelayedPeer(&node)
 			sendPeerUpdate = true
 		}
 	}
@@ -232,19 +232,11 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	serverConf := logic.GetServerInfo()
-	key, keyErr := logic.RetrievePublicTrafficKey()
-	if keyErr != nil {
-		logger.Log(0, "error retrieving key:", keyErr.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
-		return
-	}
 	_ = logic.CheckHostPorts(host)
-	serverConf.TrafficKey = key
 	response := models.HostPull{
 		Host:              *host,
 		Nodes:             logic.GetHostNodes(host),
-		ServerConfig:      serverConf,
+		ServerConfig:      hPU.ServerConfig,
 		Peers:             hPU.Peers,
 		PeerIDs:           hPU.PeerIDs,
 		HostNetworkInfo:   hPU.HostNetworkInfo,
@@ -257,6 +249,9 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		EgressWithDomains: hPU.EgressWithDomains,
 		EndpointDetection: logic.IsEndpointDetectionEnabled(),
 		DnsNameservers:    hPU.DnsNameservers,
+		ReplacePeers:      hPU.ReplacePeers,
+		AutoRelayNodes:    hPU.AutoRelayNodes,
+		GwNodes:           hPU.GwNodes,
 	}
 
 	logger.Log(1, hostID, host.Name, "completed a pull")
@@ -292,6 +287,19 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 	newHost := newHostData.ConvertAPIHostToNMHost(currHost)
 
 	logic.UpdateHost(newHost, currHost) // update the in memory struct values
+	if newHost.DNS != "yes" {
+		// check if any node is internet gw
+		for _, nodeID := range newHost.Nodes {
+			node, err := logic.GetNodeByID(nodeID)
+			if err != nil {
+				continue
+			}
+			if node.IsInternetGateway {
+				newHost.DNS = "yes"
+				break
+			}
+		}
+	}
 	if err = logic.UpsertHost(newHost); err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -363,8 +371,7 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	var sendPeerUpdate bool
-	var replacePeers bool
+	var sendPeerUpdate, sendDeletedNodeUpdate, replacePeers bool
 	var hostUpdate models.HostUpdate
 	err = json.NewDecoder(r.Body).Decode(&hostUpdate)
 	if err != nil {
@@ -376,6 +383,10 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 	switch hostUpdate.Action {
 	case models.CheckIn:
 		sendPeerUpdate = mq.HandleHostCheckin(&hostUpdate.Host, currentHost)
+		changed := logic.CheckHostPorts(currentHost)
+		if changed {
+			mq.HostUpdate(&models.HostUpdate{Action: models.UpdateHost, Host: *currentHost})
+		}
 	case models.UpdateHost:
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
 			//remove old peer entry
@@ -388,7 +399,8 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
 			return
 		}
-
+	case models.UpdateNode:
+		sendDeletedNodeUpdate, sendPeerUpdate = logic.UpdateHostNode(&hostUpdate.Host, &hostUpdate.Node)
 	case models.UpdateMetrics:
 		mq.UpdateMetricsFallBack(hostUpdate.Node.ID.String(), hostUpdate.NewMetrics)
 	case models.EgressUpdate:
@@ -403,14 +415,23 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 			e.Update(db.WithContext(r.Context()))
 		}
 		sendPeerUpdate = true
+	case models.SignalHost:
+		mq.SignalPeer(hostUpdate.Signal)
+	case models.DeleteHost:
+		go mq.DeleteAndCleanupHost(currentHost)
 	}
-
-	if sendPeerUpdate {
-		err := mq.PublishPeerUpdate(replacePeers)
-		if err != nil {
-			slog.Error("failed to publish peer update", "error", err)
+	go func() {
+		if sendDeletedNodeUpdate {
+			mq.PublishDeletedNodePeerUpdate(&hostUpdate.Node)
 		}
-	}
+		if sendPeerUpdate {
+			err := mq.PublishPeerUpdate(replacePeers)
+			if err != nil {
+				slog.Error("failed to publish peer update", "error", err)
+			}
+		}
+	}()
+
 	logic.ReturnSuccessResponse(w, r, "updated host data")
 }
 
@@ -440,11 +461,7 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 			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)
+		go mq.PublishMqUpdatesForDeletedNode(node, false)
 
 	}
 	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
@@ -494,6 +511,10 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 			Type: models.DeviceSub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: currHost,
+			New: nil,
+		},
 	})
 	apiHostData := currHost.ConvertNMHostToAPI()
 	logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
@@ -698,10 +719,6 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	var gwClients []models.ExtClient
-	if node.IsIngressGateway {
-		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(
@@ -712,7 +729,7 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	go func() {
-		mq.PublishMqUpdatesForDeletedNode(*node, true, gwClients)
+		mq.PublishMqUpdatesForDeletedNode(*node, true)
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 		}
@@ -1233,6 +1250,9 @@ func approvePendingHost(w http.ResponseWriter, r *http.Request) {
 		})
 		return
 	}
+	if key.AutoAssignGateway {
+		newNode.AutoAssignGateway = true
+	}
 	if len(key.Groups) > 0 {
 		newNode.Tags = make(map[models.TagID]struct{})
 		for _, tagI := range key.Groups {
@@ -1262,7 +1282,7 @@ func approvePendingHost(w http.ResponseWriter, r *http.Request) {
 	}
 
 	logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
-	hostactions.AddAction(models.HostUpdate{
+	mq.HostUpdate(&models.HostUpdate{
 		Action: models.JoinHostToNetwork,
 		Host:   *h,
 		Node:   *newNode,

+ 5 - 0
controllers/inet_gws.go

@@ -72,6 +72,11 @@ func createInternetGw(w http.ResponseWriter, r *http.Request) {
 				mq.PublishPeerUpdate(false)
 			}()
 		}
+		go func() {
+			logic.ResetAutoRelayedPeer(&node)
+			mq.PublishPeerUpdate(false)
+		}()
+
 	}
 	if node.IsGw && node.IngressDNS == "" {
 		node.IngressDNS = "1.1.1.1"

+ 72 - 13
controllers/network.go

@@ -16,6 +16,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/servercfg"
@@ -42,6 +43,8 @@ func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/networks/{networkname}/egress_routes", logic.SecurityCheck(true, http.HandlerFunc(getNetworkEgressRoutes)))
+	r.HandleFunc("/api/networks/{networkname}/old_acl_status", logic.SecurityCheck(true, http.HandlerFunc(OldNetworkACLStatus))).
+		Methods(http.MethodGet)
 }
 
 // @Summary     Lists all networks
@@ -430,6 +433,40 @@ func getNetworkACL(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(networkACL)
 }
 
+// @Summary     Check a Old ACL Status (Access Control List)
+// @Router      /api/networks/{networkname}/old_acl_status [get]
+// @Tags        Networks
+// @Security    oauth
+// @Param       networkname path string true "Network name"
+// @Produce     json
+// @Success     200 {object} acls.ACLContainer
+// @Failure     500 {object} models.ErrorResponse
+func OldNetworkACLStatus(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	netname := params["networkname"]
+	var networkACL acls.ACLContainer
+	networkACL, err := nodeacls.FetchAllACLs(nodeacls.NetworkID(netname))
+	if err != nil {
+		logic.ReturnSuccessResponse(w, r, "false")
+		return
+	}
+	disableOldAcls := true
+	for _, aclNode := range networkACL {
+		for _, allowed := range aclNode {
+			if allowed != acls.Allowed {
+				disableOldAcls = false
+				break
+			}
+		}
+	}
+	msg := "true"
+	if disableOldAcls {
+		msg = "false"
+	}
+	logic.ReturnSuccessResponse(w, r, msg)
+}
+
 // @Summary     Get a network Egress routes
 // @Router      /api/networks/{networkname}/egress_routes [get]
 // @Tags        Networks
@@ -528,6 +565,10 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 			Type: models.NetworkSub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: network,
+			New: nil,
+		},
 	})
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	w.WriteHeader(http.StatusOK)
@@ -642,20 +683,38 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", currHost.Name)
-			if err = mq.HostUpdate(&models.HostUpdate{
-				Action: models.JoinHostToNetwork,
-				Host:   *currHost,
-				Node:   *newNode,
-			}); err != nil {
-				logger.Log(
-					0,
-					r.Header.Get("user"),
-					"failed to add host to network:",
-					currHost.ID.String(),
-					network.NetID,
-					err.Error(),
-				)
+			if len(currHost.Nodes) == 1 {
+				if err = mq.HostUpdate(&models.HostUpdate{
+					Action: models.RequestPull,
+					Host:   *currHost,
+					Node:   *newNode,
+				}); err != nil {
+					logger.Log(
+						0,
+						r.Header.Get("user"),
+						"failed to add host to network:",
+						currHost.ID.String(),
+						network.NetID,
+						err.Error(),
+					)
+				}
+			} else {
+				if err = mq.HostUpdate(&models.HostUpdate{
+					Action: models.JoinHostToNetwork,
+					Host:   *currHost,
+					Node:   *newNode,
+				}); err != nil {
+					logger.Log(
+						0,
+						r.Header.Get("user"),
+						"failed to add host to network:",
+						currHost.ID.String(),
+						network.NetID,
+						err.Error(),
+					)
+				}
 			}
+
 			// make  host failover
 			logic.CreateFailOver(*newNode)
 			// make host remote access gateway

+ 65 - 5
controllers/node.go

@@ -565,6 +565,10 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
+	if currentNode.IsAutoRelay && !newNode.IsAutoRelay {
+		logic.ResetAutoRelay(newNode)
+	}
+
 	if newNode.IsInternetGateway && len(newNode.InetNodeReq.InetNodeClientIDs) > 0 {
 		err = logic.ValidateInetGwReq(*newNode, newNode.InetNodeReq, newNode.IsInternetGateway && currentNode.IsInternetGateway)
 		if err != nil {
@@ -574,6 +578,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		newNode.RelayedNodes = append(newNode.RelayedNodes, newNode.InetNodeReq.InetNodeClientIDs...)
 		newNode.RelayedNodes = logic.UniqueStrings(newNode.RelayedNodes)
 	}
+
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	if relayUpdate && newNode.IsRelay {
 		err = logic.ValidateRelay(models.RelayRequest{
@@ -593,6 +598,12 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	if newNode.IsInternetGateway {
+		if host.DNS != "yes" {
+			host.DNS = "yes"
+			logic.UpsertHost(host)
+		}
+	}
 	aclUpdate := currentNode.DefaultACL != newNode.DefaultACL
 
 	err = logic.UpdateNode(&currentNode, newNode)
@@ -618,6 +629,38 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	if !newNode.IsInternetGateway {
 		logic.UnsetInternetGw(newNode)
 	}
+	if currentNode.AutoAssignGateway && !newNode.AutoAssignGateway {
+		// if relayed remove it
+		if newNode.IsRelayed {
+			relayNode, err := logic.GetNodeByID(newNode.RelayedBy)
+			if err == nil {
+				logic.RemoveAllFromSlice(relayNode.RelayedNodes, newNode.ID.String())
+				logic.UpsertNode(&relayNode)
+			}
+			newNode.IsRelayed = false
+			newNode.RelayedBy = ""
+		}
+	}
+	if (currentNode.IsRelayed) && newNode.AutoAssignGateway {
+		// if relayed remove it
+		if currentNode.IsRelayed {
+			relayNode, err := logic.GetNodeByID(currentNode.RelayedBy)
+			if err == nil {
+				logic.RemoveAllFromSlice(relayNode.RelayedNodes, currentNode.ID.String())
+				logic.UpsertNode(&relayNode)
+			}
+			newNode.IsRelayed = false
+			newNode.RelayedBy = ""
+		}
+		if len(currentNode.AutoRelayedPeers) > 0 {
+			logic.ResetAutoRelayedPeer(&currentNode)
+		}
+	}
+	if !currentNode.AutoAssignGateway && newNode.AutoAssignGateway {
+		if len(currentNode.AutoRelayedPeers) > 0 {
+			logic.ResetAutoRelayedPeer(&currentNode)
+		}
+	}
 	logic.UpsertNode(newNode)
 	logic.GetNodeStatus(newNode, false)
 
@@ -655,10 +698,31 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		if err := mq.NodeUpdate(newNode); err != nil {
 			slog.Error("error publishing node update to node", "node", newNode.ID, "error", err)
 		}
+		// if !newNode.Connected {
+		// 	mq.HostUpdate(&models.HostUpdate{Host: *host, Action: models.SignalPull})
+		// }
+		allNodes, err := logic.GetAllNodes()
+		if err == nil {
+			mq.PublishSingleHostPeerUpdate(host, allNodes, nil, nil, false, nil)
+		}
+		if servercfg.IsPro && newNode.AutoAssignGateway {
+			mq.HostUpdate(&models.HostUpdate{Action: models.CheckAutoAssignGw, Host: *host, Node: *newNode})
+		}
 		mq.PublishPeerUpdate(false)
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 		}
+		if !newNode.Connected {
+			metrics, err := logic.GetMetrics(newNode.ID.String())
+			if err == nil {
+				for peer, connectivity := range metrics.Connectivity {
+					connectivity.Connected = false
+					metrics.Connectivity[peer] = connectivity
+				}
+
+				_ = logic.UpdateMetrics(newNode.ID.String(), metrics)
+			}
+		}
 	}(aclUpdate, relayUpdate, newNode)
 }
 
@@ -682,10 +746,6 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 	}
 	forceDelete := r.URL.Query().Get("force") == "true"
 	fromNode := r.Header.Get("requestfrom") == "node"
-	var gwClients []models.ExtClient
-	if node.IsIngressGateway {
-		gwClients = logic.GetGwExtclients(node.ID.String(), node.Network)
-	}
 	purge := forceDelete || fromNode
 	if err := logic.DeleteNode(&node, purge); err != nil {
 		logic.ReturnErrorResponse(
@@ -698,5 +758,5 @@ 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 mq.PublishMqUpdatesForDeletedNode(node, !fromNode, gwClients)
+	go mq.PublishMqUpdatesForDeletedNode(node, !fromNode)
 }

+ 9 - 51
controllers/server.go

@@ -1,19 +1,17 @@
 package controller
 
 import (
-	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/gravitl/netmaker/db"
-	"github.com/gravitl/netmaker/schema"
-	"github.com/google/go-cmp/cmp"
 	"net/http"
 	"os"
 	"strings"
 	"syscall"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
+
 	"github.com/gorilla/mux"
 	"golang.org/x/exp/slog"
 
@@ -82,56 +80,10 @@ func memProfile(w http.ResponseWriter, r *http.Request) {
 }
 
 func getUsage(w http.ResponseWriter, _ *http.Request) {
-	type usage struct {
-		Hosts            int `json:"hosts"`
-		Clients          int `json:"clients"`
-		Networks         int `json:"networks"`
-		Users            int `json:"users"`
-		Ingresses        int `json:"ingresses"`
-		Egresses         int `json:"egresses"`
-		Relays           int `json:"relays"`
-		InternetGateways int `json:"internet_gateways"`
-		FailOvers        int `json:"fail_overs"`
-	}
-	var serverUsage usage
-	hosts, err := logic.GetAllHostsWithStatus(models.OnlineSt)
-	if err == nil {
-		serverUsage.Hosts = len(hosts)
-	}
-	clients, err := logic.GetAllExtClientsWithStatus(models.OnlineSt)
-	if err == nil {
-		serverUsage.Clients = len(clients)
-	}
-	users, err := logic.GetUsers()
-	if err == nil {
-		serverUsage.Users = len(users)
-	}
-	networks, err := logic.GetNetworks()
-	if err == nil {
-		serverUsage.Networks = len(networks)
-	}
-	// TODO this part bellow can be optimized to get nodes just once
-	ingresses, err := logic.GetAllIngresses()
-	if err == nil {
-		serverUsage.Ingresses = len(ingresses)
-	}
-	serverUsage.Egresses, _ = (&schema.Egress{}).Count(db.WithContext(context.TODO()))
-	relays, err := logic.GetRelays()
-	if err == nil {
-		serverUsage.Relays = len(relays)
-	}
-	gateways, err := logic.GetInternetGateways()
-	if err == nil {
-		serverUsage.InternetGateways = len(gateways)
-	}
-	failOvers, err := logic.GetAllFailOvers()
-	if err == nil {
-		serverUsage.FailOvers = len(failOvers)
-	}
 	w.Header().Set("Content-Type", "application/json")
 	json.NewEncoder(w).Encode(models.SuccessResponse{
 		Code:     http.StatusOK,
-		Response: serverUsage,
+		Response: logic.GetCurrentServerUsage(),
 	})
 }
 
@@ -149,6 +101,7 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
 		IsPro            bool      `json:"is_pro"`
 		TrialEndDate     time.Time `json:"trial_end_date"`
 		IsOnTrialLicense bool      `json:"is_on_trial_license"`
+		Version          string    `json:"version"`
 	}
 
 	licenseErr := ""
@@ -173,6 +126,7 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
 		IsBrokerConnOpen: mq.IsConnectionOpen(),
 		LicenseError:     licenseErr,
 		IsPro:            servercfg.IsPro,
+		Version:          servercfg.Version,
 		//TrialEndDate:     trialEndDate,
 		//IsOnTrialLicense: isOnTrial,
 	}
@@ -328,6 +282,10 @@ func reInit(curr, new models.ServerSettings, force bool) {
 	logic.EmailInit()
 	logic.SetVerbosity(int(logic.GetServerSettings().Verbosity))
 	logic.ResetIDPSyncHook()
+	if curr.MetricInterval != new.MetricInterval {
+		logic.GetMetricsMonitor().Stop()
+		logic.GetMetricsMonitor().Start()
+	}
 	// check if auto update is changed
 	if force {
 		if curr.NetclientAutoUpdate != new.NetclientAutoUpdate {

+ 8 - 0
controllers/user.go

@@ -244,6 +244,10 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
 			Info: a,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: a,
+			New: nil,
+		},
 	})
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token")
 }
@@ -1580,6 +1584,10 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 			Type: models.UserSub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: user,
+			New: nil,
+		},
 	})
 	// check and delete extclient with this ownerID
 	go func() {

+ 1 - 1
docker/Dockerfile-go-builder

@@ -1,4 +1,4 @@
-FROM golang:1.23.0-alpine3.20
+FROM golang:1.24.0-alpine3.20
 ARG version 
 RUN apk add --no-cache build-base
 WORKDIR /app

+ 27 - 29
go.mod

@@ -1,13 +1,11 @@
 module github.com/gravitl/netmaker
 
-go 1.23.0
-
-toolchain go1.23.7
+go 1.24.0
 
 require (
 	github.com/blang/semver v3.5.1+incompatible
-	github.com/eclipse/paho.mqtt.golang v1.5.0
-	github.com/go-playground/validator/v10 v10.27.0
+	github.com/eclipse/paho.mqtt.golang v1.5.1
+	github.com/go-playground/validator/v10 v10.28.0
 	github.com/golang-jwt/jwt/v4 v4.5.2
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
@@ -17,14 +15,14 @@ require (
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/seancfoley/ipaddress-go v1.7.1
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	github.com/stretchr/testify v1.11.0
+	github.com/stretchr/testify v1.11.1
 	github.com/txn2/txeh v1.5.5
 	go.uber.org/automaxprocs v1.6.0
-	golang.org/x/crypto v0.41.0
-	golang.org/x/net v0.43.0 // indirect
-	golang.org/x/oauth2 v0.30.0
-	golang.org/x/sys v0.35.0 // indirect
-	golang.org/x/text v0.28.0 // indirect
+	golang.org/x/crypto v0.43.0
+	golang.org/x/net v0.46.0 // indirect
+	golang.org/x/oauth2 v0.32.0
+	golang.org/x/sys v0.37.0 // indirect
+	golang.org/x/text v0.30.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -32,11 +30,11 @@ require (
 require (
 	filippo.io/edwards25519 v1.1.0
 	github.com/c-robinson/iplib v1.0.8
-	github.com/posthog/posthog-go v1.6.4
+	github.com/posthog/posthog-go v1.6.12
 )
 
 require (
-	github.com/coreos/go-oidc/v3 v3.15.0
+	github.com/coreos/go-oidc/v3 v3.16.0
 	github.com/gorilla/websocket v1.5.3
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 )
@@ -48,25 +46,25 @@ require (
 	github.com/matryer/is v1.4.1
 	github.com/okta/okta-sdk-golang/v5 v5.0.6
 	github.com/pquerna/otp v1.5.0
-	github.com/spf13/cobra v1.9.1
-	google.golang.org/api v0.248.0
+	github.com/spf13/cobra v1.10.1
+	google.golang.org/api v0.253.0
 	gopkg.in/mail.v2 v2.3.1
-	gorm.io/datatypes v1.2.6
+	gorm.io/datatypes v1.2.7
 	gorm.io/driver/postgres v1.6.0
 	gorm.io/driver/sqlite v1.6.0
-	gorm.io/gorm v1.30.1
+	gorm.io/gorm v1.31.0
 )
 
 require (
-	cloud.google.com/go/auth v0.16.5 // indirect
+	cloud.google.com/go/auth v0.17.0 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
-	cloud.google.com/go/compute/metadata v0.8.0 // indirect
+	cloud.google.com/go/compute/metadata v0.9.0 // indirect
 	github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.10 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
-	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
+	github.com/go-jose/go-jose/v4 v4.1.3 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-sql-driver/mysql v1.8.1 // indirect
@@ -94,15 +92,15 @@ require (
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/seancfoley/bintree v1.3.1 // indirect
-	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/spf13/pflag v1.0.9 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
-	go.opentelemetry.io/otel v1.36.0 // indirect
-	go.opentelemetry.io/otel/metric v1.36.0 // indirect
-	go.opentelemetry.io/otel/trace v1.36.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
-	google.golang.org/grpc v1.74.2 // indirect
-	google.golang.org/protobuf v1.36.7 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
+	google.golang.org/grpc v1.76.0 // indirect
+	google.golang.org/protobuf v1.36.10 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gorm.io/driver/mysql v1.5.6 // indirect
 )
@@ -116,5 +114,5 @@ require (
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/sync v0.16.0 // indirect
+	golang.org/x/sync v0.17.0 // indirect
 )

+ 62 - 60
go.sum

@@ -1,9 +1,9 @@
-cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
-cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
+cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
+cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
-cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
-cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
+cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
@@ -14,8 +14,8 @@ github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
 github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
-github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
-github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
+github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
+github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -23,16 +23,16 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
-github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
-github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
+github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
+github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
-github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
+github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
 github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
-github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
-github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
+github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
+github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -44,8 +44,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
-github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
+github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -140,8 +140,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posthog/posthog-go v1.6.4 h1:vPo6Z8T1R+aUBugXg1+psD8qZYSOtFktzhj6H8rzOBI=
-github.com/posthog/posthog-go v1.6.4/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY=
+github.com/posthog/posthog-go v1.6.12 h1:rsOBL/YdMfLJtOVjKJLgdzYmvaL3aIW6IVbAteSe+aI=
+github.com/posthog/posthog-go v1.6.12/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY=
 github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
 github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
@@ -160,10 +160,10 @@ github.com/seancfoley/ipaddress-go v1.7.1 h1:fDWryS+L8iaaH5RxIKbY0xB5Z+Zxk8xoXLN
 github.com/seancfoley/ipaddress-go v1.7.1/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
-github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
-github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
-github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -175,8 +175,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
-github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -186,24 +186,24 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
-go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
-golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
-golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
 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=
@@ -214,15 +214,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
-golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
-golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
-golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
+golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
-golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 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=
@@ -232,8 +232,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
-golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -246,10 +246,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
-golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
-golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
-golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
 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=
@@ -257,18 +257,20 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
-google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/api v0.253.0 h1:apU86Eq9Q2eQco3NsUYFpVTfy7DwemojL7LmbAj7g/I=
+google.golang.org/api v0.253.0/go.mod h1:PX09ad0r/4du83vZVAaGg7OaeyGnaUmT/CYPNvtLCbw=
 google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
 google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
-google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
-google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
-google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
-google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
+google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
+google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -279,8 +281,8 @@ gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck=
-gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
+gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
+gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
 gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
 gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
@@ -290,5 +292,5 @@ gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwy
 gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
 gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
 gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
-gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
-gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
+gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
+gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

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

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       containers:
       - name: netclient
-        image: gravitl/netclient:v1.1.0
+        image: gravitl/netclient:v1.2.0
         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:v1.1.0
+        image: gravitl/netclient:v1.2.0
         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:v1.1.0
+        image: gravitl/netmaker-ui:v1.2.0
         ports:
         - containerPort: 443
         env:

+ 12 - 12
logic/acls.go

@@ -1465,18 +1465,6 @@ func GetDefaultPolicy(netID models.NetworkID, ruleType models.AclPolicyType) (mo
 	return acl, nil
 }
 
-// ListUserPolicies - lists all user policies in a network
-func ListUserPolicies(netID models.NetworkID) []models.Acl {
-	allAcls := ListAcls()
-	userAcls := []models.Acl{}
-	for _, acl := range allAcls {
-		if acl.NetworkID == netID && acl.RuleType == models.UserPolicy {
-			userAcls = append(userAcls, acl)
-		}
-	}
-	return userAcls
-}
-
 // ListAcls - lists all acl policies
 func ListAclsByNetwork(netID models.NetworkID) ([]models.Acl, error) {
 
@@ -1522,6 +1510,18 @@ func ListDevicePolicies(netID models.NetworkID) []models.Acl {
 	return deviceAcls
 }
 
+// ListUserPolicies - lists all user policies in a network
+func ListUserPolicies(netID models.NetworkID) []models.Acl {
+	allAcls := ListAcls()
+	userAcls := []models.Acl{}
+	for _, acl := range allAcls {
+		if acl.NetworkID == netID && acl.RuleType == models.UserPolicy {
+			userAcls = append(userAcls, acl)
+		}
+	}
+	return userAcls
+}
+
 func ConvAclTagToValueMap(acltags []models.AclPolicyTag) map[string]struct{} {
 	aclValueMap := make(map[string]struct{})
 	for _, aclTagI := range acltags {

+ 0 - 4
logic/acls/nodeacls/retrieve.go

@@ -7,16 +7,12 @@ import (
 	"sync"
 
 	"github.com/gravitl/netmaker/logic/acls"
-	"github.com/gravitl/netmaker/servercfg"
 )
 
 var NodesAllowedACLMutex = &sync.Mutex{}
 
 // AreNodesAllowed - checks if nodes are allowed to communicate in their network ACL
 func AreNodesAllowed(networkID NetworkID, node1, node2 NodeID) bool {
-	if !servercfg.IsOldAclEnabled() {
-		return true
-	}
 	NodesAllowedACLMutex.Lock()
 	defer NodesAllowedACLMutex.Unlock()
 	var currentNetworkACL, err = FetchAllACLs(networkID)

+ 64 - 25
logic/dns.go

@@ -432,31 +432,22 @@ func validateNameserverReq(ns schema.Nameserver) error {
 	if len(ns.Servers) == 0 {
 		return errors.New("atleast one nameserver should be specified")
 	}
-	network, err := GetNetwork(ns.NetworkID)
+	_, err := GetNetwork(ns.NetworkID)
 	if err != nil {
 		return errors.New("invalid network id")
 	}
-	_, cidr, err4 := net.ParseCIDR(network.AddressRange)
-	_, cidr6, err6 := net.ParseCIDR(network.AddressRange6)
 	for _, nsIPStr := range ns.Servers {
 		nsIP := net.ParseIP(nsIPStr)
 		if nsIP == nil {
 			return errors.New("invalid nameserver " + nsIPStr)
 		}
-		if err4 == nil && nsIP.To4() != nil {
-			if cidr.Contains(nsIP) {
-				return errors.New("cannot use netmaker IP as nameserver")
-			}
-		} else if err6 == nil && cidr6.Contains(nsIP) {
-			return errors.New("cannot use netmaker IP as nameserver")
-		}
 	}
-	if !ns.MatchAll && len(ns.MatchDomains) == 0 {
+	if !ns.MatchAll && len(ns.Domains) == 0 {
 		return errors.New("atleast one match domain is required")
 	}
 	if !ns.MatchAll {
-		for _, matchDomain := range ns.MatchDomains {
-			if !IsValidMatchDomain(matchDomain) {
+		for _, domain := range ns.Domains {
+			if !IsValidMatchDomain(domain.Domain) {
 				return errors.New("invalid match domain")
 			}
 		}
@@ -478,6 +469,15 @@ func validateNameserverReq(ns schema.Nameserver) error {
 }
 
 func getNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
+	filters := make(map[string]bool)
+	if node.Address.IP != nil {
+		filters[node.Address.IP.String()] = true
+	}
+
+	if node.Address6.IP != nil {
+		filters[node.Address6.IP.String()] = true
+	}
+
 	ns := &schema.Nameserver{
 		NetworkID: node.Network,
 	}
@@ -486,22 +486,30 @@ func getNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 		if !nsI.Status {
 			continue
 		}
+
+		filteredIps := FilterOutIPs(nsI.Servers, filters)
+		if len(filteredIps) == 0 {
+			continue
+		}
+
 		_, all := nsI.Tags["*"]
 		if all {
-			for _, matchDomain := range nsI.MatchDomains {
+			for _, domain := range nsI.Domains {
 				returnNsLi = append(returnNsLi, models.Nameserver{
-					IPs:         nsI.Servers,
-					MatchDomain: matchDomain,
+					IPs:            filteredIps,
+					MatchDomain:    domain.Domain,
+					IsSearchDomain: domain.IsSearchDomain,
 				})
 			}
 			continue
 		}
 
 		if _, ok := nsI.Nodes[node.ID.String()]; ok {
-			for _, matchDomain := range nsI.MatchDomains {
+			for _, domain := range nsI.Domains {
 				returnNsLi = append(returnNsLi, models.Nameserver{
-					IPs:         nsI.Servers,
-					MatchDomain: matchDomain,
+					IPs:            filteredIps,
+					MatchDomain:    domain.Domain,
+					IsSearchDomain: domain.IsSearchDomain,
 				})
 			}
 		}
@@ -528,6 +536,16 @@ func getNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 		if err != nil {
 			continue
 		}
+
+		filters := make(map[string]bool)
+		if node.Address.IP != nil {
+			filters[node.Address.IP.String()] = true
+		}
+
+		if node.Address6.IP != nil {
+			filters[node.Address6.IP.String()] = true
+		}
+
 		ns := &schema.Nameserver{
 			NetworkID: node.Network,
 		}
@@ -536,22 +554,30 @@ func getNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 			if !nsI.Status {
 				continue
 			}
+
+			filteredIps := FilterOutIPs(nsI.Servers, filters)
+			if len(filteredIps) == 0 {
+				continue
+			}
+
 			_, all := nsI.Tags["*"]
 			if all {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
-						IPs:         nsI.Servers,
-						MatchDomain: matchDomain,
+						IPs:            filteredIps,
+						MatchDomain:    domain.Domain,
+						IsSearchDomain: domain.IsSearchDomain,
 					})
 				}
 				continue
 			}
 
 			if _, ok := nsI.Nodes[node.ID.String()]; ok {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
-						IPs:         nsI.Servers,
-						MatchDomain: matchDomain,
+						IPs:            filteredIps,
+						MatchDomain:    domain.Domain,
+						IsSearchDomain: domain.IsSearchDomain,
 					})
 				}
 
@@ -631,3 +657,16 @@ func IsValidMatchDomain(s string) bool {
 	}
 	return true
 }
+
+// FilterOutIPs removes ips in the filters map from the ips slice.
+func FilterOutIPs(ips []string, filters map[string]bool) []string {
+	var filteredIps []string
+	for _, ip := range ips {
+		_, ok := filters[ip]
+		if !ok {
+			filteredIps = append(filteredIps, ip)
+		}
+	}
+
+	return filteredIps
+}

+ 75 - 2
logic/egress.go

@@ -13,7 +13,9 @@ import (
 	"github.com/gravitl/netmaker/servercfg"
 )
 
-func ValidateEgressReq(e *schema.Egress) error {
+var ValidateEgressReq = validateEgressReq
+
+func validateEgressReq(e *schema.Egress) error {
 	if e.Network == "" {
 		return errors.New("network id is empty")
 	}
@@ -162,6 +164,42 @@ func AddEgressInfoToPeerByAccess(node, targetNode *models.Node, eli []schema.Egr
 
 			}
 		}
+		for tagID := range targetNode.Tags {
+			if metric, ok := e.Tags[tagID.String()]; ok {
+				m64, err := metric.(json.Number).Int64()
+				if err != nil {
+					m64 = 256
+				}
+				m := uint32(m64)
+				if e.Range != "" {
+					req.Ranges = append(req.Ranges, e.Range)
+				} else {
+					req.Ranges = append(req.Ranges, e.DomainAns...)
+				}
+
+				if e.Range != "" {
+					req.Ranges = append(req.Ranges, e.Range)
+					req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+						Network:     e.Range,
+						Nat:         e.Nat,
+						RouteMetric: m,
+					})
+				}
+				if e.Domain != "" && len(e.DomainAns) > 0 {
+					req.Ranges = append(req.Ranges, e.DomainAns...)
+					for _, domainAnsI := range e.DomainAns {
+						req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+							Network:     domainAnsI,
+							Nat:         e.Nat,
+							RouteMetric: m,
+						})
+					}
+
+				}
+				break
+			}
+		}
+
 	}
 	if targetNode.Mutex != nil {
 		targetNode.Mutex.Lock()
@@ -181,7 +219,7 @@ func AddEgressInfoToPeerByAccess(node, targetNode *models.Node, eli []schema.Egr
 }
 
 func GetEgressDomainsByAccess(user *models.User, network models.NetworkID) (domains []string) {
-	acls, _ := ListAclsByNetwork(network)
+	acls := ListUserPolicies(network)
 	eli, _ := (&schema.Egress{Network: network.String()}).ListByNetwork(db.WithContext(context.TODO()))
 	defaultDevicePolicy, _ := GetDefaultPolicy(network, models.UserPolicy)
 	isDefaultPolicyActive := defaultDevicePolicy.Enabled
@@ -240,6 +278,41 @@ func GetNodeEgressInfo(targetNode *models.Node, eli []schema.Egress, acls []mode
 			}
 
 		}
+		for tagID := range targetNode.Tags {
+			if metric, ok := e.Tags[tagID.String()]; ok {
+				m64, err := metric.(json.Number).Int64()
+				if err != nil {
+					m64 = 256
+				}
+				m := uint32(m64)
+				if e.Range != "" {
+					req.Ranges = append(req.Ranges, e.Range)
+				} else {
+					req.Ranges = append(req.Ranges, e.DomainAns...)
+				}
+
+				if e.Range != "" {
+					req.Ranges = append(req.Ranges, e.Range)
+					req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+						Network:     e.Range,
+						Nat:         e.Nat,
+						RouteMetric: m,
+					})
+				}
+				if e.Domain != "" && len(e.DomainAns) > 0 {
+					req.Ranges = append(req.Ranges, e.DomainAns...)
+					for _, domainAnsI := range e.DomainAns {
+						req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+							Network:     domainAnsI,
+							Nat:         e.Nat,
+							RouteMetric: m,
+						})
+					}
+
+				}
+				break
+			}
+		}
 	}
 	if targetNode.Mutex != nil {
 		targetNode.Mutex.Lock()

+ 15 - 12
logic/enrollmentkey.go

@@ -38,23 +38,26 @@ var (
 )
 
 // CreateEnrollmentKey - creates a new enrollment key in db
-func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID, defaultKey, autoEgress bool) (*models.EnrollmentKey, error) {
+func CreateEnrollmentKey(uses int, expiration time.Time, networks,
+	tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID,
+	defaultKey, autoEgress, autoAssignGw bool) (*models.EnrollmentKey, error) {
 	newKeyID, err := getUniqueEnrollmentID()
 	if err != nil {
 		return nil, err
 	}
 	k := &models.EnrollmentKey{
-		Value:         newKeyID,
-		Expiration:    time.Time{},
-		UsesRemaining: 0,
-		Unlimited:     unlimited,
-		Networks:      []string{},
-		Tags:          []string{},
-		Type:          models.Undefined,
-		Relay:         relay,
-		Groups:        groups,
-		Default:       defaultKey,
-		AutoEgress:    autoEgress,
+		Value:             newKeyID,
+		Expiration:        time.Time{},
+		UsesRemaining:     0,
+		Unlimited:         unlimited,
+		Networks:          []string{},
+		Tags:              []string{},
+		Type:              models.Undefined,
+		Relay:             relay,
+		Groups:            groups,
+		Default:           defaultKey,
+		AutoEgress:        autoEgress,
+		AutoAssignGateway: autoAssignGw,
 	}
 	if uses > 0 {
 		k.UsesRemaining = uses

+ 16 - 15
logic/enrollmentkey_test.go

@@ -1,11 +1,12 @@
 package logic
 
 import (
-	"github.com/gravitl/netmaker/db"
-	"github.com/gravitl/netmaker/schema"
 	"testing"
 	"time"
 
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/schema"
+
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/models"
@@ -19,35 +20,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, nil, false, uuid.Nil, false, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false, false)
 		assert.Nil(t, newKey)
 		assert.NotNil(t, err)
 		assert.ErrorIs(t, err, models.ErrInvalidEnrollmentKey)
 	})
 	t.Run("Can_Create_Key_Uses", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false)
+		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false, false)
 		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, nil, false, uuid.Nil, false, false)
+		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, nil, false, uuid.Nil, false, false, false)
 		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, nil, true, uuid.Nil, false, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false, false, false)
 		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, nil, true, uuid.Nil, false, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false, false)
 		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"}, nil, true, uuid.Nil, false, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, nil, true, uuid.Nil, false, false, false)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Tags) == 2)
@@ -70,7 +71,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false, false)
 	t.Run("Can_Delete_Key", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		err := DeleteEnrollmentKey(newKey.Value, false)
@@ -94,7 +95,7 @@ func TestDecrement_EnrollmentKey(t *testing.T) {
 
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false)
+	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false, false)
 	t.Run("Check_initial_uses", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		assert.Equal(t, newKey.UsesRemaining, 1)
@@ -121,9 +122,9 @@ func TestUsability_EnrollmentKey(t *testing.T) {
 
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false)
-	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, nil, false, uuid.Nil, false, false)
-	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false, false)
+	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false, false)
+	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, nil, false, uuid.Nil, false, false, false)
+	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false, false, false)
 	t.Run("Check if valid use key can be used", func(t *testing.T) {
 		assert.Equal(t, key1.UsesRemaining, 1)
 		ok := TryToUseEnrollmentKey(key1)
@@ -162,7 +163,7 @@ func TestTokenize_EnrollmentKeys(t *testing.T) {
 
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false, false)
 	const defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5"
 	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
@@ -198,7 +199,7 @@ func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false, false)
 	const b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
 

+ 8 - 1
logic/gateway.go

@@ -198,6 +198,7 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	}
 	node.IsIngressGateway = true
 	node.IsGw = true
+	SetAutoRelay(&node)
 	node.IsInternetGateway = ingress.IsInternetGateway
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
@@ -217,6 +218,9 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 		if _, exists := FailOverExists(node.Network); exists {
 			ResetFailedOverPeer(&node)
 		}
+
+		ResetAutoRelayedPeer(&node)
+
 	}
 	node.SetLastModified()
 	node.Metadata = ingress.Metadata
@@ -342,7 +346,7 @@ func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool
 		if err != nil {
 			return err
 		}
-		if clientNode.IsFailOver {
+		if clientNode.IsFailOver || clientNode.IsAutoRelay {
 			return errors.New("failover node cannot be set to use internet gateway")
 		}
 		clientHost, err := GetHost(clientNode.HostID.String())
@@ -370,6 +374,9 @@ func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool
 		if clientNode.FailedOverBy != uuid.Nil {
 			ResetFailedOverPeer(&clientNode)
 		}
+		if len(clientNode.AutoRelayedPeers) > 0 {
+			ResetAutoRelayedPeer(&clientNode)
+		}
 
 		if clientNode.IsRelayed && clientNode.RelayedBy != inetNode.ID.String() {
 			return fmt.Errorf("node %s is being relayed", clientHost.Name)

+ 27 - 0
logic/hosts.go

@@ -345,6 +345,9 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 			if node.FailedOverBy != uuid.Nil {
 				ResetFailedOverPeer(&node)
 			}
+			if len(node.AutoRelayedPeers) > 0 {
+				ResetAutoRelayedPeer(&node)
+			}
 		}
 	}
 
@@ -382,6 +385,30 @@ func UpsertHost(h *models.Host) error {
 	return nil
 }
 
+// UpdateHostNode -  handles updates from client nodes
+func UpdateHostNode(h *models.Host, newNode *models.Node) (publishDeletedNodeUpdate, publishPeerUpdate bool) {
+	currentNode, err := GetNodeByID(newNode.ID.String())
+	if err != nil {
+		return
+	}
+	ifaceDelta := IfaceDelta(&currentNode, newNode)
+	newNode.SetLastCheckIn()
+	if err := UpdateNode(&currentNode, newNode); err != nil {
+		slog.Error("error saving node", "name", h.Name, "network", newNode.Network, "error", err)
+		return
+	}
+	if ifaceDelta { // reduce number of unneeded updates, by only sending on iface changes
+		if !newNode.Connected {
+			publishDeletedNodeUpdate = true
+		}
+		publishPeerUpdate = true
+		// reset failover data for this node
+		ResetFailedOverPeer(newNode)
+		ResetAutoRelayedPeer(newNode)
+	}
+	return
+}
+
 // RemoveHost - removes a given host from server
 func RemoveHost(h *models.Host, forceDelete bool) error {
 	if !forceDelete && len(h.Nodes) > 0 {

+ 64 - 0
logic/metrics.go

@@ -1,9 +1,73 @@
 package logic
 
 import (
+	"context"
+	"math"
+	"strconv"
+	"time"
+
 	"github.com/gravitl/netmaker/models"
 )
 
+type MetricsMonitor struct {
+	cancel context.CancelFunc
+}
+
+var metricsMonitor MetricsMonitor
+
+func GetMetricsMonitor() *MetricsMonitor {
+	return &metricsMonitor
+}
+
+func (m *MetricsMonitor) Start() {
+	if m.cancel != nil {
+		m.cancel()
+		m.cancel = nil
+	}
+
+	var ctx context.Context
+	ctx, m.cancel = context.WithCancel(context.Background())
+
+	go func(ctx context.Context) {
+		metricsInterval, _ := strconv.Atoi(GetServerSettings().MetricInterval)
+		if metricsInterval == 0 {
+			return
+		}
+
+		checkInterval := time.Duration(2*metricsInterval) * time.Minute
+		for {
+			select {
+			case <-time.After(checkInterval):
+				nodes, _ := GetAllNodes()
+				for _, node := range nodes {
+					if node.Connected || node.PendingDelete {
+						continue
+					}
+
+					nodeMetrics, err := GetMetrics(node.ID.String())
+					if err == nil {
+						inc := math.Round(float64(time.Since(nodeMetrics.UpdatedAt)) / float64(time.Minute))
+						for peer, peerMetrics := range nodeMetrics.Connectivity {
+							peerMetrics.TotalTime += int64(inc)
+							peerMetrics.PercentUp = 100.0 * (float64(peerMetrics.Uptime) / float64(peerMetrics.TotalTime))
+							nodeMetrics.Connectivity[peer] = peerMetrics
+						}
+
+						_ = UpdateMetrics(node.ID.String(), nodeMetrics)
+					}
+				}
+			case <-ctx.Done():
+				return
+			}
+		}
+	}(ctx)
+}
+
+func (m *MetricsMonitor) Stop() {
+	m.cancel()
+	m.cancel = nil
+}
+
 var DeleteMetrics = func(string) error {
 	return nil
 }

+ 5 - 4
logic/networks.go

@@ -307,6 +307,7 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 		uuid.Nil,
 		true,
 		false,
+		false,
 	)
 
 	return network, nil
@@ -388,7 +389,7 @@ func UniqueAddressCache(networkName string, reverse bool) (net.IP, error) {
 	}
 
 	if network.IsIPv4 == "no" {
-		return add, fmt.Errorf("IPv4 not active on network " + networkName)
+		return add, fmt.Errorf("IPv4 not active on network %s", networkName)
 	}
 	//ensure AddressRange is valid
 	if _, _, err := net.ParseCIDR(network.AddressRange); err != nil {
@@ -431,7 +432,7 @@ func UniqueAddressDB(networkName string, reverse bool) (net.IP, error) {
 	}
 
 	if network.IsIPv4 == "no" {
-		return add, fmt.Errorf("IPv4 not active on network " + networkName)
+		return add, fmt.Errorf("IPv4 not active on network %s", networkName)
 	}
 	//ensure AddressRange is valid
 	if _, _, err := net.ParseCIDR(network.AddressRange); err != nil {
@@ -529,7 +530,7 @@ func UniqueAddress6DB(networkName string, reverse bool) (net.IP, error) {
 		return add, err
 	}
 	if network.IsIPv6 == "no" {
-		return add, fmt.Errorf("IPv6 not active on network " + networkName)
+		return add, fmt.Errorf("IPv6 not active on network %s", networkName)
 	}
 
 	//ensure AddressRange is valid
@@ -573,7 +574,7 @@ func UniqueAddress6Cache(networkName string, reverse bool) (net.IP, error) {
 		return add, err
 	}
 	if network.IsIPv6 == "no" {
-		return add, fmt.Errorf("IPv6 not active on network " + networkName)
+		return add, fmt.Errorf("IPv6 not active on network %s", networkName)
 	}
 
 	//ensure AddressRange is valid

+ 33 - 2
logic/nodes.go

@@ -15,10 +15,12 @@ import (
 	validator "github.com/go-playground/validator/v10"
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"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/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/validation"
 	"github.com/seancfoley/ipaddress-go/ipaddr"
@@ -221,6 +223,9 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 		}
 		newNode.EgressDetails = models.EgressDetails{}
 		newNode.SetLastModified()
+		if !currentNode.Connected && newNode.Connected {
+			newNode.SetLastCheckIn()
+		}
 		if data, err := json.Marshal(newNode); err != nil {
 			return err
 		} else {
@@ -277,6 +282,9 @@ func DeleteNode(node *models.Node, purge bool) error {
 	if node.FailedOverBy != uuid.Nil {
 		ResetFailedOverPeer(node)
 	}
+	if len(node.AutoRelayedPeers) > 0 {
+		ResetAutoRelayedPeer(node)
+	}
 	if node.IsRelay {
 		// unset all the relayed nodes
 		SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
@@ -322,6 +330,29 @@ func DeleteNode(node *models.Node, purge bool) error {
 		return err
 	}
 
+	filters := make(map[string]bool)
+	if node.Address.IP != nil {
+		filters[node.Address.IP.String()] = true
+	}
+
+	if node.Address6.IP != nil {
+		filters[node.Address6.IP.String()] = true
+	}
+
+	nameservers, _ := (&schema.Nameserver{
+		NetworkID: node.Network,
+	}).ListByNetwork(db.WithContext(context.TODO()))
+	for _, ns := range nameservers {
+		ns.Servers = FilterOutIPs(ns.Servers, filters)
+		if len(ns.Servers) > 0 {
+			_ = ns.Update(db.WithContext(context.TODO()))
+		} else {
+			// TODO: deleting a nameserver dns server could cause trouble for other nodes.
+			// TODO: try to figure out a sequence that works the best.
+			_ = ns.Delete(db.WithContext(context.TODO()))
+		}
+	}
+
 	go RemoveNodeFromAclPolicy(*node)
 	go RemoveNodeFromEgress(*node)
 	return nil
@@ -685,7 +716,7 @@ func createNode(node *models.Node) error {
 			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")
+		return fmt.Errorf("invalid address: ipv4 %s is not unique", node.Address.String())
 	}
 	if node.Address6.IP == nil {
 		if parentNetwork.IsIPv6 == "yes" {
@@ -699,7 +730,7 @@ func createNode(node *models.Node) error {
 			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")
+		return fmt.Errorf("invalid address: ipv6 %s is not unique", node.Address6.String())
 	}
 	node.ID = uuid.New()
 	//Create a JWT for the node

+ 65 - 12
logic/peers.go

@@ -44,6 +44,25 @@ var (
 	}
 )
 
+var (
+	// ResetAutoRelay - function to reset autorelayed peers on this node
+	ResetAutoRelay = func(autoRelayNode *models.Node) error {
+		return nil
+	}
+	// ResetAutoRelayedPeer - removes relayed peers for node
+	ResetAutoRelayedPeer = func(failedOverNode *models.Node) error {
+		return nil
+	}
+	// GetAutoRelayPeerIps - gets autorelay peerips
+	GetAutoRelayPeerIps = func(peer, node *models.Node) []net.IPNet {
+		return []net.IPNet{}
+	}
+	// SetAutoRelay - sets autorelay flag on the node
+	SetAutoRelay = func(node *models.Node) {
+		node.IsAutoRelay = false
+	}
+)
+
 // GetHostPeerInfo - fetches required peer info per network
 func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 	peerInfo := models.HostPeerInfo{
@@ -53,6 +72,7 @@ func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 	if err != nil {
 		return peerInfo, err
 	}
+	serverInfo := GetServerInfo()
 	for _, nodeID := range host.Nodes {
 		nodeID := nodeID
 		node, err := GetNodeByID(nodeID)
@@ -89,7 +109,7 @@ func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 			if peer.Action != models.NODE_DELETE &&
 				!peer.PendingDelete &&
 				peer.Connected &&
-				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
+				(!serverInfo.OldAClsSupport || nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String()))) &&
 				(allowedToComm) {
 
 				networkPeersInfo[peerHost.PublicKey.String()] = models.IDandAddr{
@@ -143,6 +163,8 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		HostNetworkInfo: models.HostInfoMap{},
 		ServerConfig:    GetServerInfo(),
 		DnsNameservers:  GetNameserversForHost(host),
+		AutoRelayNodes:  make(map[models.NetworkID][]models.Node),
+		GwNodes:         make(map[models.NetworkID][]models.Node),
 	}
 	if host.DNS == "no" {
 		hostPeerUpdate.ManageDNS = false
@@ -178,8 +200,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE ||
 			(!node.LastCheckIn.IsZero() && time.Since(node.LastCheckIn) > time.Hour) {
-			continue
+			if deletedNode == nil || deletedNode.ID != node.ID {
+				continue
+			}
 		}
+		hostPeerUpdate.Nodes = append(hostPeerUpdate.Nodes, node)
 		acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
 		eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
 		GetNodeEgressInfo(&node, eli, acls)
@@ -252,9 +277,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				node.Mutex.Lock()
 			}
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
+			peerAutoRelayID, isAutoRelayPeer := node.AutoRelayedPeers[peer.ID.String()]
 			if node.Mutex != nil {
 				node.Mutex.Unlock()
 			}
+
 			if peer.EgressDetails.IsEgressGateway {
 				peerKey := peerHost.PublicKey.String()
 				if isFailOverPeer && peer.FailedOverBy.String() != node.ID.String() {
@@ -267,6 +294,16 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 						}
 					}
 				}
+				if isAutoRelayPeer && peerAutoRelayID != node.ID.String() {
+					// get relay host
+					autoRelayNode, err := GetNodeByID(peerAutoRelayID)
+					if err == nil {
+						relayHost, err := GetHost(autoRelayNode.HostID.String())
+						if err == nil {
+							peerKey = relayHost.PublicKey.String()
+						}
+					}
+				}
 				if peer.IsRelayed && (peer.RelayedBy != node.ID.String()) {
 					// get relay host
 					relayNode, err := GetNodeByID(peer.RelayedBy)
@@ -292,9 +329,25 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			if peer.IsIngressGateway {
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node)...)
 			}
+			var allowedToComm bool
+			if defaultDevicePolicy.Enabled {
+				allowedToComm = true
+			} else {
+				allowedToComm = IsPeerAllowed(node, peer, false)
+			}
+			if allowedToComm {
+				if peer.IsAutoRelay {
+					hostPeerUpdate.AutoRelayNodes[models.NetworkID(peer.Network)] = append(hostPeerUpdate.AutoRelayNodes[models.NetworkID(peer.Network)],
+						peer)
+				}
+				if node.AutoAssignGateway && peer.IsGw {
+					hostPeerUpdate.GwNodes[models.NetworkID(peer.Network)] = append(hostPeerUpdate.GwNodes[models.NetworkID(peer.Network)],
+						peer)
+				}
+			}
 
 			if (node.IsRelayed && node.RelayedBy != peer.ID.String()) ||
-				(peer.IsRelayed && peer.RelayedBy != node.ID.String()) || isFailOverPeer {
+				(peer.IsRelayed && peer.RelayedBy != node.ID.String()) || isFailOverPeer || isAutoRelayPeer {
 				// if node is relayed and peer is not the relay, set remove to true
 				if _, ok := peerIndexMap[peerHost.PublicKey.String()]; ok {
 					continue
@@ -349,6 +402,9 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			if isFailOverPeer && peer.FailedOverBy == node.ID && !peer.IsStatic {
 				peerEndpoint = nil
 			}
+			if isAutoRelayPeer && peerAutoRelayID == node.ID.String() && !peer.IsStatic {
+				peerEndpoint = nil
+			}
 
 			peerConfig.Endpoint = &net.UDPAddr{
 				IP:   peerEndpoint,
@@ -358,21 +414,15 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			if uselocal {
 				peerConfig.Endpoint.Port = peerHost.ListenPort
 			}
-			var allowedToComm bool
-			if defaultDevicePolicy.Enabled {
-				allowedToComm = true
-			} else {
-				allowedToComm = IsPeerAllowed(node, peer, false)
-			}
+
 			if peer.Action != models.NODE_DELETE &&
 				!peer.PendingDelete &&
 				peer.Connected &&
-				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
+				(!hostPeerUpdate.ServerConfig.OldAClsSupport || nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String()))) &&
 				(allowedToComm) &&
 				(deletedNode == nil || (peer.ID.String() != deletedNode.ID.String())) {
 				peerConfig.AllowedIPs = GetAllowedIPs(&node, &peer, nil) // only append allowed IPs if valid connection
 			}
-
 			var nodePeer wgtypes.PeerConfig
 			if _, ok := peerIndexMap[peerHost.PublicKey.String()]; !ok {
 				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
@@ -582,7 +632,7 @@ func filterConflictingEgressRoutes(node, peer models.Node) []string {
 		}
 	}
 
-	return egressIPs
+	return UniqueStrings(egressIPs)
 }
 
 func filterConflictingEgressRoutesWithMetric(node, peer models.Node) []models.EgressRangeMetric {
@@ -711,6 +761,9 @@ func getNodeAllowedIPs(peer, node *models.Node) []net.IPNet {
 	if peer.IsFailOver {
 		allowedips = append(allowedips, GetFailOverPeerIps(peer, node)...)
 	}
+	if peer.IsAutoRelay {
+		allowedips = append(allowedips, GetAutoRelayPeerIps(peer, node)...)
+	}
 	return allowedips
 }
 

+ 17 - 5
logic/relay.go

@@ -82,6 +82,13 @@ func SetRelayedNodes(setRelayed bool, relay string, relayed []string) []models.N
 		}
 		returnnodes = append(returnnodes, node)
 	}
+	relayNode, _ := GetNodeByID(relay)
+	if setRelayed {
+		relayNode.RelayedNodes = relayed
+	} else {
+		relayNode.RelayedNodes = []string{}
+	}
+	UpsertNode(&relayNode)
 	return returnnodes
 }
 
@@ -129,18 +136,21 @@ func ValidateRelay(relay models.RelayRequest, update bool) error {
 		if relayedNode.InternetGwID != "" && relayedNode.InternetGwID != relay.NodeID {
 			return errors.New("cannot relay an internet client (" + relayedNodeID + ")")
 		}
-		if relayedNode.IsFailOver {
-			return errors.New("cannot relay a failOver (" + relayedNodeID + ")")
+		if relayedNode.IsFailOver || relayedNode.IsAutoRelay {
+			return errors.New("cannot relay a auto relay node (" + relayedNodeID + ")")
 		}
 		if relayedNode.FailedOverBy != uuid.Nil {
 			ResetFailedOverPeer(&relayedNode)
 		}
+		if len(relayedNode.AutoRelayedPeers) > 0 {
+			ResetAutoRelayedPeer(&relayedNode)
+		}
 	}
 	return err
 }
 
 // UpdateRelayNodes - updates relay nodes
-func updateRelayNodes(relay string, oldNodes []string, newNodes []string) []models.Node {
+func UpdateRelayNodes(relay string, oldNodes []string, newNodes []string) []models.Node {
 	_ = SetRelayedNodes(false, relay, oldNodes)
 	return SetRelayedNodes(true, relay, newNodes)
 }
@@ -163,11 +173,12 @@ func RelayUpdates(currentNode, newNode *models.Node) bool {
 
 // 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)
+	updatenodes := UpdateRelayNodes(currentNode.ID.String(), currentNode.RelayedNodes, newNode.RelayedNodes)
 	if len(updatenodes) > 0 {
 		for _, relayedNode := range updatenodes {
 			node := relayedNode
 			ResetFailedOverPeer(&node)
+			ResetAutoRelayedPeer(&node)
 		}
 	}
 }
@@ -225,6 +236,7 @@ func GetAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNe
 		logger.Log(0, "error getting network clients", err.Error())
 		return
 	}
+	serverSettings := GetServerSettings()
 	acls, _ := ListAclsByNetwork(models.NetworkID(relay.Network))
 	eli, _ := (&schema.Egress{Network: relay.Network}).ListByNetwork(db.WithContext(context.TODO()))
 	defaultPolicy, _ := GetDefaultPolicy(models.NetworkID(relay.Network), models.DevicePolicy)
@@ -236,7 +248,7 @@ func GetAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNe
 			continue
 		}
 		AddEgressInfoToPeerByAccess(relayed, &peer, eli, acls, defaultPolicy.Enabled)
-		if nodeacls.AreNodesAllowed(nodeacls.NetworkID(relayed.Network), nodeacls.NodeID(relayed.ID.String()), nodeacls.NodeID(peer.ID.String())) {
+		if !serverSettings.OldAClsSupport || nodeacls.AreNodesAllowed(nodeacls.NetworkID(relayed.Network), nodeacls.NodeID(relayed.ID.String()), nodeacls.NodeID(peer.ID.String())) {
 			allowedIPs = append(allowedIPs, GetAllowedIPs(relayed, &peer, nil)...)
 		}
 	}

+ 2 - 2
logic/security.go

@@ -2,10 +2,11 @@ package logic
 
 import (
 	"errors"
-	"github.com/golang-jwt/jwt/v4"
 	"net/http"
 	"strings"
 
+	"github.com/golang-jwt/jwt/v4"
+
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
@@ -24,7 +25,6 @@ var GlobalPermissionsCheck = func(username string, r *http.Request) error { retu
 
 // SecurityCheck - Check if user has appropriate permissions
 func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
-
 	return func(w http.ResponseWriter, r *http.Request) {
 		r.Header.Set("ismaster", "no")
 		isGlobalAccesss := r.Header.Get("IS_GLOBAL_ACCESS") == "yes"

+ 41 - 1
logic/settings.go

@@ -11,6 +11,8 @@ import (
 
 	"github.com/gravitl/netmaker/config"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 )
@@ -61,7 +63,10 @@ func UpsertServerSettings(s models.ServerSettings) error {
 		}
 	}
 	s.GroupFilters = groupFilters
-
+	if !s.OldAClsSupport {
+		// set defaults for old acl settings
+		go setDefaultsforOldAclCfg()
+	}
 	data, err := json.Marshal(s)
 	if err != nil {
 		return err
@@ -73,6 +78,36 @@ func UpsertServerSettings(s models.ServerSettings) error {
 	return nil
 }
 
+func setDefaultsforOldAclCfg() {
+	nets, _ := GetNetworks()
+	for _, netI := range nets {
+		if netI.DefaultACL != "yes" {
+			netI.DefaultACL = "yes"
+			UpsertNetwork(netI)
+		}
+		networkACL, err := nodeacls.FetchAllACLs(nodeacls.NetworkID(netI.NetID))
+		if err != nil {
+			continue
+		}
+		for id, aclNode := range networkACL {
+			for aclID, allowed := range aclNode {
+				if allowed != acls.Allowed {
+					aclNode.Allow(aclID)
+				}
+			}
+			networkACL.UpdateACL(id, aclNode)
+		}
+		networkACL.Save(acls.ContainerID(netI.NetID))
+	}
+	nodes, _ := GetAllNodes()
+	for _, node := range nodes {
+		if node.DefaultACL != "yes" {
+			node.DefaultACL = "yes"
+			UpsertNode(&node)
+		}
+	}
+}
+
 func GetUserSettings(userID string) models.UserSettings {
 	data, err := database.FetchRecord(database.SERVER_SETTINGS, userID)
 	if err != nil {
@@ -145,6 +180,7 @@ func GetServerSettingsFromEnv() (s models.ServerSettings) {
 		DefaultDomain:              servercfg.GetDefaultDomain(),
 		Stun:                       servercfg.IsStunEnabled(),
 		StunServers:                servercfg.GetStunServers(),
+		OldAClsSupport:             false,
 	}
 
 	return
@@ -245,6 +281,10 @@ func GetServerInfo() models.ServerConfig {
 	cfg.StunServers = serverSettings.StunServers
 	cfg.DefaultDomain = serverSettings.DefaultDomain
 	cfg.EndpointDetection = serverSettings.EndpointDetection
+	cfg.PeerConnectionCheckInterval = serverSettings.PeerConnectionCheckInterval
+	cfg.OldAClsSupport = serverSettings.OldAClsSupport
+	key, _ := RetrievePublicTrafficKey()
+	cfg.TrafficKey = key
 	return cfg
 }
 

+ 78 - 0
logic/usage.go

@@ -0,0 +1,78 @@
+package logic
+
+import (
+	"context"
+
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+)
+
+func GetCurrentServerUsage() (limits models.Usage) {
+	limits.SetDefaults()
+	hosts, hErr := GetAllHostsWithStatus(models.OnlineSt)
+	if hErr == nil {
+		limits.Hosts = len(hosts)
+	}
+	clients, cErr := GetAllExtClientsWithStatus(models.OnlineSt)
+	if cErr == nil {
+		limits.Clients = len(clients)
+	}
+	users, err := GetUsers()
+	if err == nil {
+		limits.Users = len(users)
+	}
+	networks, err := GetNetworks()
+	if err == nil {
+		limits.Networks = len(networks)
+	}
+	limits.Egresses, _ = (&schema.Egress{}).Count(db.WithContext(context.TODO()))
+
+	nodes, _ := GetAllNodes()
+
+	for _, client := range clients {
+		nodes = append(nodes, client.ConvertToStaticNode())
+	}
+
+	limits.NetworkUsage = make(map[string]models.NetworkUsage)
+	for _, network := range networks {
+		limits.NetworkUsage[network.NetID] = models.NetworkUsage{}
+	}
+
+	for _, node := range nodes {
+		netUsage, ok := limits.NetworkUsage[node.Network]
+		if !ok {
+			// if network doesn't exist, this node is probably awaiting cleanup.
+			// so ignore.
+			continue
+		}
+
+		netUsage.Nodes++
+		if node.IsStatic {
+			netUsage.Clients++
+		}
+		if node.IsIngressGateway {
+			limits.Ingresses++
+			netUsage.Ingresses++
+		}
+		if node.EgressDetails.IsEgressGateway {
+			netUsage.Egresses++
+		}
+		if node.IsRelay {
+			limits.Relays++
+			netUsage.Relays++
+		}
+		if node.IsInternetGateway {
+			limits.InternetGateways++
+			netUsage.InternetGateways++
+		}
+		if node.IsAutoRelay {
+			limits.FailOvers++
+			netUsage.FailOvers++
+		}
+
+		limits.NetworkUsage[node.Network] = netUsage
+	}
+
+	return
+}

+ 13 - 0
logic/util.go

@@ -166,6 +166,19 @@ 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 {

+ 0 - 11
logic/wireguard.go

@@ -9,20 +9,9 @@ func IfaceDelta(currentNode *models.Node, newNode *models.Node) bool {
 	// single comparison statements
 	if newNode.Address.String() != currentNode.Address.String() ||
 		newNode.Address6.String() != currentNode.Address6.String() ||
-		newNode.IsRelay != currentNode.IsRelay ||
 		newNode.Connected != currentNode.Connected {
 		return true
 	}
-	if newNode.IsRelay {
-		if len(currentNode.RelayedNodes) != len(newNode.RelayedNodes) {
-			return true
-		}
-		for _, node := range newNode.RelayedNodes {
-			if !StringSliceContains(currentNode.RelayedNodes, node) {
-				return true
-			}
-		}
-	}
 	return false
 }
 

+ 2 - 2
main.go

@@ -35,10 +35,10 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var version = "v1.1.0"
+var version = "v1.2.0"
 
 //	@title			NetMaker
-//	@version		1.1.0
+//	@version		1.2.0
 //	@description	NetMaker API Docs
 //	@tag.name	    APIUsage
 //	@tag.description.markdown

+ 88 - 13
migrate/migrate.go

@@ -18,6 +18,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/schema"
@@ -43,6 +44,37 @@ func Run() {
 	migrateNameservers()
 	resync()
 	deleteOldExtclients()
+	checkAndDeprecateOldAcls()
+}
+
+func checkAndDeprecateOldAcls() {
+	// check if everything is allowed on old acl and disable old acls
+	nets, _ := logic.GetNetworks()
+	disableOldAcls := true
+	for _, netI := range nets {
+		networkACL, err := nodeacls.FetchAllACLs(nodeacls.NetworkID(netI.NetID))
+		if err != nil {
+			continue
+		}
+		for _, aclNode := range networkACL {
+			for _, allowed := range aclNode {
+				if allowed != acls.Allowed {
+					disableOldAcls = false
+					break
+				}
+			}
+		}
+		if disableOldAcls {
+			netI.DefaultACL = "yes"
+			logic.UpsertNetwork(netI)
+		}
+	}
+	if disableOldAcls {
+		settings := logic.GetServerSettings()
+		settings.OldAClsSupport = false
+		logic.UpsertServerSettings(settings)
+	}
+
 }
 
 func updateNetworks() {
@@ -68,14 +100,37 @@ func migrateNameservers() {
 		if err != nil {
 			continue
 		}
+
+		ns := &schema.Nameserver{
+			NetworkID: netI.NetID,
+		}
+		nameservers, _ := ns.ListByNetwork(db.WithContext(context.TODO()))
+		for _, nsI := range nameservers {
+			if len(nsI.Domains) != 0 {
+				for _, matchDomain := range nsI.MatchDomains {
+					nsI.Domains = append(nsI.Domains, schema.NameserverDomain{
+						Domain: matchDomain,
+					})
+				}
+
+				nsI.MatchDomains = []string{}
+
+				_ = nsI.Update(db.WithContext(context.TODO()))
+			}
+		}
+
 		if len(netI.NameServers) > 0 {
 			ns := schema.Nameserver{
-				ID:           uuid.NewString(),
-				Name:         "upstream nameservers",
-				NetworkID:    netI.NetID,
-				Servers:      []string{},
-				MatchAll:     true,
-				MatchDomains: []string{"."},
+				ID:        uuid.NewString(),
+				Name:      "upstream nameservers",
+				NetworkID: netI.NetID,
+				Servers:   []string{},
+				MatchAll:  true,
+				Domains: []schema.NameserverDomain{
+					{
+						Domain: ".",
+					},
+				},
 				Tags: datatypes.JSONMap{
 					"*": struct{}{},
 				},
@@ -115,12 +170,16 @@ func migrateNameservers() {
 				continue
 			}
 			ns := schema.Nameserver{
-				ID:           uuid.NewString(),
-				Name:         fmt.Sprintf("%s gw nameservers", h.Name),
-				NetworkID:    node.Network,
-				Servers:      []string{node.IngressDNS},
-				MatchAll:     true,
-				MatchDomains: []string{"."},
+				ID:        uuid.NewString(),
+				Name:      fmt.Sprintf("%s gw nameservers", h.Name),
+				NetworkID: node.Network,
+				Servers:   []string{node.IngressDNS},
+				MatchAll:  true,
+				Domains: []schema.NameserverDomain{
+					{
+						Domain: ".",
+					},
+				},
 				Nodes: datatypes.JSONMap{
 					node.ID.String(): struct{}{},
 				},
@@ -309,6 +368,7 @@ func updateEnrollmentKeys() {
 			uuid.Nil,
 			true,
 			false,
+			false,
 		)
 
 	}
@@ -433,6 +493,9 @@ func removeInterGw(egressRanges []string) ([]string, bool) {
 
 func updateAcls() {
 	// get all networks
+	if !logic.GetServerSettings().OldAClsSupport {
+		return
+	}
 	networks, err := logic.GetNetworks()
 	if err != nil && !database.IsEmptyRecord(err) {
 		slog.Error("acls migration failed. error getting networks", "error", err)
@@ -810,11 +873,20 @@ func migrateToEgressV1() {
 }
 
 func migrateSettings() {
-	_, err := database.FetchRecord(database.SERVER_SETTINGS, logic.ServerSettingsDBKey)
+	settingsD := make(map[string]interface{})
+	data, err := database.FetchRecord(database.SERVER_SETTINGS, logic.ServerSettingsDBKey)
 	if database.IsEmptyRecord(err) {
 		logic.UpsertServerSettings(logic.GetServerSettingsFromEnv())
+	} else if err == nil {
+		json.Unmarshal([]byte(data), &settingsD)
 	}
 	settings := logic.GetServerSettings()
+	if _, ok := settingsD["old_acl_support"]; !ok {
+		settings.OldAClsSupport = servercfg.IsOldAclEnabled()
+	}
+	if settings.PeerConnectionCheckInterval == "" {
+		settings.PeerConnectionCheckInterval = "15"
+	}
 	if settings.AuditLogsRetentionPeriodInDays == 0 {
 		settings.AuditLogsRetentionPeriodInDays = 7
 	}
@@ -824,6 +896,9 @@ func migrateSettings() {
 	if settings.JwtValidityDurationClients == 0 {
 		settings.JwtValidityDurationClients = servercfg.GetJwtValidityDurationFromEnv() / 60
 	}
+	if settings.StunServers == "" {
+		settings.StunServers = servercfg.GetStunServers()
+	}
 	logic.UpsertServerSettings(settings)
 }
 

+ 33 - 19
models/api_node.go

@@ -17,22 +17,26 @@ type ApiNodeStatus struct {
 
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
 type ApiNode struct {
-	ID                            string              `json:"id,omitempty" validate:"required,min=5,id_unique"`
-	HostID                        string              `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
-	Address                       string              `json:"address" validate:"omitempty,cidrv4"`
-	Address6                      string              `json:"address6" validate:"omitempty,cidrv6"`
-	LocalAddress                  string              `json:"localaddress" validate:"omitempty,cidr"`
-	AllowedIPs                    []string            `json:"allowedips"`
-	LastModified                  int64               `json:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
-	ExpirationDateTime            int64               `json:"expdatetime" swaggertype:"primitive,integer" format:"int64"`
-	LastCheckIn                   int64               `json:"lastcheckin" swaggertype:"primitive,integer" format:"int64"`
-	LastPeerUpdate                int64               `json:"lastpeerupdate" swaggertype:"primitive,integer" format:"int64"`
-	Network                       string              `json:"network"`
-	NetworkRange                  string              `json:"networkrange"`
-	NetworkRange6                 string              `json:"networkrange6"`
-	IsRelayed                     bool                `json:"isrelayed"`
-	IsRelay                       bool                `json:"isrelay"`
-	IsGw                          bool                `json:"is_gw"`
+	ID                 string            `json:"id,omitempty" validate:"required,min=5,id_unique"`
+	HostID             string            `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
+	Address            string            `json:"address" validate:"omitempty,cidrv4"`
+	Address6           string            `json:"address6" validate:"omitempty,cidrv6"`
+	LocalAddress       string            `json:"localaddress" validate:"omitempty,cidr"`
+	AllowedIPs         []string          `json:"allowedips"`
+	LastModified       int64             `json:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
+	ExpirationDateTime int64             `json:"expdatetime" swaggertype:"primitive,integer" format:"int64"`
+	LastCheckIn        int64             `json:"lastcheckin" swaggertype:"primitive,integer" format:"int64"`
+	LastPeerUpdate     int64             `json:"lastpeerupdate" swaggertype:"primitive,integer" format:"int64"`
+	Network            string            `json:"network"`
+	NetworkRange       string            `json:"networkrange"`
+	NetworkRange6      string            `json:"networkrange6"`
+	IsRelayed          bool              `json:"isrelayed"`
+	IsRelay            bool              `json:"isrelay"`
+	IsGw               bool              `json:"is_gw"`
+	IsAutoRelay        bool              `json:"is_auto_relay"`
+	AutoRelayedPeers   map[string]string `json:"auto_relayed_peers"`
+	AutoAssignGateway  bool              `json:"auto_assign_gw"`
+	//AutoRelayedBy                 uuid.UUID           `json:"auto_relayed_by"`
 	RelayedBy                     string              `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
 	RelayedNodes                  []string            `json:"relaynodes" yaml:"relayedNodes"`
 	IsEgressGateway               bool                `json:"isegressgateway"`
@@ -75,12 +79,16 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.ID, _ = uuid.Parse(a.ID)
 	convertedNode.HostID, _ = uuid.Parse(a.HostID)
 	//convertedNode.IsRelay = a.IsRelay
+	if a.RelayedBy != "" && !a.IsRelayed {
+		a.IsRelayed = true
+	}
 	convertedNode.IsRelayed = a.IsRelayed
 	convertedNode.RelayedBy = a.RelayedBy
 	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.PendingDelete = a.PendingDelete
-	convertedNode.FailedOverBy = currentNode.FailedOverBy
-	convertedNode.FailOverPeers = currentNode.FailOverPeers
+	convertedNode.FailedOverBy = uuid.Nil
+	convertedNode.FailOverPeers = make(map[string]struct{})
+	convertedNode.IsFailOver = false
 	//convertedNode.IsIngressGateway = a.IsIngressGateway
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
@@ -134,10 +142,12 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	}
 	convertedNode.Tags = a.Tags
 	convertedNode.IsGw = a.IsGw
+	convertedNode.IsAutoRelay = a.IsAutoRelay
 	if convertedNode.IsGw {
 		convertedNode.IsRelay = true
 		convertedNode.IsIngressGateway = true
 	}
+	convertedNode.AutoAssignGateway = a.AutoAssignGateway
 	return &convertedNode
 }
 
@@ -189,6 +199,10 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.IsGw = nm.IsGw
 	apiNode.RelayedBy = nm.RelayedBy
 	apiNode.RelayedNodes = nm.RelayedNodes
+	apiNode.IsAutoRelay = nm.IsAutoRelay
+	//apiNode.AutoRelayedBy = nm.AutoRelayedBy
+	apiNode.AutoRelayedPeers = nm.AutoRelayedPeers
+	apiNode.AutoAssignGateway = nm.AutoAssignGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
@@ -200,7 +214,7 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.IsInternetGateway = nm.IsInternetGateway
 	apiNode.InternetGwID = nm.InternetGwID
 	apiNode.InetNodeReq = nm.InetNodeReq
-	apiNode.IsFailOver = nm.IsFailOver
+	apiNode.IsFailOver = false
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailedOverBy = nm.FailedOverBy
 	apiNode.Metadata = nm.Metadata

+ 1 - 1
models/egress.go

@@ -6,7 +6,7 @@ type EgressReq struct {
 	Network     string         `json:"network"`
 	Description string         `json:"description"`
 	Nodes       map[string]int `json:"nodes"`
-	Tags        []string       `json:"tags"`
+	Tags        map[string]int `json:"tags"`
 	Range       string         `json:"range"`
 	Domain      string         `json:"domain"`
 	Nat         bool           `json:"nat"`

+ 23 - 21
models/enrollment_key.go

@@ -43,31 +43,33 @@ const EnrollmentKeyLength = 32
 
 // EnrollmentKey - the key used to register hosts and join them to specific networks
 type EnrollmentKey struct {
-	Expiration    time.Time `json:"expiration"`
-	UsesRemaining int       `json:"uses_remaining"`
-	Value         string    `json:"value"`
-	Networks      []string  `json:"networks"`
-	Unlimited     bool      `json:"unlimited"`
-	Tags          []string  `json:"tags"`
-	Token         string    `json:"token,omitempty"` // B64 value of EnrollmentToken
-	Type          KeyType   `json:"type"`
-	Relay         uuid.UUID `json:"relay"`
-	Groups        []TagID   `json:"groups"`
-	Default       bool      `json:"default"`
-	AutoEgress    bool      `json:"auto_egress"`
+	Expiration        time.Time `json:"expiration"`
+	UsesRemaining     int       `json:"uses_remaining"`
+	Value             string    `json:"value"`
+	Networks          []string  `json:"networks"`
+	Unlimited         bool      `json:"unlimited"`
+	Tags              []string  `json:"tags"`
+	Token             string    `json:"token,omitempty"` // B64 value of EnrollmentToken
+	Type              KeyType   `json:"type"`
+	Relay             uuid.UUID `json:"relay"`
+	Groups            []TagID   `json:"groups"`
+	Default           bool      `json:"default"`
+	AutoEgress        bool      `json:"auto_egress"`
+	AutoAssignGateway bool      `json:"auto_assign_gw"`
 }
 
 // APIEnrollmentKey - used to create enrollment keys via API
 type APIEnrollmentKey struct {
-	Expiration    int64    `json:"expiration" swaggertype:"primitive,integer" format:"int64"`
-	UsesRemaining int      `json:"uses_remaining"`
-	Networks      []string `json:"networks"`
-	Unlimited     bool     `json:"unlimited"`
-	Tags          []string `json:"tags" validate:"required,dive,min=3,max=32"`
-	Type          KeyType  `json:"type"`
-	Relay         string   `json:"relay"`
-	Groups        []TagID  `json:"groups"`
-	AutoEgress    bool     `json:"auto_egress"`
+	Expiration        int64    `json:"expiration" swaggertype:"primitive,integer" format:"int64"`
+	UsesRemaining     int      `json:"uses_remaining"`
+	Networks          []string `json:"networks"`
+	Unlimited         bool     `json:"unlimited"`
+	Tags              []string `json:"tags" validate:"required,dive,min=3,max=32"`
+	Type              KeyType  `json:"type"`
+	Relay             string   `json:"relay"`
+	Groups            []TagID  `json:"groups"`
+	AutoEgress        bool     `json:"auto_egress"`
+	AutoAssignGateway bool     `json:"auto_assign_gw"`
 }
 
 // RegisterResponse - the response to a successful enrollment register

+ 17 - 11
models/host.go

@@ -106,6 +106,8 @@ const (
 	SignalHost HostMqAction = "SIGNAL_HOST"
 	// UpdateHost - constant for host update action
 	UpdateHost HostMqAction = "UPDATE_HOST"
+	// UpdateNode - constant for Node update action
+	UpdateNode HostMqAction = "UPDATE_NODE"
 	// DeleteHost - constant for host delete action
 	DeleteHost HostMqAction = "DELETE_HOST"
 	// JoinHostToNetwork - constant for host network join action
@@ -126,6 +128,8 @@ const (
 	UpdateMetrics HostMqAction = "UPDATE_METRICS"
 	// EgressUpdate - const for egress update action
 	EgressUpdate HostMqAction = "EGRESS_UPDATE"
+	// CHECK_ASSIGN_GW - const for to auto assign gw action
+	CheckAutoAssignGw HostMqAction = "CHECK_AUTO_ASSIGN_GW"
 )
 
 // SignalAction - turn peer signal action
@@ -156,17 +160,19 @@ type HostTurnRegister struct {
 
 // Signal - struct for signalling peer
 type Signal struct {
-	Server         string       `json:"server"`
-	FromHostPubKey string       `json:"from_host_pubkey"`
-	ToHostPubKey   string       `json:"to_host_pubkey"`
-	FromHostID     string       `json:"from_host_id"`
-	ToHostID       string       `json:"to_host_id"`
-	FromNodeID     string       `json:"from_node_id"`
-	ToNodeID       string       `json:"to_node_id"`
-	Reply          bool         `json:"reply"`
-	Action         SignalAction `json:"action"`
-	IsPro          bool         `json:"is_pro"`
-	TimeStamp      int64        `json:"timestamp"`
+	Server               string           `json:"server"`
+	FromHostPubKey       string           `json:"from_host_pubkey"`
+	ToHostPubKey         string           `json:"to_host_pubkey"`
+	FromHostID           string           `json:"from_host_id"`
+	ToHostID             string           `json:"to_host_id"`
+	FromNodeID           string           `json:"from_node_id"`
+	ToNodeID             string           `json:"to_node_id"`
+	NetworkID            string           `json:"networkID"`
+	Reply                bool             `json:"reply"`
+	AutoRelayNodeMetrics map[string]int64 `json:"auto_relay_node_metrics"`
+	Action               SignalAction     `json:"action"`
+	IsPro                bool             `json:"is_pro"`
+	TimeStamp            int64            `json:"timestamp"`
 }
 
 // RegisterMsg - login message struct for hosts to join via SSO login

+ 1 - 0
models/metrics.go

@@ -10,6 +10,7 @@ type Metrics struct {
 	NodeID       string            `json:"node_id" bson:"node_id" yaml:"node_id"`
 	NodeName     string            `json:"node_name" bson:"node_name" yaml:"node_name"`
 	Connectivity map[string]Metric `json:"connectivity" bson:"connectivity" yaml:"connectivity"`
+	UpdatedAt    time.Time         `json:"updated_at" bson:"updated_at" yaml:"updated_at"`
 }
 
 // Metric - holds a metric for data between nodes

+ 12 - 2
models/mqtt.go

@@ -13,6 +13,7 @@ type HostPeerInfo struct {
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
 	Host              Host                  `json:"host"`
+	Nodes             []Node                `json:"nodes"`
 	ChangeDefaultGw   bool                  `json:"change_default_gw"`
 	DefaultGwIp       net.IP                `json:"default_gw_ip"`
 	IsInternetGw      bool                  `json:"is_inet_gw"`
@@ -30,6 +31,8 @@ type HostPeerUpdate struct {
 	NameServers       []string              `json:"name_servers"`
 	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
 	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
+	AutoRelayNodes    map[NetworkID][]Node  `json:"auto_relay_nodes"`
+	GwNodes           map[NetworkID][]Node  `json:"gw_nodes"`
 	ServerConfig
 	OldPeerUpdateFields
 }
@@ -41,8 +44,9 @@ type EgressDomain struct {
 	Domain string `json:"domain"`
 }
 type Nameserver struct {
-	IPs         []string `json:"ips"`
-	MatchDomain string   `json:"match_domain"`
+	IPs            []string `json:"ips"`
+	MatchDomain    string   `json:"match_domain"`
+	IsSearchDomain bool     `json:"is_search_domain"`
 }
 
 type OldPeerUpdateFields struct {
@@ -132,3 +136,9 @@ type FwUpdate struct {
 type FailOverMeReq struct {
 	NodeID string `json:"node_id"`
 }
+
+// AutoRelayMeReq - struct for autorelay req
+type AutoRelayMeReq struct {
+	NodeID        string `json:"node_id"`
+	AutoRelayGwID string `json:"auto_relay_gw_id"`
+}

+ 29 - 23
models/node.go

@@ -87,34 +87,39 @@ type CommonNode struct {
 	IsGw                bool      `json:"is_gw"             yaml:"is_gw"`
 	RelayedNodes        []string  `json:"relaynodes"          yaml:"relayedNodes"`
 	IngressDNS          string    `json:"ingressdns"          yaml:"ingressdns"`
+	AutoAssignGateway   bool      `json:"auto_assign_gw"`
 }
 
 // Node - a model of a network node
 type Node struct {
 	CommonNode
-	PendingDelete              bool                 `json:"pendingdelete"           bson:"pendingdelete"           yaml:"pendingdelete"`
-	LastModified               time.Time            `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified"`
-	LastCheckIn                time.Time            `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin"`
-	LastPeerUpdate             time.Time            `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate"`
-	ExpirationDateTime         time.Time            `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime"`
-	EgressGatewayNatEnabled    bool                 `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
-	EgressGatewayRequest       EgressGatewayRequest `json:"egressgatewayrequest"    bson:"egressgatewayrequest"    yaml:"egressgatewayrequest"`
-	IngressGatewayRange        string               `json:"ingressgatewayrange"     bson:"ingressgatewayrange"     yaml:"ingressgatewayrange"`
-	IngressGatewayRange6       string               `json:"ingressgatewayrange6"    bson:"ingressgatewayrange6"    yaml:"ingressgatewayrange6"`
-	IngressPersistentKeepalive int32                `json:"ingresspersistentkeepalive"     bson:"ingresspersistentkeepalive"     yaml:"ingresspersistentkeepalive"`
-	IngressMTU                 int32                `json:"ingressmtu"     bson:"ingressmtu"     yaml:"ingressmtu"`
+	PendingDelete              bool                 `json:"pendingdelete"`
+	LastModified               time.Time            `json:"lastmodified"`
+	LastCheckIn                time.Time            `json:"lastcheckin"`
+	LastPeerUpdate             time.Time            `json:"lastpeerupdate"`
+	ExpirationDateTime         time.Time            `json:"expdatetime"`
+	EgressGatewayNatEnabled    bool                 `json:"egressgatewaynatenabled"`
+	EgressGatewayRequest       EgressGatewayRequest `json:"egressgatewayrequest"`
+	IngressGatewayRange        string               `json:"ingressgatewayrange"`
+	IngressGatewayRange6       string               `json:"ingressgatewayrange6"`
+	IngressPersistentKeepalive int32                `json:"ingresspersistentkeepalive"`
+	IngressMTU                 int32                `json:"ingressmtu"`
 	Metadata                   string               `json:"metadata"`
 	// == PRO ==
-	DefaultACL        string              `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
-	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
-	IsFailOver        bool                `json:"is_fail_over"                                           yaml:"is_fail_over"`
-	FailOverPeers     map[string]struct{} `json:"fail_over_peers"                                       yaml:"fail_over_peers"`
-	FailedOverBy      uuid.UUID           `json:"failed_over_by"                                         yaml:"failed_over_by"`
-	IsInternetGateway bool                `json:"isinternetgateway"                                      yaml:"isinternetgateway"`
-	InetNodeReq       InetNodeReq         `json:"inet_node_req"                                          yaml:"inet_node_req"`
-	InternetGwID      string              `json:"internetgw_node_id"                                     yaml:"internetgw_node_id"`
-	AdditionalRagIps  []net.IP            `json:"additional_rag_ips"                                     yaml:"additional_rag_ips"                                     swaggertype:"array,number"`
-	Tags              map[TagID]struct{}  `json:"tags" yaml:"tags"`
+	DefaultACL  string `json:"defaultacl,omitempty" validate:"checkyesornoorunset"`
+	OwnerID     string `json:"ownerid,omitempty"`
+	IsFailOver  bool   `json:"is_fail_over"`
+	IsAutoRelay bool   `json:"is_auto_relay"`
+	//AutoRelayedPeers   map[string]struct{} `json:"auto_relayed_peers"`
+	AutoRelayedPeers map[string]string `json:"auto_relayed_peers_v1"`
+	//AutoRelayedBy     uuid.UUID           `json:"auto_relayed_by"`
+	FailOverPeers     map[string]struct{} `json:"fail_over_peers"`
+	FailedOverBy      uuid.UUID           `json:"failed_over_by"`
+	IsInternetGateway bool                `json:"isinternetgateway"`
+	InetNodeReq       InetNodeReq         `json:"inet_node_req"`
+	InternetGwID      string              `json:"internetgw_node_id"`
+	AdditionalRagIps  []net.IP            `json:"additional_rag_ips" swaggertype:"array,number"`
+	Tags              map[TagID]struct{}  `json:"tags"`
 	IsStatic          bool                `json:"is_static"`
 	IsUserNode        bool                `json:"is_user_node"`
 	StaticNode        ExtClient           `json:"static_node"`
@@ -442,10 +447,10 @@ func (newNode *Node) Fill(
 	if newNode.ExpirationDateTime.IsZero() {
 		newNode.ExpirationDateTime = currentNode.ExpirationDateTime
 	}
-	if newNode.LastPeerUpdate.IsZero() {
+	if newNode.LastPeerUpdate.IsZero() || currentNode.LastPeerUpdate.After(newNode.LastPeerUpdate) {
 		newNode.LastPeerUpdate = currentNode.LastPeerUpdate
 	}
-	if newNode.LastCheckIn.IsZero() {
+	if newNode.LastCheckIn.IsZero() || currentNode.LastCheckIn.After(newNode.LastCheckIn) {
 		newNode.LastCheckIn = currentNode.LastCheckIn
 	}
 	if newNode.Network == "" {
@@ -481,6 +486,7 @@ func (newNode *Node) Fill(
 	if newNode.IsFailOver != currentNode.IsFailOver {
 		newNode.IsFailOver = currentNode.IsFailOver
 	}
+	newNode.FailOverPeers = currentNode.FailOverPeers
 	if newNode.Tags == nil {
 		if currentNode.Tags == nil {
 			currentNode.Tags = make(map[TagID]struct{})

+ 2 - 0
models/settings.go

@@ -48,6 +48,8 @@ type ServerSettings struct {
 	Stun                           bool   `json:"stun"`
 	StunServers                    string `json:"stun_servers"`
 	AuditLogsRetentionPeriodInDays int    `json:"audit_logs_retention_period"`
+	OldAClsSupport                 bool   `json:"old_acl_support"`
+	PeerConnectionCheckInterval    string `json:"peer_connection_check_interval"`
 }
 
 type UserSettings struct {

+ 30 - 24
models/structs.go

@@ -22,6 +22,8 @@ type FeatureFlags struct {
 	EnableOAuth             bool `json:"enable_oauth"`
 	EnableIDPIntegration    bool `json:"enable_idp_integration"`
 	AllowMultiServerLicense bool `json:"allow_multi_server_license"`
+	EnableGwsHA             bool `json:"enable_gws_ha"`
+	EnableDeviceApproval    bool `json:"enable_device_approval"`
 }
 
 // AuthParams - struct for auth params
@@ -52,9 +54,11 @@ type UserRemoteGws struct {
 	AllowedEndpoints  []string   `json:"allowed_endpoints"`
 	NetworkAddresses  []string   `json:"network_addresses"`
 	Status            NodeStatus `json:"status"`
+	ManageDNS         bool       `json:"manage_dns"`
 	DnsAddress        string     `json:"dns_address"`
 	Addresses         string     `json:"addresses"`
 	MatchDomains      []string   `json:"match_domains"`
+	SearchDomains     []string   `json:"search_domains"`
 }
 
 // UserRAGs - struct for user access gws
@@ -266,9 +270,9 @@ type HostPull struct {
 	NameServers       []string              `json:"name_servers"`
 	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
 	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
-}
-
-type DefaultGwInfo struct {
+	AutoRelayNodes    map[NetworkID][]Node  `json:"auto_relay_nodes"`
+	GwNodes           map[NetworkID][]Node  `json:"gw_nodes"`
+	ReplacePeers      bool                  `json:"replace_peers"`
 }
 
 // NodeGet - struct for a single node get response
@@ -291,27 +295,29 @@ type NodeJoinResponse struct {
 
 // ServerConfig - struct for dealing with the server information for a netclient
 type ServerConfig struct {
-	CoreDNSAddr       string `yaml:"corednsaddr"`
-	API               string `yaml:"api"`
-	APIHost           string `yaml:"apihost"`
-	APIPort           string `yaml:"apiport"`
-	DNSMode           string `yaml:"dnsmode"`
-	Version           string `yaml:"version"`
-	MQPort            string `yaml:"mqport"`
-	MQUserName        string `yaml:"mq_username"`
-	MQPassword        string `yaml:"mq_password"`
-	BrokerType        string `yaml:"broker_type"`
-	Server            string `yaml:"server"`
-	Broker            string `yaml:"broker"`
-	IsPro             bool   `yaml:"isee" json:"Is_EE"`
-	TrafficKey        []byte `yaml:"traffickey"`
-	MetricInterval    string `yaml:"metric_interval"`
-	MetricsPort       int    `yaml:"metrics_port"`
-	ManageDNS         bool   `yaml:"manage_dns"`
-	Stun              bool   `yaml:"stun"`
-	StunServers       string `yaml:"stun_servers"`
-	EndpointDetection bool   `yaml:"endpoint_detection"`
-	DefaultDomain     string `yaml:"default_domain"`
+	CoreDNSAddr                 string `yaml:"corednsaddr"`
+	API                         string `yaml:"api"`
+	APIHost                     string `yaml:"apihost"`
+	APIPort                     string `yaml:"apiport"`
+	DNSMode                     string `yaml:"dnsmode"`
+	Version                     string `yaml:"version"`
+	MQPort                      string `yaml:"mqport"`
+	MQUserName                  string `yaml:"mq_username"`
+	MQPassword                  string `yaml:"mq_password"`
+	BrokerType                  string `yaml:"broker_type"`
+	Server                      string `yaml:"server"`
+	Broker                      string `yaml:"broker"`
+	IsPro                       bool   `yaml:"isee" json:"Is_EE"`
+	TrafficKey                  []byte `yaml:"traffickey"`
+	MetricInterval              string `yaml:"metric_interval"`
+	MetricsPort                 int    `yaml:"metrics_port"`
+	ManageDNS                   bool   `yaml:"manage_dns"`
+	Stun                        bool   `yaml:"stun"`
+	StunServers                 string `yaml:"stun_servers"`
+	EndpointDetection           bool   `yaml:"endpoint_detection"`
+	DefaultDomain               string `yaml:"default_domain"`
+	PeerConnectionCheckInterval string `yaml:"peer_connection_check_interval"`
+	OldAClsSupport              bool   `json:"-"`
 }
 
 // User.NameInCharset - returns if name is in charset below or not

+ 40 - 0
models/usage.go

@@ -0,0 +1,40 @@
+package models
+
+// Usage - struct for license usage
+type Usage struct {
+	Servers          int                     `json:"servers"`
+	Users            int                     `json:"users"`
+	Hosts            int                     `json:"hosts"`
+	Clients          int                     `json:"clients"`
+	Networks         int                     `json:"networks"`
+	Ingresses        int                     `json:"ingresses"`
+	Egresses         int                     `json:"egresses"`
+	Relays           int                     `json:"relays"`
+	InternetGateways int                     `json:"internet_gateways"`
+	FailOvers        int                     `json:"fail_overs"`
+	NetworkUsage     map[string]NetworkUsage `json:"network_usage"`
+}
+
+type NetworkUsage struct {
+	Nodes            int `json:"nodes"`
+	Clients          int `json:"clients"`
+	Ingresses        int `json:"ingresses"`
+	Egresses         int `json:"egresses"`
+	Relays           int `json:"relays"`
+	InternetGateways int `json:"internet_gateways"`
+	FailOvers        int `json:"fail_overs"`
+}
+
+// SetDefaults - sets the default values for usage
+func (l *Usage) SetDefaults() {
+	l.Clients = 0
+	l.Servers = 1
+	l.Hosts = 0
+	l.Users = 1
+	l.Networks = 0
+	l.Ingresses = 0
+	l.Egresses = 0
+	l.Relays = 0
+	l.InternetGateways = 0
+	l.NetworkUsage = make(map[string]NetworkUsage)
+}

+ 1 - 0
models/user_mgmt.go

@@ -102,6 +102,7 @@ const (
 	AdminRole      UserRoleID = "admin"
 	ServiceUser    UserRoleID = "service-user"
 	PlatformUser   UserRoleID = "platform-user"
+	Auditor        UserRoleID = "auditor"
 	NetworkAdmin   UserRoleID = "network-admin"
 	NetworkUser    UserRoleID = "network-user"
 )

+ 56 - 39
mq/handlers.go

@@ -108,21 +108,28 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 	case models.CheckIn:
 		sendPeerUpdate = HandleHostCheckin(&hostUpdate.Host, currentHost)
 	case models.Acknowledgement:
+		nodes, err := logic.GetAllNodes()
+		if err != nil {
+			return
+		}
 		hu := hostactions.GetAction(currentHost.ID.String())
 		if hu != nil {
 			if err = HostUpdate(hu); err != nil {
 				slog.Error("failed to send new node to host", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 				return
 			} else {
-				nodes, err := logic.GetAllNodes()
-				if err != nil {
-					return
-				}
+
 				if err = PublishSingleHostPeerUpdate(currentHost, nodes, nil, nil, false, nil); err != nil {
 					slog.Error("failed peers publish after join acknowledged", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 					return
 				}
 			}
+		} else {
+			// send latest host update
+			HostUpdate(&models.HostUpdate{
+				Action: models.UpdateHost,
+				Host:   *currentHost})
+			PublishSingleHostPeerUpdate(currentHost, nodes, nil, nil, false, nil)
 		}
 	case models.UpdateHost:
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
@@ -136,42 +143,10 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 			return
 		}
 	case models.DeleteHost:
-		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-			// delete EMQX credentials for host
-			if err := emqx.DeleteEmqxUser(currentHost.ID.String()); err != nil {
-				slog.Error("failed to remove host credentials from EMQX", "id", currentHost.ID, "error", err)
-			}
-		}
-
-		// 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
-		}
-		if err := logic.RemoveHostByID(currentHost.ID.String()); err != nil {
-			slog.Error("failed to delete host", "id", currentHost.ID, "error", err)
-			return
-		}
-		if servercfg.IsDNSMode() {
-			logic.SetDNS()
-		}
+		DeleteAndCleanupHost(currentHost)
 		sendPeerUpdate = true
 	case models.SignalHost:
-		signalPeer(hostUpdate.Signal)
+		SignalPeer(hostUpdate.Signal)
 
 	}
 
@@ -183,13 +158,55 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 	}
 }
 
-func signalPeer(signal models.Signal) {
+func DeleteAndCleanupHost(h *models.Host) {
+	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
+		// delete EMQX credentials for host
+		if err := emqx.DeleteEmqxUser(h.ID.String()); err != nil {
+			slog.Error("failed to remove host credentials from EMQX", "id", h.ID, "error", err)
+		}
+	}
+
+	// notify of deleted peer change
+
+	for _, nodeID := range h.Nodes {
+		node, err := logic.GetNodeByID(nodeID)
+		if err == nil {
+			PublishMqUpdatesForDeletedNode(node, false)
+		}
+	}
+
+	if err := logic.DisassociateAllNodesFromHost(h.ID.String()); err != nil {
+		slog.Error("failed to delete all nodes of host", "id", h.ID, "error", err)
+		return
+	}
+	if err := logic.RemoveHostByID(h.ID.String()); err != nil {
+		slog.Error("failed to delete host", "id", h.ID, "error", err)
+		return
+	}
+	if servercfg.IsDNSMode() {
+		logic.SetDNS()
+	}
+}
+
+func SignalPeer(signal models.Signal) {
 
 	if signal.ToHostPubKey == "" {
 		msg := "insufficient data to signal peer"
 		logger.Log(0, msg)
 		return
 	}
+	node, err := logic.GetNodeByID(signal.FromNodeID)
+	if err != nil {
+		return
+	}
+	peer, err := logic.GetNodeByID(signal.ToNodeID)
+	if err != nil {
+		return
+	}
+	if node.Network != peer.Network {
+		return
+	}
+	signal.NetworkID = node.Network
 	signal.IsPro = servercfg.IsPro
 	peerHost, err := logic.GetHost(signal.ToHostID)
 	if err != nil {

+ 1 - 1
mq/publishers.go

@@ -198,7 +198,7 @@ func ServerStartNotify() error {
 }
 
 // PublishMqUpdatesForDeletedNode - published all the required updates for deleted node
-func PublishMqUpdatesForDeletedNode(node models.Node, sendNodeUpdate bool, gwClients []models.ExtClient) {
+func PublishMqUpdatesForDeletedNode(node models.Node, sendNodeUpdate bool) {
 	// notify of peer change
 	node.PendingDelete = true
 	node.Action = models.NODE_DELETE

+ 1 - 1
pro/auth/headless_callback.go

@@ -98,7 +98,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 	var response bytes.Buffer
 	if err := ssoCallbackTemplate.Execute(&response, ssoCallbackTemplateConfig{
 		User: userClaims.getUserName(),
-		Verb: "Authenticated",
+		Verb: "authenticated",
 	}); err != nil {
 		logger.Log(0, "Could not render SSO callback template ", err.Error())
 		response := returnErrTemplate(userClaims.getUserName(), "Could not render SSO callback template", state, reqKeyIf)

+ 1 - 1
pro/auth/register_callback.go

@@ -86,7 +86,7 @@ func HandleHostSSOCallback(w http.ResponseWriter, r *http.Request) {
 	var response bytes.Buffer
 	if err := ssoCallbackTemplate.Execute(&response, ssoCallbackTemplateConfig{
 		User: userClaims.getUserName(),
-		Verb: "Authenticated",
+		Verb: "authenticated",
 	}); err != nil {
 		logger.Log(0, "Could not render SSO callback template ", err.Error())
 		response := returnErrTemplate(reqKeyIf.User, "Could not render SSO callback template", state, reqKeyIf)

+ 62 - 19
pro/auth/sync.go

@@ -11,11 +11,13 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/pro/idp"
 	"github.com/gravitl/netmaker/pro/idp/azure"
 	"github.com/gravitl/netmaker/pro/idp/google"
 	"github.com/gravitl/netmaker/pro/idp/okta"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"github.com/gravitl/netmaker/servercfg"
 )
 
 var (
@@ -149,7 +151,8 @@ func syncUsers(idpUsers []idp.User) error {
 	for _, user := range idpUsers {
 		if user.AccountArchived {
 			// delete the user if it has been archived.
-			_ = logic.DeleteUser(user.Username)
+			user := dbUsersMap[user.Username]
+			_ = deleteAndCleanUpUser(&user)
 			continue
 		}
 
@@ -209,14 +212,14 @@ func syncUsers(idpUsers []idp.User) error {
 	}
 
 	for _, user := range dbUsersMap {
-		if user.ExternalIdentityProviderID == "" {
-			continue
-		}
-		if _, ok := idpUsersMap[user.UserName]; !ok {
-			// delete the user if it has been deleted on idp.
-			err = logic.DeleteUser(user.UserName)
-			if err != nil {
-				return err
+		if user.ExternalIdentityProviderID != "" {
+			if _, ok := idpUsersMap[user.UserName]; !ok {
+				// delete the user if it has been deleted on idp
+				// or is filtered out.
+				err = deleteAndCleanUpUser(&user)
+				if err != nil {
+					return err
+				}
 			}
 		}
 	}
@@ -277,7 +280,11 @@ func syncGroups(idpGroups []idp.Group) error {
 			dbGroup.ExternalIdentityProviderID = group.ID
 			dbGroup.Name = group.Name
 			dbGroup.Default = false
-			dbGroup.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{})
+			dbGroup.NetworkRoles = map[models.NetworkID]map[models.UserRoleID]struct{}{
+				models.AllNetworks: {
+					proLogic.GetDefaultGlobalUserRoleID(): {},
+				},
+			}
 			err := proLogic.CreateUserGroup(&dbGroup)
 			if err != nil {
 				return err
@@ -324,8 +331,9 @@ func syncGroups(idpGroups []idp.Group) error {
 	for _, group := range dbGroups {
 		if group.ExternalIdentityProviderID != "" {
 			if _, ok := idpGroupsMap[group.ExternalIdentityProviderID]; !ok {
-				// delete the group if it has been deleted on idp.
-				err = proLogic.DeleteUserGroup(group.ID)
+				// delete the group if it has been deleted on idp
+				// or is filtered out.
+				err = proLogic.DeleteAndCleanUpGroup(&group)
 				if err != nil {
 					return err
 				}
@@ -355,6 +363,7 @@ func GetIDPSyncStatus() models.IDPSyncStatus {
 		}
 	}
 }
+
 func filterUsersByGroupMembership(idpUsers []idp.User, idpGroups []idp.Group) []idp.User {
 	usersMap := make(map[string]int)
 	for i, user := range idpUsers {
@@ -395,14 +404,14 @@ func filterGroupsByMembers(idpGroups []idp.Group, idpUsers []idp.User) []idp.Gro
 			if _, ok := usersMap[member]; ok {
 				members = append(members, member)
 			}
+		}
 
-			if len(members) > 0 {
-				// the group at index `i` has members from the `idpUsers` list,
-				// so we keep it.
-				filteredGroupsMap[i] = true
-				// filter out members that were not provided in the `idpUsers` list.
-				idpGroups[i].Members = members
-			}
+		if len(members) > 0 {
+			// the group at index `i` has members from the `idpUsers` list,
+			// so we keep it.
+			filteredGroupsMap[i] = true
+			// filter out members that were not provided in the `idpUsers` list.
+			idpGroups[i].Members = members
 		}
 	}
 
@@ -415,3 +424,37 @@ func filterGroupsByMembers(idpGroups []idp.Group, idpUsers []idp.User) []idp.Gro
 
 	return filteredGroups
 }
+
+// TODO: deduplicate
+// The cyclic import between the package logic and mq requires this
+// function to be duplicated in multiple places.
+func deleteAndCleanUpUser(user *models.User) error {
+	err := logic.DeleteUser(user.UserName)
+	if err != nil {
+		return err
+	}
+
+	// check and delete extclient with this ownerID
+	go func() {
+		extclients, err := logic.GetAllExtClients()
+		if err != nil {
+			return
+		}
+		for _, extclient := range extclients {
+			if extclient.OwnerID == user.UserName {
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err == nil {
+					_ = mq.PublishDeletedClientPeerUpdate(&extclient)
+				}
+			}
+		}
+
+		go logic.DeleteUserInvite(user.UserName)
+		go mq.PublishPeerUpdate(false)
+		if servercfg.IsDNSMode() {
+			go logic.SetDNS()
+		}
+	}()
+
+	return nil
+}

+ 660 - 0
pro/controllers/auto_relay.go

@@ -0,0 +1,660 @@
+package controllers
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/gorilla/mux"
+	controller "github.com/gravitl/netmaker/controllers"
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"github.com/gravitl/netmaker/schema"
+	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slog"
+)
+
+// AutoRelayHandlers - handlers for AutoRelay
+func AutoRelayHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/node/{nodeid}/auto_relay", controller.Authorize(true, false, "host", http.HandlerFunc(getAutoRelayGws))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/node/{nodeid}/auto_relay", logic.SecurityCheck(true, http.HandlerFunc(setAutoRelay))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/node/{nodeid}/auto_relay", logic.SecurityCheck(true, http.HandlerFunc(unsetAutoRelay))).
+		Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/node/{network}/auto_relay/reset", logic.SecurityCheck(true, http.HandlerFunc(resetAutoRelayGw))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/node/{nodeid}/auto_relay_me", controller.Authorize(true, false, "host", http.HandlerFunc(autoRelayME))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/node/{nodeid}/auto_relay_me", controller.Authorize(true, false, "host", http.HandlerFunc(autoRelayMEUpdate))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/node/{nodeid}/auto_relay_check", controller.Authorize(true, false, "host", http.HandlerFunc(checkautoRelayCtx))).
+		Methods(http.MethodGet)
+}
+
+// @Summary     Get auto relay nodes
+// @Router      /api/v1/node/{nodeid}/auto_relay [get]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     404 {object} models.ErrorResponse
+func getAutoRelayGws(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	// confirm host exists
+	node, err := logic.GetNodeByID(nodeid)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	autoRelayNodes := proLogic.DoesAutoRelayExist(node.Network)
+	if len(autoRelayNodes) == 0 {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("autorelay node not found"), "notfound"),
+		)
+		return
+	}
+	defaultPolicy, err := logic.GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	returnautoRelayNodes := []models.Node{}
+	if !defaultPolicy.Enabled {
+		for _, autoRelayNode := range autoRelayNodes {
+			if logic.IsPeerAllowed(node, autoRelayNode, false) {
+				returnautoRelayNodes = append(returnautoRelayNodes, autoRelayNode)
+			}
+		}
+	} else {
+		returnautoRelayNodes = autoRelayNodes
+	}
+	w.Header().Set("Content-Type", "application/json")
+	logic.ReturnSuccessResponseWithJson(w, r, returnautoRelayNodes, "get autorelay node successfully")
+}
+
+// @Summary     Create AutoRelay node
+// @Router      /api/v1/node/{nodeid}/auto_relay [post]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func setAutoRelay(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	// confirm host exists
+	node, err := logic.GetNodeByID(nodeid)
+	if err != nil {
+		slog.Error("failed to get node:", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = proLogic.CreateAutoRelay(node)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	go mq.PublishPeerUpdate(false)
+	w.Header().Set("Content-Type", "application/json")
+	logic.ReturnSuccessResponseWithJson(w, r, node, "created autorelay successfully")
+}
+
+// @Summary     Reset AutoRelay for a network
+// @Router      /api/v1/node/{network}/auto_relay/reset [post]
+// @Tags        PRO
+// @Param       network path string true "Network ID"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func resetAutoRelayGw(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	net := params["network"]
+	nodes, err := logic.GetNetworkNodes(net)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, node := range nodes {
+		if len(node.AutoRelayedPeers) > 0 {
+			if node.Mutex != nil {
+				node.Mutex.Lock()
+			}
+			node.AutoRelayedPeers = make(map[string]string)
+			if node.Mutex != nil {
+				node.Mutex.Unlock()
+			}
+			logic.UpsertNode(&node)
+		}
+	}
+	go mq.PublishPeerUpdate(false)
+	w.Header().Set("Content-Type", "application/json")
+	logic.ReturnSuccessResponse(w, r, "autorelay has been reset successfully")
+}
+
+// @Summary     Delete autorelay node
+// @Router      /api/v1/node/{nodeid}/auto_relay [delete]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func unsetAutoRelay(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	// confirm host exists
+	node, err := logic.GetNodeByID(nodeid)
+	if err != nil {
+		slog.Error("failed to get node:", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	node.IsAutoRelay = false
+	// Reset AutoRelayed Peers
+	err = logic.UpsertNode(&node)
+	if err != nil {
+		slog.Error("failed to upsert node", "node", node.ID.String(), "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	if servercfg.CacheEnabled() {
+		proLogic.RemoveAutoRelayFromCache(node.Network)
+	}
+	go func() {
+		proLogic.ResetAutoRelay(&node)
+		mq.PublishPeerUpdate(false)
+	}()
+	w.Header().Set("Content-Type", "application/json")
+	logic.ReturnSuccessResponseWithJson(w, r, node, "deleted autorelay successfully")
+}
+
+// @Summary     AutoRelay me
+// @Router      /api/v1/node/{nodeid}/auto_relay_me [post]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Accept      json
+// @Param       body body models.AutoRelayMeReq true "AutoRelay request"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func autoRelayME(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	// confirm host exists
+	node, err := logic.GetNodeByID(nodeid)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get node:", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var autoRelayReq models.AutoRelayMeReq
+	err = json.NewDecoder(r.Body).Decode(&autoRelayReq)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	autoRelayNode, err := logic.GetNodeByID(autoRelayReq.AutoRelayGwID)
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				fmt.Errorf("req-from: %s, autorelay node doesn't exist in the network", host.Name),
+				"badrequest",
+			),
+		)
+		return
+	}
+
+	var sendPeerUpdate bool
+	peerNode, err := logic.GetNodeByID(autoRelayReq.NodeID)
+	if err != nil {
+		slog.Error("peer not found: ", "nodeid", autoRelayReq.NodeID, "error", err)
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer not found"), "badrequest"),
+		)
+		return
+	}
+	eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	acls, _ := logic.ListAclsByNetwork(models.NetworkID(node.Network))
+	logic.GetNodeEgressInfo(&node, eli, acls)
+	logic.GetNodeEgressInfo(&peerNode, eli, acls)
+	logic.GetNodeEgressInfo(&autoRelayNode, eli, acls)
+	if peerNode.IsAutoRelay {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as autorelay"), "badrequest"),
+		)
+		return
+	}
+	if node.IsAutoRelay {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is acting as autorelay"), "badrequest"),
+		)
+		return
+	}
+	if peerNode.IsAutoRelay {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as autorelay"), "badrequest"),
+		)
+		return
+	}
+	if node.IsRelayed && node.RelayedBy == peerNode.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is relayed by peer node"), "badrequest"),
+		)
+		return
+	}
+	if node.IsRelay && peerNode.RelayedBy == node.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node acting as relay for the peer node"), "badrequest"),
+		)
+		return
+	}
+	if (node.InternetGwID != "" && autoRelayNode.IsInternetGateway && node.InternetGwID != autoRelayNode.ID.String()) ||
+		(peerNode.InternetGwID != "" && autoRelayNode.IsInternetGateway && peerNode.InternetGwID != autoRelayNode.ID.String()) {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node using a internet gw by the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if node.IsInternetGateway && peerNode.InternetGwID == node.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node acting as internet gw for the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if node.InternetGwID != "" && node.InternetGwID == peerNode.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node using a internet gw by the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	err = proLogic.SetAutoRelayCtx(autoRelayNode, node, peerNode)
+	if err != nil {
+		slog.Debug("failed to create autorelay", "id", node.ID.String(),
+			"network", node.Network, "error", err)
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("failed to create autorelay: %v", err), "internal"),
+		)
+		return
+	}
+	slog.Info(
+		"[auto-relay] created relay on node",
+		"node",
+		node.ID.String(),
+		"network",
+		node.Network,
+	)
+	sendPeerUpdate = true
+
+	if sendPeerUpdate {
+		go mq.PublishPeerUpdate(false)
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	logic.ReturnSuccessResponse(w, r, "relayed successfully")
+}
+
+// @Summary     AutoRelay me
+// @Router      /api/v1/node/{nodeid}/auto_relay_me [put]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Accept      json
+// @Param       body body models.AutoRelayMeReq true "AutoRelay request"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func autoRelayMEUpdate(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	// confirm host exists
+	node, err := logic.GetNodeByID(nodeid)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get node:", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var autoRelayReq models.AutoRelayMeReq
+	err = json.NewDecoder(r.Body).Decode(&autoRelayReq)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if autoRelayReq.AutoRelayGwID == "" {
+		if node.AutoAssignGateway {
+			// unset current gw
+			if node.RelayedBy != "" {
+				// unset relayed node from the curr relay
+				currRelayNode, err := logic.GetNodeByID(node.RelayedBy)
+				if err == nil {
+					if currRelayNode.Mutex != nil {
+						currRelayNode.Mutex.Lock()
+					}
+					newRelayedNodes := logic.RemoveAllFromSlice(currRelayNode.RelayedNodes, node.ID.String())
+					currRelayNode.RelayedNodes = newRelayedNodes
+					logic.UpsertNode(&currRelayNode)
+					node.RelayedBy = ""
+					node.IsRelayed = false
+					logic.UpsertNode(&node)
+					if currRelayNode.Mutex != nil {
+						currRelayNode.Mutex.Unlock()
+					}
+				}
+			}
+		} else {
+			peerNode, err := logic.GetNodeByID(autoRelayReq.NodeID)
+			if err != nil {
+				slog.Error("peer not found: ", "nodeid", autoRelayReq.NodeID, "error", err)
+				logic.ReturnErrorResponse(
+					w,
+					r,
+					logic.FormatError(errors.New("peer not found"), "badrequest"),
+				)
+				return
+			}
+			delete(node.AutoRelayedPeers, peerNode.ID.String())
+			delete(peerNode.AutoRelayedPeers, node.ID.String())
+			logic.UpsertNode(&node)
+			logic.UpsertNode(&peerNode)
+		}
+		allNodes, err := logic.GetAllNodes()
+		if err == nil {
+			mq.PublishSingleHostPeerUpdate(host, allNodes, nil, nil, false, nil)
+		}
+		go mq.PublishPeerUpdate(false)
+		if node.AutoAssignGateway {
+			mq.HostUpdate(&models.HostUpdate{Action: models.CheckAutoAssignGw, Host: *host, Node: node})
+		}
+		logic.ReturnSuccessResponse(w, r, "unrelayed successfully")
+		return
+	}
+	autoRelayNode, err := logic.GetNodeByID(autoRelayReq.AutoRelayGwID)
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				fmt.Errorf("req-from: %s, autorelay node doesn't exist in the network", host.Name),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if !autoRelayNode.IsGw {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				fmt.Errorf(" autorelay node is not a gw"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if node.AutoAssignGateway {
+		if node.RelayedBy != autoRelayReq.AutoRelayGwID {
+			if node.RelayedBy != "" {
+				// unset relayed node from the curr relay
+				currRelayNode, err := logic.GetNodeByID(node.RelayedBy)
+				if err == nil {
+					newRelayedNodes := logic.RemoveAllFromSlice(currRelayNode.RelayedNodes, node.ID.String())
+					logic.UpdateRelayNodes(currRelayNode.ID.String(), currRelayNode.RelayedNodes, newRelayedNodes)
+				}
+			}
+			newNodes := []string{node.ID.String()}
+			newNodes = append(newNodes, autoRelayNode.RelayedNodes...)
+			logic.UpdateRelayNodes(autoRelayNode.ID.String(), autoRelayNode.RelayedNodes, newNodes)
+			go mq.PublishPeerUpdate(false)
+		}
+		w.Header().Set("Content-Type", "application/json")
+		logic.ReturnSuccessResponse(w, r, "relayed successfully")
+		return
+	}
+	peerNode, err := logic.GetNodeByID(autoRelayReq.NodeID)
+	if err != nil {
+		slog.Error("peer not found: ", "nodeid", autoRelayReq.NodeID, "error", err)
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer not found"), "badrequest"),
+		)
+		return
+	}
+	if len(node.AutoRelayedPeers) == 0 {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("node is not auto relayed"), "badrequest"))
+		return
+	}
+
+	if !autoRelayNode.IsAutoRelay {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("requested node is not a auto relay node"), "badrequest"))
+		return
+	}
+	if node.AutoRelayedPeers[peerNode.ID.String()] == peerNode.AutoRelayedPeers[node.ID.String()] {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("already using requested relay node"), "badrequest"))
+		return
+	}
+	node.AutoRelayedPeers[peerNode.ID.String()] = autoRelayReq.AutoRelayGwID
+	peerNode.AutoRelayedPeers[node.ID.String()] = autoRelayReq.AutoRelayGwID
+	logic.UpsertNode(&node)
+	slog.Info(
+		"[auto-relay] created relay on node",
+		"node",
+		node.ID.String(),
+		"network",
+		node.Network,
+	)
+	go mq.PublishPeerUpdate(false)
+	w.Header().Set("Content-Type", "application/json")
+	logic.ReturnSuccessResponse(w, r, "relayed successfully")
+}
+
+// @Summary     checkautoRelayCtx
+// @Router      /api/v1/node/{nodeid}/auto_relay_check [get]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Accept      json
+// @Param       body body models.AutoRelayMeReq true "autorelay request"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func checkautoRelayCtx(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	// confirm host exists
+	node, err := logic.GetNodeByID(nodeid)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get node:", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var autoRelayReq models.AutoRelayMeReq
+	err = json.NewDecoder(r.Body).Decode(&autoRelayReq)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	autoRelayNode, err := logic.GetNodeByID(autoRelayReq.AutoRelayGwID)
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				fmt.Errorf("req-from: %s, autorelay node doesn't exist in the network", host.Name),
+				"badrequest",
+			),
+		)
+		return
+	}
+
+	peerNode, err := logic.GetNodeByID(autoRelayReq.NodeID)
+	if err != nil {
+		slog.Error("peer not found: ", "nodeid", autoRelayReq.NodeID, "error", err)
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer not found"), "badrequest"),
+		)
+		return
+	}
+	eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	acls, _ := logic.ListAclsByNetwork(models.NetworkID(node.Network))
+	logic.GetNodeEgressInfo(&node, eli, acls)
+	logic.GetNodeEgressInfo(&peerNode, eli, acls)
+	logic.GetNodeEgressInfo(&autoRelayNode, eli, acls)
+	if peerNode.IsAutoRelay {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as autorelay"), "badrequest"),
+		)
+		return
+	}
+	if node.IsAutoRelay {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is acting as autorelay"), "badrequest"),
+		)
+		return
+	}
+	if peerNode.IsAutoRelay {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as autorelay"), "badrequest"),
+		)
+		return
+	}
+	if node.IsRelayed && node.RelayedBy == peerNode.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is relayed by peer node"), "badrequest"),
+		)
+		return
+	}
+	if node.IsRelay && peerNode.RelayedBy == node.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node acting as relay for the peer node"), "badrequest"),
+		)
+		return
+	}
+	if (node.InternetGwID != "" && autoRelayNode.IsInternetGateway && node.InternetGwID != autoRelayNode.ID.String()) ||
+		(peerNode.InternetGwID != "" && autoRelayNode.IsInternetGateway && peerNode.InternetGwID != autoRelayNode.ID.String()) {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node using a internet gw by the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if node.IsInternetGateway && peerNode.InternetGwID == node.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node acting as internet gw for the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if node.InternetGwID != "" && node.InternetGwID == peerNode.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node using a internet gw by the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if ok := logic.IsPeerAllowed(node, peerNode, true); !ok {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("peers are not allowed to communicate"),
+				"badrequest",
+			),
+		)
+		return
+	}
+
+	err = proLogic.CheckAutoRelayCtx(autoRelayNode, node, peerNode)
+	if err != nil {
+		slog.Error("autorelay ctx cannot be set ", "error", err)
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("autorelay ctx cannot be set: %v", err), "internal"),
+		)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	logic.ReturnSuccessResponse(w, r, "autorelay can be set")
+}

+ 4 - 0
pro/controllers/tags.go

@@ -293,6 +293,10 @@ func deleteTag(w http.ResponseWriter, r *http.Request) {
 		},
 		NetworkID: tag.Network,
 		Origin:    models.Dashboard,
+		Diff: models.Diff{
+			Old: tag,
+			New: nil,
+		},
 	})
 	logic.ReturnSuccessResponse(w, r, "deleted tag "+tagID)
 }

+ 33 - 37
pro/controllers/users.go

@@ -352,6 +352,12 @@ func deleteUserInvite(w http.ResponseWriter, r *http.Request) {
 			Type: models.UserInviteSub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: models.UserInvite{
+				Email: email,
+			},
+			New: nil,
+		},
 	})
 	logic.ReturnSuccessResponse(w, r, "deleted user invite")
 }
@@ -851,11 +857,13 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default user group"), "badrequest"))
 		return
 	}
-	err = proLogic.DeleteUserGroup(models.UserGroupID(gid))
+	err = proLogic.DeleteAndCleanUpGroup(&userG)
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+
+	// TODO: log event in proLogic.DeleteAndCleanUpGroup so that all deletions are logged.
 	logic.LogEvent(&models.Event{
 		Action: models.Delete,
 		Source: models.Subject{
@@ -870,43 +878,12 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
 			Type: models.UserGroupSub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: userG,
+			New: nil,
+		},
 	})
-	replacePeers := false
-	go func() {
-		for networkID := range userG.NetworkRoles {
-			acls, err := logic.ListAclsByNetwork(networkID)
-			if err != nil {
-				continue
-			}
 
-			for _, acl := range acls {
-				var hasGroupSrc bool
-				newAclSrc := make([]models.AclPolicyTag, 0)
-				for _, src := range acl.Src {
-					if src.ID == models.UserGroupAclID && src.Value == userG.ID.String() {
-						hasGroupSrc = true
-					} else {
-						newAclSrc = append(newAclSrc, src)
-					}
-				}
-
-				if hasGroupSrc {
-					if len(newAclSrc) == 0 {
-						// no other src exists, delete acl.
-						_ = logic.DeleteAcl(acl)
-					} else {
-						// other sources exist, update acl.
-						acl.Src = newAclSrc
-						_ = logic.UpsertAcl(acl)
-					}
-					replacePeers = true
-				}
-			}
-		}
-	}()
-
-	go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.ID, userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
-	go mq.PublishPeerUpdate(replacePeers)
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
 }
 
@@ -1096,6 +1073,10 @@ func deleteRole(w http.ResponseWriter, r *http.Request) {
 			Type: models.UserRoleSub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: role,
+			New: nil,
+		},
 	})
 	go proLogic.UpdatesUserGwAccessOnRoleUpdates(role.NetworkLevelAccess, make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope), role.NetworkID.String())
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user role")
@@ -1475,6 +1456,7 @@ func getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) {
 		Metadata:          node.Metadata,
 		AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 		NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+		ManageDNS:         host.DNS == "yes",
 		DnsAddress:        node.IngressDNS,
 		Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 	}
@@ -1583,7 +1565,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			}
 		}
 
-		if !found && len(extClients) > 0 {
+		if !found && len(extClients) > 0 && deviceID == "" {
 			// TODO: prevent ip clashes.
 			gwClient = extClients[0]
 		}
@@ -1626,6 +1608,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 			Status:            node.Status,
+			ManageDNS:         host.DNS == "yes",
 			DnsAddress:        node.IngressDNS,
 			Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 		}
@@ -1633,6 +1616,9 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			hNs := logic.GetNameserversForNode(&node)
 			for _, nsI := range hNs {
 				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
+				if nsI.IsSearchDomain {
+					gw.SearchDomains = append(gw.SearchDomains, nsI.MatchDomain)
+				}
 			}
 		}
 		gw.MatchDomains = append(gw.MatchDomains, logic.GetEgressDomainsByAccess(user, models.NetworkID(node.Network))...)
@@ -1677,6 +1663,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 			Status:            node.Status,
+			ManageDNS:         host.DNS == "yes",
 			DnsAddress:        node.IngressDNS,
 			Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 		}
@@ -1684,6 +1671,9 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			hNs := logic.GetNameserversForNode(&node)
 			for _, nsI := range hNs {
 				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
+				if nsI.IsSearchDomain {
+					gw.SearchDomains = append(gw.SearchDomains, nsI.MatchDomain)
+				}
 			}
 		}
 		gw.MatchDomains = append(gw.MatchDomains, logic.GetEgressDomainsByAccess(user, models.NetworkID(node.Network))...)
@@ -1884,6 +1874,12 @@ func deletePendingUser(w http.ResponseWriter, r *http.Request) {
 			Type: models.PendingUserSub,
 		},
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: models.User{
+				UserName: username,
+			},
+			New: nil,
+		},
 	})
 	logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
 }

+ 1 - 1
pro/email/invite.go

@@ -53,7 +53,7 @@ func (invite UserInvitedMail) GetBody(info Notification) string {
 		WithHtml("</ol>").
 		WithParagraph("Important Information:").
 		WithHtml("<ul>").
-		WithHtml(fmt.Sprintf("<li>When connecting through RAC, please enter your server connection ID: %s.</li>", connectionID)).
+		WithHtml(fmt.Sprintf("<li>When connecting through Netmaker Desktop, please enter your server connection ID: %s.</li>", connectionID)).
 		WithHtml("</ul>").
 		WithParagraph(fmt.Sprintf("If you have any questions or need assistance, please contact our support team at <a href=\"mailto:%s\">%s</a>.", supportEmail, supportEmail)).
 		WithParagraph("Best Regards,").

+ 6 - 2
pro/idp/azure/azure.go

@@ -226,15 +226,19 @@ func (a *Client) getAccessToken() (string, error) {
 }
 
 func buildPrefixFilter(field string, prefixes []string) string {
+	return url.PathEscape("$filter=" + buildCondition(field, prefixes))
+}
+
+func buildCondition(field string, prefixes []string) string {
 	if len(prefixes) == 0 {
 		return ""
 	}
 
 	if len(prefixes) == 1 {
-		return fmt.Sprintf("$filter=startswith(%s,'%s')", field, prefixes[0])
+		return fmt.Sprintf("startswith(%s,'%s')", field, prefixes[0])
 	}
 
-	return buildPrefixFilter(field, prefixes[:1]) + "%20or%20" + buildPrefixFilter(field, prefixes[1:])
+	return buildCondition(field, prefixes[:1]) + " or " + buildCondition(field, prefixes[1:])
 }
 
 type getUsersResponse struct {

+ 13 - 0
pro/initialize.go

@@ -36,6 +36,7 @@ func InitPro() {
 		proControllers.EventHandlers,
 		proControllers.TagHandlers,
 		proControllers.NetworkHandlers,
+		proControllers.AutoRelayHandlers,
 	)
 	controller.ListRoles = proControllers.ListRoles
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
@@ -92,15 +93,26 @@ func InitPro() {
 		}
 		proLogic.LoadNodeMetricsToCache()
 		proLogic.InitFailOverCache()
+		if servercfg.CacheEnabled() {
+			proLogic.InitAutoRelayCache()
+		}
 		auth.ResetIDPSyncHook()
 		email.Init()
 		go proLogic.EventWatcher()
+
+		logic.GetMetricsMonitor().Start()
 	})
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
 	logic.FailOverExists = proLogic.FailOverExists
 	logic.CreateFailOver = proLogic.CreateFailOver
 	logic.GetFailOverPeerIps = proLogic.GetFailOverPeerIps
+
+	logic.ResetAutoRelay = proLogic.ResetAutoRelay
+	logic.ResetAutoRelayedPeer = proLogic.ResetAutoRelayedPeer
+	logic.SetAutoRelay = proLogic.SetAutoRelay
+	logic.GetAutoRelayPeerIps = proLogic.GetAutoRelayPeerIps
+
 	logic.DenyClientNodeAccess = proLogic.DenyClientNode
 	logic.IsClientNodeAllowed = proLogic.IsClientNodeAllowed
 	logic.AllowClientNodeAccess = proLogic.RemoveDeniedNodeFromClient
@@ -159,6 +171,7 @@ func InitPro() {
 	logic.GetNameserversForHost = proLogic.GetNameserversForHost
 	logic.GetNameserversForNode = proLogic.GetNameserversForNode
 	logic.ValidateNameserverReq = proLogic.ValidateNameserverReq
+	logic.ValidateEgressReq = proLogic.ValidateEgressReq
 
 }
 

+ 1 - 1
pro/license.go

@@ -85,7 +85,7 @@ func ValidateLicense() (err error) {
 
 	licenseSecret := LicenseSecret{
 		AssociatedID: netmakerTenantID,
-		Usage:        getCurrentServerUsage(),
+		Usage:        logic.GetCurrentServerUsage(),
 	}
 
 	secretData, err := json.Marshal(&licenseSecret)

+ 297 - 0
pro/logic/auto_relay.go

@@ -0,0 +1,297 @@
+package logic
+
+import (
+	"context"
+	"errors"
+	"net"
+	"sync"
+
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slog"
+)
+
+var autoRelayCtxMutex = &sync.RWMutex{}
+var autoRelayCacheMutex = &sync.RWMutex{}
+var autoRelayCache = make(map[models.NetworkID][]string)
+
+func InitAutoRelayCache() {
+	autoRelayCacheMutex.Lock()
+	defer autoRelayCacheMutex.Unlock()
+	allNodes, err := logic.GetAllNodes()
+	if err != nil {
+		return
+	}
+	for _, node := range allNodes {
+		if node.IsAutoRelay {
+			autoRelayCache[models.NetworkID(node.Network)] = append(autoRelayCache[models.NetworkID(node.Network)], node.ID.String())
+		}
+	}
+
+}
+func SetAutoRelay(node *models.Node) {
+	node.IsAutoRelay = true
+}
+
+func CheckAutoRelayCtx(autoRelayNode, victimNode, peerNode models.Node) error {
+	autoRelayCtxMutex.RLock()
+	defer autoRelayCtxMutex.RUnlock()
+	if peerNode.AutoRelayedPeers == nil {
+		return nil
+	}
+	if victimNode.AutoRelayedPeers == nil {
+		return nil
+	}
+	if peerNode.Mutex != nil {
+		peerNode.Mutex.Lock()
+	}
+	autoRelayNodeIDPeerNode, peerHasAutoRelayed := peerNode.AutoRelayedPeers[victimNode.ID.String()]
+	if peerNode.Mutex != nil {
+		peerNode.Mutex.Unlock()
+	}
+	if victimNode.Mutex != nil {
+		victimNode.Mutex.Lock()
+	}
+	autoRelayNodeIDVictim, victimHasAutoRelayed := victimNode.AutoRelayedPeers[peerNode.ID.String()]
+	if victimNode.Mutex != nil {
+		victimNode.Mutex.Unlock()
+	}
+	if peerHasAutoRelayed && victimHasAutoRelayed && autoRelayNodeIDVictim == autoRelayNodeIDPeerNode {
+		return errors.New("auto relay ctx is already set")
+	}
+	return nil
+}
+func SetAutoRelayCtx(autoRelayNode, victimNode, peerNode models.Node) error {
+	autoRelayCtxMutex.Lock()
+	defer autoRelayCtxMutex.Unlock()
+	if peerNode.AutoRelayedPeers == nil {
+		peerNode.AutoRelayedPeers = make(map[string]string)
+	}
+	if victimNode.AutoRelayedPeers == nil {
+		victimNode.AutoRelayedPeers = make(map[string]string)
+	}
+	if peerNode.Mutex != nil {
+		peerNode.Mutex.Lock()
+	}
+	autoRelayNodeIDPeerNode, peerHasAutoRelayed := peerNode.AutoRelayedPeers[victimNode.ID.String()]
+	if peerNode.Mutex != nil {
+		peerNode.Mutex.Unlock()
+	}
+	if victimNode.Mutex != nil {
+		victimNode.Mutex.Lock()
+	}
+	autoRelayNodeIDVictim, victimHasAutoRelayed := victimNode.AutoRelayedPeers[peerNode.ID.String()]
+	if victimNode.Mutex != nil {
+		victimNode.Mutex.Unlock()
+	}
+	if peerHasAutoRelayed && victimHasAutoRelayed && autoRelayNodeIDVictim == autoRelayNodeIDPeerNode {
+		return errors.New("auto relay ctx is already set")
+	}
+	if peerNode.Mutex != nil {
+		peerNode.Mutex.Lock()
+	}
+	peerNode.AutoRelayedPeers[victimNode.ID.String()] = autoRelayNode.ID.String()
+	if peerNode.Mutex != nil {
+		peerNode.Mutex.Unlock()
+	}
+	if victimNode.Mutex != nil {
+		victimNode.Mutex.Lock()
+	}
+	victimNode.AutoRelayedPeers[peerNode.ID.String()] = autoRelayNode.ID.String()
+	if victimNode.Mutex != nil {
+		victimNode.Mutex.Unlock()
+	}
+	if err := logic.UpsertNode(&victimNode); err != nil {
+		return err
+	}
+	if err := logic.UpsertNode(&peerNode); err != nil {
+		return err
+	}
+	return nil
+}
+
+// GetAutoRelayNode - gets the host acting as autoRelay
+func GetAutoRelayNode(network string, allNodes []models.Node) (models.Node, error) {
+	nodes := logic.GetNetworkNodesMemory(allNodes, network)
+	for _, node := range nodes {
+		if node.IsAutoRelay {
+			return node, nil
+		}
+	}
+	return models.Node{}, errors.New("auto relay not found")
+}
+
+func RemoveAutoRelayFromCache(network string) {
+	autoRelayCacheMutex.Lock()
+	defer autoRelayCacheMutex.Unlock()
+	delete(autoRelayCache, models.NetworkID(network))
+}
+
+func SetAutoRelayInCache(node models.Node) {
+	autoRelayCacheMutex.Lock()
+	defer autoRelayCacheMutex.Unlock()
+	autoRelayCache[models.NetworkID(node.Network)] = append(autoRelayCache[models.NetworkID(node.Network)], node.ID.String())
+}
+
+// DoesAutoRelayExist - checks if autorelay exists already in the network
+func DoesAutoRelayExist(network string) (autoRelayNodes []models.Node) {
+	autoRelayCacheMutex.RLock()
+	defer autoRelayCacheMutex.RUnlock()
+	if !servercfg.CacheEnabled() {
+		nodes, _ := logic.GetNetworkNodes(network)
+		for _, node := range nodes {
+			if node.IsAutoRelay {
+				autoRelayNodes = append(autoRelayNodes, node)
+			}
+		}
+	}
+	if nodeIDs, ok := autoRelayCache[models.NetworkID(network)]; ok {
+		for _, nodeID := range nodeIDs {
+			autoRelayNode, err := logic.GetNodeByID(nodeID)
+			if err == nil {
+				autoRelayNodes = append(autoRelayNodes, autoRelayNode)
+			}
+		}
+
+	}
+	return
+}
+
+// ResetAutoRelayedPeer - removes auto relayed over node from network peers
+func ResetAutoRelayedPeer(autoRelayedNode *models.Node) error {
+	nodes, err := logic.GetNetworkNodes(autoRelayedNode.Network)
+	if err != nil {
+		return err
+	}
+	autoRelayedNode.AutoRelayedPeers = make(map[string]string)
+	err = logic.UpsertNode(autoRelayedNode)
+	if err != nil {
+		return err
+	}
+	for _, node := range nodes {
+		if node.AutoRelayedPeers == nil || node.ID == autoRelayedNode.ID {
+			continue
+		}
+		delete(node.AutoRelayedPeers, autoRelayedNode.ID.String())
+		logic.UpsertNode(&node)
+	}
+	return nil
+}
+
+// ResetAutoRelay - reset autorelayed peers
+func ResetAutoRelay(autoRelayNode *models.Node) error {
+	// Unset autorelayed peers
+	nodes, err := logic.GetNetworkNodes(autoRelayNode.Network)
+	if err != nil {
+		return err
+	}
+	for _, node := range nodes {
+		for autoRelayedPeerID, autoRelayID := range node.AutoRelayedPeers {
+			if autoRelayID != autoRelayNode.ID.String() {
+				continue
+			}
+			delete(node.AutoRelayedPeers, autoRelayedPeerID)
+			logic.UpsertNode(&node)
+			peer, err := logic.GetNodeByID(autoRelayedPeerID)
+			if err == nil {
+				delete(peer.AutoRelayedPeers, node.ID.String())
+				logic.UpsertNode(&peer)
+			}
+		}
+	}
+	return nil
+}
+
+// GetAutoRelayPeerIps - adds the autorelayed peerIps by the peer
+func GetAutoRelayPeerIps(peer, node *models.Node) []net.IPNet {
+	allowedips := []net.IPNet{}
+	eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	acls, _ := logic.ListAclsByNetwork(models.NetworkID(node.Network))
+	for autoRelayedpeerID, autoRelayID := range node.AutoRelayedPeers {
+		if peer.ID.String() != autoRelayID {
+			continue
+		}
+		autoRelayedpeer, err := logic.GetNodeByID(autoRelayedpeerID)
+		if err == nil {
+			logic.GetNodeEgressInfo(&autoRelayedpeer, eli, acls)
+			if autoRelayedpeer.Address.IP != nil {
+				allowed := net.IPNet{
+					IP:   autoRelayedpeer.Address.IP,
+					Mask: net.CIDRMask(32, 32),
+				}
+				allowedips = append(allowedips, allowed)
+			}
+			if autoRelayedpeer.Address6.IP != nil {
+				allowed := net.IPNet{
+					IP:   autoRelayedpeer.Address6.IP,
+					Mask: net.CIDRMask(128, 128),
+				}
+				allowedips = append(allowedips, allowed)
+			}
+			if autoRelayedpeer.EgressDetails.IsEgressGateway {
+				allowedips = append(allowedips, logic.GetEgressIPs(&autoRelayedpeer)...)
+			}
+			if autoRelayedpeer.IsRelay {
+				for _, id := range autoRelayedpeer.RelayedNodes {
+					rNode, _ := logic.GetNodeByID(id)
+					logic.GetNodeEgressInfo(&rNode, eli, acls)
+					if rNode.Address.IP != nil {
+						allowed := net.IPNet{
+							IP:   rNode.Address.IP,
+							Mask: net.CIDRMask(32, 32),
+						}
+						allowedips = append(allowedips, allowed)
+					}
+					if rNode.Address6.IP != nil {
+						allowed := net.IPNet{
+							IP:   rNode.Address6.IP,
+							Mask: net.CIDRMask(128, 128),
+						}
+						allowedips = append(allowedips, allowed)
+					}
+					if rNode.EgressDetails.IsEgressGateway {
+						allowedips = append(allowedips, logic.GetEgressIPs(&rNode)...)
+					}
+				}
+			}
+			// handle ingress gateway peers
+			if autoRelayedpeer.IsIngressGateway {
+				extPeers, _, _, err := logic.GetExtPeers(&autoRelayedpeer, node)
+				if err != nil {
+					logger.Log(2, "could not retrieve ext peers for ", peer.ID.String(), err.Error())
+				}
+				for _, extPeer := range extPeers {
+					allowedips = append(allowedips, extPeer.AllowedIPs...)
+				}
+			}
+		}
+	}
+	return allowedips
+}
+
+func CreateAutoRelay(node models.Node) error {
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		return err
+	}
+	if host.OS != models.OS_Types.Linux {
+		return errors.New("only linux nodes are allowed to be set as autoRelay")
+	}
+	if node.IsRelayed {
+		return errors.New("relayed node cannot be set as autoRelay")
+	}
+	node.IsAutoRelay = true
+	err = logic.UpsertNode(&node)
+	if err != nil {
+		slog.Error("failed to upsert node", "node", node.ID.String(), "error", err)
+		return err
+	}
+	if servercfg.CacheEnabled() {
+		SetAutoRelayInCache(node)
+	}
+	return nil
+}

+ 59 - 21
pro/logic/dns.go

@@ -40,12 +40,12 @@ func ValidateNameserverReq(ns schema.Nameserver) error {
 			return errors.New("cannot use netmaker IP as nameserver")
 		}
 	}
-	if !ns.MatchAll && len(ns.MatchDomains) == 0 {
+	if !ns.MatchAll && len(ns.Domains) == 0 {
 		return errors.New("atleast one match domain is required")
 	}
 	if !ns.MatchAll {
-		for _, matchDomain := range ns.MatchDomains {
-			if !logic.IsValidMatchDomain(matchDomain) {
+		for _, domain := range ns.Domains {
+			if !logic.IsValidMatchDomain(domain.Domain) {
 				return errors.New("invalid match domain")
 			}
 		}
@@ -65,6 +65,15 @@ func ValidateNameserverReq(ns schema.Nameserver) error {
 }
 
 func GetNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
+	filters := make(map[string]bool)
+	if node.Address.IP != nil {
+		filters[node.Address.IP.String()] = true
+	}
+
+	if node.Address6.IP != nil {
+		filters[node.Address6.IP.String()] = true
+	}
+
 	ns := &schema.Nameserver{
 		NetworkID: node.Network,
 	}
@@ -73,12 +82,19 @@ func GetNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 		if !nsI.Status {
 			continue
 		}
+
+		filteredIps := logic.FilterOutIPs(nsI.Servers, filters)
+		if len(filteredIps) == 0 {
+			continue
+		}
+
 		_, all := nsI.Tags["*"]
 		if all {
-			for _, matchDomain := range nsI.MatchDomains {
+			for _, domain := range nsI.Domains {
 				returnNsLi = append(returnNsLi, models.Nameserver{
-					IPs:         nsI.Servers,
-					MatchDomain: matchDomain,
+					IPs:            filteredIps,
+					MatchDomain:    domain.Domain,
+					IsSearchDomain: domain.IsSearchDomain,
 				})
 			}
 			continue
@@ -86,10 +102,11 @@ func GetNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 		foundTag := false
 		for tagI := range node.Tags {
 			if _, ok := nsI.Tags[tagI.String()]; ok {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
-						IPs:         nsI.Servers,
-						MatchDomain: matchDomain,
+						IPs:            filteredIps,
+						MatchDomain:    domain.Domain,
+						IsSearchDomain: domain.IsSearchDomain,
 					})
 				}
 				foundTag = true
@@ -102,10 +119,11 @@ func GetNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 			continue
 		}
 		if _, ok := nsI.Nodes[node.ID.String()]; ok {
-			for _, matchDomain := range nsI.MatchDomains {
+			for _, domain := range nsI.Domains {
 				returnNsLi = append(returnNsLi, models.Nameserver{
-					IPs:         nsI.Servers,
-					MatchDomain: matchDomain,
+					IPs:            nsI.Servers,
+					MatchDomain:    domain.Domain,
+					IsSearchDomain: domain.IsSearchDomain,
 				})
 			}
 		}
@@ -126,11 +144,22 @@ func GetNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 	if h.DNS != "yes" {
 		return
 	}
+
 	for _, nodeID := range h.Nodes {
 		node, err := logic.GetNodeByID(nodeID)
 		if err != nil {
 			continue
 		}
+
+		filters := make(map[string]bool)
+		if node.Address.IP != nil {
+			filters[node.Address.IP.String()] = true
+		}
+
+		if node.Address6.IP != nil {
+			filters[node.Address6.IP.String()] = true
+		}
+
 		ns := &schema.Nameserver{
 			NetworkID: node.Network,
 		}
@@ -139,12 +168,19 @@ func GetNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 			if !nsI.Status {
 				continue
 			}
+
+			filteredIps := logic.FilterOutIPs(nsI.Servers, filters)
+			if len(filteredIps) == 0 {
+				continue
+			}
+
 			_, all := nsI.Tags["*"]
 			if all {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
-						IPs:         nsI.Servers,
-						MatchDomain: matchDomain,
+						IPs:            filteredIps,
+						MatchDomain:    domain.Domain,
+						IsSearchDomain: domain.IsSearchDomain,
 					})
 				}
 				continue
@@ -152,10 +188,11 @@ func GetNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 			foundTag := false
 			for tagI := range node.Tags {
 				if _, ok := nsI.Tags[tagI.String()]; ok {
-					for _, matchDomain := range nsI.MatchDomains {
+					for _, domain := range nsI.Domains {
 						returnNsLi = append(returnNsLi, models.Nameserver{
-							IPs:         nsI.Servers,
-							MatchDomain: matchDomain,
+							IPs:            filteredIps,
+							MatchDomain:    domain.Domain,
+							IsSearchDomain: domain.IsSearchDomain,
 						})
 					}
 					foundTag = true
@@ -168,10 +205,11 @@ func GetNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 				continue
 			}
 			if _, ok := nsI.Nodes[node.ID.String()]; ok {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
-						IPs:         nsI.Servers,
-						MatchDomain: matchDomain,
+						IPs:            nsI.Servers,
+						MatchDomain:    domain.Domain,
+						IsSearchDomain: domain.IsSearchDomain,
 					})
 				}
 			}

+ 56 - 0
pro/logic/egress.go

@@ -0,0 +1,56 @@
+package logic
+
+import (
+	"context"
+	"errors"
+
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+	"github.com/gravitl/netmaker/servercfg"
+	"gorm.io/datatypes"
+)
+
+func ValidateEgressReq(e *schema.Egress) error {
+	if e.Network == "" {
+		return errors.New("network id is empty")
+	}
+	_, err := logic.GetNetwork(e.Network)
+	if err != nil {
+		return errors.New("failed to get network " + err.Error())
+	}
+
+	if !servercfg.IsPro && len(e.Nodes) > 1 {
+		return errors.New("can only set one routing node on CE")
+	}
+
+	if len(e.Nodes) > 0 {
+		for k := range e.Nodes {
+			_, err := logic.GetNodeByID(k)
+			if err != nil {
+				return errors.New("invalid routing node " + err.Error())
+			}
+		}
+	}
+	if len(e.Tags) > 0 {
+		e.Nodes = make(datatypes.JSONMap)
+		for tagID := range e.Tags {
+			_, err := GetTag(models.TagID(tagID))
+			if err != nil {
+				return errors.New("invalid tag " + tagID)
+			}
+		}
+	}
+	return nil
+}
+
+func RemoveTagFromEgress(net models.NetworkID, tagID models.TagID) {
+	eli, _ := (&schema.Egress{Network: net.String()}).ListByNetwork(db.WithContext(context.TODO()))
+	for _, eI := range eli {
+		if _, ok := eI.Tags[tagID.String()]; ok {
+			delete(eI.Tags, tagID.String())
+			eI.Update(db.WithContext(context.TODO()))
+		}
+	}
+}

+ 1 - 21
pro/logic/failover.go

@@ -12,7 +12,6 @@ import (
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/schema"
-	"golang.org/x/exp/slog"
 )
 
 var failOverCtxMutex = &sync.RWMutex{}
@@ -263,25 +262,6 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 }
 
 func CreateFailOver(node models.Node) error {
-	if _, exists := FailOverExists(node.Network); exists {
-		return errors.New("failover already exists in the network")
-	}
-	host, err := logic.GetHost(node.HostID.String())
-	if err != nil {
-		return err
-	}
-	if host.OS != models.OS_Types.Linux {
-		return errors.New("only linux nodes are allowed to be set as failover")
-	}
-	if node.IsRelayed {
-		return errors.New("relayed node cannot be set as failover")
-	}
-	node.IsFailOver = true
-	err = logic.UpsertNode(&node)
-	if err != nil {
-		slog.Error("failed to upsert node", "node", node.ID.String(), "error", err)
-		return err
-	}
-	SetFailOverInCache(node)
+
 	return nil
 }

+ 1 - 0
pro/logic/metrics.go

@@ -93,6 +93,7 @@ func GetMetrics(nodeid string) (*models.Metrics, error) {
 
 // UpdateMetrics - updates the metrics of a given client
 func UpdateMetrics(nodeid string, metrics *models.Metrics) error {
+	metrics.UpdatedAt = time.Now()
 	data, err := json.Marshal(metrics)
 	if err != nil {
 		return err

+ 16 - 1
pro/logic/migrate.go

@@ -263,10 +263,11 @@ func MigrateToGws() {
 		return
 	}
 	for _, node := range nodes {
-		if node.IsIngressGateway || node.IsRelay || node.IsInternetGateway {
+		if node.IsIngressGateway || node.IsRelay || node.IsInternetGateway || node.IsFailOver {
 			node.IsGw = true
 			node.IsIngressGateway = true
 			node.IsRelay = true
+			node.IsAutoRelay = true
 			if node.Tags == nil {
 				node.Tags = make(map[models.TagID]struct{})
 			}
@@ -274,6 +275,20 @@ func MigrateToGws() {
 			delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.OldRemoteAccessTagName)))
 			logic.UpsertNode(&node)
 		}
+		// deprecate failover  and initialise auto relay fields
+		if node.IsFailOver {
+			node.IsFailOver = false
+			node.FailOverPeers = make(map[string]struct{})
+			node.FailedOverBy = uuid.Nil
+			node.AutoRelayedPeers = make(map[string]string)
+			logic.UpsertNode(&node)
+		}
+		if node.FailedOverBy != uuid.Nil || len(node.FailOverPeers) > 0 {
+			node.FailOverPeers = make(map[string]struct{})
+			node.FailedOverBy = uuid.Nil
+			node.AutoRelayedPeers = make(map[string]string)
+			logic.UpsertNode(&node)
+		}
 		if node.IsInternetGateway && len(node.InetNodeReq.InetNodeClientIDs) > 0 {
 			node.RelayedNodes = append(node.RelayedNodes, node.InetNodeReq.InetNodeClientIDs...)
 			node.RelayedNodes = logic.UniqueStrings(node.RelayedNodes)

+ 23 - 0
pro/logic/security.go

@@ -44,6 +44,15 @@ func NetworkPermissionsCheck(username string, r *http.Request) error {
 	if userRole.FullAccess {
 		return nil
 	}
+
+	if userRole.ID == models.Auditor {
+		if r.Method == http.MethodGet {
+			return nil
+		} else {
+			return errors.New("access denied")
+		}
+	}
+
 	// get info from header to determine the target rsrc
 	targetRsrc := r.Header.Get("TARGET_RSRC")
 	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
@@ -160,6 +169,20 @@ func GlobalPermissionsCheck(username string, r *http.Request) error {
 	if userRole.FullAccess {
 		return nil
 	}
+
+	if userRole.ID == models.Auditor {
+		if r.Method == http.MethodGet {
+			return nil
+		} else {
+			if (r.Method == http.MethodPut || r.Method == http.MethodPost) &&
+				strings.Contains(r.URL.Path, "/api/users/"+username) {
+				return nil
+			}
+
+			return errors.New("access denied")
+		}
+	}
+
 	targetRsrc := r.Header.Get("TARGET_RSRC")
 	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
 	if targetRsrc == "" {

+ 1 - 0
pro/logic/tags.go

@@ -78,6 +78,7 @@ func DeleteTag(tagID models.TagID, removeFromPolicy bool) error {
 		// remove tag used on acl policy
 		go RemoveDeviceTagFromAclPolicies(tagID, tag.Network)
 	}
+	go RemoveTagFromEgress(tag.Network, tagID)
 	extclients, _ := logic.GetNetworkExtClients(tag.Network.String())
 	for _, extclient := range extclients {
 		if _, ok := extclient.Tags[tagID]; ok {

+ 85 - 1
pro/logic/user_mgmt.go

@@ -43,6 +43,20 @@ var PlatformUserUserPermissionTemplate = models.UserRolePermissionTemplate{
 	},
 }
 
+var AuditorUserPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:                  models.Auditor,
+	Default:             true,
+	DenyDashboardAccess: false,
+	FullAccess:          false,
+	NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+		models.NetworkRsrc: {
+			models.AllNetworkRsrcID: models.RsrcPermissionScope{
+				Read: true,
+			},
+		},
+	},
+}
+
 var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
 	ID:         globalNetworksAdminRoleID,
 	Name:       "Network Admins",
@@ -122,6 +136,8 @@ func UserRolesInit() {
 	database.Insert(ServiceUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	d, _ = json.Marshal(PlatformUserUserPermissionTemplate)
 	database.Insert(PlatformUserUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(AuditorUserPermissionTemplate)
+	database.Insert(AuditorUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	d, _ = json.Marshal(NetworkAdminAllPermissionTemplate)
 	database.Insert(NetworkAdminAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	d, _ = json.Marshal(NetworkUserAllPermissionTemplate)
@@ -620,6 +636,22 @@ func GetUserGroup(gid models.UserGroupID) (models.UserGroup, error) {
 	return ug, nil
 }
 
+func GetDefaultGlobalAdminGroupID() models.UserGroupID {
+	return globalNetworksAdminGroupID
+}
+
+func GetDefaultGlobalUserGroupID() models.UserGroupID {
+	return globalNetworksUserGroupID
+}
+
+func GetDefaultGlobalAdminRoleID() models.UserRoleID {
+	return globalNetworksAdminRoleID
+}
+
+func GetDefaultGlobalUserRoleID() models.UserRoleID {
+	return globalNetworksUserRoleID
+}
+
 func GetDefaultNetworkAdminGroupID(networkID models.NetworkID) models.UserGroupID {
 	return models.UserGroupID(fmt.Sprintf("%s-%s-grp", networkID, models.NetworkAdmin))
 }
@@ -672,6 +704,52 @@ func UpdateUserGroup(g models.UserGroup) error {
 	return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
 }
 
+func DeleteAndCleanUpGroup(group *models.UserGroup) error {
+	err := DeleteUserGroup(group.ID)
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		var replacePeers bool
+		for networkID := range group.NetworkRoles {
+			acls, err := logic.ListAclsByNetwork(networkID)
+			if err != nil {
+				continue
+			}
+
+			for _, acl := range acls {
+				var hasGroupSrc bool
+				newAclSrc := make([]models.AclPolicyTag, 0)
+				for _, src := range acl.Src {
+					if src.ID == models.UserGroupAclID && src.Value == group.ID.String() {
+						hasGroupSrc = true
+					} else {
+						newAclSrc = append(newAclSrc, src)
+					}
+				}
+
+				if hasGroupSrc {
+					if len(newAclSrc) == 0 {
+						// no other src exists, delete acl.
+						_ = logic.DeleteAcl(acl)
+					} else {
+						// other sources exist, update acl.
+						acl.Src = newAclSrc
+						_ = logic.UpsertAcl(acl)
+					}
+					replacePeers = true
+				}
+			}
+		}
+
+		go UpdatesUserGwAccessOnGrpUpdates(group.ID, group.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
+		go mq.PublishPeerUpdate(replacePeers)
+	}()
+
+	return nil
+}
+
 // DeleteUserGroup - deletes user group
 func DeleteUserGroup(gid models.UserGroupID) error {
 	g, err := GetUserGroup(gid)
@@ -930,6 +1008,13 @@ func FilterNetworksByRole(allnetworks []models.Network, user models.User) []mode
 	}
 	if !platformRole.FullAccess {
 		allNetworkRoles := make(map[models.NetworkID]struct{})
+		_, ok := platformRole.NetworkLevelAccess[models.NetworkRsrc]
+		if ok {
+			perm, ok := platformRole.NetworkLevelAccess[models.NetworkRsrc][models.AllNetworkRsrcID]
+			if ok && perm.Read {
+				return allnetworks
+			}
+		}
 		if len(user.NetworkRoles) > 0 {
 			for netID := range user.NetworkRoles {
 				if netID == models.AllNetworks {
@@ -949,7 +1034,6 @@ func FilterNetworksByRole(allnetworks []models.Network, user models.User) []mode
 								return allnetworks
 							}
 							allNetworkRoles[netID] = struct{}{}
-
 						}
 					}
 				}

+ 3 - 29
pro/types.go

@@ -5,6 +5,7 @@ package pro
 
 import (
 	"errors"
+
 	"github.com/gravitl/netmaker/models"
 )
 
@@ -40,35 +41,8 @@ type ValidatedLicense struct {
 
 // LicenseSecret - the encrypted struct for sending user-id
 type LicenseSecret struct {
-	AssociatedID string `json:"associated_id" binding:"required"` // UUID for user foreign key to User table
-	Usage        Usage  `json:"limits"        binding:"required"`
-}
-
-// Usage - struct for license usage
-type Usage struct {
-	Servers          int `json:"servers"`
-	Users            int `json:"users"`
-	Hosts            int `json:"hosts"`
-	Clients          int `json:"clients"`
-	Networks         int `json:"networks"`
-	Ingresses        int `json:"ingresses"`
-	Egresses         int `json:"egresses"`
-	Relays           int `json:"relays"`
-	InternetGateways int `json:"internet_gateways"`
-	FailOvers        int `json:"fail_overs"`
-}
-
-// Usage.SetDefaults - sets the default values for usage
-func (l *Usage) SetDefaults() {
-	l.Clients = 0
-	l.Servers = 1
-	l.Hosts = 0
-	l.Users = 1
-	l.Networks = 0
-	l.Ingresses = 0
-	l.Egresses = 0
-	l.Relays = 0
-	l.InternetGateways = 0
+	AssociatedID string       `json:"associated_id" binding:"required"` // UUID for user foreign key to User table
+	Usage        models.Usage `json:"limits"        binding:"required"`
 }
 
 // ValidateLicenseRequest - used for request to validate license endpoint

+ 0 - 44
pro/util.go

@@ -4,12 +4,7 @@
 package pro
 
 import (
-	"context"
 	"encoding/base64"
-	"github.com/gravitl/netmaker/db"
-	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/schema"
-	"github.com/gravitl/netmaker/logic"
 )
 
 // base64encode - base64 encode helper function
@@ -26,42 +21,3 @@ func base64decode(input string) []byte {
 
 	return bytes
 }
-
-func getCurrentServerUsage() (limits Usage) {
-	limits.SetDefaults()
-	hosts, hErr := logic.GetAllHostsWithStatus(models.OnlineSt)
-	if hErr == nil {
-		limits.Hosts = len(hosts)
-	}
-	clients, cErr := logic.GetAllExtClientsWithStatus(models.OnlineSt)
-	if cErr == nil {
-		limits.Clients = len(clients)
-	}
-	users, err := logic.GetUsers()
-	if err == nil {
-		limits.Users = len(users)
-	}
-	networks, err := logic.GetNetworks()
-	if err == nil {
-		limits.Networks = len(networks)
-	}
-	// TODO this part bellow can be optimized to get nodes just once
-	ingresses, err := logic.GetAllIngresses()
-	if err == nil {
-		limits.Ingresses = len(ingresses)
-	}
-	limits.Egresses, _ = (&schema.Egress{}).Count(db.WithContext(context.TODO()))
-	relays, err := logic.GetRelays()
-	if err == nil {
-		limits.Relays = len(relays)
-	}
-	gateways, err := logic.GetInternetGateways()
-	if err == nil {
-		limits.InternetGateways = len(gateways)
-	}
-	failovers, err := logic.GetAllFailOvers()
-	if err == nil {
-		limits.FailOvers = len(failovers)
-	}
-	return
-}

+ 30 - 16
release.md

@@ -1,37 +1,51 @@
-## Netmaker v1.1.0 Release Notes 🚀 
+## Netmaker v1.2.0 Release Notes 🚀 
 
-## What’s New
+## 🚀 What’s New
 
-- Okta IDP Integration – Seamless authentication and user provisioning with Okta.
+### 🌍 Auto-Relays (formerly Failovers)
 
-- Egress Domain-Based Routing – Route traffic based on domain names, not just network CIDRs.
+- Failovers are now Auto-Relays with High Availability (HA) support.
 
-- DNS Nameservers with Match Domain Functionality – Fine-grained DNS resolution control per domain.
+- Enables global routing optimization based on real-time latency between peers across regions.
 
-- Service User Management – Platform Network Admins can now add service users directly to networks.
+### 🔁 Gateway High Availability
 
-- Device Approval Workflow – Require admin approval before devices can join a network.
+- Gateways can now automatically assign peer relays and fallback to healthy nodes when primary gateways become unavailable.
 
-- Auto-Created User Group Policies – Automatically generate network access policies for new user groups.
+### 🌐 Egress HA with Latency-Aware Routing
 
-- User Session Expiry Controls – Set session timeouts for both Dashboard and Client Apps.
+- Egress gateways now dynamically select the optimal route based on latency, ensuring faster and more resilient connectivity.
 
-## Improvements & Fixes 🛠 
+### 🧭 DNS Search Domains
 
-- Access Control Lists (ACLs): Enhanced functionality and flexibility.
+- Added DNS search domain functionality for simplified hostname resolution across distributed networks.
 
-- User Management UX: Streamlined workflows for easier administration.
+### 👥 New User Roles
 
-- IDP User/Group Filtering: Improved filtering capabilities for large organizations.
+- Introduced a User Auditor role for security and compliance use-cases, offering read-only visibility into system activity.
 
-- Stability Enhancements: More reliable connections for nodes using Internet Gateways.
+### 🧩 Onboarding Flow
+
+- Streamlined user onboarding experience during signup for workspace setup.
+
+### ⚙️ Dynamic ACL Deprecation
+
+- Added logic to automatically deprecate outdated ACLs on demand, reducing stale configurations and improving policy hygiene.
+
+## 🧰 Improvements & Fixes
+
+- Metrics Enrichment: Enhanced uptime and connection-status data.
+
+- DNS Control Fixes: Fixed toggle behavior for enabling/disabling Netmaker DNS on hosts.
+
+- Device Approvals: Improved logic for device approval management.
+
+- Egress Domain Updates: Fixed domain-related issues in egress configurations to ensure consistent routing behavior.
 
 ## Known Issues 🐞
 
 - WireGuard DNS issue on Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Netmaker Desktop, previously known as the Remote Access Client (RAC), and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04.
 
-- Inaccurate uptime info in metrics involving ipv4-only and ipv6-only traffic
-
 - netclients cannot auto-upgrade on ipv6-only machines.
 
 - Need to optimize multi-network netclient join with enrollment key

+ 13 - 6
schema/dns.go

@@ -9,12 +9,14 @@ import (
 )
 
 type Nameserver struct {
-	ID           string                      `gorm:"primaryKey" json:"id"`
-	Name         string                      `gorm:"name" json:"name"`
-	NetworkID    string                      `gorm:"network_id" json:"network_id"`
-	Description  string                      `gorm:"description" json:"description"`
-	Servers      datatypes.JSONSlice[string] `gorm:"servers" json:"servers"`
-	MatchAll     bool                        `gorm:"match_all" json:"match_all"`
+	ID          string                                `gorm:"primaryKey" json:"id"`
+	Name        string                                `gorm:"name" json:"name"`
+	NetworkID   string                                `gorm:"network_id" json:"network_id"`
+	Description string                                `gorm:"description" json:"description"`
+	Servers     datatypes.JSONSlice[string]           `gorm:"servers" json:"servers"`
+	MatchAll    bool                                  `gorm:"match_all" json:"match_all"`
+	Domains     datatypes.JSONSlice[NameserverDomain] `gorm:"domains" json:"domains"`
+	// TODO: deprecate
 	MatchDomains datatypes.JSONSlice[string] `gorm:"match_domains" json:"match_domains"`
 	Tags         datatypes.JSONMap           `gorm:"tags" json:"tags"`
 	Nodes        datatypes.JSONMap           `gorm:"nodes" json:"nodes"`
@@ -24,6 +26,11 @@ type Nameserver struct {
 	UpdatedAt    time.Time                   `gorm:"updated_at" json:"updated_at"`
 }
 
+type NameserverDomain struct {
+	Domain         string `json:"domain"`
+	IsSearchDomain bool   `json:"is_search_domain"`
+}
+
 func (ns *Nameserver) Get(ctx context.Context) error {
 	return db.FromContext(ctx).Model(&Nameserver{}).First(&ns).Where("id = ?", ns.ID).Error
 }

+ 28 - 28
scripts/nm-quick.sh

@@ -4,9 +4,9 @@ CONFIG_FILE=netmaker.env
 # location of nm-quick.sh (usually `/root`)
 SCRIPT_DIR=$(dirname "$(realpath "$0")")
 CONFIG_PATH="$SCRIPT_DIR/$CONFIG_FILE"
-NM_QUICK_VERSION="0.1.1"
+NM_QUICK_VERSION="1.0.0"
 #LATEST=$(curl -s https://api.github.com/repos/gravitl/netmaker/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d [:space:],\")
-LATEST=v1.1.0
+LATEST=v1.2.0
 BRANCH=master
 if [ $(id -u) -ne 0 ]; then
 	echo "This script must be run as root"
@@ -150,12 +150,12 @@ setup_netclient() {
 # configure_netclient - configures server's netclient as a default host and an ingress gateway
 configure_netclient() {
 	sleep 2
-	NODE_ID=$(sudo cat /etc/netclient/nodes.json | jq -r .netmaker.id)
-	if [ "$NODE_ID" = "" ] || [ "$NODE_ID" = "null" ]; then
-		echo "Error obtaining NODE_ID for the new network"
-		exit 1
-	fi
-	echo "register complete. New node ID: $NODE_ID"
+	# NODE_ID=$(sudo cat /etc/netclient/nodes.json | jq -r .netmaker.id)
+	# if [ "$NODE_ID" = "" ] || [ "$NODE_ID" = "null" ]; then
+	# 	echo "Error obtaining NODE_ID for the new network"
+	# 	exit 1
+	# fi
+	# echo "register complete. New node ID: $NODE_ID"
 	HOST_ID=$(sudo cat /etc/netclient/netclient.json | jq -r .id)
 	if [ "$HOST_ID" = "" ] || [ "$HOST_ID" = "null" ]; then
 		echo "Error obtaining HOST_ID for the new network"
@@ -167,13 +167,13 @@ configure_netclient() {
 	set +e
 	nmctl host update $HOST_ID --default
 	sleep 5
-	nmctl node create_remote_access_gateway netmaker $NODE_ID
-	sleep 2
-	# set failover
-	if [ "$INSTALL_TYPE" = "pro" ]; then
-	    #setup failOver
-		curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/v1/node/${NODE_ID}/failover" --header "Authorization: Bearer ${MASTER_KEY}"
-	fi
+	# nmctl node create_remote_access_gateway netmaker $NODE_ID
+	# sleep 2
+	# # set failover
+	# if [ "$INSTALL_TYPE" = "pro" ]; then
+	#     #setup failOver
+	# 	curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/v1/node/${NODE_ID}/failover" --header "Authorization: Bearer ${MASTER_KEY}"
+	# fi
 	set -e
 }
 
@@ -694,24 +694,24 @@ test_connection() {
 setup_mesh() {
 
 	wait_seconds 5
-	networks=$(nmctl network list -o json)
-	if [[ ${networks} != "null" ]]; then
-		netmakerNet=$(nmctl network list -o json | jq -r '.[] | .netid' | grep -w "netmaker")
-	fi
-	# create netmaker network
-	if [[ ${netmakerNet} = "" ]]; then
-		echo "Creating netmaker network (100.64.0.0/16)"
-		# TODO causes "Error Status: 400 Response: {"Code":400,"Message":"could not find any records"}"
-		nmctl network create --name netmaker --ipv4_addr 100.64.0.0/16
-	fi
+	# networks=$(nmctl network list -o json)
+	# if [[ ${networks} != "null" ]]; then
+	# 	netmakerNet=$(nmctl network list -o json | jq -r '.[] | .netid' | grep -w "netmaker")
+	# fi
+	# # create netmaker network
+	# if [[ ${netmakerNet} = "" ]]; then
+	# 	echo "Creating netmaker network (100.64.0.0/16)"
+	# 	# TODO causes "Error Status: 400 Response: {"Code":400,"Message":"could not find any records"}"
+	# 	nmctl network create --name netmaker --ipv4_addr 100.64.0.0/16
+	# fi
 	# create enrollment key for netmaker network
-	local netmakerTag=$(nmctl enrollment_key list | jq -r '.[] | .tags[0]' | grep -w "netmaker")
+	local netmakerTag=$(nmctl enrollment_key list | jq -r '.[] | .tags[0]' | grep -w "firstJoinKey")
 	if [[ ${netmakerTag} = "" ]]; then
-		nmctl enrollment_key create --tags netmaker --unlimited --networks netmaker
+		nmctl enrollment_key create --tags firstJoinKey --unlimited
 	fi
 	echo "Obtaining enrollment key..."
 	# key exists already, fetch token
-	TOKEN=$(nmctl enrollment_key list | jq -r '.[] | select(.tags[0]=="netmaker") | .token')
+	TOKEN=$(nmctl enrollment_key list | jq -r '.[] | select(.tags[0]=="firstJoinKey") | .token')
 	wait_seconds 3
 }
 

+ 5 - 1
servercfg/serverconf.go

@@ -750,7 +750,11 @@ func IsStunEnabled() bool {
 }
 
 func GetStunServers() string {
-	return os.Getenv("STUN_SERVERS")
+	stunservers := os.Getenv("STUN_SERVERS")
+	if stunservers == "" {
+		stunservers = "stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302"
+	}
+	return stunservers
 }
 
 // GetEnvironment returns the environment the server is running in (e.g. dev, staging, prod...)

+ 1 - 1
swagger.yaml

@@ -1511,7 +1511,7 @@ info:
   contact: {}
   description: NetMaker API Docs
   title: NetMaker
-  version: 1.1.0
+  version: 1.2.0
 paths:
   /api/dns:
     get: