Browse Source

Merge pull request #2834 from gravitl/release-v0.23.0

v0.23.0
Abhishek K 1 year ago
parent
commit
089df3d4cf
54 changed files with 1873 additions and 679 deletions
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 1 1
      Dockerfile
  3. 1 1
      Dockerfile-quick
  4. 1 1
      README.md
  5. 35 13
      auth/host_session.go
  6. 1 1
      compose/docker-compose.netclient.yml
  7. 1 1
      controllers/config/dnsconfig/Corefile
  8. 1 1
      controllers/docs.go
  9. 2 8
      controllers/enrollmentkeys.go
  10. 54 9
      controllers/ext_client.go
  11. 14 8
      controllers/hosts.go
  12. 0 3
      controllers/migrate.go
  13. 217 19
      controllers/network.go
  14. 25 30
      controllers/node.go
  15. 1 5
      controllers/node_test.go
  16. 2 2
      database/rqlite.go
  17. 11 11
      go.mod
  18. 22 27
      go.sum
  19. 1 1
      k8s/client/netclient-daemonset.yaml
  20. 1 1
      k8s/client/netclient.yaml
  21. 1 1
      k8s/server/netmaker-ui.yaml
  22. 17 17
      logic/acls/common.go
  23. 7 1
      logic/acls/nodeacls/modify.go
  24. 13 2
      logic/acls/nodeacls/retrieve.go
  25. 19 0
      logic/clients.go
  26. 4 0
      logic/extpeers.go
  27. 21 25
      logic/gateway.go
  28. 30 0
      logic/nodes.go
  29. 48 19
      logic/peers.go
  30. 2 1
      logic/relay.go
  31. 1 1
      main.go
  32. 125 0
      migrate/migrate.go
  33. 17 12
      models/api_node.go
  34. 4 0
      models/extclient.go
  35. 3 0
      models/mqtt.go
  36. 9 6
      models/node.go
  37. 13 0
      models/structs.go
  38. 29 373
      mq/emqx.go
  39. 264 0
      mq/emqx_cloud.go
  40. 428 0
      mq/emqx_on_prem.go
  41. 2 2
      mq/handlers.go
  42. 23 16
      mq/mq.go
  43. 1 20
      pro/controllers/failover.go
  44. 164 0
      pro/controllers/inet_gws.go
  45. 4 0
      pro/controllers/users.go
  46. 7 2
      pro/initialize.go
  47. 27 0
      pro/logic/ext_acls.go
  48. 24 0
      pro/logic/failover.go
  49. 121 7
      pro/logic/nodes.go
  50. 5 6
      pro/logic/relays.go
  51. 15 15
      release.md
  52. 1 7
      scripts/nm-upgrade.sh
  53. 31 2
      servercfg/serverconf.go
  54. 1 1
      swagger.yml

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

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

+ 1 - 1
Dockerfile

@@ -6,7 +6,7 @@ COPY . .
 
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
-FROM alpine:3.19.0
+FROM alpine:3.19.1
 
 # add a c lib
 # set the working directory

+ 1 - 1
Dockerfile-quick

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

+ 1 - 1
README.md

@@ -16,7 +16,7 @@
 
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.22.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.23.0-informational?style=flat-square" />
   </a>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />

+ 35 - 13
auth/host_session.go

@@ -86,6 +86,24 @@ func SessionHandler(conn *websocket.Conn) {
 			return
 		}
 		req.Pass = req.Host.ID.String()
