Browse Source

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

v1.2.0
Abhishek K 1 month ago
parent
commit
e9b00001f5
85 changed files with 2711 additions and 733 deletions
  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
       label: Version
       description: What version are you running?
       description: What version are you running?
       options:
       options:
+        - v1.2.0
         - v1.1.0
         - v1.1.0
         - v1.0.0
         - v1.0.0
         - v0.99.0
         - v0.99.0

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

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

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

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

+ 2 - 2
Dockerfile

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

+ 1 - 1
Dockerfile-quick

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

+ 1 - 1
README.md

@@ -16,7 +16,7 @@
 
 
 <p align="center">
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
   <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>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />
     <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 {
 		if err = conn.WriteMessage(messageType, reponseData); err != nil {
 			logger.Log(0, "error during message writing:", err.Error())
 			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
 	case <-timeout: // the read from req.answerCh has timed out
 		logger.Log(0, "timeout signal recv,exiting oauth socket conn")
 		logger.Log(0, "timeout signal recv,exiting oauth socket conn")
 		break
 		break
@@ -272,24 +272,47 @@ func CheckNetRegAndHostUpdate(key models.EnrollmentKey, h *models.Host, username
 				continue
 				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)
 			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 err == nil || strings.Contains(err.Error(), "host already part of network") {
 				if len(key.Groups) > 0 {
 				if len(key.Groups) > 0 {
 					newNode.Tags = make(map[models.TagID]struct{})
 					newNode.Tags = make(map[models.TagID]struct{})

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

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

+ 4 - 0
controllers/acls.go

@@ -426,6 +426,10 @@ func deleteAcl(w http.ResponseWriter, r *http.Request) {
 		},
 		},
 		NetworkID: acl.NetworkID,
 		NetworkID: acl.NetworkID,
 		Origin:    models.Dashboard,
 		Origin:    models.Dashboard,
+		Diff: models.Diff{
+			Old: acl,
+			New: nil,
+		},
 	})
 	})
 	go mq.PublishPeerUpdate(true)
 	go mq.PublishPeerUpdate(true)
 	logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name)
 	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 {
 	if req.MatchAll {
-		req.MatchDomains = []string{"."}
+		req.Domains = []schema.NameserverDomain{
+			{
+				Domain: ".",
+			},
+		}
 	}
 	}
 	ns := schema.Nameserver{
 	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()))
 	err = ns.Create(db.WithContext(r.Context()))
@@ -242,7 +246,7 @@ func updateNs(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	ns.Servers = updateNs.Servers
 	ns.Servers = updateNs.Servers
 	ns.Tags = updateNs.Tags
 	ns.Tags = updateNs.Tags
-	ns.MatchDomains = updateNs.MatchDomains
+	ns.Domains = updateNs.Domains
 	ns.MatchAll = updateNs.MatchAll
 	ns.MatchAll = updateNs.MatchAll
 	ns.Description = updateNs.Description
 	ns.Description = updateNs.Description
 	ns.Name = updateNs.Name
 	ns.Name = updateNs.Name
@@ -312,6 +316,10 @@ func deleteNs(w http.ResponseWriter, r *http.Request) {
 		},
 		},
 		NetworkID: models.NetworkID(ns.NetworkID),
 		NetworkID: models.NetworkID(ns.NetworkID),
 		Origin:    models.Dashboard,
 		Origin:    models.Dashboard,
+		Diff: models.Diff{
+			Old: ns,
+			New: nil,
+		},
 	})
 	})
 
 
 	go mq.PublishPeerUpdate(false)
 	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"),
 		CreatedBy:   r.Header.Get("user"),
 		CreatedAt:   time.Now().UTC(),
 		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 {
 	if err := logic.ValidateEgressReq(&e); err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		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.Nodes = make(datatypes.JSONMap)
 	e.Tags = 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 {
 	if e.Domain != req.Domain {
 		e.DomainAns = datatypes.JSONSlice[string]{}
 		e.DomainAns = datatypes.JSONSlice[string]{}
@@ -386,6 +400,10 @@ func deleteEgress(w http.ResponseWriter, r *http.Request) {
 		},
 		},
 		NetworkID: models.NetworkID(e.Network),
 		NetworkID: models.NetworkID(e.Network),
 		Origin:    models.Dashboard,
 		Origin:    models.Dashboard,
+		Diff: models.Diff{
+			Old: e,
+			New: nil,
+		},
 	})
 	})
 	// delete related acl policies
 	// delete related acl policies
 	acls := logic.ListAcls()
 	acls := logic.ListAcls()

+ 5 - 0
controllers/enrollmentkeys.go

@@ -97,6 +97,10 @@ func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 			Type: models.EnrollmentKeySub,
 			Type: models.EnrollmentKeySub,
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: key,
+			New: nil,
+		},
 	})
 	})
 	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
 	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
@@ -181,6 +185,7 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		relayId,
 		relayId,
 		false,
 		false,
 		enrollmentKeyBody.AutoEgress,
 		enrollmentKeyBody.AutoEgress,
+		enrollmentKeyBody.AutoAssignGateway,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
 		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)
 	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(extclient)
+
 	go func() {
 	go func() {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
 			slog.Error(
 			slog.Error(
@@ -969,6 +971,13 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 	network := params["network"]
 	network := params["network"]
 	extclient, err := logic.GetExtClient(clientid, network)
 	extclient, err := logic.GetExtClient(clientid, network)
 	if err != nil {
 	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"])
 		err = errors.New("Could not delete extclient " + params["clientid"])
 		logger.Log(0, r.Header.Get("user"),
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to get extclient [%s],network [%s]: %v", clientid, network, err))
 			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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	if req.IsInternetGateway {
+		if host.DNS != "yes" {
+			host.DNS = "yes"
+			logic.UpsertHost(host)
+		}
+	}
 	for _, relayedNodeID := range relayNode.RelayedNodes {
 	for _, relayedNodeID := range relayNode.RelayedNodes {
 		relayedNode, err := logic.GetNodeByID(relayedNodeID)
 		relayedNode, err := logic.GetNodeByID(relayedNodeID)
 		if err == nil {
 		if err == nil {
 			if relayedNode.FailedOverBy != uuid.Nil {
 			if relayedNode.FailedOverBy != uuid.Nil {
 				go logic.ResetFailedOverPeer(&relayedNode)
 				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)
 					mq.PublishPeerUpdate(false)
 				}()
 				}()
 			}
 			}
+
+			go func() {
+				logic.ResetAutoRelayedPeer(&node)
+				mq.PublishPeerUpdate(false)
+			}()
+
 		}
 		}
 		if node.IsGw && node.IngressDNS == "" {
 		if node.IsGw && node.IngressDNS == "" {
 			node.IngressDNS = "1.1.1.1"
 			node.IngressDNS = "1.1.1.1"
@@ -190,6 +205,10 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	logic.UnsetInternetGw(&node)
 	logic.UnsetInternetGw(&node)
 	node.IsGw = false
 	node.IsGw = false
+	if node.IsAutoRelay {
+		logic.ResetAutoRelay(&node)
+	}
+	node.IsAutoRelay = false
 	logic.UpsertNode(&node)
 	logic.UpsertNode(&node)
 	logger.Log(1, r.Header.Get("user"), "deleted gw", nodeid, "on network", netid)
 	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,
 			Type: models.GatewaySub,
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: node,
+			New: node,
+		},
 	})
 	})
 	logic.GetNodeStatus(&node, false)
 	logic.GetNodeStatus(&node, false)
 	apiNode := node.ConvertToAPINode()
 	apiNode := node.ConvertToAPINode()

+ 51 - 31
controllers/hosts.go

@@ -13,7 +13,6 @@ import (
 	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/schema"
 	"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)
 			//slog.Error("failed to get node:", "id", node.ID, "error", err)
 			continue
 			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.ResetFailedOverPeer(&node)
+			logic.ResetAutoRelayedPeer(&node)
 			sendPeerUpdate = true
 			sendPeerUpdate = true
 		}
 		}
 	}
 	}
@@ -232,19 +232,11 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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)
 	_ = logic.CheckHostPorts(host)
-	serverConf.TrafficKey = key
 	response := models.HostPull{
 	response := models.HostPull{
 		Host:              *host,
 		Host:              *host,
 		Nodes:             logic.GetHostNodes(host),
 		Nodes:             logic.GetHostNodes(host),
-		ServerConfig:      serverConf,
+		ServerConfig:      hPU.ServerConfig,
 		Peers:             hPU.Peers,
 		Peers:             hPU.Peers,
 		PeerIDs:           hPU.PeerIDs,
 		PeerIDs:           hPU.PeerIDs,
 		HostNetworkInfo:   hPU.HostNetworkInfo,
 		HostNetworkInfo:   hPU.HostNetworkInfo,
@@ -257,6 +249,9 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		EgressWithDomains: hPU.EgressWithDomains,
 		EgressWithDomains: hPU.EgressWithDomains,
 		EndpointDetection: logic.IsEndpointDetectionEnabled(),
 		EndpointDetection: logic.IsEndpointDetectionEnabled(),
 		DnsNameservers:    hPU.DnsNameservers,
 		DnsNameservers:    hPU.DnsNameservers,
+		ReplacePeers:      hPU.ReplacePeers,
+		AutoRelayNodes:    hPU.AutoRelayNodes,
+		GwNodes:           hPU.GwNodes,
 	}
 	}
 
 
 	logger.Log(1, hostID, host.Name, "completed a pull")
 	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)
 	newHost := newHostData.ConvertAPIHostToNMHost(currHost)
 
 
 	logic.UpdateHost(newHost, currHost) // update the in memory struct values
 	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 {
 	if err = logic.UpsertHost(newHost); err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())
 		logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
-	var sendPeerUpdate bool
-	var replacePeers bool
+	var sendPeerUpdate, sendDeletedNodeUpdate, replacePeers bool
 	var hostUpdate models.HostUpdate
 	var hostUpdate models.HostUpdate
 	err = json.NewDecoder(r.Body).Decode(&hostUpdate)
 	err = json.NewDecoder(r.Body).Decode(&hostUpdate)
 	if err != nil {
 	if err != nil {
@@ -376,6 +383,10 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 	switch hostUpdate.Action {
 	switch hostUpdate.Action {
 	case models.CheckIn:
 	case models.CheckIn:
 		sendPeerUpdate = mq.HandleHostCheckin(&hostUpdate.Host, currentHost)
 		sendPeerUpdate = mq.HandleHostCheckin(&hostUpdate.Host, currentHost)
+		changed := logic.CheckHostPorts(currentHost)
+		if changed {
+			mq.HostUpdate(&models.HostUpdate{Action: models.UpdateHost, Host: *currentHost})
+		}
 	case models.UpdateHost:
 	case models.UpdateHost:
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
 			//remove old peer entry
 			//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))
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
 			return
 			return
 		}
 		}
-
+	case models.UpdateNode:
+		sendDeletedNodeUpdate, sendPeerUpdate = logic.UpdateHostNode(&hostUpdate.Host, &hostUpdate.Node)
 	case models.UpdateMetrics:
 	case models.UpdateMetrics:
 		mq.UpdateMetricsFallBack(hostUpdate.Node.ID.String(), hostUpdate.NewMetrics)
 		mq.UpdateMetricsFallBack(hostUpdate.Node.ID.String(), hostUpdate.NewMetrics)
 	case models.EgressUpdate:
 	case models.EgressUpdate:
@@ -403,14 +415,23 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 			e.Update(db.WithContext(r.Context()))
 			e.Update(db.WithContext(r.Context()))
 		}
 		}
 		sendPeerUpdate = true
 		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")
 	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)
 			slog.Error("failed to get node", "nodeid", nodeID, "error", err)
 			continue
 			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 {
 	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
@@ -494,6 +511,10 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 			Type: models.DeviceSub,
 			Type: models.DeviceSub,
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: currHost,
+			New: nil,
+		},
 	})
 	})
 	apiHostData := currHost.ConvertNMHostToAPI()
 	apiHostData := currHost.ConvertNMHostToAPI()
 	logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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)
 	logger.Log(1, "deleting node", node.ID.String(), "from host", currHost.Name)
 	if err := logic.DeleteNode(node, forceDelete); err != nil {
 	if err := logic.DeleteNode(node, forceDelete); err != nil {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
@@ -712,7 +729,7 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	go func() {
 	go func() {
-		mq.PublishMqUpdatesForDeletedNode(*node, true, gwClients)
+		mq.PublishMqUpdatesForDeletedNode(*node, true)
 		if servercfg.IsDNSMode() {
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 			logic.SetDNS()
 		}
 		}
@@ -1233,6 +1250,9 @@ func approvePendingHost(w http.ResponseWriter, r *http.Request) {
 		})
 		})
 		return
 		return
 	}
 	}