+		user, err := logic.GetUser(req.User)
+		if err != nil {
+			logger.Log(0, "failed to get user", req.User, "from database")
+			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+			if err != nil {
+				logger.Log(0, "error during message writing:", err.Error())
+			}
+			return
+		}
+		if !user.IsAdmin && !user.IsSuperAdmin {
+			logger.Log(0, "user", req.User, "is neither an admin or superadmin. denying registeration")
+			conn.WriteMessage(messageType, []byte("cannot register with a non-admin or non-superadmin"))
+			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+			if err != nil {
+				logger.Log(0, "error during message writing:", err.Error())
+			}
+			return
+		}
 
 		if err = netcache.Set(stateStr, req); err != nil { // give the user's host access in the DB
 			logger.Log(0, "machine failed to complete join on network,", registerMessage.Network, "-", err.Error())
@@ -129,14 +147,13 @@ func SessionHandler(conn *websocket.Conn) {
 	select {
 	case result := <-answer: // a read from req.answerCh has occurred
 		// add the host, if not exists, handle like enrollment registration
-		hostPass := result.Host.HostPass
 		if !logic.HostExists(&result.Host) { // check if host already exists, add if not
 			if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-				if err := mq.CreateEmqxUser(result.Host.ID.String(), result.Host.HostPass, false); err != nil {
+				if err := mq.GetEmqxHandler().CreateEmqxUser(result.Host.ID.String(), result.Host.HostPass); err != nil {
 					logger.Log(0, "failed to create host credentials for EMQX: ", err.Error())
 					return
 				}
-				if err := mq.CreateHostACL(result.Host.ID.String(), servercfg.GetServerInfo().Server); err != nil {
+				if err := mq.GetEmqxHandler().CreateHostACL(result.Host.ID.String(), servercfg.GetServerInfo().Server); err != nil {
 					logger.Log(0, "failed to add host ACL rules to EMQX: ", err.Error())
 					return
 				}
@@ -185,11 +202,6 @@ func SessionHandler(conn *websocket.Conn) {
 		}
 		server := servercfg.GetServerInfo()
 		server.TrafficKey = key
-		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-			// set MQ username and password for EMQX clients
-			server.MQUserName = result.Host.ID.String()
-			server.MQPassword = hostPass
-		}
 		result.Host.HostPass = ""
 		response := models.RegisterResponse{
 			ServerConf:    server,
@@ -233,11 +245,15 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 				continue
 			}
 			if relayNodeId != uuid.Nil && !newNode.IsRelayed {
-				newNode.IsRelayed = true
-				newNode.RelayedBy = relayNodeId.String()
-				slog.Info(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
-				if err := logic.UpsertNode(newNode); err != nil {
-					slog.Error("failed to update node", "nodeid", relayNodeId.String())
+				// check if relay node exists and acting as relay
+				relaynode, err := logic.GetNodeByID(relayNodeId.String())
+				if err == nil && relaynode.IsRelay {
+					newNode.IsRelayed = true
+					newNode.RelayedBy = relayNodeId.String()
+					slog.Info(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
+					if err := logic.UpsertNode(newNode); err != nil {
+						slog.Error("failed to update node", "nodeid", relayNodeId.String())
+					}
 				}
 			}
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
@@ -246,6 +262,12 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 				Host:   *h,
 				Node:   *newNode,
 			})
+			if h.IsDefault {
+				// make  host failover
+				logic.CreateFailOver(*newNode)
+				// make host remote access gateway
+				logic.CreateIngressGateway(network, newNode.ID.String(), models.IngressRequest{})
+			}
 		}
 	}
 	if servercfg.IsMessageQueueBackend() {

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

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

+ 1 - 1
controllers/config/dnsconfig/Corefile

@@ -1,4 +1,4 @@
-skynet  {
+. {
     reload 15s
     hosts /root/dnsconfig/netmaker.hosts {
 	fallthrough	

+ 1 - 1
controllers/docs.go

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

+ 2 - 8
controllers/enrollmentkeys.go

@@ -305,18 +305,17 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
-	hostPass := newHost.HostPass
 	if !hostExists {
 		newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
 		// register host
 		logic.CheckHostPorts(&newHost)
 		// create EMQX credentials and ACLs for host
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-			if err := mq.CreateEmqxUser(newHost.ID.String(), newHost.HostPass, false); err != nil {
+			if err := mq.GetEmqxHandler().CreateEmqxUser(newHost.ID.String(), newHost.HostPass); err != nil {
 				logger.Log(0, "failed to create host credentials for EMQX: ", err.Error())
 				return
 			}
-			if err := mq.CreateHostACL(newHost.ID.String(), servercfg.GetServerInfo().Server); err != nil {
+			if err := mq.GetEmqxHandler().CreateHostACL(newHost.ID.String(), servercfg.GetServerInfo().Server); err != nil {
 				logger.Log(0, "failed to add host ACL rules to EMQX: ", err.Error())
 				return
 			}
@@ -361,11 +360,6 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	// ready the response
 	server := servercfg.GetServerInfo()
 	server.TrafficKey = key
-	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-		// set MQ username and password for EMQX clients
-		server.MQUserName = newHost.ID.String()
-		server.MQPassword = hostPass
-	}
 	response := models.RegisterResponse{
 		ServerConf:    server,
 		RequestedHost: newHost,

+ 54 - 9
controllers/ext_client.go

@@ -6,12 +6,16 @@ import (
 	"fmt"
 	"net"
 	"net/http"
+	"reflect"
 	"strconv"
+	"strings"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/servercfg"
 
 	"github.com/gravitl/netmaker/models"
@@ -218,7 +222,7 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 		gwendpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), host.ListenPort)
 	}
 	var newAllowedIPs string
-	if logic.IsInternetGw(gwnode) {
+	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
 		egressrange := "0.0.0.0/0"
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 			egressrange += "," + "::/0"
@@ -250,11 +254,28 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	if host.MTU != 0 {
 		defaultMTU = host.MTU
 	}
+
+	postUp := strings.Builder{}
+	if client.PostUp != "" && params["type"] != "qr" {
+		for _, loc := range strings.Split(client.PostUp, "\n") {
+			postUp.WriteString(fmt.Sprintf("PostUp = %s\n", loc))
+		}
+	}
+
+	postDown := strings.Builder{}
+	if client.PostDown != "" && params["type"] != "qr" {
+		for _, loc := range strings.Split(client.PostDown, "\n") {
+			postDown.WriteString(fmt.Sprintf("PostDown = %s\n", loc))
+		}
+	}
+
 	config := fmt.Sprintf(`[Interface]
 Address = %s
 PrivateKey = %s
 MTU = %d
 %s
+%s
+%s
 
 [Peer]
 PublicKey = %s
@@ -266,10 +287,13 @@ Endpoint = %s
 		client.PrivateKey,
 		defaultMTU,
 		defaultDNS,
+		postUp.String(),
+		postDown.String(),
 		host.PublicKey,
 		newAllowedIPs,
 		gwendpoint,
-		keepalive)
+		keepalive,
+	)
 
 	if params["type"] == "qr" {
 		bytes, err := qrcode.Encode(config, qrcode.Medium, 220)
@@ -330,7 +354,6 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	var customExtClient models.CustomExtClient
-
 	if err := json.NewDecoder(r.Body).Decode(&customExtClient); err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
@@ -407,14 +430,14 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		extclient.Enabled = parentNetwork.DefaultACL == "yes"
 	}
 
-	if err := logic.SetClientDefaultACLs(&extclient); err != nil {
-		slog.Error("failed to set default acls for extclient", "user", r.Header.Get("user"), "network", node.Network, "error", err)
+	if err = logic.CreateExtClient(&extclient); err != nil {
+		slog.Error("failed to create extclient", "user", r.Header.Get("user"), "network", node.Network, "error", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
 
-	if err = logic.CreateExtClient(&extclient); err != nil {
-		slog.Error("failed to create extclient", "user", r.Header.Get("user"), "network", node.Network, "error", err)
+	if err := logic.SetClientDefaultACLs(&extclient); err != nil {
+		slog.Error("failed to set default acls for extclient", "user", r.Header.Get("user"), "network", node.Network, "error", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
@@ -486,7 +509,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 	var changedID = update.ClientID != oldExtClient.ClientID
 
-	if len(update.DeniedACLs) != len(oldExtClient.DeniedACLs) {
+	if !reflect.DeepEqual(update.DeniedACLs, oldExtClient.DeniedACLs) {
 		sendPeerUpdate = true
 		logic.SetClientACLs(&oldExtClient, update.DeniedACLs)
 	}
@@ -499,7 +522,6 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 	newclient := logic.UpdateExtClient(&oldExtClient, &update)
 	if err := logic.DeleteExtClient(oldExtClient.Network, oldExtClient.ClientID); err != nil {
-
 		slog.Error("failed to delete ext client", "user", r.Header.Get("user"), "id", oldExtClient.ClientID, "network", oldExtClient.Network, "error", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
@@ -593,6 +615,24 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// delete client acls
+	var networkAcls acls.ACLContainer
+	networkAcls, err = networkAcls.Get(acls.ContainerID(network))
+	if err != nil {
+		slog.Error("failed to get network acls", "err", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for objId := range networkAcls {
+		delete(networkAcls[objId], acls.AclID(clientid))
+	}
+	delete(networkAcls, acls.AclID(clientid))
+	if _, err = networkAcls.Save(acls.ContainerID(network)); err != nil {
+		slog.Error("failed to update network acls", "err", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
 	go func() {
 		if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
 			logger.Log(1, "error setting ext peers on "+ingressnode.ID.String()+": "+err.Error())
@@ -609,6 +649,11 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 
 // validateCustomExtClient	Validates the extclient object
 func validateCustomExtClient(customExtClient *models.CustomExtClient, checkID bool) error {
+	v := validator.New()
+	err := v.Struct(customExtClient)
+	if err != nil {
+		return err
+	}
 	//validate clientid
 	if customExtClient.ClientID != "" {
 		if err := isValid(customExtClient.ClientID, checkID); err != nil {

+ 14 - 8
controllers/hosts.go

@@ -124,9 +124,6 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	serverConf := servercfg.GetServerInfo()
-	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-		serverConf.MQUserName = hostID
-	}
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	if keyErr != nil {
 		logger.Log(0, "error retrieving key:", keyErr.Error())
@@ -144,6 +141,9 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		HostNetworkInfo: hPU.HostNetworkInfo,
 		EgressRoutes:    hPU.EgressRoutes,
 		FwUpdate:        hPU.FwUpdate,
+		ChangeDefaultGw: hPU.ChangeDefaultGw,
+		DefaultGwIp:     hPU.DefaultGwIp,
+		IsInternetGw:    hPU.IsInternetGw,
 	}
 
 	logger.Log(1, hostID, "completed a pull")
@@ -254,7 +254,7 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	case models.UpdateMetrics:
-		// mq.UpdateMetricsFallBack(hostUpdate.Node.ID.String(), hostUpdate.NewMetrics)
+		mq.UpdateMetricsFallBack(hostUpdate.Node.ID.String(), hostUpdate.NewMetrics)
 	}
 	logic.ReturnSuccessResponse(w, r, "updated host data")
 
@@ -298,7 +298,7 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 	}
 	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 		// delete EMQX credentials for host
-		if err := mq.DeleteEmqxUser(currHost.ID.String()); err != nil {
+		if err := mq.GetEmqxHandler().DeleteEmqxUser(currHost.ID.String()); err != nil {
 			slog.Error("failed to remove host credentials from EMQX", "id", currHost.ID, "error", err)
 		}
 	}
@@ -354,6 +354,12 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	logger.Log(1, "added new node", newNode.ID.String(), "to host", currHost.Name)
+	if currHost.IsDefault {
+		// make  host failover
+		logic.CreateFailOver(*newNode)
+		// make host remote access gateway
+		logic.CreateIngressGateway(network, newNode.ID.String(), models.IngressRequest{})
+	}
 	go func() {
 		mq.HostUpdate(&models.HostUpdate{
 			Action: models.JoinHostToNetwork,
@@ -549,15 +555,15 @@ func authenticateHost(response http.ResponseWriter, request *http.Request) {
 
 	// Create EMQX creds and ACLs if not found
 	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-		if err := mq.CreateEmqxUser(host.ID.String(), authRequest.Password, false); err != nil {
+		if err := mq.GetEmqxHandler().CreateEmqxUser(host.ID.String(), authRequest.Password); err != nil {
 			slog.Error("failed to create host credentials for EMQX: ", err.Error())
 		} else {
-			if err := mq.CreateHostACL(host.ID.String(), servercfg.GetServerInfo().Server); err != nil {
+			if err := mq.GetEmqxHandler().CreateHostACL(host.ID.String(), servercfg.GetServerInfo().Server); err != nil {
 				slog.Error("failed to add host ACL rules to EMQX: ", err.Error())
 			}
 			for _, nodeID := range host.Nodes {
 				if node, err := logic.GetNodeByID(nodeID); err == nil {
-					if err = mq.AppendNodeUpdateACL(host.ID.String(), node.Network, node.ID.String(), servercfg.GetServer()); err != nil {
+					if err = mq.GetEmqxHandler().AppendNodeUpdateACL(host.ID.String(), node.Network, node.ID.String(), servercfg.GetServer()); err != nil {
 						slog.Error("failed to add ACLs for EMQX node", "error", err)
 					}
 				} else {

+ 0 - 3
controllers/migrate.go

@@ -71,9 +71,6 @@ func migrate(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 			server = servercfg.GetServerInfo()
-			if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-				server.MQUserName = host.ID.String()
-			}
 			key, keyErr := logic.RetrievePublicTrafficKey()
 			if keyErr != nil {
 				slog.Error("retrieving traffickey", "error", err)

+ 217 - 19
controllers/network.go

@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"golang.org/x/exp/slog"
 
@@ -17,6 +18,7 @@ import (
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/servercfg"
 )
 
 func networkHandlers(r *mux.Router) {
@@ -27,6 +29,7 @@ func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(updateNetwork))).Methods(http.MethodPut)
 	// ACLs
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACL))).Methods(http.MethodPut)
+	r.HandleFunc("/api/networks/{networkname}/acls/v2", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACLv2))).Methods(http.MethodPut)
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).Methods(http.MethodGet)
 }
 
@@ -129,7 +132,7 @@ func updateNetworkACL(w http.ResponseWriter, r *http.Request) {
 	// send peer updates
 	go func() {
 		if err = mq.PublishPeerUpdate(false); err != nil {
-			logger.Log(0, "failed to publish peer update after ACL update on", netname)
+			logger.Log(0, "failed to publish peer update after ACL update on network:", netname)
 		}
 	}()
 
@@ -137,6 +140,196 @@ func updateNetworkACL(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newNetACL)
 }
 
+// swagger:route PUT /api/networks/{networkname}/acls/v2 networks updateNetworkACL
+//
+// Update a network ACL (Access Control List).
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: aclContainerResponse
+func updateNetworkACLv2(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	netname := params["networkname"]
+	var networkACLChange acls.ACLContainer
+	networkACLChange, err := networkACLChange.Get(acls.ContainerID(netname))
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to fetch ACLs for network [%s]: %v", netname, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	err = json.NewDecoder(r.Body).Decode(&networkACLChange)
+	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
+	}
+
+	// clone req body to use as return data successful update
+	retData := make(acls.ACLContainer)
+	data, err := json.Marshal(networkACLChange)
+	if err != nil {
+		slog.Error("failed to marshal networkACLChange whiles cloning", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	err = json.Unmarshal(data, &retData)
+	if err != nil {
+		slog.Error("failed to unmarshal networkACLChange whiles cloning", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	allNodes, err := logic.GetAllNodes()
+	if err != nil {
+		slog.Error("failed to fetch all nodes", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	networkNodes := make([]models.Node, 0)
+	for _, node := range allNodes {
+		if node.Network == netname {
+			networkNodes = append(networkNodes, node)
+		}
+	}
+	networkNodesIdMap := make(map[string]models.Node)
+	for _, node := range networkNodes {
+		networkNodesIdMap[node.ID.String()] = node
+	}
+	networkClients, err := logic.GetNetworkExtClients(netname)
+	if err != nil {
+		slog.Error("failed to fetch network clients", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	networkClientsMap := make(map[string]models.ExtClient)
+	for _, client := range networkClients {
+		networkClientsMap[client.ClientID] = client
+	}
+
+	// keep track of ingress gateways to disconnect from their clients
+	// this is required because PublishPeerUpdate only somehow does not stop communication
+	// between blocked clients and their ingress
+	assocClientsToDisconnectPerHost := make(map[uuid.UUID][]models.ExtClient)
+
+	// update client acls and then, remove client acls from req data to pass to existing functions
+	for id, acl := range networkACLChange {
+		// for node acls
+		if _, ok := networkNodesIdMap[string(id)]; ok {
+			nodeId := string(id)
+			// check acl update, then remove client entries
+			for id2 := range acl {
+				if _, ok := networkNodesIdMap[string(id2)]; !ok {
+					// update client acl
+					clientId := string(id2)
+					if client, ok := networkClientsMap[clientId]; ok {
+						if client.DeniedACLs == nil {
+							client.DeniedACLs = make(map[string]struct{})
+						}
+						if acl[acls.AclID(clientId)] == acls.NotAllowed {
+							client.DeniedACLs[nodeId] = struct{}{}
+						} else {
+							delete(client.DeniedACLs, string(nodeId))
+						}
+						networkClientsMap[clientId] = client
+					}
+				}
+			}
+		} else {
+			// for client acls
+			clientId := string(id)
+			for id2 := range acl {
+				if _, ok := networkNodesIdMap[string(id2)]; !ok {
+					// update client acl
+					clientId2 := string(id2)
+					if client, ok := networkClientsMap[clientId]; ok {
+						if client.DeniedACLs == nil {
+							client.DeniedACLs = make(map[string]struct{})
+						}
+						{
+							// TODO: review this when client-to-client acls are supported
+							// if acl[acls.AclID(clientId2)] == acls.NotAllowed {
+							// 	client.DeniedACLs[clientId2] = struct{}{}
+							// } else {
+							// 	delete(client.DeniedACLs, clientId2)
+							// }
+							delete(client.DeniedACLs, clientId2)
+						}
+						networkClientsMap[clientId] = client
+					}
+				} else {
+					nodeId2 := string(id2)
+					if networkClientsMap[clientId].IngressGatewayID == nodeId2 && acl[acls.AclID(nodeId2)] == acls.NotAllowed {
+						assocClientsToDisconnectPerHost[networkNodesIdMap[nodeId2].HostID] = append(assocClientsToDisconnectPerHost[networkNodesIdMap[nodeId2].HostID], networkClientsMap[clientId])
+					}
+				}
+			}
+		}
+	}
+
+	// update each client in db for pro servers
+	if servercfg.IsPro {
+		for _, client := range networkClientsMap {
+			client := client
+			err := logic.DeleteExtClient(client.Network, client.ClientID)
+			if err != nil {
+				slog.Error("failed to delete client during update", "client", client.ClientID, "error", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+			err = logic.SaveExtClient(&client)
+			if err != nil {
+				slog.Error("failed to save client during update", "client", client.ClientID, "error", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+		}
+	}
+
+	_, err = networkACLChange.Save(acls.ContainerID(netname))
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to update ACLs for network [%s]: %v", netname, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	logger.Log(1, r.Header.Get("user"), "updated ACLs for network", netname)
+
+	// send peer updates
+	go func() {
+		if err = mq.PublishPeerUpdate(false); err != nil {
+			logger.Log(0, "failed to publish peer update after ACL update on network:", netname)
+		}
+
+		// update ingress gateways of associated clients
+		hosts, err := logic.GetAllHosts()
+		if err != nil {
+			slog.Error("failed to fetch hosts after network ACL update. skipping publish extclients ACL", "network", netname)
+			return
+		}
+		hostsMap := make(map[uuid.UUID]models.Host)
+		for _, host := range hosts {
+			hostsMap[host.ID] = host
+		}
+		for hostId, clients := range assocClientsToDisconnectPerHost {
+			if host, ok := hostsMap[hostId]; ok {
+				if err = mq.PublishSingleHostPeerUpdate(&host, allNodes, nil, clients, false); err != nil {
+					slog.Error("failed to publish peer update to ingress after ACL update on network", "network", netname, "host", hostId)
+				}
+			}
+		}
+	}()
+
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(networkACLChange)
+}
+
 // swagger:route GET /api/networks/{networkname}/acls networks getNetworkACL
 //
 // Get a network ACL (Access Control List).
@@ -288,25 +481,30 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-
-	defaultHosts := logic.GetDefaultHosts()
-	for i := range defaultHosts {
-		currHost := &defaultHosts[i]
-		newNode, err := logic.UpdateHostNetwork(currHost, network.NetID, true)
-		if err != nil {
-			logger.Log(0, r.Header.Get("user"), "failed to add host to network:", currHost.ID.String(), network.NetID, err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
-		}
-		logger.Log(1, "added new node", newNode.ID.String(), "to host", currHost.Name)
-		if err = mq.HostUpdate(&models.HostUpdate{
-			Action: models.JoinHostToNetwork,
-			Host:   *currHost,
-			Node:   *newNode,
-		}); err != nil {
-			logger.Log(0, r.Header.Get("user"), "failed to add host to network:", currHost.ID.String(), network.NetID, err.Error())
+	go func() {
+		defaultHosts := logic.GetDefaultHosts()
+		for i := range defaultHosts {
+			currHost := &defaultHosts[i]
+			newNode, err := logic.UpdateHostNetwork(currHost, network.NetID, true)
+			if err != nil {
+				logger.Log(0, r.Header.Get("user"), "failed to add host to network:", currHost.ID.String(), network.NetID, err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+			logger.Log(1, "added new node", newNode.ID.String(), "to host", currHost.Name)
+			if err = mq.HostUpdate(&models.HostUpdate{
+				Action: models.JoinHostToNetwork,
+				Host:   *currHost,
+				Node:   *newNode,
+			}); err != nil {
+				logger.Log(0, r.Header.Get("user"), "failed to add host to network:", currHost.ID.String(), network.NetID, err.Error())
+			}
+			// make  host failover
+			logic.CreateFailOver(*newNode)
+			// make host remote access gateway
+			logic.CreateIngressGateway(network.NetID, newNode.ID.String(), models.IngressRequest{})
 		}
-	}
+	}()
 
 	logger.Log(1, r.Header.Get("user"), "created network", network.NetID)
 	w.WriteHeader(http.StatusOK)

+ 25 - 30
controllers/node.go

@@ -345,7 +345,7 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	nodeid := params["nodeid"]
 
-	node, err := validateParams(nodeid, params["network"])
+	node, err := logic.ValidateParams(nodeid, params["network"])
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
@@ -372,10 +372,6 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	server := servercfg.GetServerInfo()
-	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-		// set MQ username for EMQX clients
-		server.MQUserName = host.ID.String()
-	}
 	response := models.NodeGet{
 		Node:         node,
 		Host:         *host,
@@ -406,9 +402,9 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 func createEgressGateway(w http.ResponseWriter, r *http.Request) {
 	var gateway models.EgressGatewayRequest
 	var params = mux.Vars(r)
-	node, err := validateParams(params["nodeid"], params["network"])
+	node, err := logic.ValidateParams(params["nodeid"], params["network"])
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "bad request"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	w.Header().Set("Content-Type", "application/json")
@@ -457,9 +453,9 @@ func deleteEgressGateway(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	nodeid := params["nodeid"]
 	netid := params["network"]
-	node, err := validateParams(nodeid, netid)
+	node, err := logic.ValidateParams(nodeid, netid)
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "bad request"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	node, err = logic.DeleteEgressGateway(netid, nodeid)
@@ -501,9 +497,9 @@ func createIngressGateway(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	nodeid := params["nodeid"]
 	netid := params["network"]
-	node, err := validateParams(nodeid, netid)
+	node, err := logic.ValidateParams(nodeid, netid)
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "bad request"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	var request models.IngressRequest
@@ -544,9 +540,9 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	nodeid := params["nodeid"]
 	netid := params["network"]
-	node, err := validateParams(nodeid, netid)
+	node, err := logic.ValidateParams(nodeid, netid)
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "bad request"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	node, removedClients, err := logic.DeleteIngressGateway(nodeid)
@@ -622,9 +618,9 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 
 	//start here
 	nodeid := params["nodeid"]
-	currentNode, err := validateParams(nodeid, params["network"])
+	currentNode, err := logic.ValidateParams(nodeid, params["network"])
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "bad request"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	var newData models.ApiNode
@@ -635,7 +631,19 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	if len(newData.Metadata) > 255 {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("metadata cannot be longer than 255 characters"), "badrequest"))
+		return
+	}
 	newNode := newData.ConvertToServerNode(&currentNode)
+	if newNode.IsInternetGateway != currentNode.IsInternetGateway {
+		if newNode.IsInternetGateway {
+			logic.SetInternetGw(newNode, models.InetNodeReq{})
+		} else {
+			logic.UnsetInternetGw(newNode)
+		}
+
+	}
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	_, err = logic.GetHost(newNode.HostID.String())
 	if err != nil {
@@ -695,9 +703,9 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 	// get params
 	var params = mux.Vars(r)
 	var nodeid = params["nodeid"]
-	node, err := validateParams(nodeid, params["network"])
+	node, err := logic.ValidateParams(nodeid, params["network"])
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "bad request"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	forceDelete := r.URL.Query().Get("force") == "true"
@@ -716,16 +724,3 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 	logger.Log(1, r.Header.Get("user"), "Deleted node", nodeid, "from network", params["network"])
 	go mq.PublishMqUpdatesForDeletedNode(node, !fromNode, gwClients)
 }
-
-func validateParams(nodeid, netid string) (models.Node, error) {
-	node, err := logic.GetNodeByID(nodeid)
-	if err != nil {
-		slog.Error("error fetching node", "node", nodeid, "error", err.Error())
-		return node, fmt.Errorf("error fetching node during parameter validation: %v", err)
-	}
-	if node.Network != netid {
-		slog.Error("network url param does not match node id", "url nodeid", netid, "node", node.Network)
-		return node, fmt.Errorf("network url param does not match node network")
-	}
-	return node, nil
-}

+ 1 - 5
controllers/node_test.go

@@ -132,11 +132,7 @@ func TestGetNetworkNodes(t *testing.T) {
 
 func TestValidateEgressGateway(t *testing.T) {
 	var gateway models.EgressGatewayRequest
-	t.Run("EmptyRange", func(t *testing.T) {
-		gateway.Ranges = []string{}
-		err := logic.ValidateEgressGateway(gateway)
-		assert.EqualError(t, err, "IP Ranges Cannot Be Empty")
-	})
+
 	t.Run("Success", func(t *testing.T) {
 		gateway.Ranges = []string{"10.100.100.0/24"}
 		err := logic.ValidateEgressGateway(gateway)

+ 2 - 2
database/rqlite.go

@@ -29,8 +29,8 @@ func initRqliteDatabase() error {
 	if err != nil {
 		return err
 	}
-	RQliteDatabase = conn
-	RQliteDatabase.SetConsistencyLevel("strong")
+	RQliteDatabase = *conn
+	RQliteDatabase.SetConsistencyLevel(gorqlite.ConsistencyLevelStrong)
 	return nil
 }
 

+ 11 - 11
go.mod

@@ -4,21 +4,21 @@ go 1.19
 
 require (
 	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.16.0
+	github.com/go-playground/validator/v10 v10.18.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
-	github.com/google/uuid v1.5.0
+	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.19
-	github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f
+	github.com/mattn/go-sqlite3 v1.14.22
+	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.8.4
 	github.com/txn2/txeh v1.5.5
-	golang.org/x/crypto v0.17.0
-	golang.org/x/net v0.19.0 // indirect
-	golang.org/x/oauth2 v0.15.0
-	golang.org/x/sys v0.15.0 // indirect
+	golang.org/x/crypto v0.19.0
+	golang.org/x/net v0.21.0 // indirect
+	golang.org/x/oauth2 v0.17.0
+	golang.org/x/sys v0.17.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	google.golang.org/protobuf v1.31.0 // indirect
@@ -27,7 +27,7 @@ require (
 
 require (
 	filippo.io/edwards25519 v1.1.0
-	github.com/c-robinson/iplib v1.0.7
+	github.com/c-robinson/iplib v1.0.8
 	github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0
 )
 
@@ -47,7 +47,7 @@ require (
 
 require (
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
@@ -61,7 +61,7 @@ require (
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/hashicorp/go-version v1.6.0
-	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect

+ 22 - 27
go.sum

@@ -5,8 +5,8 @@ cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2Aawl
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/c-robinson/iplib v1.0.7 h1:Dh9AINAlkc+NsNzZuFiVs+pi3AjN+0B7mu01KHdJKHU=
-github.com/c-robinson/iplib v1.0.7/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
+github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4=
+github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
 github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -18,8 +18,8 @@ github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQ
 github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
 github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
-github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
 github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
 github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -27,8 +27,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
-github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
+github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -38,8 +38,8 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
 github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -52,8 +52,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
 github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
-github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
@@ -62,8 +62,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
-github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -73,8 +73,8 @@ github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0/go.mod h1:oa2sA
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f h1:BSnJgAfHzEp7o8PYJ7YfwAVHhqu7BYUTggcn/LGlUWY=
-github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f/go.mod h1:UW/gxgQwSePTvL1KA8QEHsXeYHP4xkoXgbDdN781p34=
+github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa h1:hxMLFbj+F444JAS5nUQxTDZwUxwCRqg3WkNqhiDzXrM=
+github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@@ -85,12 +85,7 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
@@ -102,8 +97,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -111,10 +106,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
-golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
-golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
-golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
+golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
@@ -125,8 +120,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

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

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

+ 1 - 1
k8s/client/netclient.yaml

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

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

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

+ 17 - 17
logic/acls/common.go

@@ -12,7 +12,7 @@ import (
 var (
 	aclCacheMutex = &sync.RWMutex{}
 	aclCacheMap   = make(map[ContainerID]ACLContainer)
-	aclMutex      = &sync.RWMutex{}
+	AclMutex      = &sync.RWMutex{}
 )
 
 func fetchAclContainerFromCache(containerID ContainerID) (aclCont ACLContainer, ok bool) {
@@ -38,22 +38,22 @@ func DeleteAclFromCache(containerID ContainerID) {
 
 // ACL.Allow - allows access by ID in memory
 func (acl ACL) Allow(ID AclID) {
-	aclMutex.Lock()
-	defer aclMutex.Unlock()
+	AclMutex.Lock()
+	defer AclMutex.Unlock()
 	acl[ID] = Allowed
 }
 
 // ACL.DisallowNode - disallows access by ID in memory
 func (acl ACL) Disallow(ID AclID) {
-	aclMutex.Lock()
-	defer aclMutex.Unlock()
+	AclMutex.Lock()
+	defer AclMutex.Unlock()
 	acl[ID] = NotAllowed
 }
 
 // ACL.Remove - removes a node from a ACL in memory
 func (acl ACL) Remove(ID AclID) {
-	aclMutex.Lock()
-	defer aclMutex.Unlock()
+	AclMutex.Lock()
+	defer AclMutex.Unlock()
 	delete(acl, ID)
 }
 
@@ -64,24 +64,24 @@ func (acl ACL) Save(containerID ContainerID, ID AclID) (ACL, error) {
 
 // ACL.IsAllowed - sees if ID is allowed in referring ACL
 func (acl ACL) IsAllowed(ID AclID) (allowed bool) {
-	aclMutex.RLock()
+	AclMutex.RLock()
 	allowed = acl[ID] == Allowed
-	aclMutex.RUnlock()
+	AclMutex.RUnlock()
 	return
 }
 
 // ACLContainer.UpdateACL - saves the state of a ACL in the ACLContainer in memory
 func (aclContainer ACLContainer) UpdateACL(ID AclID, acl ACL) ACLContainer {
-	aclMutex.Lock()
-	defer aclMutex.Unlock()
+	AclMutex.Lock()
+	defer AclMutex.Unlock()
 	aclContainer[ID] = acl
 	return aclContainer
 }
 
 // ACLContainer.RemoveACL - removes the state of a ACL in the ACLContainer in memory
 func (aclContainer ACLContainer) RemoveACL(ID AclID) ACLContainer {
-	aclMutex.Lock()
-	defer aclMutex.Unlock()
+	AclMutex.Lock()
+	defer AclMutex.Unlock()
 	delete(aclContainer, ID)
 	return aclContainer
 }
@@ -127,8 +127,8 @@ func (aclContainer ACLContainer) Get(containerID ContainerID) (ACLContainer, err
 
 // fetchACLContainer - fetches all current rules in given ACL container
 func fetchACLContainer(containerID ContainerID) (ACLContainer, error) {
-	aclMutex.RLock()
-	defer aclMutex.RUnlock()
+	AclMutex.RLock()
+	defer AclMutex.RUnlock()
 	if servercfg.CacheEnabled() {
 		if aclContainer, ok := fetchAclContainerFromCache(containerID); ok {
 			return aclContainer, nil
@@ -171,8 +171,8 @@ func upsertACL(containerID ContainerID, ID AclID, acl ACL) (ACL, error) {
 // upsertACLContainer - Inserts or updates a network ACL given the json string of the ACL and the container ID
 // if nil, create it
 func upsertACLContainer(containerID ContainerID, aclContainer ACLContainer) (ACLContainer, error) {
-	aclMutex.Lock()
-	defer aclMutex.Unlock()
+	AclMutex.Lock()
+	defer AclMutex.Unlock()
 	if aclContainer == nil {
 		aclContainer = make(ACLContainer)
 	}

+ 7 - 1
logic/acls/nodeacls/modify.go

@@ -22,12 +22,14 @@ func CreateNodeACL(networkID NetworkID, nodeID NodeID, defaultVal byte) (acls.AC
 			return nil, err
 		}
 	}
+	acls.AclMutex.Lock()
 	var newNodeACL = make(acls.ACL)
 	for existingNodeID := range currentNetworkACL {
 		currentNetworkACL[existingNodeID][acls.AclID(nodeID)] = defaultVal // set the old nodes to default value for new node
 		newNodeACL[existingNodeID] = defaultVal                            // set the old nodes in new node ACL to default value
 	}
-	currentNetworkACL[acls.AclID(nodeID)] = newNodeACL                        // append the new node's ACL
+	currentNetworkACL[acls.AclID(nodeID)] = newNodeACL // append the new node's ACL
+	acls.AclMutex.Unlock()
 	retNetworkACL, err := currentNetworkACL.Save(acls.ContainerID(networkID)) // insert into db
 	if err != nil {
 		return nil, err
@@ -63,7 +65,9 @@ func UpdateNodeACL(networkID NetworkID, nodeID NodeID, acl acls.ACL) (acls.ACL,
 	if err != nil {
 		return nil, err
 	}
+	acls.AclMutex.Lock()
 	currentNetworkACL[acls.AclID(nodeID)] = acl
+	acls.AclMutex.Unlock()
 	return currentNetworkACL[acls.AclID(nodeID)].Save(acls.ContainerID(networkID), acls.AclID(nodeID))
 }
 
@@ -73,12 +77,14 @@ func RemoveNodeACL(networkID NetworkID, nodeID NodeID) (acls.ACLContainer, error
 	if err != nil {
 		return nil, err
 	}
+	acls.AclMutex.Lock()
 	for currentNodeID := range currentNetworkACL {
 		if NodeID(currentNodeID) != nodeID {
 			currentNetworkACL[currentNodeID].Remove(acls.AclID(nodeID))
 		}
 	}
 	delete(currentNetworkACL, acls.AclID(nodeID))
+	acls.AclMutex.Unlock()
 	return currentNetworkACL.Save(acls.ContainerID(networkID))
 }
 

+ 13 - 2
logic/acls/nodeacls/retrieve.go

@@ -13,7 +13,11 @@ func AreNodesAllowed(networkID NetworkID, node1, node2 NodeID) bool {
 	if err != nil {
 		return false
 	}
-	return currentNetworkACL[acls.AclID(node1)].IsAllowed(acls.AclID(node2)) && currentNetworkACL[acls.AclID(node2)].IsAllowed(acls.AclID(node1))
+	var allowed bool
+	acls.AclMutex.RLock()
+	allowed = currentNetworkACL[acls.AclID(node1)].IsAllowed(acls.AclID(node2)) && currentNetworkACL[acls.AclID(node2)].IsAllowed(acls.AclID(node1))
+	acls.AclMutex.RUnlock()
+	return allowed
 }
 
 // FetchNodeACL - fetches a specific node's ACL in a given network
@@ -22,10 +26,15 @@ func FetchNodeACL(networkID NetworkID, nodeID NodeID) (acls.ACL, error) {
 	if err != nil {
 		return nil, err
 	}
+	var acl acls.ACL
+	acls.AclMutex.RLock()
 	if currentNetworkACL[acls.AclID(nodeID)] == nil {
+		acls.AclMutex.RUnlock()
 		return nil, fmt.Errorf("no node ACL present for node %s", nodeID)
 	}
-	return currentNetworkACL[acls.AclID(nodeID)], nil
+	acl = currentNetworkACL[acls.AclID(nodeID)]
+	acls.AclMutex.RUnlock()
+	return acl, nil
 }
 
 // FetchNodeACLJson - fetches a node's acl in given network except returns the json string
@@ -34,6 +43,8 @@ func FetchNodeACLJson(networkID NetworkID, nodeID NodeID) (acls.ACLJson, error)
 	if err != nil {
 		return "", err
 	}
+	acls.AclMutex.RLock()
+	defer acls.AclMutex.RUnlock()
 	jsonData, err := json.Marshal(&currentNodeACL)
 	if err != nil {
 		return "", err

+ 19 - 0
logic/clients.go

@@ -4,7 +4,9 @@ import (
 	"errors"
 	"sort"
 
+	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
 )
 
 // functions defined here, handle client ACLs, should be set on ee
@@ -23,6 +25,23 @@ var (
 		return true
 	}
 	SetClientDefaultACLs = func(ec *models.ExtClient) error {
+		// allow all on CE
+		networkAcls := acls.ACLContainer{}
+		networkAcls, err := networkAcls.Get(acls.ContainerID(ec.Network))
+		if err != nil {
+			slog.Error("failed to get network acls", "error", err)
+			return err
+		}
+		networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{}
+		for objId := range networkAcls {
+			networkAcls[objId][acls.AclID(ec.ClientID)] = acls.Allowed
+			networkAcls[acls.AclID(ec.ClientID)][objId] = acls.Allowed
+		}
+		delete(networkAcls[acls.AclID(ec.ClientID)], acls.AclID(ec.ClientID))
+		if _, err = networkAcls.Save(acls.ContainerID(ec.Network)); err != nil {
+			slog.Error("failed to update network acls", "error", err)
+			return err
+		}
 		return nil
 	}
 	SetClientACLs = func(ec *models.ExtClient, newACLs map[string]struct{}) {

+ 4 - 0
logic/extpeers.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net"
 	"reflect"
+	"strings"
 	"sync"
 	"time"
 
@@ -276,6 +277,9 @@ func UpdateExtClient(old *models.ExtClient, update *models.CustomExtClient) mode
 	if update.DeniedACLs != nil && !reflect.DeepEqual(old.DeniedACLs, update.DeniedACLs) {
 		new.DeniedACLs = update.DeniedACLs
 	}
+	// replace any \r\n with \n in postup and postdown from HTTP request
+	new.PostUp = strings.Replace(update.PostUp, "\r\n", "\n", -1)
+	new.PostDown = strings.Replace(update.PostDown, "\r\n", "\n", -1)
 	return new
 }
 

+ 21 - 25
logic/gateway.go

@@ -7,17 +7,13 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
 )
 
-var (
-	// SetInternetGw - sets the node as internet gw based on flag bool
-	SetInternetGw = func(node *models.Node, flag bool) {
-	}
-	// IsInternetGw - checks if node is acting as internet gw
-	IsInternetGw = func(node models.Node) bool {
-		return false
-	}
-)
+// IsInternetGw - checks if node is acting as internet gw
+func IsInternetGw(node models.Node) bool {
+	return node.IsInternetGateway
+}
 
 // GetInternetGateways - gets all the nodes that are internet gateways
 func GetInternetGateways() ([]models.Node, error) {
@@ -27,13 +23,8 @@ func GetInternetGateways() ([]models.Node, error) {
 	}
 	igs := make([]models.Node, 0)
 	for _, node := range nodes {
-		if !node.IsEgressGateway {
-			continue
-		}
-		for _, ran := range node.EgressGatewayRanges {
-			if ran == "0.0.0.0/0" {
-				igs = append(igs, node)
-			}
+		if node.IsInternetGateway {
+			igs = append(igs, node)
 		}
 	}
 	return igs, nil
@@ -106,6 +97,9 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	if err != nil {
 		return models.Node{}, err
 	}
+	if gateway.Ranges == nil {
+		gateway.Ranges = make([]string, 0)
+	}
 	node.IsEgressGateway = true
 	node.EgressGatewayRanges = gateway.Ranges
 	node.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)
@@ -119,13 +113,7 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 
 // ValidateEgressGateway - validates the egress gateway model
 func ValidateEgressGateway(gateway models.EgressGatewayRequest) error {
-	var err error
-
-	empty := len(gateway.Ranges) == 0
-	if empty {
-		err = errors.New("IP Ranges Cannot Be Empty")
-	}
-	return err
+	return nil
 }
 
 // DeleteEgressGateway - deletes egress from node
@@ -170,11 +158,16 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 		return models.Node{}, err
 	}
 	node.IsIngressGateway = true
-	SetInternetGw(&node, ingress.IsInternetGateway)
+	if !servercfg.IsPro {
+		node.IsInternetGateway = ingress.IsInternetGateway
+	}
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressDNS = ingress.ExtclientDNS
 	node.SetLastModified()
+	if node.Metadata == "" {
+		node.Metadata = "This host can be used for remote access"
+	}
 	err = UpsertNode(&node)
 	if err != nil {
 		return models.Node{}, err
@@ -223,8 +216,11 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 	logger.Log(3, "deleting ingress gateway")
 	node.LastModified = time.Now()
 	node.IsIngressGateway = false
-	node.IsInternetGateway = false
+	if !servercfg.IsPro {
+		node.IsInternetGateway = false
+	}
 	node.IngressGatewayRange = ""
+	node.Metadata = ""
 	err = UpsertNode(&node)
 	if err != nil {
 		return models.Node{}, removedClients, err

+ 30 - 0
logic/nodes.go

@@ -218,6 +218,23 @@ func DeleteNode(node *models.Node, purge bool) error {
 		// unset all the relayed nodes
 		SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
 	}
+	if node.InternetGwID != "" {
+		inetNode, err := GetNodeByID(node.InternetGwID)
+		if err == nil {
+			clientNodeIDs := []string{}
+			for _, inetNodeClientID := range inetNode.InetNodeReq.InetNodeClientIDs {
+				if inetNodeClientID == node.ID.String() {
+					continue
+				}
+				clientNodeIDs = append(clientNodeIDs, inetNodeClientID)
+			}
+			inetNode.InetNodeReq.InetNodeClientIDs = clientNodeIDs
+			UpsertNode(&inetNode)
+		}
+	}
+	if node.IsInternetGateway {
+		UnsetInternetGw(node)
+	}
 
 	if !purge && !alreadyDeleted {
 		newnode := *node
@@ -598,3 +615,16 @@ func SortApiNodes(unsortedNodes []models.ApiNode) {
 		return unsortedNodes[i].ID < unsortedNodes[j].ID
 	})
 }
+
+func ValidateParams(nodeid, netid string) (models.Node, error) {
+	node, err := GetNodeByID(nodeid)
+	if err != nil {
+		slog.Error("error fetching node", "node", nodeid, "error", err.Error())
+		return node, fmt.Errorf("error fetching node during parameter validation: %v", err)
+	}
+	if node.Network != netid {
+		slog.Error("network url param does not match node id", "url nodeid", netid, "node", node.Network)
+		return node, fmt.Errorf("network url param does not match node network")
+	}
+	return node, nil
+}

+ 48 - 19
logic/peers.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"errors"
+	"fmt"
 	"net"
 	"net/netip"
 
@@ -28,6 +29,30 @@ var (
 	GetFailOverPeerIps = func(peer, node *models.Node) []net.IPNet {
 		return []net.IPNet{}
 	}
+	// CreateFailOver - creates failover in a network
+	CreateFailOver = func(node models.Node) error {
+		return nil
+	}
+
+	// SetDefaulGw
+	SetDefaultGw = func(node models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
+		return peerUpdate
+	}
+	SetDefaultGwForRelayedUpdate = func(relayed, relay models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
+		return peerUpdate
+	}
+	// UnsetInternetGw
+	UnsetInternetGw = func(node *models.Node) {
+		node.IsInternetGateway = false
+	}
+	// SetInternetGw
+	SetInternetGw = func(node *models.Node, req models.InetNodeReq) {
+		node.IsInternetGateway = true
+	}
+	// GetAllowedIpForInetNodeClient
+	GetAllowedIpForInetNodeClient = func(node, peer *models.Node) []net.IPNet {
+		return []net.IPNet{}
+	}
 )
 
 // GetPeerUpdateForHost - gets the consolidated peer update for the host from all networks
@@ -118,7 +143,10 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			}
 			continue
 		}
-
+		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
+		if !hostPeerUpdate.IsInternetGw {
+			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
+		}
 		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
 		for _, peer := range currentPeers {
 			peer := peer
@@ -160,6 +188,9 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					peerIndexMap[peerHost.PublicKey.String()] = len(hostPeerUpdate.Peers) - 1
 					continue
 				}
+				if node.IsRelayed && node.RelayedBy == peer.ID.String() {
+					hostPeerUpdate = SetDefaultGwForRelayedUpdate(node, peer, hostPeerUpdate)
+				}
 			}
 
 			uselocal := false
@@ -247,18 +278,8 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				logger.Log(1, "error retrieving external clients:", err.Error())
 			}
 		}
-		addedInetGwRanges := false
 		if node.IsEgressGateway && node.EgressGatewayRequest.NatEnabled == "yes" && len(node.EgressGatewayRequest.Ranges) > 0 {
 			hostPeerUpdate.FwUpdate.IsEgressGw = true
-			if IsInternetGw(node) {
-				hostPeerUpdate.FwUpdate.IsEgressGw = true
-				egressrange := []string{"0.0.0.0/0"}
-				if node.Address6.IP != nil {
-					egressrange = append(egressrange, "::/0")
-				}
-				node.EgressGatewayRequest.Ranges = append(node.EgressGatewayRequest.Ranges, egressrange...)
-				addedInetGwRanges = true
-			}
 			hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
 				EgressID: node.ID.String(),
 				Network:  node.PrimaryNetworkRange(),
@@ -270,21 +291,21 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			}
 
 		}
-		if IsInternetGw(node) && !addedInetGwRanges {
+		if IsInternetGw(node) {
 			hostPeerUpdate.FwUpdate.IsEgressGw = true
 			egressrange := []string{"0.0.0.0/0"}
 			if node.Address6.IP != nil {
 				egressrange = append(egressrange, "::/0")
 			}
-			hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
-				EgressID: node.ID.String(),
+			hostPeerUpdate.FwUpdate.EgressInfo[fmt.Sprintf("%s-%s", node.ID.String(), "inet")] = models.EgressInfo{
+				EgressID: fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
 				Network:  node.PrimaryAddressIPNet(),
 				EgressGwAddr: net.IPNet{
 					IP:   net.ParseIP(node.PrimaryAddress()),
 					Mask: getCIDRMaskFromAddr(node.PrimaryAddress()),
 				},
 				EgressGWCfg: models.EgressGatewayRequest{
-					NodeID:     node.ID.String(),
+					NodeID:     fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
 					NetID:      node.Network,
 					NatEnabled: "yes",
 					Ranges:     egressrange,
@@ -350,7 +371,17 @@ func GetPeerListenPort(host *models.Host) int {
 // GetAllowedIPs - calculates the wireguard allowedip field for a peer of a node based on the peer and node settings
 func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet {
 	var allowedips []net.IPNet
-	allowedips = getNodeAllowedIPs(peer, node)
+	if peer.IsInternetGateway && node.InternetGwID == peer.ID.String() {
+		allowedips = append(allowedips, GetAllowedIpForInetNodeClient(node, peer)...)
+		return allowedips
+	}
+	if node.IsRelayed && node.RelayedBy == peer.ID.String() {
+		allowedips = append(allowedips, GetAllowedIpsForRelayed(node, peer)...)
+		if peer.InternetGwID != "" {
+			return allowedips
+		}
+	}
+	allowedips = append(allowedips, getNodeAllowedIPs(peer, node)...)
 
 	// handle ingress gateway peers
 	if peer.IsIngressGateway {
@@ -362,9 +393,7 @@ func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet
 			allowedips = append(allowedips, extPeer.AllowedIPs...)
 		}
 	}
-	if node.IsRelayed && node.RelayedBy == peer.ID.String() {
-		allowedips = append(allowedips, GetAllowedIpsForRelayed(node, peer)...)
-	}
+
 	return allowedips
 }
 

+ 2 - 1
logic/relay.go

@@ -1,8 +1,9 @@
 package logic
 
 import (
-	"github.com/gravitl/netmaker/models"
 	"net"
+
+	"github.com/gravitl/netmaker/models"
 )
 
 var GetRelays = func() ([]models.Node, error) {

+ 1 - 1
main.go

@@ -28,7 +28,7 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var version = "v0.22.0"
+var version = "v0.23.0"
 
 // Start DB Connection and start API Request Handler
 func main() {

+ 125 - 0
migrate/migrate.go

@@ -2,6 +2,7 @@ package migrate
 
 import (
 	"encoding/json"
+	"fmt"
 	"log"
 
 	"golang.org/x/exp/slog"
@@ -9,6 +10,7 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 )
@@ -19,6 +21,7 @@ func Run() {
 	assignSuperAdmin()
 	updateHosts()
 	updateNodes()
+	updateAcls()
 }
 
 func assignSuperAdmin() {
@@ -167,3 +170,125 @@ func removeInterGw(egressRanges []string) ([]string, bool) {
 	}
 	return egressRanges, update
 }
+
+func updateAcls() {
+	// get all networks
+	networks, err := logic.GetNetworks()
+	if err != nil {
+		slog.Error("acls migration failed. error getting networks", "error", err)
+		return
+	}
+
+	// get current acls per network
+	for _, network := range networks {
+		var networkAcl acls.ACLContainer
+		networkAcl, err := networkAcl.Get(acls.ContainerID(network.NetID))
+		if err != nil {
+			if database.IsEmptyRecord(err) {
+				continue
+			}
+			slog.Error(fmt.Sprintf("error during acls migration. error getting acls for network: %s", network.NetID), "error", err)
+			continue
+		}
+		// convert old acls to new acls with clients
+		// TODO: optimise O(n^2) operation
+		clients, err := logic.GetNetworkExtClients(network.NetID)
+		if err != nil {
+			slog.Error(fmt.Sprintf("error during acls migration. error getting clients for network: %s", network.NetID), "error", err)
+			continue
+		}
+		clientsIdMap := make(map[string]struct{})
+		for _, client := range clients {
+			clientsIdMap[client.ClientID] = struct{}{}
+		}
+		nodeIdsMap := make(map[string]struct{})
+		for nodeId := range networkAcl {
+			nodeIdsMap[string(nodeId)] = struct{}{}
+		}
+		/*
+			initially, networkACL has only node acls so we add client acls to it
+			final shape:
+			{
+				"node1": {
+					"node2": 2,
+					"client1": 2,
+					"client2": 1,
+				},
+				"node2": {
+					"node1": 2,
+					"client1": 2,
+					"client2": 1,
+				},
+				"client1": {
+					"node1": 2,
+					"node2": 2,
+					"client2": 1,
+				},
+				"client2": {
+					"node1": 1,
+					"node2": 1,
+					"client1": 1,
+				},
+			}
+		*/
+		for _, client := range clients {
+			networkAcl[acls.AclID(client.ClientID)] = acls.ACL{}
+			// add client values to node acls and create client acls with node values
+			for id, nodeAcl := range networkAcl {
+				// skip if not a node
+				if _, ok := nodeIdsMap[string(id)]; !ok {
+					continue
+				}
+				if nodeAcl == nil {
+					slog.Warn("acls migration bad data: nil node acl", "node", id, "network", network.NetID)
+					continue
+				}
+				nodeAcl[acls.AclID(client.ClientID)] = acls.Allowed
+				networkAcl[acls.AclID(client.ClientID)][id] = acls.Allowed
+				if client.DeniedACLs == nil {
+					continue
+				} else if _, ok := client.DeniedACLs[string(id)]; ok {
+					nodeAcl[acls.AclID(client.ClientID)] = acls.NotAllowed
+					networkAcl[acls.AclID(client.ClientID)][id] = acls.NotAllowed
+				}
+			}
+			// add clients to client acls response
+			for _, c := range clients {
+				if c.ClientID == client.ClientID {
+					continue
+				}
+				networkAcl[acls.AclID(client.ClientID)][acls.AclID(c.ClientID)] = acls.Allowed
+				if client.DeniedACLs == nil {
+					continue
+				} else if _, ok := client.DeniedACLs[c.ClientID]; ok {
+					networkAcl[acls.AclID(client.ClientID)][acls.AclID(c.ClientID)] = acls.NotAllowed
+				}
+			}
+			// delete oneself from its own acl
+			delete(networkAcl[acls.AclID(client.ClientID)], acls.AclID(client.ClientID))
+		}
+
+		// remove non-existent client and node acls
+		for objId := range networkAcl {
+			if _, ok := nodeIdsMap[string(objId)]; ok {
+				continue
+			}
+			if _, ok := clientsIdMap[string(objId)]; ok {
+				continue
+			}
+			// remove all occurances of objId from all acls
+			for objId2 := range networkAcl {
+				delete(networkAcl[objId2], objId)
+			}
+			delete(networkAcl, objId)
+		}
+
+		// save new acls
+		slog.Info(fmt.Sprintf("(migration) saving new acls for network: %s", network.NetID), "networkAcl", networkAcl)
+		if _, err := networkAcl.Save(acls.ContainerID(network.NetID)); err != nil {
+			slog.Error(fmt.Sprintf("error during acls migration. error saving new acls for network: %s", network.NetID), "error", err)
+			continue
+		}
+		slog.Info(fmt.Sprintf("(migration) successfully saved new acls for network: %s", network.NetID))
+	}
+}

+ 17 - 12
models/api_node.go

@@ -11,9 +11,9 @@ import (
 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,ipv4"`
-	Address6                string   `json:"address6" validate:"omitempty,ipv6"`
-	LocalAddress            string   `json:"localaddress" validate:"omitempty,ipv4"`
+	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"`
 	ExpirationDateTime      int64    `json:"expdatetime"`
@@ -28,20 +28,22 @@ type ApiNode struct {
 	RelayedNodes            []string `json:"relaynodes" yaml:"relayedNodes"`
 	IsEgressGateway         bool     `json:"isegressgateway"`
 	IsIngressGateway        bool     `json:"isingressgateway"`
-	IsInternetGateway       bool     `json:"isinternetgateway" yaml:"isinternetgateway"`
 	EgressGatewayRanges     []string `json:"egressgatewayranges"`
 	EgressGatewayNatEnabled bool     `json:"egressgatewaynatenabled"`
 	DNSOn                   bool     `json:"dnson"`
 	IngressDns              string   `json:"ingressdns"`
 	Server                  string   `json:"server"`
-	InternetGateway         string   `json:"internetgateway"`
 	Connected               bool     `json:"connected"`
 	PendingDelete           bool     `json:"pendingdelete"`
+	Metadata                string   `json:"metadata" validate:"max=256"`
 	// == PRO ==
-	DefaultACL    string              `json:"defaultacl,omitempty" validate:"checkyesornoorunset"`
-	IsFailOver    bool                `json:"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"`
+	DefaultACL        string              `json:"defaultacl,omitempty" validate:"checkyesornoorunset"`
+	IsFailOver        bool                `json:"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"`
 }
 
 // ApiNode.ConvertToServerNode - converts an api node to a server node
@@ -71,6 +73,8 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.IsInternetGateway = a.IsInternetGateway
 	convertedNode.EgressGatewayRequest = currentNode.EgressGatewayRequest
 	convertedNode.EgressGatewayNatEnabled = currentNode.EgressGatewayNatEnabled
+	convertedNode.InternetGwID = currentNode.InternetGwID
+	convertedNode.InetNodeReq = currentNode.InetNodeReq
 	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.DefaultACL = a.DefaultACL
 	convertedNode.OwnerID = currentNode.OwnerID
@@ -104,6 +108,7 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.LastCheckIn = time.Unix(a.LastCheckIn, 0)
 	convertedNode.LastPeerUpdate = time.Unix(a.LastPeerUpdate, 0)
 	convertedNode.ExpirationDateTime = time.Unix(a.ExpirationDateTime, 0)
+	convertedNode.Metadata = a.Metadata
 	return &convertedNode
 }
 
@@ -148,16 +153,16 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.Server = nm.Server
-	if isEmptyAddr(apiNode.InternetGateway) {
-		apiNode.InternetGateway = ""
-	}
 	apiNode.Connected = nm.Connected
 	apiNode.PendingDelete = nm.PendingDelete
 	apiNode.DefaultACL = nm.DefaultACL
 	apiNode.IsInternetGateway = nm.IsInternetGateway
+	apiNode.InternetGwID = nm.InternetGwID
+	apiNode.InetNodeReq = nm.InetNodeReq
 	apiNode.IsFailOver = nm.IsFailOver
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailedOverBy = nm.FailedOverBy
+	apiNode.Metadata = nm.Metadata
 	return &apiNode
 }
 

+ 4 - 0
models/extclient.go

@@ -18,6 +18,8 @@ type ExtClient struct {
 	OwnerID                string              `json:"ownerid" bson:"ownerid"`
 	DeniedACLs             map[string]struct{} `json:"deniednodeacls" bson:"acls,omitempty"`
 	RemoteAccessClientID   string              `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine
+	PostUp                 string              `json:"postup" bson:"postup"`
+	PostDown               string              `json:"postdown" bson:"postdown"`
 }
 
 // CustomExtClient - struct for CustomExtClient params
@@ -29,4 +31,6 @@ type CustomExtClient struct {
 	Enabled              bool                `json:"enabled,omitempty"`
 	DeniedACLs           map[string]struct{} `json:"deniednodeacls" bson:"acls,omitempty"`
 	RemoteAccessClientID string              `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine
+	PostUp               string              `json:"postup" bson:"postup" validate:"max=1024"`
+	PostDown             string              `json:"postdown" bson:"postdown" validate:"max=1024"`
 }

+ 3 - 0
models/mqtt.go

@@ -9,6 +9,9 @@ import (
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
 	Host            Host                 `json:"host" bson:"host" yaml:"host"`
+	ChangeDefaultGw bool                 `json:"change_default_gw"`
+	DefaultGwIp     net.IP               `json:"default_gw_ip"`
+	IsInternetGw    bool                 `json:"is_inet_gw"`
 	NodeAddrs       []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
 	Server          string               `json:"server" bson:"server" yaml:"server"`
 	ServerVersion   string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`

+ 9 - 6
models/node.go

@@ -66,7 +66,6 @@ type CommonNode struct {
 	IsEgressGateway     bool      `json:"isegressgateway" yaml:"isegressgateway"`
 	EgressGatewayRanges []string  `json:"egressgatewayranges" bson:"egressgatewayranges" yaml:"egressgatewayranges"`
 	IsIngressGateway    bool      `json:"isingressgateway" yaml:"isingressgateway"`
-	IsInternetGateway   bool      `json:"isinternetgateway" yaml:"isinternetgateway"`
 	IsRelayed           bool      `json:"isrelayed" bson:"isrelayed" yaml:"isrelayed"`
 	RelayedBy           string    `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
 	IsRelay             bool      `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
@@ -87,12 +86,16 @@ type Node struct {
 	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest" bson:"egressgatewayrequest" yaml:"egressgatewayrequest"`
 	IngressGatewayRange     string               `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"`
 	IngressGatewayRange6    string               `json:"ingressgatewayrange6" bson:"ingressgatewayrange6" yaml:"ingressgatewayrange6"`
+	Metadata                string               `json:"metadata"`
 	// == PRO ==
-	DefaultACL    string              `json:"defaultacl,omitempty" bson:"defaultacl,omitempty" yaml:"defaultacl,omitempty" validate:"checkyesornoorunset"`
-	OwnerID       string              `json:"ownerid,omitempty" bson:"ownerid,omitempty" yaml:"ownerid,omitempty"`
-	IsFailOver    bool                `json:"is_fail_over" yaml:"is_fail_over"`
-	FailOverPeers map[string]struct{} `json:"fail_over_peers" yaml:"fail_over_peers"`
-	FailedOverBy  uuid.UUID           `json:"failed_over_by" yaml:"failed_over_by"`
+	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"`
 }
 
 // LegacyNode - legacy struct for node model

+ 13 - 0
models/structs.go

@@ -1,6 +1,7 @@
 package models
 
 import (
+	"net"
 	"strings"
 	"time"
 
@@ -71,6 +72,7 @@ type UserRemoteGws struct {
 	IsInternetGateway bool      `json:"is_internet_gateway"`
 	GwClient          ExtClient `json:"gw_client"`
 	GwPeerPublicKey   string    `json:"gw_peer_public_key"`
+	Metadata          string    `json:"metadata"`
 }
 
 // UserRemoteGwsReq - struct to hold user remote acccess gws req
@@ -195,6 +197,11 @@ type IngressRequest struct {
 	IsInternetGateway bool   `json:"is_internet_gw"`
 }
 
+// InetNodeReq - exit node request struct
+type InetNodeReq struct {
+	InetNodeClientIDs []string `json:"inet_node_client_ids"`
+}
+
 // ServerUpdateData - contains data to configure server
 // and if it should set peers
 type ServerUpdateData struct {
@@ -233,6 +240,12 @@ type HostPull struct {
 	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty"  yaml:"host_network_info,omitempty"`
 	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
 	FwUpdate        FwUpdate              `json:"fw_update"`
+	ChangeDefaultGw bool                  `json:"change_default_gw"`
+	DefaultGwIp     net.IP                `json:"default_gw_ip"`
+	IsInternetGw    bool                  `json:"is_inet_gw"`
+}
+
+type DefaultGwInfo struct {
 }
 
 // NodeGet - struct for a single node get response

+ 29 - 373
mq/emqx.go

@@ -1,386 +1,42 @@
 package mq
 
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"strings"
-	"sync"
+import "github.com/gravitl/netmaker/servercfg"
 
-	"github.com/gravitl/netmaker/servercfg"
-)
+var emqx Emqx
 
-const already_exists = "ALREADY_EXISTS"
-
-type (
-	emqxUser struct {
-		UserID   string `json:"user_id"`
-		Password string `json:"password"`
-		Admin    bool   `json:"is_superuser"`
-	}
-
-	emqxLogin struct {
-		Username string `json:"username"`
-		Password string `json:"password"`
-	}
-
-	emqxLoginResponse struct {
-		License struct {
-			Edition string `json:"edition"`
-		} `json:"license"`
-		Token   string `json:"token"`
-		Version string `json:"version"`
-	}
-
-	aclRule struct {
-		Topic      string `json:"topic"`
-		Permission string `json:"permission"`
-		Action     string `json:"action"`
-	}
-
-	aclObject struct {
-		Rules    []aclRule `json:"rules"`
-		Username string    `json:"username,omitempty"`
-	}
-)
-
-func getEmqxAuthToken() (string, error) {
-	payload, err := json.Marshal(&emqxLogin{
-		Username: servercfg.GetMqUserName(),
-		Password: servercfg.GetMqPassword(),
-	})
-	if err != nil {
-		return "", err
-	}
-	resp, err := http.Post(servercfg.GetEmqxRestEndpoint()+"/api/v5/login", "application/json", bytes.NewReader(payload))
-	if err != nil {
-		return "", err
-	}
-	msg, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return "", err
-	}
-	if resp.StatusCode != http.StatusOK {
-		return "", fmt.Errorf("error during EMQX login %v", string(msg))
-	}
-	var loginResp emqxLoginResponse
-	if err := json.Unmarshal(msg, &loginResp); err != nil {
-		return "", err
-	}
-	return loginResp.Token, nil
-}
-
-// CreateEmqxUser - creates an EMQX user
-func CreateEmqxUser(username, password string, admin bool) error {
-	token, err := getEmqxAuthToken()
-	if err != nil {
-		return err
-	}
-	payload, err := json.Marshal(&emqxUser{
-		UserID:   username,
-		Password: password,
-		Admin:    admin,
-	})
-	if err != nil {
-		return err
-	}
-	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication/password_based:built_in_database/users", bytes.NewReader(payload))
-	if err != nil {
-		return err
-	}
-	req.Header.Add("content-type", "application/json")
-	req.Header.Add("authorization", "Bearer "+token)
-	resp, err := (&http.Client{}).Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode >= 300 {
-		msg, err := io.ReadAll(resp.Body)
-		if err != nil {
-			return err
-		}
-		if !strings.Contains(string(msg), already_exists) {
-			return fmt.Errorf("error creating EMQX user %v", string(msg))
-		}
-	}
-	return nil
-}
-
-// DeleteEmqxUser - deletes an EMQX user
-func DeleteEmqxUser(username string) error {
-	token, err := getEmqxAuthToken()
-	if err != nil {
-		return err
-	}
-	req, err := http.NewRequest(http.MethodDelete, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication/password_based:built_in_database/users/"+username, nil)
-	if err != nil {
-		return err
-	}
-	req.Header.Add("authorization", "Bearer "+token)
-	resp, err := (&http.Client{}).Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode >= 300 {
-		msg, err := io.ReadAll(resp.Body)
-		if err != nil {
-			return err
-		}
-		return fmt.Errorf("error deleting EMQX user %v", string(msg))
-	}
-	return nil
-}
-
-// CreateEmqxDefaultAuthenticator - creates a default authenticator based on password and using EMQX's built in database as storage
-func CreateEmqxDefaultAuthenticator() error {
-	token, err := getEmqxAuthToken()
-	if err != nil {
-		return err
-	}
-	payload, err := json.Marshal(&struct {
-		Mechanism  string `json:"mechanism"`
-		Backend    string `json:"backend"`
-		UserIDType string `json:"user_id_type"`
-	}{Mechanism: "password_based", Backend: "built_in_database", UserIDType: "username"})
-	if err != nil {
-		return err
-	}
-	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication", bytes.NewReader(payload))
-	if err != nil {
-		return err
-	}
-	req.Header.Add("content-type", "application/json")
-	req.Header.Add("authorization", "Bearer "+token)
-	resp, err := (&http.Client{}).Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != http.StatusOK {
-		msg, err := io.ReadAll(resp.Body)
-		if err != nil {
-			return err
-		}
-		return fmt.Errorf("error creating default EMQX authenticator %v", string(msg))
-	}
-	return nil
+type Emqx interface {
+	GetType() servercfg.Emqxdeploy
+	CreateEmqxUser(username, password string) error
+	CreateEmqxUserforServer() error
+	CreateEmqxDefaultAuthenticator() error
+	CreateEmqxDefaultAuthorizer() error
+	CreateDefaultDenyRule() error
+	CreateHostACL(hostID, serverName string) error
+	AppendNodeUpdateACL(hostID, nodeNetwork, nodeID, serverName string) error
+	GetUserACL(username string) (*aclObject, error)
+	DeleteEmqxUser(username string) error
 }
 
-// CreateEmqxDefaultAuthorizer - creates a default ACL authorization mechanism based on the built in database
-func CreateEmqxDefaultAuthorizer() error {
-	token, err := getEmqxAuthToken()
-	if err != nil {
-		return err
-	}
-	payload, err := json.Marshal(&struct {
-		Enable bool   `json:"enable"`
-		Type   string `json:"type"`
-	}{Enable: true, Type: "built_in_database"})
-	if err != nil {
-		return err
-	}
-	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources", bytes.NewReader(payload))
-	if err != nil {
-		return err
+func init() {
+	if servercfg.GetBrokerType() != servercfg.EmqxBrokerType {
+		return
 	}
-	req.Header.Add("content-type", "application/json")
-	req.Header.Add("authorization", "Bearer "+token)
-	resp, err := (&http.Client{}).Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != http.StatusNoContent {
-		msg, err := io.ReadAll(resp.Body)
-		if err != nil {
-			return err
+	if servercfg.GetEmqxDeployType() == servercfg.EmqxCloudDeploy {
+		emqx = &EmqxCloud{
+			URL:       servercfg.GetEmqxRestEndpoint(),
+			AppID:     servercfg.GetEmqxAppID(),
+			AppSecret: servercfg.GetEmqxAppSecret(),
 		}
-		return fmt.Errorf("error creating default EMQX ACL authorization mechanism %v", string(msg))
-	}
-	return nil
-}
-
-// GetUserACL - returns ACL rules by username
-func GetUserACL(username string) (*aclObject, error) {
-	token, err := getEmqxAuthToken()
-	if err != nil {
-		return nil, err
-	}
-	req, err := http.NewRequest(http.MethodGet, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/username/"+username, nil)
-	if err != nil {
-		return nil, err
-	}
-	req.Header.Add("content-type", "application/json")
-	req.Header.Add("authorization", "Bearer "+token)
-	resp, err := (&http.Client{}).Do(req)
-	if err != nil {
-		return nil, err
-	}
-	defer resp.Body.Close()
-	response, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, err
-	}
-	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("error fetching ACL rules %v", string(response))
-	}
-	body := new(aclObject)
-	if err := json.Unmarshal(response, body); err != nil {
-		return nil, err
-	}
-	return body, nil
-}
-
-// CreateDefaultDenyRule - creates a rule to deny access to all topics for all users by default
-// to allow user access to topics use the `mq.CreateUserAccessRule` function
-func CreateDefaultDenyRule() error {
-	token, err := getEmqxAuthToken()
-	if err != nil {
-		return err
-	}
-	payload, err := json.Marshal(&aclObject{Rules: []aclRule{{Topic: "#", Permission: "deny", Action: "all"}}})
-	if err != nil {
-		return err
-	}
-	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/all", bytes.NewReader(payload))
-	if err != nil {
-		return err
-	}
-	req.Header.Add("content-type", "application/json")
-	req.Header.Add("authorization", "Bearer "+token)
-	resp, err := (&http.Client{}).Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != http.StatusNoContent {
-		msg, err := io.ReadAll(resp.Body)
-		if err != nil {
-			return err
-		}
-		return fmt.Errorf("error creating default ACL rules %v", string(msg))
-	}
-	return nil
-}
-
-// CreateHostACL - create host ACL rules
-func CreateHostACL(hostID, serverName string) error {
-	token, err := getEmqxAuthToken()
-	if err != nil {
-		return err
-	}
-	payload, err := json.Marshal(&aclObject{
-		Username: hostID,
-		Rules: []aclRule{
-			{
-				Topic:      fmt.Sprintf("peers/host/%s/%s", hostID, serverName),
-				Permission: "allow",
-				Action:     "all",
-			},
-			{
-				Topic:      fmt.Sprintf("host/update/%s/%s", hostID, serverName),
-				Permission: "allow",
-				Action:     "all",
-			},
-			{
-				Topic:      fmt.Sprintf("host/serverupdate/%s/%s", serverName, hostID),
-				Permission: "allow",
-				Action:     "all",
-			},
-		},
-	})
-	if err != nil {
-		return err
-	}
-	req, err := http.NewRequest(http.MethodPut, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/username/"+hostID, bytes.NewReader(payload))
-	if err != nil {
-		return err
-	}
-	req.Header.Add("content-type", "application/json")
-	req.Header.Add("authorization", "Bearer "+token)
-	resp, err := (&http.Client{}).Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != http.StatusNoContent {
-		msg, err := io.ReadAll(resp.Body)
-		if err != nil {
-			return err
+	} else {
+		emqx = &EmqxOnPrem{
+			URL:      servercfg.GetEmqxRestEndpoint(),
+			UserName: servercfg.GetMqUserName(),
+			Password: servercfg.GetMqPassword(),
 		}
-		return fmt.Errorf("error adding ACL Rules for user %s Error: %v", hostID, string(msg))
 	}
-	return nil
 }
 
-// a lock required for preventing simultaneous updates to the same ACL object leading to overwriting each other
-// might occur when multiple nodes belonging to the same host are created at the same time
-var nodeAclMux sync.Mutex
-
-// AppendNodeUpdateACL - adds ACL rule for subscribing to node updates for a node ID
-func AppendNodeUpdateACL(hostID, nodeNetwork, nodeID, serverName string) error {
-	nodeAclMux.Lock()
-	defer nodeAclMux.Unlock()
-	token, err := getEmqxAuthToken()
-	if err != nil {
-		return err
-	}
-	aclObject, err := GetUserACL(hostID)
-	if err != nil {
-		return err
-	}
-	aclObject.Rules = append(aclObject.Rules, []aclRule{
-		{
-			Topic:      fmt.Sprintf("node/update/%s/%s", nodeNetwork, nodeID),
-			Permission: "allow",
-			Action:     "subscribe",
-		},
-		{
-			Topic:      fmt.Sprintf("ping/%s/%s", serverName, nodeID),
-			Permission: "allow",
-			Action:     "all",
-		},
-		{
-			Topic:      fmt.Sprintf("update/%s/%s", serverName, nodeID),
-			Permission: "allow",
-			Action:     "all",
-		},
-		{
-			Topic:      fmt.Sprintf("signal/%s/%s", serverName, nodeID),
-			Permission: "allow",
-			Action:     "all",
-		},
-		{
-			Topic:      fmt.Sprintf("metrics/%s/%s", serverName, nodeID),
-			Permission: "allow",
-			Action:     "all",
-		},
-	}...)
-	payload, err := json.Marshal(aclObject)
-	if err != nil {
-		return err
-	}
-	req, err := http.NewRequest(http.MethodPut, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/username/"+hostID, bytes.NewReader(payload))
-	if err != nil {
-		return err
-	}
-	req.Header.Add("content-type", "application/json")
-	req.Header.Add("authorization", "Bearer "+token)
-	resp, err := (&http.Client{}).Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != http.StatusNoContent {
-		msg, err := io.ReadAll(resp.Body)
-		if err != nil {
-			return err
-		}
-		return fmt.Errorf("error adding ACL Rules for user %s Error: %v", hostID, string(msg))
-	}
-	return nil
+// GetEmqxHandler - gets emqx handler
+func GetEmqxHandler() Emqx {
+	return emqx
 }

+ 264 - 0
mq/emqx_cloud.go

@@ -0,0 +1,264 @@
+package mq
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+type EmqxCloud struct {
+	URL       string
+	AppID     string
+	AppSecret string
+}
+
+type userCreateReq struct {
+	UserName string `json:"username"`
+	Password string `json:"password"`
+}
+
+type cloudAcl struct {
+	UserName string `json:"username"`
+	Topic    string `json:"topic"`
+	Action   string `json:"action"`
+	Access   string `json:"access"`
+}
+
+func (e *EmqxCloud) GetType() servercfg.Emqxdeploy { return servercfg.EmqxCloudDeploy }
+
+func (e *EmqxCloud) CreateEmqxUser(username, pass string) error {
+
+	payload := userCreateReq{
+		UserName: username,
+		Password: pass,
+	}
+	data, _ := json.Marshal(payload)
+	client := &http.Client{}
+	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/auth_username", e.URL), strings.NewReader(string(data)))
+	if err != nil {
+		return err
+	}
+	req.SetBasicAuth(e.AppID, e.AppSecret)
+	req.Header.Add("Content-Type", "application/json")
+
+	res, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer res.Body.Close()
+
+	body, err := io.ReadAll(res.Body)
+	if err != nil {
+		return err
+	}
+	if res.StatusCode != http.StatusOK {
+		return errors.New("request failed " + string(body))
+	}
+	return nil
+}
+
+func (e *EmqxCloud) CreateEmqxUserforServer() error {
+	payload := userCreateReq{
+		UserName: servercfg.GetMqUserName(),
+		Password: servercfg.GetMqPassword(),
+	}
+	data, _ := json.Marshal(payload)
+	client := &http.Client{}
+	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/auth_username", e.URL), strings.NewReader(string(data)))
+	if err != nil {
+		return err
+	}
+	req.SetBasicAuth(e.AppID, e.AppSecret)
+	req.Header.Add("Content-Type", "application/json")
+
+	res, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer res.Body.Close()
+
+	body, err := io.ReadAll(res.Body)
+	if err != nil {
+		return err
+	}
+	if res.StatusCode != http.StatusOK {
+		return errors.New("request failed " + string(body))
+	}
+	// add acls
+	acls := []cloudAcl{
+		{
+			UserName: servercfg.GetMqUserName(),
+			Topic:    fmt.Sprintf("update/%s/#", servercfg.GetServer()),
+			Access:   "allow",
+			Action:   "sub",
+		},
+		{
+			UserName: servercfg.GetMqUserName(),
+			Topic:    fmt.Sprintf("host/serverupdate/%s/#", servercfg.GetServer()),
+			Access:   "allow",
+			Action:   "sub",
+		},
+		{
+			UserName: servercfg.GetMqUserName(),
+			Topic:    fmt.Sprintf("signal/%s/#", servercfg.GetServer()),
+			Access:   "allow",
+			Action:   "sub",
+		},
+		{
+			UserName: servercfg.GetMqUserName(),
+			Topic:    fmt.Sprintf("metrics/%s/#", servercfg.GetServer()),
+			Access:   "allow",
+			Action:   "sub",
+		},
+		{
+			UserName: servercfg.GetMqUserName(),
+			Topic:    "peers/host/#",
+			Access:   "allow",
+			Action:   "pub",
+		},
+		{
+			UserName: servercfg.GetMqUserName(),
+			Topic:    "node/update/#",
+			Access:   "allow",
+			Action:   "pub",
+		},
+		{
+
+			UserName: servercfg.GetMqUserName(),
+			Topic:    "host/update/#",
+			Access:   "allow",
+			Action:   "pub",
+		},
+	}
+
+	return e.createacls(acls)
+}
+
+func (e *EmqxCloud) CreateEmqxDefaultAuthenticator() error { return nil } // ignore
+
+func (e *EmqxCloud) CreateEmqxDefaultAuthorizer() error { return nil } // ignore
+
+func (e *EmqxCloud) CreateDefaultDenyRule() error {
+	return nil
+}
+
+func (e *EmqxCloud) createacls(acls []cloudAcl) error {
+	payload, err := json.Marshal(acls)
+	if err != nil {
+		return err
+	}
+	client := &http.Client{}
+	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/acl", e.URL), strings.NewReader(string(payload)))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("Content-Type", "application/json")
+	req.SetBasicAuth(e.AppID, e.AppSecret)
+	res, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer res.Body.Close()
+
+	body, err := io.ReadAll(res.Body)
+	if err != nil {
+		return err
+	}
+	if res.StatusCode != http.StatusOK {
+		return errors.New("request failed " + string(body))
+	}
+	return nil
+}
+
+func (e *EmqxCloud) CreateHostACL(hostID, serverName string) error {
+	acls := []cloudAcl{
+		{
+			UserName: hostID,
+			Topic:    fmt.Sprintf("peers/host/%s/%s", hostID, serverName),
+			Access:   "allow",
+			Action:   "sub",
+		},
+		{
+			UserName: hostID,
+			Topic:    fmt.Sprintf("host/update/%s/%s", hostID, serverName),
+			Access:   "allow",
+			Action:   "sub",
+		},
+		{
+			UserName: hostID,
+			Topic:    fmt.Sprintf("host/serverupdate/%s/%s", serverName, hostID),
+			Access:   "allow",
+			Action:   "pub",
+		},
+	}
+
+	return e.createacls(acls)
+}
+
+func (e *EmqxCloud) AppendNodeUpdateACL(hostID, nodeNetwork, nodeID, serverName string) error {
+	acls := []cloudAcl{
+		{
+			UserName: hostID,
+			Topic:    fmt.Sprintf("node/update/%s/%s", nodeNetwork, nodeID),
+			Access:   "allow",
+			Action:   "sub",
+		},
+		{
+			UserName: hostID,
+			Topic:    fmt.Sprintf("ping/%s/%s", serverName, nodeID),
+			Access:   "allow",
+			Action:   "pubsub",
+		},
+		{
+			UserName: hostID,
+			Topic:    fmt.Sprintf("update/%s/%s", serverName, nodeID),
+			Access:   "allow",
+			Action:   "pubsub",
+		},
+		{
+			UserName: hostID,
+			Topic:    fmt.Sprintf("signal/%s/%s", serverName, nodeID),
+			Access:   "allow",
+			Action:   "pubsub",
+		},
+		{
+			UserName: hostID,
+			Topic:    fmt.Sprintf("metrics/%s/%s", serverName, nodeID),
+			Access:   "allow",
+			Action:   "pubsub",
+		},
+	}
+
+	return e.createacls(acls)
+}
+
+func (e *EmqxCloud) GetUserACL(username string) (*aclObject, error) { return nil, nil } // ununsed on cloud since it doesn't overwrite acls list
+
+func (e *EmqxCloud) DeleteEmqxUser(username string) error {
+
+	client := &http.Client{}
+	req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/auth_username/%s", e.URL, username), nil)
+	if err != nil {
+		return err
+	}
+	req.SetBasicAuth(e.AppID, e.AppSecret)
+	res, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer res.Body.Close()
+
+	body, err := io.ReadAll(res.Body)
+	if err != nil {
+		return err
+	}
+	if res.StatusCode != http.StatusOK {
+		return errors.New("request failed " + string(body))
+	}
+	return nil
+}

+ 428 - 0
mq/emqx_on_prem.go

@@ -0,0 +1,428 @@
+package mq
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"sync"
+
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+type EmqxOnPrem struct {
+	URL      string
+	UserName string
+	Password string
+}
+
+const already_exists = "ALREADY_EXISTS"
+
+type (
+	emqxUser struct {
+		UserID   string `json:"user_id"`
+		Password string `json:"password"`
+		Admin    bool   `json:"is_superuser"`
+	}
+
+	emqxLogin struct {
+		Username string `json:"username"`
+		Password string `json:"password"`
+	}
+
+	emqxLoginResponse struct {
+		License struct {
+			Edition string `json:"edition"`
+		} `json:"license"`
+		Token   string `json:"token"`
+		Version string `json:"version"`
+	}
+
+	aclRule struct {
+		Topic      string `json:"topic"`
+		Permission string `json:"permission"`
+		Action     string `json:"action"`
+	}
+
+	aclObject struct {
+		Rules    []aclRule `json:"rules"`
+		Username string    `json:"username,omitempty"`
+	}
+)
+
+func getEmqxAuthToken() (string, error) {
+	payload, err := json.Marshal(&emqxLogin{
+		Username: servercfg.GetMqUserName(),
+		Password: servercfg.GetMqPassword(),
+	})
+	if err != nil {
+		return "", err
+	}
+	resp, err := http.Post(servercfg.GetEmqxRestEndpoint()+"/api/v5/login", "application/json", bytes.NewReader(payload))
+	if err != nil {
+		return "", err
+	}
+	msg, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return "", fmt.Errorf("error during EMQX login %v", string(msg))
+	}
+	var loginResp emqxLoginResponse
+	if err := json.Unmarshal(msg, &loginResp); err != nil {
+		return "", err
+	}
+	return loginResp.Token, nil
+}
+
+func (e *EmqxOnPrem) GetType() servercfg.Emqxdeploy { return servercfg.EmqxOnPremDeploy }
+
+// CreateEmqxUser - creates an EMQX user
+func (e *EmqxOnPrem) CreateEmqxUser(username, password string) error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	payload, err := json.Marshal(&emqxUser{
+		UserID:   username,
+		Password: password,
+	})
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication/password_based:built_in_database/users", bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode >= 300 {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		if !strings.Contains(string(msg), already_exists) {
+			return fmt.Errorf("error creating EMQX user %v", string(msg))
+		}
+	}
+	return nil
+}
+func (e *EmqxOnPrem) CreateEmqxUserforServer() error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	payload, err := json.Marshal(&emqxUser{
+		UserID:   servercfg.GetMqUserName(),
+		Password: servercfg.GetMqPassword(),
+		Admin:    true,
+	})
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication/password_based:built_in_database/users", bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode >= 300 {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		if !strings.Contains(string(msg), already_exists) {
+			return fmt.Errorf("error creating EMQX user %v", string(msg))
+		}
+	}
+	return nil
+}
+
+// DeleteEmqxUser - deletes an EMQX user
+func (e *EmqxOnPrem) DeleteEmqxUser(username string) error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodDelete, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication/password_based:built_in_database/users/"+username, nil)
+	if err != nil {
+		return err
+	}
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode >= 300 {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error deleting EMQX user %v", string(msg))
+	}
+	return nil
+}
+
+// CreateEmqxDefaultAuthenticator - creates a default authenticator based on password and using EMQX's built in database as storage
+func (e *EmqxOnPrem) CreateEmqxDefaultAuthenticator() error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	payload, err := json.Marshal(&struct {
+		Mechanism  string `json:"mechanism"`
+		Backend    string `json:"backend"`
+		UserIDType string `json:"user_id_type"`
+	}{Mechanism: "password_based", Backend: "built_in_database", UserIDType: "username"})
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication", bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error creating default EMQX authenticator %v", string(msg))
+	}
+	return nil
+}
+
+// CreateEmqxDefaultAuthorizer - creates a default ACL authorization mechanism based on the built in database
+func (e *EmqxOnPrem) CreateEmqxDefaultAuthorizer() error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	payload, err := json.Marshal(&struct {
+		Enable bool   `json:"enable"`
+		Type   string `json:"type"`
+	}{Enable: true, Type: "built_in_database"})
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources", bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusNoContent {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error creating default EMQX ACL authorization mechanism %v", string(msg))
+	}
+	return nil
+}
+
+// GetUserACL - returns ACL rules by username
+func (e *EmqxOnPrem) GetUserACL(username string) (*aclObject, error) {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return nil, err
+	}
+	req, err := http.NewRequest(http.MethodGet, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/username/"+username, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	response, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("error fetching ACL rules %v", string(response))
+	}
+	body := new(aclObject)
+	if err := json.Unmarshal(response, body); err != nil {
+		return nil, err
+	}
+	return body, nil
+}
+
+// CreateDefaultDenyRule - creates a rule to deny access to all topics for all users by default
+// to allow user access to topics use the `mq.CreateUserAccessRule` function
+func (e *EmqxOnPrem) CreateDefaultDenyRule() error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	payload, err := json.Marshal(&aclObject{Rules: []aclRule{{Topic: "#", Permission: "deny", Action: "all"}}})
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/all", bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusNoContent {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error creating default ACL rules %v", string(msg))
+	}
+	return nil
+}
+
+// CreateHostACL - create host ACL rules
+func (e *EmqxOnPrem) CreateHostACL(hostID, serverName string) error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	payload, err := json.Marshal(&aclObject{
+		Username: hostID,
+		Rules: []aclRule{
+			{
+				Topic:      fmt.Sprintf("peers/host/%s/%s", hostID, serverName),
+				Permission: "allow",
+				Action:     "all",
+			},
+			{
+				Topic:      fmt.Sprintf("host/update/%s/%s", hostID, serverName),
+				Permission: "allow",
+				Action:     "all",
+			},
+			{
+				Topic:      fmt.Sprintf("host/serverupdate/%s/%s", serverName, hostID),
+				Permission: "allow",
+				Action:     "all",
+			},
+		},
+	})
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPut, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/username/"+hostID, bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusNoContent {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error adding ACL Rules for user %s Error: %v", hostID, string(msg))
+	}
+	return nil
+}
+
+// a lock required for preventing simultaneous updates to the same ACL object leading to overwriting each other
+// might occur when multiple nodes belonging to the same host are created at the same time
+var nodeAclMux sync.Mutex
+
+// AppendNodeUpdateACL - adds ACL rule for subscribing to node updates for a node ID
+func (e *EmqxOnPrem) AppendNodeUpdateACL(hostID, nodeNetwork, nodeID, serverName string) error {
+	nodeAclMux.Lock()
+	defer nodeAclMux.Unlock()
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	aclObject, err := emqx.GetUserACL(hostID)
+	if err != nil {
+		return err
+	}
+	aclObject.Rules = append(aclObject.Rules, []aclRule{
+		{
+			Topic:      fmt.Sprintf("node/update/%s/%s", nodeNetwork, nodeID),
+			Permission: "allow",
+			Action:     "subscribe",
+		},
+		{
+			Topic:      fmt.Sprintf("ping/%s/%s", serverName, nodeID),
+			Permission: "allow",
+			Action:     "all",
+		},
+		{
+			Topic:      fmt.Sprintf("update/%s/%s", serverName, nodeID),
+			Permission: "allow",
+			Action:     "all",
+		},
+		{
+			Topic:      fmt.Sprintf("signal/%s/%s", serverName, nodeID),
+			Permission: "allow",
+			Action:     "all",
+		},
+		{
+			Topic:      fmt.Sprintf("metrics/%s/%s", serverName, nodeID),
+			Permission: "allow",
+			Action:     "all",
+		},
+	}...)
+	payload, err := json.Marshal(aclObject)
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPut, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/username/"+hostID, bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusNoContent {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error adding ACL Rules for user %s Error: %v", hostID, string(msg))
+	}
+	return nil
+}

+ 2 - 2
mq/handlers.go

@@ -114,7 +114,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 				return
 			} else {
 				if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-					if err = AppendNodeUpdateACL(hu.Host.ID.String(), hu.Node.Network, hu.Node.ID.String(), servercfg.GetServer()); err != nil {
+					if err = emqx.AppendNodeUpdateACL(hu.Host.ID.String(), hu.Node.Network, hu.Node.ID.String(), servercfg.GetServer()); err != nil {
 						slog.Error("failed to add ACLs for EMQX node", "error", err)
 						return
 					}
@@ -143,7 +143,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 	case models.DeleteHost:
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 			// delete EMQX credentials for host
-			if err := DeleteEmqxUser(currentHost.ID.String()); err != nil {
+			if err := emqx.DeleteEmqxUser(currentHost.ID.String()); err != nil {
 				slog.Error("failed to remove host credentials from EMQX", "id", currentHost.ID, "error", err)
 			}
 		}

+ 23 - 16
mq/mq.go

@@ -41,24 +41,31 @@ func setMqOptions(user, password string, opts *mqtt.ClientOptions) {
 // SetupMQTT creates a connection to broker and return client
 func SetupMQTT() {
 	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-		time.Sleep(10 * time.Second) // wait for the REST endpoint to be ready
-		// setup authenticator and create admin user
-		if err := CreateEmqxDefaultAuthenticator(); err != nil {
-			logger.Log(0, err.Error())
-		}
-		DeleteEmqxUser(servercfg.GetMqUserName())
-		if err := CreateEmqxUser(servercfg.GetMqUserName(), servercfg.GetMqPassword(), true); err != nil {
-			log.Fatal(err)
-		}
-		// create an ACL authorization source for the built in EMQX MNESIA database
-		if err := CreateEmqxDefaultAuthorizer(); err != nil {
-			logger.Log(0, err.Error())
-		}
-		// create a default deny ACL to all topics for all users
-		if err := CreateDefaultDenyRule(); err != nil {
-			log.Fatal(err)
+		if emqx.GetType() == servercfg.EmqxOnPremDeploy {
+			time.Sleep(10 * time.Second) // wait for the REST endpoint to be ready
+			// setup authenticator and create admin user
+			if err := emqx.CreateEmqxDefaultAuthenticator(); err != nil {
+				logger.Log(0, err.Error())
+			}
+			emqx.DeleteEmqxUser(servercfg.GetMqUserName())
+			if err := emqx.CreateEmqxUserforServer(); err != nil {
+				log.Fatal(err)
+			}
+			// create an ACL authorization source for the built in EMQX MNESIA database
+			if err := emqx.CreateEmqxDefaultAuthorizer(); err != nil {
+				logger.Log(0, err.Error())
+			}
+			// create a default deny ACL to all topics for all users
+			if err := emqx.CreateDefaultDenyRule(); err != nil {
+				log.Fatal(err)
+			}
+		} else {
+			if err := emqx.CreateEmqxUserforServer(); err != nil {
+				log.Fatal(err)
+			}
 		}
 	}
+
 	opts := mqtt.NewClientOptions()
 	setMqOptions(servercfg.GetMqUserName(), servercfg.GetMqPassword(), opts)
 	opts.SetOnConnectHandler(func(client mqtt.Client) {

+ 1 - 20
pro/controllers/failover.go

@@ -46,27 +46,8 @@ func createfailOver(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if _, exists := proLogic.FailOverExists(node.Network); exists {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failover exists already in the network"), "badrequest"))
-		return
-	}
-	host, err := logic.GetHost(node.HostID.String())
+	err = proLogic.CreateFailOver(node)
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("error getting host"+err.Error()), "badrequest"))
-		return
-	}
-	if host.OS != models.OS_Types.Linux {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only linux nodes can act as failovers"), "badrequest"))
-		return
-	}
-	if node.IsRelayed {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot set relayed node as failover"), "badrequest"))
-		return
-	}
-	node.IsFailOver = true
-	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
 	}

+ 164 - 0
pro/controllers/inet_gws.go

@@ -0,0 +1,164 @@
+package controllers
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+
+	"github.com/gorilla/mux"
+	"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"
+)
+
+// InetHandlers - handlers for internet gw
+func InetHandlers(r *mux.Router) {
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(createInternetGw))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(updateInternetGw))).Methods(http.MethodPut)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(deleteInternetGw))).Methods(http.MethodDelete)
+}
+
+// swagger:route POST /api/nodes/{network}/{nodeid}/inet_gw nodes createInternetGw
+//
+// Create an inet node.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: nodeResponse
+func createInternetGw(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	w.Header().Set("Content-Type", "application/json")
+	nodeid := params["nodeid"]
+	netid := params["network"]
+	node, err := logic.ValidateParams(nodeid, netid)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if node.IsInternetGateway {
+		logic.ReturnSuccessResponse(w, r, "node is already acting as internet gateway")
+		return
+	}
+	var request models.InetNodeReq
+	err = json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		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, "internal"))
+		return
+	}
+	if host.OS != models.OS_Types.Linux {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only linux nodes can be made internet gws"), "badrequest"))
+		return
+	}
+	err = proLogic.ValidateInetGwReq(node, request, false)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	proLogic.SetInternetGw(&node, request)
+	err = logic.UpsertNode(&node)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	apiNode := node.ConvertToAPINode()
+	logger.Log(1, r.Header.Get("user"), "created ingress gateway on node", nodeid, "on network", netid)
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(apiNode)
+	go mq.PublishPeerUpdate(false)
+}
+
+// swagger:route PUT /api/nodes/{network}/{nodeid}/inet_gw nodes updateInternetGw
+//
+// update an inet node.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: nodeResponse
+func updateInternetGw(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	w.Header().Set("Content-Type", "application/json")
+	nodeid := params["nodeid"]
+	netid := params["network"]
+	node, err := logic.ValidateParams(nodeid, netid)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var request models.InetNodeReq
+	err = json.NewDecoder(r.Body).Decode(&request)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if !node.IsInternetGateway {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("node is not a internet gw"), "badrequest"))
+		return
+	}
+	err = proLogic.ValidateInetGwReq(node, request, true)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	proLogic.UnsetInternetGw(&node)
+	proLogic.SetInternetGw(&node, request)
+	err = logic.UpsertNode(&node)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	apiNode := node.ConvertToAPINode()
+	logger.Log(1, r.Header.Get("user"), "created ingress gateway on node", nodeid, "on network", netid)
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(apiNode)
+	go mq.PublishPeerUpdate(false)
+}
+
+// swagger:route DELETE /api/nodes/{network}/{nodeid}/inet_gw nodes deleteInternetGw
+//
+// Delete an internet gw.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: nodeResponse
+func deleteInternetGw(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	w.Header().Set("Content-Type", "application/json")
+	nodeid := params["nodeid"]
+	netid := params["network"]
+	node, err := logic.ValidateParams(nodeid, netid)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	proLogic.UnsetInternetGw(&node)
+	err = logic.UpsertNode(&node)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	apiNode := node.ConvertToAPINode()
+	logger.Log(1, r.Header.Get("user"), "created ingress gateway on node", nodeid, "on network", netid)
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(apiNode)
+	go mq.PublishPeerUpdate(false)
+}