+	if key.AutoAssignGateway {
+		newNode.AutoAssignGateway = true
+	}
 	if len(key.Groups) > 0 {
 	if len(key.Groups) > 0 {
 		newNode.Tags = make(map[models.TagID]struct{})
 		newNode.Tags = make(map[models.TagID]struct{})
 		for _, tagI := range key.Groups {
 		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)
 	logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
-	hostactions.AddAction(models.HostUpdate{
+	mq.HostUpdate(&models.HostUpdate{
 		Action: models.JoinHostToNetwork,
 		Action: models.JoinHostToNetwork,
 		Host:   *h,
 		Host:   *h,
 		Node:   *newNode,
 		Node:   *newNode,

+ 5 - 0
controllers/inet_gws.go

@@ -72,6 +72,11 @@ func createInternetGw(w http.ResponseWriter, r *http.Request) {
 				mq.PublishPeerUpdate(false)
 				mq.PublishPeerUpdate(false)
 			}()
 			}()
 		}
 		}
+		go func() {
+			logic.ResetAutoRelayedPeer(&node)
+			mq.PublishPeerUpdate(false)
+		}()
+
 	}
 	}
 	if node.IsGw && node.IngressDNS == "" {
 	if node.IsGw && node.IngressDNS == "" {
 		node.IngressDNS = "1.1.1.1"
 		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/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/servercfg"
 	"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))).
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/networks/{networkname}/egress_routes", logic.SecurityCheck(true, http.HandlerFunc(getNetworkEgressRoutes)))
 	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
 // @Summary     Lists all networks
@@ -430,6 +433,40 @@ func getNetworkACL(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(networkACL)
 	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
 // @Summary     Get a network Egress routes
 // @Router      /api/networks/{networkname}/egress_routes [get]
 // @Router      /api/networks/{networkname}/egress_routes [get]
 // @Tags        Networks
 // @Tags        Networks
@@ -528,6 +565,10 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 			Type: models.NetworkSub,
 			Type: models.NetworkSub,
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: network,
+			New: nil,
+		},
 	})
 	})
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
@@ -642,20 +683,38 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 				return
 				return
 			}
 			}
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", currHost.Name)
 			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
 			// make  host failover
 			logic.CreateFailOver(*newNode)
 			logic.CreateFailOver(*newNode)
 			// make host remote access gateway
 			// make host remote access gateway

+ 65 - 5
controllers/node.go

@@ -565,6 +565,10 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		return
 	}
 	}