+ 4 - 0
pro/controllers/users.go

@@ -212,6 +212,7 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Connected:         true,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
+					Metadata:          node.Metadata,
 				})
 				userGws[node.Network] = gws
 				delete(user.RemoteGwIDs, node.ID.String())
@@ -226,6 +227,7 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Connected:         true,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
+					Metadata:          node.Metadata,
 				})
 				userGws[node.Network] = gws
 				processedAdminNodeIds[node.ID.String()] = struct{}{}
@@ -258,6 +260,7 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 				Network:           node.Network,
 				IsInternetGateway: node.IsInternetGateway,
 				GwPeerPublicKey:   host.PublicKey.String(),
+				Metadata:          node.Metadata,
 			})
 			userGws[node.Network] = gws
 		}
@@ -284,6 +287,7 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Network:           node.Network,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
+					Metadata:          node.Metadata,
 				})
 				userGws[node.Network] = gws
 			}

+ 7 - 2
pro/initialize.go

@@ -31,6 +31,7 @@ func InitPro() {
 		proControllers.RelayHandlers,
 		proControllers.UserHandlers,
 		proControllers.FailOverHandlers,
+		proControllers.InetHandlers,
 	)
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
 		// == License Handling ==
@@ -83,6 +84,7 @@ func InitPro() {
 	})
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
+	logic.CreateFailOver = proLogic.CreateFailOver
 	logic.GetFailOverPeerIps = proLogic.GetFailOverPeerIps
 	logic.DenyClientNodeAccess = proLogic.DenyClientNode
 	logic.IsClientNodeAllowed = proLogic.IsClientNodeAllowed
@@ -99,9 +101,12 @@ func InitPro() {
 	logic.UpdateRelayed = proLogic.UpdateRelayed
 	logic.SetRelayedNodes = proLogic.SetRelayedNodes
 	logic.RelayUpdates = proLogic.RelayUpdates
-	logic.IsInternetGw = proLogic.IsInternetGw
-	logic.SetInternetGw = proLogic.SetInternetGw
 	logic.GetTrialEndDate = getTrialEndDate
+	logic.SetDefaultGw = proLogic.SetDefaultGw
+	logic.SetDefaultGwForRelayedUpdate = proLogic.SetDefaultGwForRelayedUpdate
+	logic.UnsetInternetGw = proLogic.UnsetInternetGw
+	logic.SetInternetGw = proLogic.SetInternetGw
+	logic.GetAllowedIpForInetNodeClient = proLogic.GetAllowedIpForInetNodeClient
 	mq.UpdateMetrics = proLogic.MQUpdateMetrics
 	mq.UpdateMetricsFallBack = proLogic.MQUpdateMetricsFallBack
 }

+ 27 - 0
pro/logic/ext_acls.go

@@ -5,6 +5,7 @@ import (
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
 )
 
 // DenyClientNode - add a denied node to an ext client's list
@@ -55,14 +56,40 @@ func SetClientDefaultACLs(ec *models.ExtClient) error {
 	if err != nil {
 		return err
 	}
+	var networkAcls acls.ACLContainer
+	networkAcls, err = networkAcls.Get(acls.ContainerID(ec.Network))
+	if err != nil {
+		slog.Error("failed to get network acls", "error", err)
+		return err
+	}
+	networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{}
 	for i := range networkNodes {
 		currNode := networkNodes[i]
 		if network.DefaultACL == "no" || currNode.DefaultACL == "no" {
 			DenyClientNode(ec, currNode.ID.String())
+			networkAcls[acls.AclID(ec.ClientID)][acls.AclID(currNode.ID.String())] = acls.NotAllowed
+			networkAcls[acls.AclID(currNode.ID.String())][acls.AclID(ec.ClientID)] = acls.NotAllowed
 		} else {
 			RemoveDeniedNodeFromClient(ec, currNode.ID.String())
+			networkAcls[acls.AclID(ec.ClientID)][acls.AclID(currNode.ID.String())] = acls.Allowed
+			networkAcls[acls.AclID(currNode.ID.String())][acls.AclID(ec.ClientID)] = acls.Allowed
 		}
 	}
+	networkClients, err := logic.GetNetworkExtClients(ec.Network)
+	if err != nil {
+		slog.Error("failed to get network clients", "error", err)
+		return err
+	}
+	for _, client := range networkClients {
+		// TODO: revisit when client-client acls are supported
+		networkAcls[acls.AclID(ec.ClientID)][acls.AclID(client.ClientID)] = acls.Allowed
+		networkAcls[acls.AclID(client.ClientID)][acls.AclID(ec.ClientID)] = acls.Allowed
+	}
+	delete(networkAcls[acls.AclID(ec.ClientID)], acls.AclID(ec.ClientID)) // remove oneself
+	if _, err = networkAcls.Save(acls.ContainerID(ec.Network)); err != nil {
+		slog.Error("failed to update network acls", "error", err)
+		return err
+	}
 	return nil
 }
 