+	if currentNode.IsAutoRelay && !newNode.IsAutoRelay {
+		logic.ResetAutoRelay(newNode)
+	}
+
 	if newNode.IsInternetGateway && len(newNode.InetNodeReq.InetNodeClientIDs) > 0 {
 	if newNode.IsInternetGateway && len(newNode.InetNodeReq.InetNodeClientIDs) > 0 {
 		err = logic.ValidateInetGwReq(*newNode, newNode.InetNodeReq, newNode.IsInternetGateway && currentNode.IsInternetGateway)
 		err = logic.ValidateInetGwReq(*newNode, newNode.InetNodeReq, newNode.IsInternetGateway && currentNode.IsInternetGateway)
 		if err != nil {
 		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 = append(newNode.RelayedNodes, newNode.InetNodeReq.InetNodeClientIDs...)
 		newNode.RelayedNodes = logic.UniqueStrings(newNode.RelayedNodes)
 		newNode.RelayedNodes = logic.UniqueStrings(newNode.RelayedNodes)
 	}
 	}
+
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	if relayUpdate && newNode.IsRelay {
 	if relayUpdate && newNode.IsRelay {
 		err = logic.ValidateRelay(models.RelayRequest{
 		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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	if newNode.IsInternetGateway {
+		if host.DNS != "yes" {
+			host.DNS = "yes"
+			logic.UpsertHost(host)
+		}
+	}
 	aclUpdate := currentNode.DefaultACL != newNode.DefaultACL
 	aclUpdate := currentNode.DefaultACL != newNode.DefaultACL
 
 
 	err = logic.UpdateNode(&currentNode, newNode)
 	err = logic.UpdateNode(&currentNode, newNode)
@@ -618,6 +629,38 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	if !newNode.IsInternetGateway {
 	if !newNode.IsInternetGateway {
 		logic.UnsetInternetGw(newNode)
 		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.UpsertNode(newNode)
 	logic.GetNodeStatus(newNode, false)
 	logic.GetNodeStatus(newNode, false)
 
 
@@ -655,10 +698,31 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		if err := mq.NodeUpdate(newNode); err != nil {
 		if err := mq.NodeUpdate(newNode); err != nil {
 			slog.Error("error publishing node update to node", "node", newNode.ID, "error", err)
 			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)
 		mq.PublishPeerUpdate(false)
 		if servercfg.IsDNSMode() {
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 			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)
 	}(aclUpdate, relayUpdate, newNode)
 }
 }
 
 
@@ -682,10 +746,6 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	forceDelete := r.URL.Query().Get("force") == "true"
 	forceDelete := r.URL.Query().Get("force") == "true"
 	fromNode := r.Header.Get("requestfrom") == "node"
 	fromNode := r.Header.Get("requestfrom") == "node"
-	var gwClients []models.ExtClient
-	if node.IsIngressGateway {
-		gwClients = logic.GetGwExtclients(node.ID.String(), node.Network)
-	}
 	purge := forceDelete || fromNode
 	purge := forceDelete || fromNode
 	if err := logic.DeleteNode(&node, purge); err != nil {
 	if err := logic.DeleteNode(&node, purge); err != nil {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
@@ -698,5 +758,5 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 
 
 	logic.ReturnSuccessResponse(w, r, nodeid+" deleted.")
 	logic.ReturnSuccessResponse(w, r, nodeid+" deleted.")
 	logger.Log(1, r.Header.Get("user"), "Deleted node", nodeid, "from network", params["network"])
 	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
 package controller
 
 
 import (
 import (
-	"context"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"github.com/gravitl/netmaker/db"
-	"github.com/gravitl/netmaker/schema"
-	"github.com/google/go-cmp/cmp"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"strings"
 	"strings"
 	"syscall"
 	"syscall"
 	"time"
 	"time"
 
 
+	"github.com/google/go-cmp/cmp"
+
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 
 
@@ -82,56 +80,10 @@ func memProfile(w http.ResponseWriter, r *http.Request) {
 }
 }
 
 
 func getUsage(w http.ResponseWriter, _ *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")
 	w.Header().Set("Content-Type", "application/json")
 	json.NewEncoder(w).Encode(models.SuccessResponse{
 	json.NewEncoder(w).Encode(models.SuccessResponse{
 		Code:     http.StatusOK,
 		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"`
 		IsPro            bool      `json:"is_pro"`
 		TrialEndDate     time.Time `json:"trial_end_date"`
 		TrialEndDate     time.Time `json:"trial_end_date"`
 		IsOnTrialLicense bool      `json:"is_on_trial_license"`
 		IsOnTrialLicense bool      `json:"is_on_trial_license"`
+		Version          string    `json:"version"`
 	}
 	}
 
 
 	licenseErr := ""
 	licenseErr := ""
@@ -173,6 +126,7 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
 		IsBrokerConnOpen: mq.IsConnectionOpen(),
 		IsBrokerConnOpen: mq.IsConnectionOpen(),
 		LicenseError:     licenseErr,
 		LicenseError:     licenseErr,
 		IsPro:            servercfg.IsPro,
 		IsPro:            servercfg.IsPro,
+		Version:          servercfg.Version,
 		//TrialEndDate:     trialEndDate,
 		//TrialEndDate:     trialEndDate,
 		//IsOnTrialLicense: isOnTrial,
 		//IsOnTrialLicense: isOnTrial,
 	}
 	}
@@ -328,6 +282,10 @@ func reInit(curr, new models.ServerSettings, force bool) {
 	logic.EmailInit()
 	logic.EmailInit()
 	logic.SetVerbosity(int(logic.GetServerSettings().Verbosity))
 	logic.SetVerbosity(int(logic.GetServerSettings().Verbosity))
 	logic.ResetIDPSyncHook()
 	logic.ResetIDPSyncHook()
+	if curr.MetricInterval != new.MetricInterval {
+		logic.GetMetricsMonitor().Stop()
+		logic.GetMetricsMonitor().Start()
+	}
 	// check if auto update is changed
 	// check if auto update is changed
 	if force {
 	if force {
 		if curr.NetclientAutoUpdate != new.NetclientAutoUpdate {
 		if curr.NetclientAutoUpdate != new.NetclientAutoUpdate {

+ 8 - 0
controllers/user.go

@@ -244,6 +244,10 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
 			Info: a,
 			Info: a,
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: a,
+			New: nil,
+		},
 	})
 	})
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token")
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token")
 }
 }
@@ -1580,6 +1584,10 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 			Type: models.UserSub,
 			Type: models.UserSub,
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: user,
+			New: nil,
+		},
 	})
 	})
 	// check and delete extclient with this ownerID
 	// check and delete extclient with this ownerID
 	go func() {
 	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 
 ARG version 
 RUN apk add --no-cache build-base
 RUN apk add --no-cache build-base
 WORKDIR /app
 WORKDIR /app

+ 27 - 29
go.mod

@@ -1,13 +1,11 @@
 module github.com/gravitl/netmaker
 module github.com/gravitl/netmaker
 
 
-go 1.23.0
-
-toolchain go1.23.7
+go 1.24.0
 
 
 require (
 require (
 	github.com/blang/semver v3.5.1+incompatible
 	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/golang-jwt/jwt/v4 v4.5.2
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/handlers v1.5.2
@@ -17,14 +15,14 @@ require (
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/seancfoley/ipaddress-go v1.7.1
 	github.com/seancfoley/ipaddress-go v1.7.1
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	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
 	github.com/txn2/txeh v1.5.5
 	go.uber.org/automaxprocs v1.6.0
 	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
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 	gopkg.in/yaml.v3 v3.0.1
 )
 )
@@ -32,11 +30,11 @@ require (
 require (
 require (
 	filippo.io/edwards25519 v1.1.0
 	filippo.io/edwards25519 v1.1.0
 	github.com/c-robinson/iplib v1.0.8
 	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 (
 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
 	github.com/gorilla/websocket v1.5.3
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 )
 )
@@ -48,25 +46,25 @@ require (
 	github.com/matryer/is v1.4.1
 	github.com/matryer/is v1.4.1
 	github.com/okta/okta-sdk-golang/v5 v5.0.6
 	github.com/okta/okta-sdk-golang/v5 v5.0.6
 	github.com/pquerna/otp v1.5.0
 	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
 	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/postgres v1.6.0
 	gorm.io/driver/sqlite v1.6.0
 	gorm.io/driver/sqlite v1.6.0
-	gorm.io/gorm v1.30.1
+	gorm.io/gorm v1.31.0
 )
 )
 
 
 require (
 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/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/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // 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/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/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-sql-driver/mysql v1.8.1 // 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/rivo/uniseg v0.2.0 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/seancfoley/bintree v1.3.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/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.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
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gorm.io/driver/mysql v1.5.6 // 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/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // 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 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 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/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 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
 github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 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/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 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
 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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 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 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/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.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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 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.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 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 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 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
 github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
 github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 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/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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 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.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.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.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 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
 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=
 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/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 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
 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 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 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-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.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.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.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 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 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-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.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.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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-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.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.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.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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 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.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.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.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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/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.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 h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
 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 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
 google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
 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 h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 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=
 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 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 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
 gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
 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 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
 gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
 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.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
       hostNetwork: true
       containers:
       containers:
       - name: netclient
       - name: netclient
-        image: gravitl/netclient:v1.1.0
+        image: gravitl/netclient:v1.2.0
         env:
         env:
         - name: TOKEN
         - name: TOKEN
           value: "TOKEN_VALUE"
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/client/netclient.yaml

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

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

@@ -15,7 +15,7 @@ spec:
     spec:
     spec:
       containers:
       containers:
       - name: netmaker-ui
       - name: netmaker-ui
-        image: gravitl/netmaker-ui:v1.1.0
+        image: gravitl/netmaker-ui:v1.2.0
         ports:
         ports:
         - containerPort: 443
         - containerPort: 443
         env:
         env:

+ 12 - 12
logic/acls.go

@@ -1465,18 +1465,6 @@ func GetDefaultPolicy(netID models.NetworkID, ruleType models.AclPolicyType) (mo
 	return acl, nil
 	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
 // ListAcls - lists all acl policies
 func ListAclsByNetwork(netID models.NetworkID) ([]models.Acl, error) {
 func ListAclsByNetwork(netID models.NetworkID) ([]models.Acl, error) {
 
 
@@ -1522,6 +1510,18 @@ func ListDevicePolicies(netID models.NetworkID) []models.Acl {
 	return deviceAcls
 	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{} {
 func ConvAclTagToValueMap(acltags []models.AclPolicyTag) map[string]struct{} {
 	aclValueMap := make(map[string]struct{})
 	aclValueMap := make(map[string]struct{})
 	for _, aclTagI := range acltags {
 	for _, aclTagI := range acltags {

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

@@ -7,16 +7,12 @@ import (
 	"sync"
 	"sync"
 
 
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls"
-	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
 var NodesAllowedACLMutex = &sync.Mutex{}
 var NodesAllowedACLMutex = &sync.Mutex{}
 
 
 // AreNodesAllowed - checks if nodes are allowed to communicate in their network ACL
 // AreNodesAllowed - checks if nodes are allowed to communicate in their network ACL
 func AreNodesAllowed(networkID NetworkID, node1, node2 NodeID) bool {
 func AreNodesAllowed(networkID NetworkID, node1, node2 NodeID) bool {
-	if !servercfg.IsOldAclEnabled() {
-		return true
-	}
 	NodesAllowedACLMutex.Lock()
 	NodesAllowedACLMutex.Lock()
 	defer NodesAllowedACLMutex.Unlock()
 	defer NodesAllowedACLMutex.Unlock()
 	var currentNetworkACL, err = FetchAllACLs(networkID)
 	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 {
 	if len(ns.Servers) == 0 {
 		return errors.New("atleast one nameserver should be specified")
 		return errors.New("atleast one nameserver should be specified")
 	}
 	}
-	network, err := GetNetwork(ns.NetworkID)
+	_, err := GetNetwork(ns.NetworkID)
 	if err != nil {
 	if err != nil {
 		return errors.New("invalid network id")
 		return errors.New("invalid network id")
 	}
 	}
-	_, cidr, err4 := net.ParseCIDR(network.AddressRange)
-	_, cidr6, err6 := net.ParseCIDR(network.AddressRange6)
 	for _, nsIPStr := range ns.Servers {
 	for _, nsIPStr := range ns.Servers {
 		nsIP := net.ParseIP(nsIPStr)
 		nsIP := net.ParseIP(nsIPStr)
 		if nsIP == nil {
 		if nsIP == nil {
 			return errors.New("invalid nameserver " + nsIPStr)
 			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")
 		return errors.New("atleast one match domain is required")
 	}
 	}
 	if !ns.MatchAll {
 	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")
 				return errors.New("invalid match domain")
 			}
 			}
 		}
 		}
@@ -478,6 +469,15 @@ func validateNameserverReq(ns schema.Nameserver) error {
 }
 }
 
 
 func getNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 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{
 	ns := &schema.Nameserver{
 		NetworkID: node.Network,
 		NetworkID: node.Network,
 	}
 	}
@@ -486,22 +486,30 @@ func getNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 		if !nsI.Status {
 		if !nsI.Status {
 			continue
 			continue
 		}
 		}
+
+		filteredIps := FilterOutIPs(nsI.Servers, filters)
+		if len(filteredIps) == 0 {
+			continue
+		}
+
 		_, all := nsI.Tags["*"]
 		_, all := nsI.Tags["*"]
 		if all {
 		if all {
-			for _, matchDomain := range nsI.MatchDomains {
+			for _, domain := range nsI.Domains {
 				returnNsLi = append(returnNsLi, models.Nameserver{
 				returnNsLi = append(returnNsLi, models.Nameserver{
-					IPs:         nsI.Servers,
-					MatchDomain: matchDomain,
+					IPs:            filteredIps,
+					MatchDomain:    domain.Domain,
+					IsSearchDomain: domain.IsSearchDomain,
 				})
 				})
 			}
 			}
 			continue
 			continue
 		}
 		}
 
 
 		if _, ok := nsI.Nodes[node.ID.String()]; ok {
 		if _, ok := nsI.Nodes[node.ID.String()]; ok {
-			for _, matchDomain := range nsI.MatchDomains {
+			for _, domain := range nsI.Domains {
 				returnNsLi = append(returnNsLi, models.Nameserver{
 				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 {
 		if err != nil {
 			continue
 			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{
 		ns := &schema.Nameserver{
 			NetworkID: node.Network,
 			NetworkID: node.Network,
 		}
 		}
@@ -536,22 +554,30 @@ func getNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 			if !nsI.Status {
 			if !nsI.Status {
 				continue
 				continue
 			}
 			}
+
+			filteredIps := FilterOutIPs(nsI.Servers, filters)
+			if len(filteredIps) == 0 {
+				continue
+			}
+
 			_, all := nsI.Tags["*"]
 			_, all := nsI.Tags["*"]
 			if all {
 			if all {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
 					returnNsLi = append(returnNsLi, models.Nameserver{
-						IPs:         nsI.Servers,
-						MatchDomain: matchDomain,
+						IPs:            filteredIps,
+						MatchDomain:    domain.Domain,
+						IsSearchDomain: domain.IsSearchDomain,
 					})
 					})
 				}
 				}
 				continue
 				continue
 			}
 			}
 
 
 			if _, ok := nsI.Nodes[node.ID.String()]; ok {
 			if _, ok := nsI.Nodes[node.ID.String()]; ok {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
 					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
 	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"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
-func ValidateEgressReq(e *schema.Egress) error {
+var ValidateEgressReq = validateEgressReq
+
+func validateEgressReq(e *schema.Egress) error {
 	if e.Network == "" {
 	if e.Network == "" {
 		return errors.New("network id is empty")
 		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 {
 	if targetNode.Mutex != nil {
 		targetNode.Mutex.Lock()
 		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) {
 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()))
 	eli, _ := (&schema.Egress{Network: network.String()}).ListByNetwork(db.WithContext(context.TODO()))
 	defaultDevicePolicy, _ := GetDefaultPolicy(network, models.UserPolicy)
 	defaultDevicePolicy, _ := GetDefaultPolicy(network, models.UserPolicy)
 	isDefaultPolicyActive := defaultDevicePolicy.Enabled
 	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 {
 	if targetNode.Mutex != nil {
 		targetNode.Mutex.Lock()
 		targetNode.Mutex.Lock()

+ 15 - 12
logic/enrollmentkey.go

@@ -38,23 +38,26 @@ var (
 )
 )
 
 
 // CreateEnrollmentKey - creates a new enrollment key in db
 // 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()
 	newKeyID, err := getUniqueEnrollmentID()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	k := &models.EnrollmentKey{
 	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 {
 	if uses > 0 {
 		k.UsesRemaining = uses
 		k.UsesRemaining = uses

+ 16 - 15
logic/enrollmentkey_test.go

@@ -1,11 +1,12 @@
 package logic
 package logic
 
 
 import (
 import (
-	"github.com/gravitl/netmaker/db"
-	"github.com/gravitl/netmaker/schema"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/schema"
+
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -19,35 +20,35 @@ func TestCreateEnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	defer database.CloseDB()
 	t.Run("Can_Not_Create_Key", func(t *testing.T) {
 	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.Nil(t, newKey)
 		assert.NotNil(t, err)
 		assert.NotNil(t, err)
 		assert.ErrorIs(t, err, models.ErrInvalidEnrollmentKey)
 		assert.ErrorIs(t, err, models.ErrInvalidEnrollmentKey)
 	})
 	})
 	t.Run("Can_Create_Key_Uses", func(t *testing.T) {
 	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.Nil(t, err)
 		assert.Equal(t, 1, newKey.UsesRemaining)
 		assert.Equal(t, 1, newKey.UsesRemaining)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 	})
 	})
 	t.Run("Can_Create_Key_Time", func(t *testing.T) {
 	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.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 	})
 	})
 	t.Run("Can_Create_Key_Unlimited", func(t *testing.T) {
 	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.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 	})
 	})
 	t.Run("Can_Create_Key_WithNetworks", func(t *testing.T) {
 	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.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Networks) == 2)
 		assert.True(t, len(newKey.Networks) == 2)
 	})
 	})
 	t.Run("Can_Create_Key_WithTags", func(t *testing.T) {
 	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.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Tags) == 2)
 		assert.True(t, len(newKey.Tags) == 2)
@@ -70,7 +71,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 
 
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	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) {
 	t.Run("Can_Delete_Key", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 		err := DeleteEnrollmentKey(newKey.Value, false)
 		err := DeleteEnrollmentKey(newKey.Value, false)
@@ -94,7 +95,7 @@ func TestDecrement_EnrollmentKey(t *testing.T) {
 
 
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	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) {
 	t.Run("Check_initial_uses", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 		assert.Equal(t, newKey.UsesRemaining, 1)
 		assert.Equal(t, newKey.UsesRemaining, 1)
@@ -121,9 +122,9 @@ func TestUsability_EnrollmentKey(t *testing.T) {
 
 
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	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) {
 	t.Run("Check if valid use key can be used", func(t *testing.T) {
 		assert.Equal(t, key1.UsesRemaining, 1)
 		assert.Equal(t, key1.UsesRemaining, 1)
 		ok := TryToUseEnrollmentKey(key1)
 		ok := TryToUseEnrollmentKey(key1)
@@ -162,7 +163,7 @@ func TestTokenize_EnrollmentKeys(t *testing.T) {
 
 
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	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 defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5"
 	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
 	const serverAddr = "api.myserver.com"
@@ -198,7 +199,7 @@ func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 
 
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	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 b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
 	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.IsIngressGateway = true
 	node.IsGw = true
 	node.IsGw = true
+	SetAutoRelay(&node)
 	node.IsInternetGateway = ingress.IsInternetGateway
 	node.IsInternetGateway = ingress.IsInternetGateway
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressGatewayRange6 = network.AddressRange6
@@ -217,6 +218,9 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 		if _, exists := FailOverExists(node.Network); exists {
 		if _, exists := FailOverExists(node.Network); exists {
 			ResetFailedOverPeer(&node)
 			ResetFailedOverPeer(&node)
 		}
 		}
+
+		ResetAutoRelayedPeer(&node)
+
 	}
 	}
 	node.SetLastModified()
 	node.SetLastModified()
 	node.Metadata = ingress.Metadata
 	node.Metadata = ingress.Metadata
@@ -342,7 +346,7 @@ func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		if clientNode.IsFailOver {
+		if clientNode.IsFailOver || clientNode.IsAutoRelay {
 			return errors.New("failover node cannot be set to use internet gateway")
 			return errors.New("failover node cannot be set to use internet gateway")
 		}
 		}
 		clientHost, err := GetHost(clientNode.HostID.String())
 		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 {
 		if clientNode.FailedOverBy != uuid.Nil {
 			ResetFailedOverPeer(&clientNode)
 			ResetFailedOverPeer(&clientNode)
 		}
 		}
+		if len(clientNode.AutoRelayedPeers) > 0 {
+			ResetAutoRelayedPeer(&clientNode)
+		}
 
 
 		if clientNode.IsRelayed && clientNode.RelayedBy != inetNode.ID.String() {
 		if clientNode.IsRelayed && clientNode.RelayedBy != inetNode.ID.String() {
 			return fmt.Errorf("node %s is being relayed", clientHost.Name)
 			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 {
 			if node.FailedOverBy != uuid.Nil {
 				ResetFailedOverPeer(&node)
 				ResetFailedOverPeer(&node)
 			}
 			}
+			if len(node.AutoRelayedPeers) > 0 {
+				ResetAutoRelayedPeer(&node)
+			}
 		}
 		}
 	}
 	}
 
 
@@ -382,6 +385,30 @@ func UpsertHost(h *models.Host) error {
 	return nil
 	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
 // RemoveHost - removes a given host from server
 func RemoveHost(h *models.Host, forceDelete bool) error {
 func RemoveHost(h *models.Host, forceDelete bool) error {
 	if !forceDelete && len(h.Nodes) > 0 {
 	if !forceDelete && len(h.Nodes) > 0 {

+ 64 - 0
logic/metrics.go

@@ -1,9 +1,73 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
+	"math"
+	"strconv"
+	"time"
+
 	"github.com/gravitl/netmaker/models"
 	"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 {
 var DeleteMetrics = func(string) error {
 	return nil
 	return nil
 }
 }

+ 5 - 4
logic/networks.go

@@ -307,6 +307,7 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 		uuid.Nil,
 		uuid.Nil,
 		true,
 		true,
 		false,
 		false,
+		false,
 	)
 	)
 
 
 	return network, nil
 	return network, nil
@@ -388,7 +389,7 @@ func UniqueAddressCache(networkName string, reverse bool) (net.IP, error) {
 	}
 	}
 
 
 	if network.IsIPv4 == "no" {
 	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
 	//ensure AddressRange is valid
 	if _, _, err := net.ParseCIDR(network.AddressRange); err != nil {
 	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" {
 	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
 	//ensure AddressRange is valid
 	if _, _, err := net.ParseCIDR(network.AddressRange); err != nil {
 	if _, _, err := net.ParseCIDR(network.AddressRange); err != nil {
@@ -529,7 +530,7 @@ func UniqueAddress6DB(networkName string, reverse bool) (net.IP, error) {
 		return add, err
 		return add, err
 	}
 	}
 	if network.IsIPv6 == "no" {
 	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
 	//ensure AddressRange is valid
@@ -573,7 +574,7 @@ func UniqueAddress6Cache(networkName string, reverse bool) (net.IP, error) {
 		return add, err
 		return add, err
 	}
 	}
 	if network.IsIPv6 == "no" {
 	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
 	//ensure AddressRange is valid

+ 33 - 2
logic/nodes.go

@@ -15,10 +15,12 @@ import (
 	validator "github.com/go-playground/validator/v10"
 	validator "github.com/go-playground/validator/v10"
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/validation"
 	"github.com/gravitl/netmaker/validation"
 	"github.com/seancfoley/ipaddress-go/ipaddr"
 	"github.com/seancfoley/ipaddress-go/ipaddr"
@@ -221,6 +223,9 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 		}
 		}
 		newNode.EgressDetails = models.EgressDetails{}
 		newNode.EgressDetails = models.EgressDetails{}
 		newNode.SetLastModified()
 		newNode.SetLastModified()
+		if !currentNode.Connected && newNode.Connected {
+			newNode.SetLastCheckIn()
+		}
 		if data, err := json.Marshal(newNode); err != nil {
 		if data, err := json.Marshal(newNode); err != nil {
 			return err
 			return err
 		} else {
 		} else {
@@ -277,6 +282,9 @@ func DeleteNode(node *models.Node, purge bool) error {
 	if node.FailedOverBy != uuid.Nil {
 	if node.FailedOverBy != uuid.Nil {
 		ResetFailedOverPeer(node)
 		ResetFailedOverPeer(node)
 	}
 	}
+	if len(node.AutoRelayedPeers) > 0 {
+		ResetAutoRelayedPeer(node)
+	}
 	if node.IsRelay {
 	if node.IsRelay {
 		// unset all the relayed nodes
 		// unset all the relayed nodes
 		SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
 		SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
@@ -322,6 +330,29 @@ func DeleteNode(node *models.Node, purge bool) error {
 		return err
 		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 RemoveNodeFromAclPolicy(*node)
 	go RemoveNodeFromEgress(*node)
 	go RemoveNodeFromEgress(*node)
 	return nil
 	return nil
@@ -685,7 +716,7 @@ func createNode(node *models.Node) error {
 			node.Address.Mask = net.CIDRMask(cidr.Mask.Size())
 			node.Address.Mask = net.CIDRMask(cidr.Mask.Size())
 		}
 		}
 	} else if !IsIPUnique(node.Network, node.Address.String(), database.NODES_TABLE_NAME, false) {
 	} 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 node.Address6.IP == nil {
 		if parentNetwork.IsIPv6 == "yes" {
 		if parentNetwork.IsIPv6 == "yes" {
@@ -699,7 +730,7 @@ func createNode(node *models.Node) error {
 			node.Address6.Mask = net.CIDRMask(cidr.Mask.Size())
 			node.Address6.Mask = net.CIDRMask(cidr.Mask.Size())
 		}
 		}
 	} else if !IsIPUnique(node.Network, node.Address6.String(), database.NODES_TABLE_NAME, true) {
 	} 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()
 	node.ID = uuid.New()
 	//Create a JWT for the node
 	//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
 // GetHostPeerInfo - fetches required peer info per network
 func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 	peerInfo := models.HostPeerInfo{
 	peerInfo := models.HostPeerInfo{
@@ -53,6 +72,7 @@ func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 	if err != nil {
 	if err != nil {
 		return peerInfo, err
 		return peerInfo, err
 	}
 	}
+	serverInfo := GetServerInfo()
 	for _, nodeID := range host.Nodes {
 	for _, nodeID := range host.Nodes {
 		nodeID := nodeID
 		nodeID := nodeID
 		node, err := GetNodeByID(nodeID)
 		node, err := GetNodeByID(nodeID)
@@ -89,7 +109,7 @@ func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 			if peer.Action != models.NODE_DELETE &&
 			if peer.Action != models.NODE_DELETE &&
 				!peer.PendingDelete &&
 				!peer.PendingDelete &&
 				peer.Connected &&
 				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) {
 				(allowedToComm) {
 
 
 				networkPeersInfo[peerHost.PublicKey.String()] = models.IDandAddr{
 				networkPeersInfo[peerHost.PublicKey.String()] = models.IDandAddr{
@@ -143,6 +163,8 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		HostNetworkInfo: models.HostInfoMap{},
 		HostNetworkInfo: models.HostInfoMap{},
 		ServerConfig:    GetServerInfo(),
 		ServerConfig:    GetServerInfo(),
 		DnsNameservers:  GetNameserversForHost(host),
 		DnsNameservers:  GetNameserversForHost(host),
+		AutoRelayNodes:  make(map[models.NetworkID][]models.Node),
+		GwNodes:         make(map[models.NetworkID][]models.Node),
 	}
 	}
 	if host.DNS == "no" {
 	if host.DNS == "no" {
 		hostPeerUpdate.ManageDNS = false
 		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 ||
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE ||
 			(!node.LastCheckIn.IsZero() && time.Since(node.LastCheckIn) > time.Hour) {
 			(!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))
 		acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
 		eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
 		eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
 		GetNodeEgressInfo(&node, eli, acls)
 		GetNodeEgressInfo(&node, eli, acls)
@@ -252,9 +277,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				node.Mutex.Lock()
 				node.Mutex.Lock()
 			}
 			}
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
+			peerAutoRelayID, isAutoRelayPeer := node.AutoRelayedPeers[peer.ID.String()]
 			if node.Mutex != nil {
 			if node.Mutex != nil {
 				node.Mutex.Unlock()
 				node.Mutex.Unlock()
 			}
 			}
+
 			if peer.EgressDetails.IsEgressGateway {
 			if peer.EgressDetails.IsEgressGateway {
 				peerKey := peerHost.PublicKey.String()
 				peerKey := peerHost.PublicKey.String()
 				if isFailOverPeer && peer.FailedOverBy.String() != node.ID.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()) {
 				if peer.IsRelayed && (peer.RelayedBy != node.ID.String()) {
 					// get relay host
 					// get relay host
 					relayNode, err := GetNodeByID(peer.RelayedBy)
 					relayNode, err := GetNodeByID(peer.RelayedBy)
@@ -292,9 +329,25 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			if peer.IsIngressGateway {
 			if peer.IsIngressGateway {
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node)...)
 				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()) ||
 			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 node is relayed and peer is not the relay, set remove to true
 				if _, ok := peerIndexMap[peerHost.PublicKey.String()]; ok {
 				if _, ok := peerIndexMap[peerHost.PublicKey.String()]; ok {
 					continue
 					continue
@@ -349,6 +402,9 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			if isFailOverPeer && peer.FailedOverBy == node.ID && !peer.IsStatic {
 			if isFailOverPeer && peer.FailedOverBy == node.ID && !peer.IsStatic {
 				peerEndpoint = nil
 				peerEndpoint = nil
 			}
 			}
+			if isAutoRelayPeer && peerAutoRelayID == node.ID.String() && !peer.IsStatic {
+				peerEndpoint = nil
+			}
 
 
 			peerConfig.Endpoint = &net.UDPAddr{
 			peerConfig.Endpoint = &net.UDPAddr{
 				IP:   peerEndpoint,
 				IP:   peerEndpoint,
@@ -358,21 +414,15 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			if uselocal {
 			if uselocal {
 				peerConfig.Endpoint.Port = peerHost.ListenPort
 				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 &&
 			if peer.Action != models.NODE_DELETE &&
 				!peer.PendingDelete &&
 				!peer.PendingDelete &&
 				peer.Connected &&
 				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) &&
 				(allowedToComm) &&
 				(deletedNode == nil || (peer.ID.String() != deletedNode.ID.String())) {
 				(deletedNode == nil || (peer.ID.String() != deletedNode.ID.String())) {
 				peerConfig.AllowedIPs = GetAllowedIPs(&node, &peer, nil) // only append allowed IPs if valid connection
 				peerConfig.AllowedIPs = GetAllowedIPs(&node, &peer, nil) // only append allowed IPs if valid connection
 			}
 			}
-
 			var nodePeer wgtypes.PeerConfig
 			var nodePeer wgtypes.PeerConfig
 			if _, ok := peerIndexMap[peerHost.PublicKey.String()]; !ok {
 			if _, ok := peerIndexMap[peerHost.PublicKey.String()]; !ok {
 				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
 				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 {
 func filterConflictingEgressRoutesWithMetric(node, peer models.Node) []models.EgressRangeMetric {
@@ -711,6 +761,9 @@ func getNodeAllowedIPs(peer, node *models.Node) []net.IPNet {
 	if peer.IsFailOver {
 	if peer.IsFailOver {
 		allowedips = append(allowedips, GetFailOverPeerIps(peer, node)...)
 		allowedips = append(allowedips, GetFailOverPeerIps(peer, node)...)
 	}
 	}
+	if peer.IsAutoRelay {
+		allowedips = append(allowedips, GetAutoRelayPeerIps(peer, node)...)
+	}
 	return allowedips
 	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)
 		returnnodes = append(returnnodes, node)
 	}
 	}
+	relayNode, _ := GetNodeByID(relay)
+	if setRelayed {
+		relayNode.RelayedNodes = relayed
+	} else {
+		relayNode.RelayedNodes = []string{}
+	}
+	UpsertNode(&relayNode)
 	return returnnodes
 	return returnnodes
 }
 }
 
 
@@ -129,18 +136,21 @@ func ValidateRelay(relay models.RelayRequest, update bool) error {
 		if relayedNode.InternetGwID != "" && relayedNode.InternetGwID != relay.NodeID {
 		if relayedNode.InternetGwID != "" && relayedNode.InternetGwID != relay.NodeID {
 			return errors.New("cannot relay an internet client (" + relayedNodeID + ")")
 			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 {
 		if relayedNode.FailedOverBy != uuid.Nil {
 			ResetFailedOverPeer(&relayedNode)
 			ResetFailedOverPeer(&relayedNode)
 		}
 		}
+		if len(relayedNode.AutoRelayedPeers) > 0 {
+			ResetAutoRelayedPeer(&relayedNode)
+		}
 	}
 	}
 	return err
 	return err
 }
 }
 
 
 // UpdateRelayNodes - updates relay nodes
 // 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)
 	_ = SetRelayedNodes(false, relay, oldNodes)
 	return SetRelayedNodes(true, relay, newNodes)
 	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
 // UpdateRelayed - updates a relay's relayed nodes, and sends updates to the relayed nodes over MQ
 func UpdateRelayed(currentNode, newNode *models.Node) {
 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 {
 	if len(updatenodes) > 0 {
 		for _, relayedNode := range updatenodes {
 		for _, relayedNode := range updatenodes {
 			node := relayedNode
 			node := relayedNode
 			ResetFailedOverPeer(&node)
 			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())
 		logger.Log(0, "error getting network clients", err.Error())
 		return
 		return
 	}
 	}
+	serverSettings := GetServerSettings()
 	acls, _ := ListAclsByNetwork(models.NetworkID(relay.Network))
 	acls, _ := ListAclsByNetwork(models.NetworkID(relay.Network))
 	eli, _ := (&schema.Egress{Network: relay.Network}).ListByNetwork(db.WithContext(context.TODO()))
 	eli, _ := (&schema.Egress{Network: relay.Network}).ListByNetwork(db.WithContext(context.TODO()))
 	defaultPolicy, _ := GetDefaultPolicy(models.NetworkID(relay.Network), models.DevicePolicy)
 	defaultPolicy, _ := GetDefaultPolicy(models.NetworkID(relay.Network), models.DevicePolicy)
@@ -236,7 +248,7 @@ func GetAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNe
 			continue
 			continue
 		}
 		}
 		AddEgressInfoToPeerByAccess(relayed, &peer, eli, acls, defaultPolicy.Enabled)
 		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)...)
 			allowedIPs = append(allowedIPs, GetAllowedIPs(relayed, &peer, nil)...)
 		}
 		}
 	}
 	}

+ 2 - 2
logic/security.go

@@ -2,10 +2,11 @@ package logic
 
 
 import (
 import (
 	"errors"
 	"errors"
-	"github.com/golang-jwt/jwt/v4"
 	"net/http"
 	"net/http"
 	"strings"
 	"strings"
 
 
+	"github.com/golang-jwt/jwt/v4"
+
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"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
 // SecurityCheck - Check if user has appropriate permissions
 func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
-
 	return func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
 		r.Header.Set("ismaster", "no")
 		r.Header.Set("ismaster", "no")
 		isGlobalAccesss := r.Header.Get("IS_GLOBAL_ACCESS") == "yes"
 		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/config"
 	"github.com/gravitl/netmaker/database"
 	"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/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
@@ -61,7 +63,10 @@ func UpsertServerSettings(s models.ServerSettings) error {
 		}
 		}
 	}
 	}
 	s.GroupFilters = groupFilters
 	s.GroupFilters = groupFilters
-
+	if !s.OldAClsSupport {
+		// set defaults for old acl settings
+		go setDefaultsforOldAclCfg()
+	}
 	data, err := json.Marshal(s)
 	data, err := json.Marshal(s)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -73,6 +78,36 @@ func UpsertServerSettings(s models.ServerSettings) error {
 	return nil
 	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 {
 func GetUserSettings(userID string) models.UserSettings {
 	data, err := database.FetchRecord(database.SERVER_SETTINGS, userID)
 	data, err := database.FetchRecord(database.SERVER_SETTINGS, userID)
 	if err != nil {
 	if err != nil {
@@ -145,6 +180,7 @@ func GetServerSettingsFromEnv() (s models.ServerSettings) {
 		DefaultDomain:              servercfg.GetDefaultDomain(),
 		DefaultDomain:              servercfg.GetDefaultDomain(),
 		Stun:                       servercfg.IsStunEnabled(),
 		Stun:                       servercfg.IsStunEnabled(),
 		StunServers:                servercfg.GetStunServers(),
 		StunServers:                servercfg.GetStunServers(),
+		OldAClsSupport:             false,
 	}
 	}
 
 
 	return
 	return
@@ -245,6 +281,10 @@ func GetServerInfo() models.ServerConfig {
 	cfg.StunServers = serverSettings.StunServers
 	cfg.StunServers = serverSettings.StunServers
 	cfg.DefaultDomain = serverSettings.DefaultDomain
 	cfg.DefaultDomain = serverSettings.DefaultDomain
 	cfg.EndpointDetection = serverSettings.EndpointDetection
 	cfg.EndpointDetection = serverSettings.EndpointDetection
+	cfg.PeerConnectionCheckInterval = serverSettings.PeerConnectionCheckInterval
+	cfg.OldAClsSupport = serverSettings.OldAClsSupport
+	key, _ := RetrievePublicTrafficKey()
+	cfg.TrafficKey = key
 	return cfg
 	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:]...)
 	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.
 // IsSlicesEqual tells whether a and b contain the same elements.
 // A nil argument is equivalent to an empty slice.
 // A nil argument is equivalent to an empty slice.
 func IsSlicesEqual(a, b []string) bool {
 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
 	// single comparison statements
 	if newNode.Address.String() != currentNode.Address.String() ||
 	if newNode.Address.String() != currentNode.Address.String() ||
 		newNode.Address6.String() != currentNode.Address6.String() ||
 		newNode.Address6.String() != currentNode.Address6.String() ||
-		newNode.IsRelay != currentNode.IsRelay ||
 		newNode.Connected != currentNode.Connected {
 		newNode.Connected != currentNode.Connected {
 		return true
 		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
 	return false
 }
 }
 
 

+ 2 - 2
main.go

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

+ 88 - 13
migrate/migrate.go

@@ -18,6 +18,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/schema"
@@ -43,6 +44,37 @@ func Run() {
 	migrateNameservers()
 	migrateNameservers()
 	resync()
 	resync()
 	deleteOldExtclients()
 	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() {
 func updateNetworks() {
@@ -68,14 +100,37 @@ func migrateNameservers() {
 		if err != nil {
 		if err != nil {
 			continue
 			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 {
 		if len(netI.NameServers) > 0 {
 			ns := schema.Nameserver{
 			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{
 				Tags: datatypes.JSONMap{
 					"*": struct{}{},
 					"*": struct{}{},
 				},
 				},
@@ -115,12 +170,16 @@ func migrateNameservers() {
 				continue
 				continue
 			}
 			}
 			ns := schema.Nameserver{
 			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{
 				Nodes: datatypes.JSONMap{
 					node.ID.String(): struct{}{},
 					node.ID.String(): struct{}{},
 				},
 				},
@@ -309,6 +368,7 @@ func updateEnrollmentKeys() {
 			uuid.Nil,
 			uuid.Nil,
 			true,
 			true,
 			false,
 			false,
+			false,
 		)
 		)
 
 
 	}
 	}
@@ -433,6 +493,9 @@ func removeInterGw(egressRanges []string) ([]string, bool) {
 
 
 func updateAcls() {
 func updateAcls() {
 	// get all networks
 	// get all networks
+	if !logic.GetServerSettings().OldAClsSupport {
+		return
+	}
 	networks, err := logic.GetNetworks()
 	networks, err := logic.GetNetworks()
 	if err != nil && !database.IsEmptyRecord(err) {
 	if err != nil && !database.IsEmptyRecord(err) {
 		slog.Error("acls migration failed. error getting networks", "error", err)
 		slog.Error("acls migration failed. error getting networks", "error", err)
@@ -810,11 +873,20 @@ func migrateToEgressV1() {
 }
 }
 
 
 func migrateSettings() {
 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) {
 	if database.IsEmptyRecord(err) {
 		logic.UpsertServerSettings(logic.GetServerSettingsFromEnv())
 		logic.UpsertServerSettings(logic.GetServerSettingsFromEnv())
+	} else if err == nil {
+		json.Unmarshal([]byte(data), &settingsD)
 	}
 	}
 	settings := logic.GetServerSettings()
 	settings := logic.GetServerSettings()
+	if _, ok := settingsD["old_acl_support"]; !ok {
+		settings.OldAClsSupport = servercfg.IsOldAclEnabled()
+	}
+	if settings.PeerConnectionCheckInterval == "" {
+		settings.PeerConnectionCheckInterval = "15"
+	}
 	if settings.AuditLogsRetentionPeriodInDays == 0 {
 	if settings.AuditLogsRetentionPeriodInDays == 0 {
 		settings.AuditLogsRetentionPeriodInDays = 7
 		settings.AuditLogsRetentionPeriodInDays = 7
 	}
 	}
@@ -824,6 +896,9 @@ func migrateSettings() {
 	if settings.JwtValidityDurationClients == 0 {
 	if settings.JwtValidityDurationClients == 0 {
 		settings.JwtValidityDurationClients = servercfg.GetJwtValidityDurationFromEnv() / 60
 		settings.JwtValidityDurationClients = servercfg.GetJwtValidityDurationFromEnv() / 60
 	}
 	}
+	if settings.StunServers == "" {
+		settings.StunServers = servercfg.GetStunServers()
+	}
 	logic.UpsertServerSettings(settings)
 	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
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
 type ApiNode struct {
 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"`
 	RelayedBy                     string              `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
 	RelayedNodes                  []string            `json:"relaynodes" yaml:"relayedNodes"`
 	RelayedNodes                  []string            `json:"relaynodes" yaml:"relayedNodes"`
 	IsEgressGateway               bool                `json:"isegressgateway"`
 	IsEgressGateway               bool                `json:"isegressgateway"`
@@ -75,12 +79,16 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.ID, _ = uuid.Parse(a.ID)
 	convertedNode.ID, _ = uuid.Parse(a.ID)
 	convertedNode.HostID, _ = uuid.Parse(a.HostID)
 	convertedNode.HostID, _ = uuid.Parse(a.HostID)
 	//convertedNode.IsRelay = a.IsRelay
 	//convertedNode.IsRelay = a.IsRelay
+	if a.RelayedBy != "" && !a.IsRelayed {
+		a.IsRelayed = true
+	}
 	convertedNode.IsRelayed = a.IsRelayed
 	convertedNode.IsRelayed = a.IsRelayed
 	convertedNode.RelayedBy = a.RelayedBy
 	convertedNode.RelayedBy = a.RelayedBy
 	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.PendingDelete = a.PendingDelete
 	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.IsIngressGateway = a.IsIngressGateway
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
@@ -134,10 +142,12 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	}
 	}
 	convertedNode.Tags = a.Tags
 	convertedNode.Tags = a.Tags
 	convertedNode.IsGw = a.IsGw
 	convertedNode.IsGw = a.IsGw
+	convertedNode.IsAutoRelay = a.IsAutoRelay
 	if convertedNode.IsGw {
 	if convertedNode.IsGw {
 		convertedNode.IsRelay = true
 		convertedNode.IsRelay = true
 		convertedNode.IsIngressGateway = true
 		convertedNode.IsIngressGateway = true
 	}
 	}
+	convertedNode.AutoAssignGateway = a.AutoAssignGateway
 	return &convertedNode
 	return &convertedNode
 }
 }
 
 
@@ -189,6 +199,10 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.IsGw = nm.IsGw
 	apiNode.IsGw = nm.IsGw
 	apiNode.RelayedBy = nm.RelayedBy
 	apiNode.RelayedBy = nm.RelayedBy
 	apiNode.RelayedNodes = nm.RelayedNodes
 	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.IsIngressGateway = nm.IsIngressGateway
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
 	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
@@ -200,7 +214,7 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.IsInternetGateway = nm.IsInternetGateway
 	apiNode.IsInternetGateway = nm.IsInternetGateway
 	apiNode.InternetGwID = nm.InternetGwID
 	apiNode.InternetGwID = nm.InternetGwID
 	apiNode.InetNodeReq = nm.InetNodeReq
 	apiNode.InetNodeReq = nm.InetNodeReq
-	apiNode.IsFailOver = nm.IsFailOver
+	apiNode.IsFailOver = false
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailedOverBy = nm.FailedOverBy
 	apiNode.FailedOverBy = nm.FailedOverBy
 	apiNode.Metadata = nm.Metadata
 	apiNode.Metadata = nm.Metadata

+ 1 - 1
models/egress.go

@@ -6,7 +6,7 @@ type EgressReq struct {
 	Network     string         `json:"network"`
 	Network     string         `json:"network"`
 	Description string         `json:"description"`
 	Description string         `json:"description"`
 	Nodes       map[string]int `json:"nodes"`
 	Nodes       map[string]int `json:"nodes"`
-	Tags        []string       `json:"tags"`
+	Tags        map[string]int `json:"tags"`
 	Range       string         `json:"range"`
 	Range       string         `json:"range"`
 	Domain      string         `json:"domain"`
 	Domain      string         `json:"domain"`
 	Nat         bool           `json:"nat"`
 	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
 // EnrollmentKey - the key used to register hosts and join them to specific networks
 type EnrollmentKey struct {
 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
 // APIEnrollmentKey - used to create enrollment keys via API
 type APIEnrollmentKey struct {
 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
 // RegisterResponse - the response to a successful enrollment register

+ 17 - 11
models/host.go

@@ -106,6 +106,8 @@ const (
 	SignalHost HostMqAction = "SIGNAL_HOST"
 	SignalHost HostMqAction = "SIGNAL_HOST"
 	// UpdateHost - constant for host update action
 	// UpdateHost - constant for host update action
 	UpdateHost HostMqAction = "UPDATE_HOST"
 	UpdateHost HostMqAction = "UPDATE_HOST"
+	// UpdateNode - constant for Node update action
+	UpdateNode HostMqAction = "UPDATE_NODE"
 	// DeleteHost - constant for host delete action
 	// DeleteHost - constant for host delete action
 	DeleteHost HostMqAction = "DELETE_HOST"
 	DeleteHost HostMqAction = "DELETE_HOST"
 	// JoinHostToNetwork - constant for host network join action
 	// JoinHostToNetwork - constant for host network join action
@@ -126,6 +128,8 @@ const (
 	UpdateMetrics HostMqAction = "UPDATE_METRICS"
 	UpdateMetrics HostMqAction = "UPDATE_METRICS"
 	// EgressUpdate - const for egress update action
 	// EgressUpdate - const for egress update action
 	EgressUpdate HostMqAction = "EGRESS_UPDATE"
 	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
 // SignalAction - turn peer signal action
@@ -156,17 +160,19 @@ type HostTurnRegister struct {
 
 
 // Signal - struct for signalling peer
 // Signal - struct for signalling peer
 type Signal struct {
 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
 // 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"`
 	NodeID       string            `json:"node_id" bson:"node_id" yaml:"node_id"`
 	NodeName     string            `json:"node_name" bson:"node_name" yaml:"node_name"`
 	NodeName     string            `json:"node_name" bson:"node_name" yaml:"node_name"`
 	Connectivity map[string]Metric `json:"connectivity" bson:"connectivity" yaml:"connectivity"`
 	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
 // 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
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
 type HostPeerUpdate struct {
 	Host              Host                  `json:"host"`
 	Host              Host                  `json:"host"`
+	Nodes             []Node                `json:"nodes"`
 	ChangeDefaultGw   bool                  `json:"change_default_gw"`
 	ChangeDefaultGw   bool                  `json:"change_default_gw"`
 	DefaultGwIp       net.IP                `json:"default_gw_ip"`
 	DefaultGwIp       net.IP                `json:"default_gw_ip"`
 	IsInternetGw      bool                  `json:"is_inet_gw"`
 	IsInternetGw      bool                  `json:"is_inet_gw"`
@@ -30,6 +31,8 @@ type HostPeerUpdate struct {
 	NameServers       []string              `json:"name_servers"`
 	NameServers       []string              `json:"name_servers"`
 	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
 	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
 	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
 	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
+	AutoRelayNodes    map[NetworkID][]Node  `json:"auto_relay_nodes"`
+	GwNodes           map[NetworkID][]Node  `json:"gw_nodes"`
 	ServerConfig
 	ServerConfig
 	OldPeerUpdateFields
 	OldPeerUpdateFields
 }
 }
@@ -41,8 +44,9 @@ type EgressDomain struct {
 	Domain string `json:"domain"`
 	Domain string `json:"domain"`
 }
 }
 type Nameserver struct {
 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 {
 type OldPeerUpdateFields struct {
@@ -132,3 +136,9 @@ type FwUpdate struct {
 type FailOverMeReq struct {
 type FailOverMeReq struct {
 	NodeID string `json:"node_id"`
 	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"`
 	IsGw                bool      `json:"is_gw"             yaml:"is_gw"`
 	RelayedNodes        []string  `json:"relaynodes"          yaml:"relayedNodes"`
 	RelayedNodes        []string  `json:"relaynodes"          yaml:"relayedNodes"`
 	IngressDNS          string    `json:"ingressdns"          yaml:"ingressdns"`
 	IngressDNS          string    `json:"ingressdns"          yaml:"ingressdns"`
+	AutoAssignGateway   bool      `json:"auto_assign_gw"`
 }
 }
 
 
 // Node - a model of a network node
 // Node - a model of a network node
 type Node struct {
 type Node struct {
 	CommonNode
 	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"`
 	Metadata                   string               `json:"metadata"`
 	// == PRO ==
 	// == 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"`
 	IsStatic          bool                `json:"is_static"`
 	IsUserNode        bool                `json:"is_user_node"`
 	IsUserNode        bool                `json:"is_user_node"`
 	StaticNode        ExtClient           `json:"static_node"`
 	StaticNode        ExtClient           `json:"static_node"`
@@ -442,10 +447,10 @@ func (newNode *Node) Fill(
 	if newNode.ExpirationDateTime.IsZero() {
 	if newNode.ExpirationDateTime.IsZero() {
 		newNode.ExpirationDateTime = currentNode.ExpirationDateTime
 		newNode.ExpirationDateTime = currentNode.ExpirationDateTime
 	}
 	}
-	if newNode.LastPeerUpdate.IsZero() {
+	if newNode.LastPeerUpdate.IsZero() || currentNode.LastPeerUpdate.After(newNode.LastPeerUpdate) {
 		newNode.LastPeerUpdate = currentNode.LastPeerUpdate
 		newNode.LastPeerUpdate = currentNode.LastPeerUpdate
 	}
 	}
-	if newNode.LastCheckIn.IsZero() {
+	if newNode.LastCheckIn.IsZero() || currentNode.LastCheckIn.After(newNode.LastCheckIn) {
 		newNode.LastCheckIn = currentNode.LastCheckIn
 		newNode.LastCheckIn = currentNode.LastCheckIn
 	}
 	}
 	if newNode.Network == "" {
 	if newNode.Network == "" {
@@ -481,6 +486,7 @@ func (newNode *Node) Fill(
 	if newNode.IsFailOver != currentNode.IsFailOver {
 	if newNode.IsFailOver != currentNode.IsFailOver {
 		newNode.IsFailOver = currentNode.IsFailOver
 		newNode.IsFailOver = currentNode.IsFailOver
 	}
 	}
+	newNode.FailOverPeers = currentNode.FailOverPeers
 	if newNode.Tags == nil {
 	if newNode.Tags == nil {
 		if currentNode.Tags == nil {
 		if currentNode.Tags == nil {
 			currentNode.Tags = make(map[TagID]struct{})
 			currentNode.Tags = make(map[TagID]struct{})

+ 2 - 0
models/settings.go

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

+ 30 - 24
models/structs.go

@@ -22,6 +22,8 @@ type FeatureFlags struct {
 	EnableOAuth             bool `json:"enable_oauth"`
 	EnableOAuth             bool `json:"enable_oauth"`
 	EnableIDPIntegration    bool `json:"enable_idp_integration"`
 	EnableIDPIntegration    bool `json:"enable_idp_integration"`
 	AllowMultiServerLicense bool `json:"allow_multi_server_license"`
 	AllowMultiServerLicense bool `json:"allow_multi_server_license"`
+	EnableGwsHA             bool `json:"enable_gws_ha"`
+	EnableDeviceApproval    bool `json:"enable_device_approval"`
 }
 }
 
 
 // AuthParams - struct for auth params
 // AuthParams - struct for auth params
@@ -52,9 +54,11 @@ type UserRemoteGws struct {
 	AllowedEndpoints  []string   `json:"allowed_endpoints"`
 	AllowedEndpoints  []string   `json:"allowed_endpoints"`
 	NetworkAddresses  []string   `json:"network_addresses"`
 	NetworkAddresses  []string   `json:"network_addresses"`
 	Status            NodeStatus `json:"status"`
 	Status            NodeStatus `json:"status"`
+	ManageDNS         bool       `json:"manage_dns"`
 	DnsAddress        string     `json:"dns_address"`
 	DnsAddress        string     `json:"dns_address"`
 	Addresses         string     `json:"addresses"`
 	Addresses         string     `json:"addresses"`
 	MatchDomains      []string   `json:"match_domains"`
 	MatchDomains      []string   `json:"match_domains"`
+	SearchDomains     []string   `json:"search_domains"`
 }
 }
 
 
 // UserRAGs - struct for user access gws
 // UserRAGs - struct for user access gws
@@ -266,9 +270,9 @@ type HostPull struct {
 	NameServers       []string              `json:"name_servers"`
 	NameServers       []string              `json:"name_servers"`
 	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
 	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
 	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
 	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
 // 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
 // ServerConfig - struct for dealing with the server information for a netclient
 type ServerConfig struct {
 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
 // 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"
 	AdminRole      UserRoleID = "admin"
 	ServiceUser    UserRoleID = "service-user"
 	ServiceUser    UserRoleID = "service-user"
 	PlatformUser   UserRoleID = "platform-user"
 	PlatformUser   UserRoleID = "platform-user"
+	Auditor        UserRoleID = "auditor"
 	NetworkAdmin   UserRoleID = "network-admin"
 	NetworkAdmin   UserRoleID = "network-admin"
 	NetworkUser    UserRoleID = "network-user"
 	NetworkUser    UserRoleID = "network-user"
 )
 )

+ 56 - 39
mq/handlers.go

@@ -108,21 +108,28 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 	case models.CheckIn:
 	case models.CheckIn:
 		sendPeerUpdate = HandleHostCheckin(&hostUpdate.Host, currentHost)
 		sendPeerUpdate = HandleHostCheckin(&hostUpdate.Host, currentHost)
 	case models.Acknowledgement:
 	case models.Acknowledgement:
+		nodes, err := logic.GetAllNodes()
+		if err != nil {
+			return
+		}
 		hu := hostactions.GetAction(currentHost.ID.String())
 		hu := hostactions.GetAction(currentHost.ID.String())
 		if hu != nil {
 		if hu != nil {
 			if err = HostUpdate(hu); err != 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)
 				slog.Error("failed to send new node to host", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 				return
 				return
 			} else {
 			} else {
-				nodes, err := logic.GetAllNodes()
-				if err != nil {
-					return
-				}
+
 				if err = PublishSingleHostPeerUpdate(currentHost, nodes, nil, nil, false, nil); err != nil {
 				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)
 					slog.Error("failed peers publish after join acknowledged", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 					return
 					return
 				}
 				}
 			}
 			}
+		} else {
+			// send latest host update
+			HostUpdate(&models.HostUpdate{
+				Action: models.UpdateHost,
+				Host:   *currentHost})
+			PublishSingleHostPeerUpdate(currentHost, nodes, nil, nil, false, nil)
 		}
 		}
 	case models.UpdateHost:
 	case models.UpdateHost:
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
@@ -136,42 +143,10 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 			return
 			return
 		}
 		}
 	case models.DeleteHost:
 	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
 		sendPeerUpdate = true
 	case models.SignalHost:
 	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 == "" {
 	if signal.ToHostPubKey == "" {
 		msg := "insufficient data to signal peer"
 		msg := "insufficient data to signal peer"
 		logger.Log(0, msg)
 		logger.Log(0, msg)
 		return
 		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
 	signal.IsPro = servercfg.IsPro
 	peerHost, err := logic.GetHost(signal.ToHostID)
 	peerHost, err := logic.GetHost(signal.ToHostID)
 	if err != nil {
 	if err != nil {

+ 1 - 1
mq/publishers.go

@@ -198,7 +198,7 @@ func ServerStartNotify() error {
 }
 }
 
 
 // PublishMqUpdatesForDeletedNode - published all the required updates for deleted node
 // 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
 	// notify of peer change
 	node.PendingDelete = true
 	node.PendingDelete = true
 	node.Action = models.NODE_DELETE
 	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
 	var response bytes.Buffer
 	if err := ssoCallbackTemplate.Execute(&response, ssoCallbackTemplateConfig{
 	if err := ssoCallbackTemplate.Execute(&response, ssoCallbackTemplateConfig{
 		User: userClaims.getUserName(),
 		User: userClaims.getUserName(),
-		Verb: "Authenticated",
+		Verb: "authenticated",
 	}); err != nil {
 	}); err != nil {
 		logger.Log(0, "Could not render SSO callback template ", err.Error())
 		logger.Log(0, "Could not render SSO callback template ", err.Error())
 		response := returnErrTemplate(userClaims.getUserName(), "Could not render SSO callback template", state, reqKeyIf)
 		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
 	var response bytes.Buffer
 	if err := ssoCallbackTemplate.Execute(&response, ssoCallbackTemplateConfig{
 	if err := ssoCallbackTemplate.Execute(&response, ssoCallbackTemplateConfig{
 		User: userClaims.getUserName(),
 		User: userClaims.getUserName(),
-		Verb: "Authenticated",
+		Verb: "authenticated",
 	}); err != nil {
 	}); err != nil {
 		logger.Log(0, "Could not render SSO callback template ", err.Error())
 		logger.Log(0, "Could not render SSO callback template ", err.Error())
 		response := returnErrTemplate(reqKeyIf.User, "Could not render SSO callback template", state, reqKeyIf)
 		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/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/pro/idp"
 	"github.com/gravitl/netmaker/pro/idp"
 	"github.com/gravitl/netmaker/pro/idp/azure"
 	"github.com/gravitl/netmaker/pro/idp/azure"
 	"github.com/gravitl/netmaker/pro/idp/google"
 	"github.com/gravitl/netmaker/pro/idp/google"
 	"github.com/gravitl/netmaker/pro/idp/okta"
 	"github.com/gravitl/netmaker/pro/idp/okta"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
 var (
 var (
@@ -149,7 +151,8 @@ func syncUsers(idpUsers []idp.User) error {
 	for _, user := range idpUsers {
 	for _, user := range idpUsers {
 		if user.AccountArchived {
 		if user.AccountArchived {
 			// delete the user if it has been archived.
 			// delete the user if it has been archived.
-			_ = logic.DeleteUser(user.Username)
+			user := dbUsersMap[user.Username]
+			_ = deleteAndCleanUpUser(&user)
 			continue
 			continue
 		}
 		}
 
 
@@ -209,14 +212,14 @@ func syncUsers(idpUsers []idp.User) error {
 	}
 	}
 
 
 	for _, user := range dbUsersMap {
 	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.ExternalIdentityProviderID = group.ID
 			dbGroup.Name = group.Name
 			dbGroup.Name = group.Name
 			dbGroup.Default = false
 			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)
 			err := proLogic.CreateUserGroup(&dbGroup)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
@@ -324,8 +331,9 @@ func syncGroups(idpGroups []idp.Group) error {
 	for _, group := range dbGroups {
 	for _, group := range dbGroups {
 		if group.ExternalIdentityProviderID != "" {
 		if group.ExternalIdentityProviderID != "" {
 			if _, ok := idpGroupsMap[group.ExternalIdentityProviderID]; !ok {
 			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 {
 				if err != nil {
 					return err
 					return err
 				}
 				}
@@ -355,6 +363,7 @@ func GetIDPSyncStatus() models.IDPSyncStatus {
 		}
 		}
 	}
 	}
 }
 }
+
 func filterUsersByGroupMembership(idpUsers []idp.User, idpGroups []idp.Group) []idp.User {
 func filterUsersByGroupMembership(idpUsers []idp.User, idpGroups []idp.Group) []idp.User {
 	usersMap := make(map[string]int)
 	usersMap := make(map[string]int)
 	for i, user := range idpUsers {
 	for i, user := range idpUsers {
@@ -395,14 +404,14 @@ func filterGroupsByMembers(idpGroups []idp.Group, idpUsers []idp.User) []idp.Gro
 			if _, ok := usersMap[member]; ok {
 			if _, ok := usersMap[member]; ok {
 				members = append(members, member)
 				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
 	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,
 		NetworkID: tag.Network,
 		Origin:    models.Dashboard,
 		Origin:    models.Dashboard,
+		Diff: models.Diff{
+			Old: tag,
+			New: nil,
+		},
 	})
 	})
 	logic.ReturnSuccessResponse(w, r, "deleted tag "+tagID)
 	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,
 			Type: models.UserInviteSub,
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: models.UserInvite{
+				Email: email,
+			},
+			New: nil,
+		},
 	})
 	})
 	logic.ReturnSuccessResponse(w, r, "deleted user invite")
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default user group"), "badrequest"))
 		return
 		return
 	}
 	}
-	err = proLogic.DeleteUserGroup(models.UserGroupID(gid))
+	err = proLogic.DeleteAndCleanUpGroup(&userG)
 	if err != nil {
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+
+	// TODO: log event in proLogic.DeleteAndCleanUpGroup so that all deletions are logged.
 	logic.LogEvent(&models.Event{
 	logic.LogEvent(&models.Event{
 		Action: models.Delete,
 		Action: models.Delete,
 		Source: models.Subject{
 		Source: models.Subject{
@@ -870,43 +878,12 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
 			Type: models.UserGroupSub,
 			Type: models.UserGroupSub,
 		},
 		},
 		Origin: models.Dashboard,
 		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")
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
 }
 }
 
 
@@ -1096,6 +1073,10 @@ func deleteRole(w http.ResponseWriter, r *http.Request) {
 			Type: models.UserRoleSub,
 			Type: models.UserRoleSub,
 		},
 		},
 		Origin: models.Dashboard,
 		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())
 	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")
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user role")
@@ -1475,6 +1456,7 @@ func getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) {
 		Metadata:          node.Metadata,
 		Metadata:          node.Metadata,
 		AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 		AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 		NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 		NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+		ManageDNS:         host.DNS == "yes",
 		DnsAddress:        node.IngressDNS,
 		DnsAddress:        node.IngressDNS,
 		Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 		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.
 			// TODO: prevent ip clashes.
 			gwClient = extClients[0]
 			gwClient = extClients[0]
 		}
 		}
@@ -1626,6 +1608,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 			Status:            node.Status,
 			Status:            node.Status,
+			ManageDNS:         host.DNS == "yes",
 			DnsAddress:        node.IngressDNS,
 			DnsAddress:        node.IngressDNS,
 			Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 			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)
 			hNs := logic.GetNameserversForNode(&node)
 			for _, nsI := range hNs {
 			for _, nsI := range hNs {
 				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
 				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))...)
 		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),
 			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 			Status:            node.Status,
 			Status:            node.Status,
+			ManageDNS:         host.DNS == "yes",
 			DnsAddress:        node.IngressDNS,
 			DnsAddress:        node.IngressDNS,
 			Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 			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)
 			hNs := logic.GetNameserversForNode(&node)
 			for _, nsI := range hNs {
 			for _, nsI := range hNs {
 				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
 				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))...)
 		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,
 			Type: models.PendingUserSub,
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
+		Diff: models.Diff{
+			Old: models.User{
+				UserName: username,
+			},
+			New: nil,
+		},
 	})
 	})
 	logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
 	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>").
 		WithHtml("</ol>").
 		WithParagraph("Important Information:").
 		WithParagraph("Important Information:").
 		WithHtml("<ul>").
 		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>").
 		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(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,").
 		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 {
 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 {
 	if len(prefixes) == 0 {
 		return ""
 		return ""
 	}
 	}
 
 
 	if len(prefixes) == 1 {
 	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 {
 type getUsersResponse struct {

+ 13 - 0
pro/initialize.go

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

+ 1 - 1
pro/license.go

@@ -85,7 +85,7 @@ func ValidateLicense() (err error) {
 
 
 	licenseSecret := LicenseSecret{
 	licenseSecret := LicenseSecret{
 		AssociatedID: netmakerTenantID,
 		AssociatedID: netmakerTenantID,
-		Usage:        getCurrentServerUsage(),
+		Usage:        logic.GetCurrentServerUsage(),
 	}
 	}
 
 
 	secretData, err := json.Marshal(&licenseSecret)
 	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")
 			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")
 		return errors.New("atleast one match domain is required")
 	}
 	}
 	if !ns.MatchAll {
 	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")
 				return errors.New("invalid match domain")
 			}
 			}
 		}
 		}
@@ -65,6 +65,15 @@ func ValidateNameserverReq(ns schema.Nameserver) error {
 }
 }
 
 
 func GetNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 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{
 	ns := &schema.Nameserver{
 		NetworkID: node.Network,
 		NetworkID: node.Network,
 	}
 	}
@@ -73,12 +82,19 @@ func GetNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 		if !nsI.Status {
 		if !nsI.Status {
 			continue
 			continue
 		}
 		}
+
+		filteredIps := logic.FilterOutIPs(nsI.Servers, filters)
+		if len(filteredIps) == 0 {
+			continue
+		}
+
 		_, all := nsI.Tags["*"]
 		_, all := nsI.Tags["*"]
 		if all {
 		if all {
-			for _, matchDomain := range nsI.MatchDomains {
+			for _, domain := range nsI.Domains {
 				returnNsLi = append(returnNsLi, models.Nameserver{
 				returnNsLi = append(returnNsLi, models.Nameserver{
-					IPs:         nsI.Servers,
-					MatchDomain: matchDomain,
+					IPs:            filteredIps,
+					MatchDomain:    domain.Domain,
+					IsSearchDomain: domain.IsSearchDomain,
 				})
 				})
 			}
 			}
 			continue
 			continue
@@ -86,10 +102,11 @@ func GetNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 		foundTag := false
 		foundTag := false
 		for tagI := range node.Tags {
 		for tagI := range node.Tags {
 			if _, ok := nsI.Tags[tagI.String()]; ok {
 			if _, ok := nsI.Tags[tagI.String()]; ok {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
 					returnNsLi = append(returnNsLi, models.Nameserver{
-						IPs:         nsI.Servers,
-						MatchDomain: matchDomain,
+						IPs:            filteredIps,
+						MatchDomain:    domain.Domain,
+						IsSearchDomain: domain.IsSearchDomain,
 					})
 					})
 				}
 				}
 				foundTag = true
 				foundTag = true
@@ -102,10 +119,11 @@ func GetNameserversForNode(node *models.Node) (returnNsLi []models.Nameserver) {
 			continue
 			continue
 		}
 		}
 		if _, ok := nsI.Nodes[node.ID.String()]; ok {
 		if _, ok := nsI.Nodes[node.ID.String()]; ok {
-			for _, matchDomain := range nsI.MatchDomains {
+			for _, domain := range nsI.Domains {
 				returnNsLi = append(returnNsLi, models.Nameserver{
 				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" {
 	if h.DNS != "yes" {
 		return
 		return
 	}
 	}
+
 	for _, nodeID := range h.Nodes {
 	for _, nodeID := range h.Nodes {
 		node, err := logic.GetNodeByID(nodeID)
 		node, err := logic.GetNodeByID(nodeID)
 		if err != nil {
 		if err != nil {
 			continue
 			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{
 		ns := &schema.Nameserver{
 			NetworkID: node.Network,
 			NetworkID: node.Network,
 		}
 		}
@@ -139,12 +168,19 @@ func GetNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 			if !nsI.Status {
 			if !nsI.Status {
 				continue
 				continue
 			}
 			}
+
+			filteredIps := logic.FilterOutIPs(nsI.Servers, filters)
+			if len(filteredIps) == 0 {
+				continue
+			}
+
 			_, all := nsI.Tags["*"]
 			_, all := nsI.Tags["*"]
 			if all {
 			if all {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
 					returnNsLi = append(returnNsLi, models.Nameserver{
-						IPs:         nsI.Servers,
-						MatchDomain: matchDomain,
+						IPs:            filteredIps,
+						MatchDomain:    domain.Domain,
+						IsSearchDomain: domain.IsSearchDomain,
 					})
 					})
 				}
 				}
 				continue
 				continue
@@ -152,10 +188,11 @@ func GetNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 			foundTag := false
 			foundTag := false
 			for tagI := range node.Tags {
 			for tagI := range node.Tags {
 				if _, ok := nsI.Tags[tagI.String()]; ok {
 				if _, ok := nsI.Tags[tagI.String()]; ok {
-					for _, matchDomain := range nsI.MatchDomains {
+					for _, domain := range nsI.Domains {
 						returnNsLi = append(returnNsLi, models.Nameserver{
 						returnNsLi = append(returnNsLi, models.Nameserver{
-							IPs:         nsI.Servers,
-							MatchDomain: matchDomain,
+							IPs:            filteredIps,
+							MatchDomain:    domain.Domain,
+							IsSearchDomain: domain.IsSearchDomain,
 						})
 						})
 					}
 					}
 					foundTag = true
 					foundTag = true
@@ -168,10 +205,11 @@ func GetNameserversForHost(h *models.Host) (returnNsLi []models.Nameserver) {
 				continue
 				continue
 			}
 			}
 			if _, ok := nsI.Nodes[node.ID.String()]; ok {
 			if _, ok := nsI.Nodes[node.ID.String()]; ok {
-				for _, matchDomain := range nsI.MatchDomains {
+				for _, domain := range nsI.Domains {
 					returnNsLi = append(returnNsLi, models.Nameserver{
 					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/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/schema"
-	"golang.org/x/exp/slog"
 )
 )
 
 
 var failOverCtxMutex = &sync.RWMutex{}
 var failOverCtxMutex = &sync.RWMutex{}
@@ -263,25 +262,6 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 }
 }
 
 
 func CreateFailOver(node models.Node) error {
 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
 	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
 // UpdateMetrics - updates the metrics of a given client
 func UpdateMetrics(nodeid string, metrics *models.Metrics) error {
 func UpdateMetrics(nodeid string, metrics *models.Metrics) error {
+	metrics.UpdatedAt = time.Now()
 	data, err := json.Marshal(metrics)
 	data, err := json.Marshal(metrics)
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 16 - 1
pro/logic/migrate.go

@@ -263,10 +263,11 @@ func MigrateToGws() {
 		return
 		return
 	}
 	}
 	for _, node := range nodes {
 	for _, node := range nodes {
-		if node.IsIngressGateway || node.IsRelay || node.IsInternetGateway {
+		if node.IsIngressGateway || node.IsRelay || node.IsInternetGateway || node.IsFailOver {
 			node.IsGw = true
 			node.IsGw = true
 			node.IsIngressGateway = true
 			node.IsIngressGateway = true
 			node.IsRelay = true
 			node.IsRelay = true
+			node.IsAutoRelay = true
 			if node.Tags == nil {
 			if node.Tags == nil {
 				node.Tags = make(map[models.TagID]struct{})
 				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)))
 			delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.OldRemoteAccessTagName)))
 			logic.UpsertNode(&node)
 			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 {
 		if node.IsInternetGateway && len(node.InetNodeReq.InetNodeClientIDs) > 0 {
 			node.RelayedNodes = append(node.RelayedNodes, node.InetNodeReq.InetNodeClientIDs...)
 			node.RelayedNodes = append(node.RelayedNodes, node.InetNodeReq.InetNodeClientIDs...)
 			node.RelayedNodes = logic.UniqueStrings(node.RelayedNodes)
 			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 {
 	if userRole.FullAccess {
 		return nil
 		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
 	// get info from header to determine the target rsrc
 	targetRsrc := r.Header.Get("TARGET_RSRC")
 	targetRsrc := r.Header.Get("TARGET_RSRC")
 	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
 	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
@@ -160,6 +169,20 @@ func GlobalPermissionsCheck(username string, r *http.Request) error {
 	if userRole.FullAccess {
 	if userRole.FullAccess {
 		return nil
 		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")
 	targetRsrc := r.Header.Get("TARGET_RSRC")
 	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
 	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
 	if targetRsrc == "" {
 	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
 		// remove tag used on acl policy
 		go RemoveDeviceTagFromAclPolicies(tagID, tag.Network)
 		go RemoveDeviceTagFromAclPolicies(tagID, tag.Network)
 	}
 	}
+	go RemoveTagFromEgress(tag.Network, tagID)
 	extclients, _ := logic.GetNetworkExtClients(tag.Network.String())
 	extclients, _ := logic.GetNetworkExtClients(tag.Network.String())
 	for _, extclient := range extclients {
 	for _, extclient := range extclients {
 		if _, ok := extclient.Tags[tagID]; ok {
 		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{
 var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
 	ID:         globalNetworksAdminRoleID,
 	ID:         globalNetworksAdminRoleID,
 	Name:       "Network Admins",
 	Name:       "Network Admins",
@@ -122,6 +136,8 @@ func UserRolesInit() {
 	database.Insert(ServiceUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	database.Insert(ServiceUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	d, _ = json.Marshal(PlatformUserUserPermissionTemplate)
 	d, _ = json.Marshal(PlatformUserUserPermissionTemplate)
 	database.Insert(PlatformUserUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	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)
 	d, _ = json.Marshal(NetworkAdminAllPermissionTemplate)
 	database.Insert(NetworkAdminAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	database.Insert(NetworkAdminAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	d, _ = json.Marshal(NetworkUserAllPermissionTemplate)
 	d, _ = json.Marshal(NetworkUserAllPermissionTemplate)
@@ -620,6 +636,22 @@ func GetUserGroup(gid models.UserGroupID) (models.UserGroup, error) {
 	return ug, nil
 	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 {
 func GetDefaultNetworkAdminGroupID(networkID models.NetworkID) models.UserGroupID {
 	return models.UserGroupID(fmt.Sprintf("%s-%s-grp", networkID, models.NetworkAdmin))
 	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)
 	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
 // DeleteUserGroup - deletes user group
 func DeleteUserGroup(gid models.UserGroupID) error {
 func DeleteUserGroup(gid models.UserGroupID) error {
 	g, err := GetUserGroup(gid)
 	g, err := GetUserGroup(gid)
@@ -930,6 +1008,13 @@ func FilterNetworksByRole(allnetworks []models.Network, user models.User) []mode
 	}
 	}
 	if !platformRole.FullAccess {
 	if !platformRole.FullAccess {
 		allNetworkRoles := make(map[models.NetworkID]struct{})
 		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 {
 		if len(user.NetworkRoles) > 0 {
 			for netID := range user.NetworkRoles {
 			for netID := range user.NetworkRoles {
 				if netID == models.AllNetworks {
 				if netID == models.AllNetworks {
@@ -949,7 +1034,6 @@ func FilterNetworksByRole(allnetworks []models.Network, user models.User) []mode
 								return allnetworks
 								return allnetworks
 							}
 							}
 							allNetworkRoles[netID] = struct{}{}
 							allNetworkRoles[netID] = struct{}{}
-
 						}
 						}
 					}
 					}
 				}
 				}

+ 3 - 29
pro/types.go

@@ -5,6 +5,7 @@ package pro
 
 
 import (
 import (
 	"errors"
 	"errors"
+
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 )
 )
 
 
@@ -40,35 +41,8 @@ type ValidatedLicense struct {
 
 
 // LicenseSecret - the encrypted struct for sending user-id
 // LicenseSecret - the encrypted struct for sending user-id
 type LicenseSecret struct {
 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
 // ValidateLicenseRequest - used for request to validate license endpoint

+ 0 - 44
pro/util.go

@@ -4,12 +4,7 @@
 package pro
 package pro
 
 
 import (
 import (
-	"context"
 	"encoding/base64"
 	"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
 // base64encode - base64 encode helper function
@@ -26,42 +21,3 @@ func base64decode(input string) []byte {
 
 
 	return bytes
 	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 🐞
 ## 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.
 - 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.
 - netclients cannot auto-upgrade on ipv6-only machines.
 
 
 - Need to optimize multi-network netclient join with enrollment key
 - Need to optimize multi-network netclient join with enrollment key

+ 13 - 6
schema/dns.go

@@ -9,12 +9,14 @@ import (
 )
 )
 
 
 type Nameserver struct {
 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"`
 	MatchDomains datatypes.JSONSlice[string] `gorm:"match_domains" json:"match_domains"`
 	Tags         datatypes.JSONMap           `gorm:"tags" json:"tags"`
 	Tags         datatypes.JSONMap           `gorm:"tags" json:"tags"`
 	Nodes        datatypes.JSONMap           `gorm:"nodes" json:"nodes"`
 	Nodes        datatypes.JSONMap           `gorm:"nodes" json:"nodes"`
@@ -24,6 +26,11 @@ type Nameserver struct {
 	UpdatedAt    time.Time                   `gorm:"updated_at" json:"updated_at"`
 	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 {
 func (ns *Nameserver) Get(ctx context.Context) error {
 	return db.FromContext(ctx).Model(&Nameserver{}).First(&ns).Where("id = ?", ns.ID).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`)
 # location of nm-quick.sh (usually `/root`)
 SCRIPT_DIR=$(dirname "$(realpath "$0")")
 SCRIPT_DIR=$(dirname "$(realpath "$0")")
 CONFIG_PATH="$SCRIPT_DIR/$CONFIG_FILE"
 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=$(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
 BRANCH=master
 if [ $(id -u) -ne 0 ]; then
 if [ $(id -u) -ne 0 ]; then
 	echo "This script must be run as root"
 	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 - configures server's netclient as a default host and an ingress gateway
 configure_netclient() {
 configure_netclient() {
 	sleep 2
 	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)
 	HOST_ID=$(sudo cat /etc/netclient/netclient.json | jq -r .id)
 	if [ "$HOST_ID" = "" ] || [ "$HOST_ID" = "null" ]; then
 	if [ "$HOST_ID" = "" ] || [ "$HOST_ID" = "null" ]; then
 		echo "Error obtaining HOST_ID for the new network"
 		echo "Error obtaining HOST_ID for the new network"
@@ -167,13 +167,13 @@ configure_netclient() {
 	set +e
 	set +e
 	nmctl host update $HOST_ID --default
 	nmctl host update $HOST_ID --default
 	sleep 5
 	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
 	set -e
 }
 }
 
 
@@ -694,24 +694,24 @@ test_connection() {
 setup_mesh() {
 setup_mesh() {
 
 
 	wait_seconds 5
 	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
 	# 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
 	if [[ ${netmakerTag} = "" ]]; then
-		nmctl enrollment_key create --tags netmaker --unlimited --networks netmaker
+		nmctl enrollment_key create --tags firstJoinKey --unlimited
 	fi
 	fi
 	echo "Obtaining enrollment key..."
 	echo "Obtaining enrollment key..."
 	# key exists already, fetch token
 	# 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
 	wait_seconds 3
 }
 }
 
 

+ 5 - 1
servercfg/serverconf.go

@@ -750,7 +750,11 @@ func IsStunEnabled() bool {
 }
 }
 
 
 func GetStunServers() string {
 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...)
 // GetEnvironment returns the environment the server is running in (e.g. dev, staging, prod...)

+ 1 - 1
swagger.yaml

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