+ 24 - 0
pro/logic/failover.go

@@ -7,6 +7,7 @@ import (
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
 )
 
 func SetFailOverCtx(failOverNode, victimNode, peerNode models.Node) error {
@@ -123,3 +124,26 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 	}
 	return allowedips
 }
+
+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 err
+	}
+	if node.IsRelayed {
+		return err
+	}
+	node.IsFailOver = true
+	err = logic.UpsertNode(&node)
+	if err != nil {
+		slog.Error("failed to upsert node", "node", node.ID.String(), "error", err)
+		return err
+	}
+	return nil
+}

+ 121 - 7
pro/logic/nodes.go

@@ -1,24 +1,132 @@
 package logic
 
 import (
-	celogic "github.com/gravitl/netmaker/logic"
+	"errors"
+	"fmt"
+	"net"
+
+	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
 )
 
-// IsInternetGw - checks if node is acting as internet gw
-func IsInternetGw(node models.Node) bool {
-	return node.IsInternetGateway
+func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool) error {
+	inetHost, err := logic.GetHost(inetNode.HostID.String())
+	if err != nil {
+		return err
+	}
+	if inetHost.FirewallInUse == models.FIREWALL_NONE {
+		return errors.New("iptables or nftables needs to be installed")
+	}
+	if inetNode.InternetGwID != "" {
+		return fmt.Errorf("node %s is using a internet gateway already", inetHost.Name)
+	}
+	if inetNode.IsRelayed {
+		return fmt.Errorf("node %s is being relayed", inetHost.Name)
+	}
+	for _, clientNodeID := range req.InetNodeClientIDs {
+		clientNode, err := logic.GetNodeByID(clientNodeID)
+		if err != nil {
+			return err
+		}
+		clientHost, err := logic.GetHost(clientNode.HostID.String())
+		if err != nil {
+			return err
+		}
+		if clientHost.OS != models.OS_Types.Linux && clientHost.OS != models.OS_Types.Windows {
+			return errors.New("can only attach linux or windows machine to a internet gateway")
+		}
+		if clientNode.IsInternetGateway {
+			return fmt.Errorf("node %s acting as internet gateway cannot use another internet gateway", clientHost.Name)
+		}
+		if update {
+			if clientNode.InternetGwID != "" && clientNode.InternetGwID != inetNode.ID.String() {
+				return fmt.Errorf("node %s is already using a internet gateway", clientHost.Name)
+			}
+		} else {
+			if clientNode.InternetGwID != "" {
+				return fmt.Errorf("node %s is already using a internet gateway", clientHost.Name)
+			}
+		}
+
+		if clientNode.IsRelayed {
+			return fmt.Errorf("node %s is being relayed", clientHost.Name)
+		}
+
+		for _, nodeID := range clientHost.Nodes {
+			node, err := logic.GetNodeByID(nodeID)
+			if err != nil {
+				continue
+			}
+			if node.InternetGwID != "" && node.InternetGwID != inetNode.ID.String() {
+				return errors.New("nodes on same host cannot use different internet gateway")
+			}
+
+		}
+	}
+	return nil
 }
 
 // SetInternetGw - sets the node as internet gw based on flag bool
-func SetInternetGw(node *models.Node, flag bool) {
-	node.IsInternetGateway = flag
+func SetInternetGw(node *models.Node, req models.InetNodeReq) {
+	node.IsInternetGateway = true
+	node.InetNodeReq = req
+	for _, clientNodeID := range req.InetNodeClientIDs {
+		clientNode, err := logic.GetNodeByID(clientNodeID)
+		if err != nil {
+			continue
+		}
+		clientNode.InternetGwID = node.ID.String()
+		logic.UpsertNode(&clientNode)
+	}
+
+}
+
+func UnsetInternetGw(node *models.Node) {
+	nodes, err := logic.GetNetworkNodes(node.Network)
+	if err != nil {
+		slog.Error("failed to get network nodes", "network", node.Network, "error", err)
+		return
+	}
+	for _, clientNode := range nodes {
+		if node.ID.String() == clientNode.InternetGwID {
+			clientNode.InternetGwID = ""
+			logic.UpsertNode(&clientNode)
+		}
+
+	}
+	node.IsInternetGateway = false
+	node.InetNodeReq = models.InetNodeReq{}
+
+}
+
+func SetDefaultGwForRelayedUpdate(relayed, relay models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
+	if relay.InternetGwID != "" {
+		peerUpdate.ChangeDefaultGw = true
+		peerUpdate.DefaultGwIp = relay.Address.IP
+
+	}
+	return peerUpdate
+}
+
+func SetDefaultGw(node models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
+	if node.InternetGwID != "" {
+
+		inetNode, err := logic.GetNodeByID(node.InternetGwID)
+		if err != nil {
+			return peerUpdate
+		}
+		peerUpdate.ChangeDefaultGw = true
+		peerUpdate.DefaultGwIp = inetNode.Address.IP
+
+	}
+	return peerUpdate
 }
 
 // GetNetworkIngresses - gets the gateways of a network
 func GetNetworkIngresses(network string) ([]models.Node, error) {
 	var ingresses []models.Node
-	netNodes, err := celogic.GetNetworkNodes(network)
+	netNodes, err := logic.GetNetworkNodes(network)
 	if err != nil {
 		return []models.Node{}, err
 	}
@@ -29,3 +137,9 @@ func GetNetworkIngresses(network string) ([]models.Node, error) {
 	}
 	return ingresses, nil
 }
+
+// GetAllowedIpsForInet - get inet cidr for node using a inet gw
+func GetAllowedIpForInetNodeClient(node, peer *models.Node) []net.IPNet {
+	_, ipnet, _ := net.ParseCIDR("0.0.0.0/0")
+	return []net.IPNet{*ipnet}
+}

+ 5 - 6
pro/logic/relays.go

@@ -103,11 +103,7 @@ func SetRelayedNodes(setRelayed bool, relay string, relayed []string) []models.N
 // ValidateRelay - checks if relay is valid
 func ValidateRelay(relay models.RelayRequest) error {
 	var err error
-	// isIp := functions.IsIpCIDR(gateway.RangeString)
-	empty := len(relay.RelayedNodes) == 0
-	if empty {
-		return errors.New("IP Ranges Cannot Be Empty")
-	}
+
 	node, err := logic.GetNodeByID(relay.NodeID)
 	if err != nil {
 		return err
@@ -135,7 +131,7 @@ func updateRelayNodes(relay string, oldNodes []string, newNodes []string) []mode
 
 func RelayUpdates(currentNode, newNode *models.Node) bool {
 	relayUpdates := false
-	if servercfg.IsPro && newNode.IsRelay && len(newNode.RelayedNodes) > 0 {
+	if servercfg.IsPro && newNode.IsRelay {
 		if len(newNode.RelayedNodes) != len(currentNode.RelayedNodes) {
 			relayUpdates = true
 		} else {
@@ -208,6 +204,9 @@ func GetAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNe
 		logger.Log(0, "RelayedByRelay called with invalid parameters")
 		return
 	}
+	if relay.InternetGwID != "" {
+		return GetAllowedIpForInetNodeClient(relayed, relay)
+	}
 	peers, err := logic.GetNetworkNodes(relay.Network)
 	if err != nil {
 		logger.Log(0, "error getting network clients", err.Error())

+ 15 - 15
release.md

@@ -1,19 +1,19 @@
+# Netmaker v0.23.0
 
-# Netmaker v0.22.0
+## Whats New ✨
 
-## Whats New
-- Revamped Internet Gateways
-- MQ fallback
-- Deprecating TURN in favour of failover hosts on Pro
-- Switch to CoreDNS for DNS resolution
-- DNS is no longer managed with OS hosts file (/etc/hosts file)
-- Add support for RAC on mobile
+- Revamped Internet Gateways: hosts and clients can now use internet gateways! More info [here](https://docs.netmaker.io/pro/internet-gateways.html)
+  On community edition, internet gateways for clients can be accessed via the Remote Access tab.
+- PostUp and PostDown commands for clients
+- EMQX cloud support
+- Metadata for Remote Access Gateways
 
-## What's Fixed
-- Expired nodes not getting deleted
-- NMCTL acl subcommand leading to dirty state
-- Enforce private network ranges
-- Minor bugs and enhacements with user management
-- Scalability issues
+## What's Fixed/Improved 🛠
 
-## Known issues
+- Allow creation of gateways, relays and egress without clients, relayed hosts and external ranges respectively
+- Make default host a remote access gateway and a failover host on joining a new network
+- Stability fixes with ACLs
+- Fixed issues with install/upgrade scripts
+- Fixed issues with CoreDNS
+
+## Known Issues 🐞

+ 1 - 7
scripts/nm-upgrade.sh

@@ -5,8 +5,7 @@ CONFIG_FILE=netmaker.env
 SCRIPT_DIR=$(dirname "$(realpath "$0")")
 CONFIG_PATH="$SCRIPT_DIR/$CONFIG_FILE"
 NM_QUICK_VERSION="0.1.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="v0.21.2"
 if [ "$(id -u)" -ne 0 ]; then
 	echo "This script must be run as root"
 	exit 1
@@ -440,7 +439,6 @@ install_netmaker() {
 		wget -qO "$SCRIPT_DIR"/Caddyfile "$CADDY_URL"
 		wget -qO "$SCRIPT_DIR"/netmaker.default.env "$BASE_URL/scripts/netmaker.default.env"
 		wget -qO "$SCRIPT_DIR"/mosquitto.conf "$BASE_URL/docker/mosquitto.conf"
-		wget -qO "$SCRIPT_DIR"/nm-certs.sh "$BASE_URL/scripts/nm-certs.sh"
 		wget -qO "$SCRIPT_DIR"/wait.sh "$BASE_URL/docker/wait.sh"
 
 	chmod +x "$SCRIPT_DIR"/wait.sh
@@ -450,10 +448,6 @@ install_netmaker() {
 	ln -fs "$SCRIPT_DIR/netmaker.env" "$SCRIPT_DIR/.env"
 	save_config
 
-	# Fetch / update certs using certbot
-	chmod +x "$SCRIPT_DIR"/nm-certs.sh
-	"$SCRIPT_DIR"/nm-certs.sh
-
 	echo "Starting containers..."
 
 	# increase the timeouts

+ 31 - 2
servercfg/serverconf.go

@@ -17,10 +17,15 @@ import (
 // EmqxBrokerType denotes the broker type for EMQX MQTT
 const EmqxBrokerType = "emqx"
 
+// Emqxdeploy - emqx deploy type
+type Emqxdeploy string
+
 var (
 	Version              = "dev"
 	IsPro                = false
 	ErrLicenseValidation error
+	EmqxCloudDeploy      Emqxdeploy = "cloud"
+	EmqxOnPremDeploy     Emqxdeploy = "on-prem"
 )
 
 // SetHost - sets the host ip
@@ -112,8 +117,13 @@ func GetRacAutoDisable() bool {
 func GetServerInfo() models.ServerConfig {
 	var cfg models.ServerConfig
 	cfg.Server = GetServer()
-	cfg.MQUserName = GetMqUserName()
-	cfg.MQPassword = GetMqPassword()
+	if GetBrokerType() == EmqxBrokerType {
+		cfg.MQUserName = "HOST_ID"
+		cfg.MQPassword = "HOST_PASS"
+	} else {
+		cfg.MQUserName = GetMqUserName()
+		cfg.MQPassword = GetMqPassword()
+	}
 	cfg.API = GetAPIConnString()
 	cfg.CoreDNSAddr = GetCoreDNSAddr()
 	cfg.APIPort = GetAPIPort()
@@ -674,3 +684,22 @@ func GetEnvironment() string {
 	}
 	return ""
 }
+
+// GetEmqxDeployType - fetches emqx deploy type this server uses
+func GetEmqxDeployType() (deployType Emqxdeploy) {
+	deployType = EmqxOnPremDeploy
+	if os.Getenv("EMQX_DEPLOY_TYPE") == string(EmqxCloudDeploy) {
+		deployType = EmqxCloudDeploy
+	}
+	return
+}
+
+// GetEmqxAppID - gets the emqx cloud app id
+func GetEmqxAppID() string {
+	return os.Getenv("EMQX_APP_ID")
+}
+
+// GetEmqxAppSecret - gets the emqx cloud app secret
+func GetEmqxAppSecret() string {
+	return os.Getenv("EMQX_APP_SECRET")
+}

+ 1 - 1
swagger.yml

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